diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a6e3655 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,292 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +module.exports = { + env: { + es6: true + }, + + parserOptions: { + sourceType: 'module' + }, + + extends: [ + 'plugin:prettier/recommended', // https://github.com/prettier/eslint-plugin-prettier#recommended-configuration + 'prettier' + ], + + plugins: ['eslint-comments', 'react', 'react-hooks', 'jest'], + + settings: { + react: { + version: 'detect' + } + }, + + ignorePatterns: ['node_modules/', '**/*.js'], + + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint/eslint-plugin'], + rules: { + '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], + 'no-unused-vars': 'off' + } + } + ], + + // Map from global var to bool specifying if it can be redefined + globals: { + jest: true, + __DEV__: true, + __dirname: false, + __fbBatchedBridgeConfig: false, + AbortController: false, + alert: false, + cancelAnimationFrame: false, + cancelIdleCallback: false, + clearImmediate: true, + clearInterval: false, + clearTimeout: false, + console: false, + document: false, + ErrorUtils: false, + escape: false, + Event: false, + EventTarget: false, + exports: false, + fetch: false, + FileReader: false, + FormData: false, + global: false, + Headers: false, + Intl: false, + Map: true, + module: false, + navigator: false, + process: false, + Promise: true, + requestAnimationFrame: true, + requestIdleCallback: true, + require: false, + Set: true, + setImmediate: true, + setInterval: false, + setTimeout: false, + URL: false, + URLSearchParams: false, + WebSocket: true, + window: false, + XMLHttpRequest: false, + JSX: true, + KeyboardEvent: true, + MouseEvent: true, + Node: true, + HTMLDivElement: true, + HTMLInputElement: true + }, + + rules: { + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-function-return-type": "error", + + // General + 'comma-dangle': [1, 'never'], // allow or disallow trailing commas + 'no-cond-assign': 1, // disallow assignment in conditional expressions + 'no-console': 0, // disallow use of console (off by default in the node environment) + 'no-const-assign': 2, // disallow assignment to const-declared variables + 'no-constant-condition': 0, // disallow use of constant expressions in conditions + 'no-control-regex': 1, // disallow control characters in regular expressions + 'no-debugger': 1, // disallow use of debugger + 'no-dupe-class-members': 2, // Disallow duplicate name in class members + 'no-dupe-keys': 2, // disallow duplicate keys when creating object literals + 'no-empty': 0, // disallow empty statements + 'no-ex-assign': 1, // disallow assigning to the exception in a catch block + 'no-extra-boolean-cast': 1, // disallow double-negation boolean casts in a boolean context + 'no-extra-parens': 0, // disallow unnecessary parentheses (off by default) + 'no-extra-semi': 1, // disallow unnecessary semicolons + 'no-func-assign': 1, // disallow overwriting functions written as function declarations + 'no-inner-declarations': 0, // disallow function or variable declarations in nested blocks + 'no-invalid-regexp': 1, // disallow invalid regular expression strings in the RegExp constructor + 'no-negated-in-lhs': 1, // disallow negation of the left operand of an in expression + 'no-obj-calls': 1, // disallow the use of object properties of the global object (Math and JSON) as functions + 'no-regex-spaces': 1, // disallow multiple spaces in a regular expression literal + 'no-reserved-keys': 0, // disallow reserved words being used as object literal keys (off by default) + 'no-sparse-arrays': 1, // disallow sparse arrays + 'no-unreachable': 2, // disallow unreachable statements after a return, throw, continue, or break statement + 'use-isnan': 1, // disallow comparisons with the value NaN + 'valid-jsdoc': 0, // Ensure JSDoc comments are valid (off by default) + 'valid-typeof': 1, // Ensure that the results of typeof are compared against a valid string + + // Best Practices + // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. + + 'block-scoped-var': 0, // treat var statements as if they were block scoped (off by default) + complexity: 0, // specify the maximum cyclomatic complexity allowed in a program (off by default) + 'consistent-return': 0, // require return statements to either always or never specify values + curly: 1, // specify curly brace conventions for all control statements + 'default-case': 0, // require default case in switch statements (off by default) + 'dot-notation': 1, // encourages use of dot notation whenever possible + eqeqeq: [1, 'allow-null'], // require the use of === and !== + 'guard-for-in': 0, // make sure for-in loops have an if statement (off by default) + 'no-alert': 1, // disallow the use of alert, confirm, and prompt + 'no-caller': 1, // disallow use of arguments.caller or arguments.callee + 'no-div-regex': 1, // disallow division operators explicitly at beginning of regular expression (off by default) + 'no-else-return': 0, // disallow else after a return in an if (off by default) + 'no-eq-null': 0, // disallow comparisons to null without a type-checking operator (off by default) + 'no-eval': 2, // disallow use of eval() + 'no-extend-native': 1, // disallow adding to native types + 'no-extra-bind': 1, // disallow unnecessary function binding + 'no-fallthrough': 1, // disallow fallthrough of case statements + 'no-floating-decimal': 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) + 'no-implied-eval': 1, // disallow use of eval()-like methods + 'no-labels': 1, // disallow use of labeled statements + 'no-iterator': 1, // disallow usage of __iterator__ property + 'no-lone-blocks': 1, // disallow unnecessary nested blocks + 'no-loop-func': 0, // disallow creation of functions within loops + 'no-multi-str': 0, // disallow use of multiline strings + 'no-native-reassign': 0, // disallow reassignments of native objects + 'no-new': 1, // disallow use of new operator when not part of the assignment or comparison + 'no-new-func': 2, // disallow use of new operator for Function object + 'no-new-wrappers': 1, // disallows creating new instances of String,Number, and Boolean + 'no-octal': 1, // disallow use of octal literals + 'no-octal-escape': 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; + 'no-proto': 1, // disallow usage of __proto__ property + 'no-redeclare': 0, // disallow declaring the same variable more then once + 'no-return-assign': 1, // disallow use of assignment in return statement + 'no-script-url': 1, // disallow use of javascript: urls. + 'no-self-compare': 1, // disallow comparisons where both sides are exactly the same (off by default) + 'no-sequences': 1, // disallow use of comma operator + 'no-unused-expressions': 0, // disallow usage of expressions in statement position + 'no-useless-escape': 1, // disallow escapes that don't have any effect in literals + 'no-void': 1, // disallow use of void operator (off by default) + 'no-warning-comments': 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) + 'no-with': 1, // disallow use of the with statement + radix: 1, // require use of the second argument for parseInt() (off by default) + 'semi-spacing': 1, // require a space after a semi-colon + 'vars-on-top': 0, // requires to declare all vars on top of their containing scope (off by default) + 'wrap-iife': 0, // require immediate function invocation to be wrapped in parentheses (off by default) + yoda: 1, // require or disallow Yoda conditions + + // Variables + // These rules have to do with variable declarations. + + 'no-catch-shadow': 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) + 'no-delete-var': 1, // disallow deletion of variables + 'no-label-var': 1, // disallow labels that share a name with a variable + 'no-shadow': 1, // disallow declaration of variables already declared in the outer scope + 'no-shadow-restricted-names': 1, // disallow shadowing of names such as arguments + 'no-undef': 2, // disallow use of undeclared variables unless mentioned in a /*global */ block + 'no-undefined': 0, // disallow use of undefined variable (off by default) + 'no-undef-init': 1, // disallow use of undefined when initializing variables + 'no-unused-vars': [1, {vars: 'all', args: 'none', ignoreRestSiblings: true}], // disallow declaration of variables that are not used in the code + 'no-use-before-define': 0, // disallow use of variables before they are defined + + // Node.js + // These rules are specific to JavaScript running on Node.js. + + 'handle-callback-err': 1, // enforces error handling in callbacks (off by default) (on by default in the node environment) + 'no-mixed-requires': 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment) + 'no-new-require': 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment) + 'no-path-concat': 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment) + 'no-process-exit': 0, // disallow process.exit() (on by default in the node environment) + 'no-restricted-modules': 1, // restrict usage of specified node modules (off by default) + 'no-sync': 0, // disallow use of synchronous methods (off by default) + + // ESLint Comments Plugin + // The following rules are made available via `eslint-plugin-eslint-comments` + 'eslint-comments/no-aggregating-enable': 1, // disallows eslint-enable comments for multiple eslint-disable comments + 'eslint-comments/no-unlimited-disable': 1, // disallows eslint-disable comments without rule names + 'eslint-comments/no-unused-disable': 1, // disallow disables that don't cover any errors + 'eslint-comments/no-unused-enable': 1, // // disallow enables that don't enable anything or enable rules that weren't disabled + + // Stylistic Issues + // These rules are purely matters of style and are quite subjective. + + 'key-spacing': 0, + 'keyword-spacing': 1, // enforce spacing before and after keywords + 'jsx-quotes': [1, 'prefer-single'], // enforces the usage of double quotes for all JSX attribute values which doesn’t 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 + } +}; diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 24c2ca1..282e3c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,23 +1,19 @@ image: node:14 stages: - - build + - build_frontend + - build_backend - test - packaging - deploy -include: - - template: Code-Quality.gitlab-ci.yml - -variables: - SAST_DISABLE_DIND: "true" - Minimize_Frontend: - stage: build + stage: build_frontend before_script: - yarn install --cache-folder .yarn script: - yarn run build + - rm build/*/*/*.map artifacts: expire_in: 2 days paths: @@ -30,11 +26,15 @@ Minimize_Frontend: Build_Backend: image: golang:latest - stage: build + stage: build_backend script: - cd apiGo - go build -v -o openmediacenter - - env GOOS=windows GOARCH=amd64 go build -v -o openmediacenter.exe + - cp -r ../build/ ./static/ + - 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: expire_in: 2 days paths: @@ -46,6 +46,7 @@ Frontend_Tests: - yarn install --cache-folder .yarn script: - yarn run test + needs: [] artifacts: reports: junit: @@ -63,14 +64,27 @@ Backend_Tests: - cd apiGo - go get -u github.com/jstemmer/go-junit-report - go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml + needs: [] artifacts: when: always reports: junit: ./apiGo/report.xml -code_quality: - tags: - - dind +lint: + stage: test + before_script: + - 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: stage: packaging @@ -98,7 +112,7 @@ Debian_Server: Test_Server: stage: deploy - image: luki42/alpineopenssh:latest + image: luki42/ssh:latest needs: - Frontend_Tests - Backend_Tests @@ -107,7 +121,7 @@ Test_Server: - master script: - eval $(ssh-agent -s) - - ssh-add <(echo "$SSH_PRIVATE_KEY") + - echo "$SSH_PRIVATE_KEY" | 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.42:/tmp/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..2d3c936 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,10 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + tabWidth: 4, + trailingComma: 'none', + printWidth: 135, + semi: true, + jsxSingleQuote: true +}; diff --git a/README.md b/README.md index dd86544..eb9aeca 100644 --- a/README.md +++ b/README.md @@ -22,24 +22,30 @@ and in dark mode: ![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png) ## Installation -First of all clone the repository. -`git clone https://gitlab.heili.eu/lukas/openmediacenter.git` +Download the latest release .deb file from the Releases page and install it via `apt install ./OpenMediaCenter-0.1.x_amd64.deb` -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 -Now you can access your MediaCenter via your servers global ip (: +Now you can access your MediaCenter via your servers global ip on port 8080 (: 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 Any contribution is appreciated. Feel free to contact me (lukas.heiligenbrunner@gmail.com), open an issue or request a new feature. diff --git a/apiGo/api/Actors.go b/apiGo/api/Actors.go index 3d405e9..1cc511f 100644 --- a/apiGo/api/Actors.go +++ b/apiGo/api/Actors.go @@ -12,32 +12,83 @@ func AddActorsHandlers() { } func getActorsFromDB() { - AddHandler("getAllActors", ActorNode, nil, func() []byte { + /** + * @api {post} /api/actor [getAllActors] + * @apiDescription Get all available Actors + * @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 + */ + AddHandler("getAllActors", ActorNode, func(info *HandlerInfo) []byte { query := "SELECT actor_id, name, thumbnail FROM actors" return jsonify(readActorsFromResultset(database.Query(query))) }) - var gaov struct { - MovieId int - } - AddHandler("getActorsOfVideo", ActorNode, &gaov, func() []byte { + /** + * @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 + */ + AddHandler("getActorsOfVideo", ActorNode, func(info *HandlerInfo) []byte { + var args struct { + MovieId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos JOIN actors a on actors_videos.actor_id = a.actor_id - WHERE actors_videos.video_id=%d`, gaov.MovieId) + WHERE actors_videos.video_id=%d`, args.MovieId) return jsonify(readActorsFromResultset(database.Query(query))) }) - var gai struct { - ActorId int - } - AddHandler("getActorInfo", ActorNode, &gai, func() []byte { + /** + * @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 + */ + AddHandler("getActorInfo", ActorNode, func(info *HandlerInfo) []byte { + var args struct { + ActorId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := fmt.Sprintf(`SELECT movie_id, movie_name FROM actors_videos JOIN videos v on v.movie_id = actors_videos.video_id - WHERE actors_videos.actor_id=%d`, gai.ActorId) + WHERE actors_videos.actor_id=%d`, args.ActorId) videos := readVideosFromResultset(database.Query(query)) - query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", gai.ActorId) + query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", args.ActorId) actor := readActorsFromResultset(database.Query(query))[0] var result = struct { @@ -53,20 +104,51 @@ func getActorsFromDB() { } func saveActorsToDB() { - var ca struct { - ActorName string - } - AddHandler("createActor", ActorNode, &ca, func() []byte { + /** + * @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 + */ + AddHandler("createActor", ActorNode, func(info *HandlerInfo) []byte { + var args struct { + ActorName string + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := "INSERT IGNORE INTO actors (name) VALUES (?)" - return database.SuccessQuery(query, ca.ActorName) + return database.SuccessQuery(query, args.ActorName) }) - var aatv struct { - ActorId int - MovieId int - } - AddHandler("addActorToVideo", ActorNode, &aatv, func() []byte { - query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", aatv.ActorId, aatv.MovieId) + /** + * @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 + */ + AddHandler("addActorToVideo", ActorNode, func(info *HandlerInfo) []byte { + var args struct { + ActorId int + MovieId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", args.ActorId, args.MovieId) return database.SuccessQuery(query) }) } diff --git a/apiGo/api/ApiBase.go b/apiGo/api/ApiBase.go index d404141..5120559 100644 --- a/apiGo/api/ApiBase.go +++ b/apiGo/api/ApiBase.go @@ -4,8 +4,9 @@ import ( "bytes" "encoding/json" "fmt" - "log" + "gopkg.in/oauth2.v3" "net/http" + "openmediacenter/apiGo/api/oauth" ) const APIPREFIX = "/api" @@ -15,73 +16,76 @@ const ( TagNode = iota SettingsNode = iota ActorNode = iota + TVShowNode = iota ) +type HandlerInfo struct { + ID string + Token string + Data map[string]interface{} +} + type actionStruct struct { Action string } type Handler struct { - action string - handler func() []byte - arguments interface{} - apiNode int + action string + handler func(info *HandlerInfo) []byte + apiNode int } -var handlers []Handler +var handlers = make(map[string]Handler) -func AddHandler(action string, apiNode int, n interface{}, h func() []byte) { +func AddHandler(action string, apiNode int, h func(info *HandlerInfo) []byte) { // append new handler to the handlers - handlers = append(handlers, Handler{action, h, n, apiNode}) + handlers[fmt.Sprintf("%s/%d", action, apiNode)] = Handler{action, h, apiNode} } -func ServerInit(port uint16) { - http.Handle(APIPREFIX+"/video", http.HandlerFunc(videoHandler)) - http.Handle(APIPREFIX+"/tags", http.HandlerFunc(tagHandler)) - http.Handle(APIPREFIX+"/settings", http.HandlerFunc(settingsHandler)) - http.Handle(APIPREFIX+"/actor", http.HandlerFunc(actorHandler)) +func ServerInit() { + http.Handle(APIPREFIX+"/video", oauth.ValidateToken(handlefunc, VideoNode)) + http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(handlefunc, TagNode)) + http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(handlefunc, SettingsNode)) + http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(handlefunc, ActorNode)) + http.Handle(APIPREFIX+"/tvshow", oauth.ValidateToken(handlefunc, TVShowNode)) - fmt.Printf("OpenMediacenter server up and running on port %d\n", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) + // initialize oauth service and add corresponding auth routes + oauth.InitOAuth() } -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() - } +func handleAPICall(action string, requestBody string, apiNode int, info *HandlerInfo) []byte { + handler, ok := handlers[fmt.Sprintf("%s/%d", action, apiNode)] + if !ok { + // handler doesn't exist! + fmt.Printf("no handler found for Action: %d/%s\n", apiNode, action) + return nil } - fmt.Printf("no handler found for Action: %d/%s\n", apiNode, action) - return nil + + // check if info even exists + if info == nil { + info = &HandlerInfo{} + } + + // parse the arguments + var args map[string]interface{} + err := json.Unmarshal([]byte(requestBody), &args) + + if err != nil { + fmt.Printf("failed to decode arguments of action %s :: %s\n", action, requestBody) + } else { + // check if map has an action + if _, ok := args["action"]; ok { + delete(args, "action") + } + + info.Data = args + } + + // call the handler + return handler.handler(info) } -func actorHandler(rw http.ResponseWriter, req *http.Request) { - handlefunc(rw, req, ActorNode) -} - -func videoHandler(rw http.ResponseWriter, req *http.Request) { - handlefunc(rw, req, VideoNode) -} - -func tagHandler(rw http.ResponseWriter, req *http.Request) { - handlefunc(rw, req, TagNode) -} - -func settingsHandler(rw http.ResponseWriter, req *http.Request) { - handlefunc(rw, req, SettingsNode) -} - -func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { +func handlefunc(rw http.ResponseWriter, req *http.Request, node int, tokenInfo *oauth2.TokenInfo) { // only allow post requests if req.Method != "POST" { return @@ -97,5 +101,13 @@ func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { fmt.Println("failed to read action from request! :: " + body) } - rw.Write(handleAPICall(t.Action, body, node)) + // load userid from received token object + id := (*tokenInfo).GetClientID() + + userinfo := &HandlerInfo{ + ID: id, + Token: (*tokenInfo).GetCode(), + } + + rw.Write(handleAPICall(t.Action, body, node, userinfo)) } diff --git a/apiGo/api/ApiBase_test.go b/apiGo/api/ApiBase_test.go index 9893c29..d692f01 100644 --- a/apiGo/api/ApiBase_test.go +++ b/apiGo/api/ApiBase_test.go @@ -5,13 +5,13 @@ import ( ) func cleanUp() { - handlers = nil + handlers = make(map[string]Handler) } func TestAddHandler(t *testing.T) { cleanUp() - AddHandler("test", ActorNode, nil, func() []byte { + AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { return nil }) if len(handlers) != 1 { @@ -23,13 +23,13 @@ func TestCallOfHandler(t *testing.T) { cleanUp() i := 0 - AddHandler("test", ActorNode, nil, func() []byte { + AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { i++ return nil }) // simulate the call of the api - handleAPICall("test", "", ActorNode) + handleAPICall("test", "", ActorNode, nil) if i != 1 { t.Errorf("Unexpected number of Lambda calls : %d/1", i) @@ -39,26 +39,32 @@ func TestCallOfHandler(t *testing.T) { func TestDecodingOfArguments(t *testing.T) { cleanUp() - var myvar struct { - Test string - TestInt int - } - AddHandler("test", ActorNode, &myvar, func() []byte { + AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { + var args struct { + Test string + TestInt int + } + err := FillStruct(&args, info.Data) + if err != nil { + t.Errorf("Error parsing args: %s", err.Error()) + return nil + } + + if args.TestInt != 42 || args.Test != "myString" { + t.Errorf("Wrong parsing of argument parameters : %d/42 - %s/myString", args.TestInt, args.Test) + } + 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) - } + handleAPICall("test", `{"Test":"myString","TestInt":42}`, ActorNode, nil) } func TestNoHandlerCovers(t *testing.T) { cleanUp() - ret := handleAPICall("test", "", ActorNode) + ret := handleAPICall("test", "", ActorNode, nil) if ret != nil { t.Error("Expect nil return within unhandled api action") diff --git a/apiGo/api/Helpers.go b/apiGo/api/Helpers.go index 1437a68..aac7b93 100644 --- a/apiGo/api/Helpers.go +++ b/apiGo/api/Helpers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "openmediacenter/apiGo/api/types" + "reflect" ) // MovieId - MovieName : pay attention to the order! @@ -61,6 +62,22 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor { return result } +// ID - Name : pay attention to the order! +func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow { + result := []types.TVShow{} + for rows.Next() { + var vid types.TVShow + err := rows.Scan(&vid.Id, &vid.Name) + if err != nil { + panic(err.Error()) // proper error handling instead of panic in your app + } + result = append(result, vid) + } + rows.Close() + + return result +} + func jsonify(v interface{}) []byte { // jsonify results str, err := json.Marshal(v) @@ -69,3 +86,45 @@ func jsonify(v interface{}) []byte { } return str } + +// setField set a specific field of an object with an object provided +func setField(obj interface{}, name string, value interface{}) error { + structValue := reflect.ValueOf(obj).Elem() + structFieldValue := structValue.FieldByName(name) + + if !structFieldValue.IsValid() { + return fmt.Errorf("no such field: %s in obj", name) + } + + if !structFieldValue.CanSet() { + return fmt.Errorf("cannot set %s field value", name) + } + + structFieldType := structFieldValue.Type() + val := reflect.ValueOf(value) + + if structFieldType != val.Type() { + if val.Type().ConvertibleTo(structFieldType) { + // if type is convertible - convert and set + structFieldValue.Set(val.Convert(structFieldType)) + } else { + return fmt.Errorf("provided value %s type didn't match obj field type and isn't convertible", name) + } + } else { + // set value if type is the same + structFieldValue.Set(val) + } + + return nil +} + +// FillStruct fill a custom struct with objects of a map +func FillStruct(i interface{}, m map[string]interface{}) error { + for k, v := range m { + err := setField(i, k, v) + if err != nil { + return err + } + } + return nil +} diff --git a/apiGo/api/Settings.go b/apiGo/api/Settings.go index 9d8d81a..711d4de 100644 --- a/apiGo/api/Settings.go +++ b/apiGo/api/Settings.go @@ -5,7 +5,10 @@ import ( "fmt" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/database" + "openmediacenter/apiGo/database/settings" "openmediacenter/apiGo/videoparser" + "regexp" + "strings" ) func AddSettingsHandlers() { @@ -15,52 +18,106 @@ func AddSettingsHandlers() { } func getSettingsFromDB() { - AddHandler("loadInitialData", SettingsNode, nil, func() []byte { - query := "SELECT DarkMode, password, mediacenter_name, video_path from settings" + /** + * @api {post} /api/settings [loadGeneralSettings] + * @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 + */ + AddHandler("loadGeneralSettings", SettingsNode, func(info *HandlerInfo) []byte { + result, _, sizes := database.GetSettings() - type InitialDataType struct { - DarkMode int - Pasword int - Mediacenter_name string - VideoPath string + var ret = struct { + Settings *types.SettingsType + Sizes *types.SettingsSizeType + }{ + Settings: &result, + Sizes: &sizes, } + return jsonify(ret) + }) - result := InitialDataType{} - - err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath) - if err != nil { - fmt.Println("error while parsing db data: " + err.Error()) - } + /** + * @api {post} /api/settings [loadInitialData] + * @apiDescription load startdata to display on homepage + * @apiName loadInitialData + * @apiGroup Settings + * + * @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 + */ + AddHandler("loadInitialData", SettingsNode, func(info *HandlerInfo) []byte { + sett := settings.LoadSettings() type InitialDataTypeResponse struct { - DarkMode bool - Pasword bool - Mediacenter_name string - VideoPath string + DarkMode bool + Pasword bool + MediacenterName string + VideoPath string + TVShowPath string + TVShowEnabled 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{ - DarkMode: result.DarkMode != 0, - Pasword: result.Pasword != -1, - Mediacenter_name: result.Mediacenter_name, - VideoPath: result.VideoPath, + DarkMode: sett.DarkMode, + Pasword: sett.Pasword != "-1", + MediacenterName: sett.MediacenterName, + VideoPath: serverVideoPath, + TVShowPath: serverTVShowPath, + TVShowEnabled: settings.TVShowsEnabled(), } str, _ := json.Marshal(res) return str }) - - AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte { - result := database.GetSettings() - return jsonify(result) - }) } func saveSettingsToDB() { - var sgs struct { - Settings types.SettingsType - } - AddHandler("saveGeneralSettings", SettingsNode, &sgs, func() []byte { + /** + * @api {post} /api/settings [saveGeneralSettings] + * @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 + */ + AddHandler("saveGeneralSettings", SettingsNode, func(info *HandlerInfo) []byte { + var args types.SettingsType + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := ` UPDATE settings SET video_path=?, @@ -71,24 +128,47 @@ func saveSettingsToDB() { DarkMode=? WHERE 1` return database.SuccessQuery(query, - sgs.Settings.VideoPath, sgs.Settings.EpisodePath, sgs.Settings.Password, - sgs.Settings.MediacenterName, sgs.Settings.TMDBGrabbing, sgs.Settings.DarkMode) + args.VideoPath, args.EpisodePath, args.Password, + args.MediacenterName, args.TMDBGrabbing, args.DarkMode) }) } // methods for handling reindexing and cleanup of db gravity 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 + */ + AddHandler("startReindex", SettingsNode, func(info *HandlerInfo) []byte { videoparser.StartReindex() 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 + */ + AddHandler("startTVShowReindex", SettingsNode, func(info *HandlerInfo) []byte { + videoparser.StartTVShowReindex() + return database.ManualSuccessResponse(nil) + }) + + /** + * @api {post} /api/settings [cleanupGravity] + * @apiDescription Start Database cleanup job + * @apiName cleanupGravity + * @apiGroup Settings + */ + AddHandler("cleanupGravity", SettingsNode, func(info *HandlerInfo) []byte { videoparser.StartCleanup() return nil }) - - AddHandler("getStatusMessage", SettingsNode, nil, func() []byte { - return jsonify(videoparser.GetStatusMessage()) - }) } diff --git a/apiGo/api/TVShows.go b/apiGo/api/TVShows.go new file mode 100644 index 0000000..7969392 --- /dev/null +++ b/apiGo/api/TVShows.go @@ -0,0 +1,161 @@ +package api + +import ( + "fmt" + "openmediacenter/apiGo/database" + "openmediacenter/apiGo/database/settings" +) + +func AddTvshowHandlers() { + // do not add handlers if tvshows not enabled + if !settings.TVShowsEnabled() { + 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 + */ + AddHandler("getTVShows", TVShowNode, func(info *HandlerInfo) []byte { + query := "SELECT id, name FROM tvshow" + rows := database.Query(query) + return jsonify(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 + */ + AddHandler("getEpisodes", TVShowNode, func(info *HandlerInfo) []byte { + var args struct { + ShowID uint32 + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + 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) + } + + return jsonify(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 + */ + AddHandler("loadEpisode", TVShowNode, func(info *HandlerInfo) []byte { + var args struct { + ID uint32 + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + 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()) + return nil + } + + ret.Path = foldername + "/" + filename + + return jsonify(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 + */ + AddHandler("readThumbnail", TVShowNode, func(info *HandlerInfo) []byte { + var args struct { + Id int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + 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 nil + } + + return pic + }) +} diff --git a/apiGo/api/Tags.go b/apiGo/api/Tags.go index 6845926..13a6a54 100644 --- a/apiGo/api/Tags.go +++ b/apiGo/api/Tags.go @@ -13,14 +13,30 @@ func AddTagHandlers() { } func deleteFromDB() { - var dT struct { - TagId int - Force bool - } - AddHandler("deleteTag", TagNode, &dT, func() []byte { + /** + * @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 + */ + AddHandler("deleteTag", TagNode, func(info *HandlerInfo) []byte { + var args struct { + TagId int + Force bool + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + // delete key constraints first - if dT.Force { - query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", dT.TagId) + if args.Force { + query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", args.TagId) err := database.Edit(query) // respond only if result not successful @@ -29,7 +45,7 @@ func deleteFromDB() { } } - query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", dT.TagId) + query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", args.TagId) err := database.Edit(query) if err == nil { @@ -37,9 +53,9 @@ func deleteFromDB() { return database.ManualSuccessResponse(err) } else { // check with regex if its the key constraint error - r, _ := regexp.Compile("^.*a foreign key constraint fails.*$") + r := regexp.MustCompile("^.*a foreign key constraint fails.*$") if r.MatchString(err.Error()) { - return []byte(`{"result":"not empty tag"}`) + return database.ManualSuccessResponse(fmt.Errorf("not empty tag")) } else { return database.ManualSuccessResponse(err) } @@ -48,27 +64,68 @@ func deleteFromDB() { } func getFromDB() { - AddHandler("getAllTags", TagNode, nil, func() []byte { + /** + * @api {post} /api/tags [getAllTags] + * @apiDescription get all available Tags + * @apiName getAllTags + * @apiGroup Tags + * + * @apiSuccess {Object[]} array of tag objects + * @apiSuccess {uint32} TagId + * @apiSuccess {string} TagName name of the Tag + */ + AddHandler("getAllTags", TagNode, func(info *HandlerInfo) []byte { query := "SELECT tag_id,tag_name from tags" return jsonify(readTagsFromResultset(database.Query(query))) }) } func addToDB() { - var ct struct { - TagName string - } - AddHandler("createTag", TagNode, &ct, func() []byte { + /** + * @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 + */ + AddHandler("createTag", TagNode, func(info *HandlerInfo) []byte { + var args struct { + TagName string + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)" - return database.SuccessQuery(query, ct.TagName) + return database.SuccessQuery(query, args.TagName) }) - var at struct { - MovieId int - TagId int - } - AddHandler("addTag", TagNode, &at, func() []byte { + /** + * @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 + */ + AddHandler("addTag", TagNode, func(info *HandlerInfo) []byte { + var args struct { + MovieId int + TagId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)" - return database.SuccessQuery(query, at.TagId, at.MovieId) + return database.SuccessQuery(query, args.TagId, args.MovieId) }) } diff --git a/apiGo/api/Video.go b/apiGo/api/Video.go index 110dcee..9e6c1d2 100644 --- a/apiGo/api/Video.go +++ b/apiGo/api/Video.go @@ -16,55 +16,163 @@ func AddVideoHandlers() { } func getVideoHandlers() { - var mrq struct { - Tag int - } - AddHandler("getMovies", VideoNode, &mrq, func() []byte { - var query string - // 1 is the id of the ALL tag - if mrq.Tag != 1 { - query = fmt.Sprintf(`SELECT movie_id,movie_name FROM videos - INNER JOIN video_tags vt on videos.movie_id = vt.video_id - INNER JOIN tags t on vt.tag_id = t.tag_id - WHERE t.tag_id = '%d' - ORDER BY likes DESC, create_date, movie_name`, mrq.Tag) - } else { - query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name" + /** + * @api {post} /api/video [getMovies] + * @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 + */ + AddHandler("getMovies", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + Tag uint32 + Sort uint8 + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil } - result := readVideosFromResultset(database.Query(query)) + 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 + // 1 is the id of the ALL tag + if args.Tag != 1 { + query = fmt.Sprintf(`SELECT movie_id,movie_name,t.tag_name FROM videos + INNER JOIN video_tags vt on videos.movie_id = vt.video_id + INNER JOIN tags t on vt.tag_id = t.tag_id + WHERE t.tag_id = %d %s`, args.Tag, SortClause) + } else { + query = fmt.Sprintf("SELECT movie_id,movie_name, (SELECT 'All' as tag_name) FROM videos %s", SortClause) + } + + var result struct { + Videos []types.VideoUnloadedType + TagName string + } + + rows := database.Query(query) + vids := []types.VideoUnloadedType{} + var name string + for rows.Next() { + var vid types.VideoUnloadedType + err := rows.Scan(&vid.MovieId, &vid.MovieName, &name) + if err != nil { + return nil + } + vids = append(vids, vid) + } + if rows.Close() != nil { + return nil + } + + // if the tag id doesn't exist the query won't return a name + if name == "" { + return nil + } + + result.Videos = vids + result.TagName = name // jsonify results str, _ := json.Marshal(result) return str }) - var rtn struct { - Movieid int - } - AddHandler("readThumbnail", VideoNode, &rtn, func() []byte { + /** + * @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 + */ + AddHandler("readThumbnail", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + Movieid int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + var pic []byte - query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id='%d'", rtn.Movieid) + query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id=%d", args.Movieid) err := database.QueryRow(query).Scan(&pic) if err != nil { - fmt.Printf("the thumbnail of movie id %d couldn't be found", rtn.Movieid) + fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Movieid) return nil } return pic }) - var grm struct { - Number int - } - AddHandler("getRandomMovies", VideoNode, &grm, func() []byte { + /** + * @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 + */ + AddHandler("getRandomMovies", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + Number int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + var result struct { Tags []types.Tag Videos []types.VideoUnloadedType } - query := fmt.Sprintf("SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT %d", grm.Number) + query := fmt.Sprintf("SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT %d", args.Number) result.Videos = readVideosFromResultset(database.Query(query)) var ids string @@ -99,13 +207,30 @@ func getVideoHandlers() { return str }) - var gsk struct { - KeyWord string - } - AddHandler("getSearchKeyWord", VideoNode, &gsk, func() []byte { + /** + * @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 + */ + AddHandler("getSearchKeyWord", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + KeyWord string + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := fmt.Sprintf(`SELECT movie_id,movie_name FROM videos WHERE movie_name LIKE '%%%s%%' - ORDER BY likes DESC, create_date DESC, movie_name`, gsk.KeyWord) + ORDER BY likes DESC, create_date DESC, movie_name`, args.KeyWord) result := readVideosFromResultset(database.Query(query)) // jsonify results @@ -116,12 +241,47 @@ func getVideoHandlers() { // function to handle stuff for loading specific videos and startdata func loadVideosHandlers() { - var lv struct { - MovieId int - } - AddHandler("loadVideo", VideoNode, &lv, func() []byte { + /** + * @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 + */ + AddHandler("loadVideo", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + MovieId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length - FROM videos WHERE movie_id=%d`, lv.MovieId) + FROM videos WHERE movie_id=%d`, args.MovieId) var res types.FullVideoType var poster []byte @@ -129,7 +289,7 @@ func loadVideosHandlers() { err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length) if err != nil { - fmt.Printf("error getting full data list of videoid - %d", lv.MovieId) + fmt.Printf("error getting full data list of videoid - %d", args.MovieId) fmt.Println(err.Error()) return nil } @@ -149,7 +309,7 @@ func loadVideosHandlers() { 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 WHERE video_tags.video_id=%d - GROUP BY t.tag_id`, lv.MovieId) + GROUP BY t.tag_id`, args.MovieId) res.Tags = readTagsFromResultset(database.Query(query)) @@ -158,14 +318,14 @@ func loadVideosHandlers() { SELECT video_tags.tag_id FROM video_tags WHERE video_id=%d) ORDER BY rand() - LIMIT 5`, lv.MovieId) + LIMIT 5`, args.MovieId) res.SuggestedTag = readTagsFromResultset(database.Query(query)) // query the actors corresponding to video query = fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos JOIN actors a on actors_videos.actor_id = a.actor_id - WHERE actors_videos.video_id=%d`, lv.MovieId) + WHERE actors_videos.video_id=%d`, args.MovieId) res.Actors = readActorsFromResultset(database.Query(query)) @@ -174,7 +334,20 @@ func loadVideosHandlers() { 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 + */ + AddHandler("getStartData", VideoNode, func(info *HandlerInfo) []byte { var result types.StartData // query settings and infotile values query := ` @@ -215,19 +388,62 @@ func loadVideosHandlers() { } func addToVideoHandlers() { - var al struct { - MovieId int - } - AddHandler("addLike", VideoNode, &al, func() []byte { - query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", al.MovieId) + /** + * @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 + */ + AddHandler("addLike", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + MovieId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", args.MovieId) return database.SuccessQuery(query) }) - var dv struct { - MovieId int - } - AddHandler("deleteVideo", VideoNode, &dv, func() []byte { - query := fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId) + /** + * @api {post} /api/video [deleteVideo] + * @apiDescription Delete a specific video from database + * @apiName deleteVideo + * @apiGroup video + * + * @apiParam {int} MovieId ID of video + * + * @apiSuccess {string} result 'success' if successfully or error message if not + */ + AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte { + var args struct { + MovieId int + } + if err := FillStruct(&args, info.Data); err != nil { + fmt.Println(err.Error()) + return nil + } + + // 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 { + return database.ManualSuccessResponse(err) + } + + query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId) return database.SuccessQuery(query) }) } diff --git a/apiGo/api/oauth/CustomClientStore.go b/apiGo/api/oauth/CustomClientStore.go new file mode 100644 index 0000000..414dfb6 --- /dev/null +++ b/apiGo/api/oauth/CustomClientStore.go @@ -0,0 +1,57 @@ +package oauth + +import ( + "gopkg.in/oauth2.v3" + "openmediacenter/apiGo/database/settings" +) + +type CustomClientStore struct { + oauth2.ClientStore +} + +type CustomClientInfo struct { + oauth2.ClientInfo + ID string + Secret string + Domain string + UserID string +} + +func NewCustomStore() oauth2.ClientStore { + s := new(CustomClientStore) + return s +} + +func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) { + password := settings.GetPassword() + // if password not set assign default password + if password == nil { + defaultpassword := "openmediacenter" + password = &defaultpassword + } + + clientinfo := CustomClientInfo{ + ID: "openmediacenter", + Secret: *password, + Domain: "http://localhost:8081", + UserID: "openmediacenter", + } + + return &clientinfo, nil +} + +func (a *CustomClientInfo) GetID() string { + return a.ID +} + +func (a *CustomClientInfo) GetSecret() string { + return a.Secret +} + +func (a *CustomClientInfo) GetDomain() string { + return a.Domain +} + +func (a *CustomClientInfo) GetUserID() string { + return a.UserID +} diff --git a/apiGo/api/oauth/Oauth.go b/apiGo/api/oauth/Oauth.go new file mode 100644 index 0000000..024b962 --- /dev/null +++ b/apiGo/api/oauth/Oauth.go @@ -0,0 +1,62 @@ +package oauth + +import ( + "gopkg.in/oauth2.v3" + "gopkg.in/oauth2.v3/errors" + "gopkg.in/oauth2.v3/manage" + "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()) + + // create new secretstore + clientStore := NewCustomStore() + 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 func(rw http.ResponseWriter, req *http.Request, node int, tokenInfo *oauth2.TokenInfo), node int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tokeninfo, err := srv.ValidationBearerToken(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + f(w, r, node, &tokeninfo) + } +} diff --git a/apiGo/api/types/Types.go b/apiGo/api/types/Types.go index 7e6d6c3..bb85f3b 100644 --- a/apiGo/api/types/Types.go +++ b/apiGo/api/types/Types.go @@ -7,12 +7,12 @@ type VideoUnloadedType struct { type FullVideoType struct { MovieName string - MovieId int + MovieId uint32 MovieUrl string Poster string - Likes int - Quality int - Length int + Likes uint64 + Quality uint16 + Length uint16 Tags []Tag SuggestedTag []Tag Actors []Actor @@ -20,22 +20,22 @@ type FullVideoType struct { type Tag struct { TagName string - TagId int + TagId uint32 } type Actor struct { - ActorId int + ActorId uint32 Name string Thumbnail string } type StartData struct { - VideoNr int - FullHdNr int - HDNr int - SDNr int - DifferentTags int - Tagged int + VideoNr uint32 + FullHdNr uint32 + HDNr uint32 + SDNr uint32 + DifferentTags uint32 + Tagged uint32 } type SettingsType struct { @@ -46,11 +46,16 @@ type SettingsType struct { PasswordEnabled bool TMDBGrabbing bool DarkMode bool - - VideoNr int - DBSize float32 - DifferentTags int - TagsAdded int - - PathPrefix string +} + +type SettingsSizeType struct { + VideoNr uint32 + DBSize float32 + DifferentTags uint32 + TagsAdded uint32 +} + +type TVShow struct { + Id uint32 + Name string } diff --git a/apiGo/database/Database.go b/apiGo/database/Database.go index 58fc930..5eb7f3f 100644 --- a/apiGo/database/Database.go +++ b/apiGo/database/Database.go @@ -90,9 +90,7 @@ func Close() { db.Close() } -func GetSettings() types.SettingsType { - var result types.SettingsType - +func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.SettingsSizeType) { // query settings and infotile values query := fmt.Sprintf(` SELECT ( @@ -120,7 +118,7 @@ func GetSettings() types.SettingsType { var DarkMode int var TMDBGrabbing int - err := QueryRow(query).Scan(&result.VideoNr, &result.DBSize, &result.DifferentTags, &result.TagsAdded, + err := QueryRow(query).Scan(&sizes.VideoNr, &sizes.DBSize, &sizes.DifferentTags, &sizes.TagsAdded, &result.VideoPath, &result.EpisodePath, &result.Password, &result.MediacenterName, &TMDBGrabbing, &DarkMode) if err != nil { @@ -130,7 +128,6 @@ func GetSettings() types.SettingsType { result.TMDBGrabbing = TMDBGrabbing != 0 result.PasswordEnabled = result.Password != "-1" result.DarkMode = DarkMode != 0 - result.PathPrefix = SettingsVideoPrefix - - return result + PathPrefix = SettingsVideoPrefix + return } diff --git a/apiGo/database/settings/DBSettings.go b/apiGo/database/settings/DBSettings.go new file mode 100644 index 0000000..75cbf51 --- /dev/null +++ b/apiGo/database/settings/DBSettings.go @@ -0,0 +1,38 @@ +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 +} diff --git a/apiGo/database/settings/Settings.go b/apiGo/database/settings/Settings.go new file mode 100644 index 0000000..409a071 --- /dev/null +++ b/apiGo/database/settings/Settings.go @@ -0,0 +1,11 @@ +package settings + +var tvShowEnabled bool + +func TVShowsEnabled() bool { + return tvShowEnabled +} + +func SetTVShowEnabled(enabled bool) { + tvShowEnabled = enabled +} diff --git a/apiGo/go.mod b/apiGo/go.mod index 3826c84..3615d28 100644 --- a/apiGo/go.mod +++ b/apiGo/go.mod @@ -2,4 +2,8 @@ module openmediacenter/apiGo go 1.16 -require github.com/go-sql-driver/mysql v1.5.0 +require ( + github.com/go-sql-driver/mysql v1.5.0 + gopkg.in/oauth2.v3 v3.12.0 + nhooyr.io/websocket v1.8.7 +) diff --git a/apiGo/go.sum b/apiGo/go.sum index d314899..1671ea2 100644 --- a/apiGo/go.sum +++ b/apiGo/go.sum @@ -1,2 +1,114 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao= +github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= +github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= +github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +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/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/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/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +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/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/apiGo/main.go b/apiGo/main.go index 7db3c84..80e138c 100644 --- a/apiGo/main.go +++ b/apiGo/main.go @@ -3,12 +3,18 @@ package main import ( "flag" "fmt" + "log" + "net/http" "openmediacenter/apiGo/api" "openmediacenter/apiGo/database" + settings2 "openmediacenter/apiGo/database/settings" + "openmediacenter/apiGo/static" + "openmediacenter/apiGo/videoparser" ) func main() { fmt.Println("init OpenMediaCenter server") + port := 8081 db, verbose, pathPrefix := handleCommandLineArguments() // todo some verbosity logger or sth @@ -26,8 +32,17 @@ func main() { api.AddSettingsHandlers() api.AddTagHandlers() api.AddActorsHandlers() + api.AddTvshowHandlers() - api.ServerInit(8081) + videoparser.SetupSettingsWebsocket() + + // add the static files + static.ServeStaticFiles() + + api.ServerInit() + + fmt.Printf("OpenMediacenter server up and running on port %d\n", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) } func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { @@ -41,8 +56,12 @@ func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex") + disableTVShowSupport := flag.Bool("DisableTVSupport", false, "Disable the TVShow support and pages") + flag.Parse() + settings2.SetTVShowEnabled(!*disableTVShowSupport) + return &database.DatabaseConfig{ DBHost: *dbhostPtr, DBPort: *dbPortPtr, diff --git a/apiGo/static/StaticServe.go b/apiGo/static/StaticServe.go new file mode 100644 index 0000000..e694302 --- /dev/null +++ b/apiGo/static/StaticServe.go @@ -0,0 +1,89 @@ +// +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) + } + }) +} diff --git a/apiGo/static/StaticServe_apionly.go b/apiGo/static/StaticServe_apionly.go new file mode 100644 index 0000000..5fcd293 --- /dev/null +++ b/apiGo/static/StaticServe_apionly.go @@ -0,0 +1,6 @@ +// +build !static + +package static + +// add nothing on no static build +func ServeStaticFiles() {} diff --git a/apiGo/videoparser/Helpers.go b/apiGo/videoparser/Helpers.go new file mode 100644 index 0000000..3c82b45 --- /dev/null +++ b/apiGo/videoparser/Helpers.go @@ -0,0 +1,120 @@ +package videoparser + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os/exec" + "strconv" +) + +func AppendMessage(message string) { + msger := TextMessage{ + MessageBase: MessageBase{Action: "message"}, + Message: message, + } + marshal, err := json.Marshal(msger) + if err != nil { + return + } + + IndexSender.Publish(marshal) +} + +func SendEvent(message string) { + msger := ReindexEvent{ + MessageBase: MessageBase{Action: "reindexAction"}, + Event: message, + } + marshal, err := json.Marshal(msger) + if err != nil { + return + } + + IndexSender.Publish(marshal) +} + +// ext dependency support check +func checkExtDependencySupport() *ExtDependencySupport { + var extDepsAvailable ExtDependencySupport + + extDepsAvailable.FFMpeg = commandExists("ffmpeg") + extDepsAvailable.MediaInfo = commandExists("mediainfo") + + return &extDepsAvailable +} + +// check if a specific system command is available +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +// parse the thumbail picture from video file +func parseFFmpegPic(path string) (*string, error) { + app := "ffmpeg" + + cmd := exec.Command(app, + "-hide_banner", + "-loglevel", "panic", + "-ss", "00:04:00", + "-i", path, + "-vframes", "1", + "-q:v", "2", + "-f", "singlejpeg", + "pipe:1") + + stdout, err := cmd.Output() + + if err != nil { + fmt.Println(err.Error()) + fmt.Println(string(err.(*exec.ExitError).Stderr)) + return nil, err + } + + strEncPic := base64.StdEncoding.EncodeToString(stdout) + if strEncPic == "" { + return nil, nil + } + backpic64 := fmt.Sprintf("data:image/jpeg;base64,%s", strEncPic) + + return &backpic64, nil +} + +func getVideoAttributes(path string) *VideoAttributes { + app := "mediainfo" + + arg0 := path + arg1 := "--Output=JSON" + + cmd := exec.Command(app, arg1, "-f", arg0) + stdout, err := cmd.Output() + + var t struct { + Media struct { + Track []struct { + Duration string + FileSize string + Width string + } + } + } + err = json.Unmarshal(stdout, &t) + + if err != nil { + fmt.Println(err.Error()) + return nil + } + + duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32) + filesize, err := strconv.Atoi(t.Media.Track[0].FileSize) + width, err := strconv.Atoi(t.Media.Track[1].Width) + + ret := VideoAttributes{ + Duration: float32(duration), + FileSize: uint(filesize), + Width: uint(width), + } + + return &ret +} diff --git a/apiGo/videoparser/ReIndexTVShows.go b/apiGo/videoparser/ReIndexTVShows.go new file mode 100644 index 0000000..f496add --- /dev/null +++ b/apiGo/videoparser/ReIndexTVShows.go @@ -0,0 +1,138 @@ +package videoparser + +import ( + "fmt" + "openmediacenter/apiGo/database" + "openmediacenter/apiGo/videoparser/tmdb" + "regexp" + "strconv" + "strings" +) + +func startTVShowReindex(files []Show) { + allTVshows := getAllTVShows() + + for _, file := range files { + // insert new TVShow entry if not existing. + insertShowIfNotExisting(file, allTVshows) + AppendMessage("Processing show: " + file.Name) + + insertEpisodesIfNotExisting(file) + } + + AppendMessage("reindex finished successfully!") + SendEvent("stop") +} + +func insertEpisodesIfNotExisting(show Show) { + query := "SELECT tvshow_episodes.name, season, episode FROM tvshow_episodes JOIN tvshow t on t.id = tvshow_episodes.tvshow_id WHERE t.name=?" + rows := database.Query(query, show.Name) + + var dbepisodes []string + for rows.Next() { + var epname string + var season int + var episode int + err := rows.Scan(&epname, &season, &episode) + if err != nil { + fmt.Println(err.Error()) + } + + dbepisodes = append(dbepisodes, fmt.Sprintf("%s S%02dE%02d.mp4", epname, season, episode)) + } + + // get those episodes that are missing in db + diff := difference(show.files, dbepisodes) + + for _, s := range diff { + AppendMessage("Adding Episode: " + s) + insertEpisode(s, show.Name) + } +} + +func insertEpisode(path string, ShowName string) { + seasonRegex := regexp.MustCompile("S[0-9][0-9]") + episodeRegex := regexp.MustCompile("E[0-9][0-9]") + matchENDPattern := regexp.MustCompile(" S[0-9][0-9]E[0-9][0-9].+$") + + seasonStr := seasonRegex.FindString(path) + episodeStr := episodeRegex.FindString(path) + extString := matchENDPattern.FindString(path) + // handle invalid matches + if len(seasonStr) != 3 || len(episodeStr) != 3 || len(extString) < 8 { + fmt.Printf("Error inserting episode: %s -- %s/%s/%s\n", path, seasonStr, episodeStr, extString) + return + } + + name := strings.TrimSuffix(path, extString) + + season, err := strconv.ParseInt(seasonStr[1:], 10, 8) + episode, err := strconv.ParseInt(episodeStr[1:], 10, 8) + if err != nil { + fmt.Println(err.Error()) + } + + query := ` +INSERT INTO tvshow_episodes (name, season, poster, tvshow_id, episode, filename) +VALUES (?, ?, ?, (SELECT tvshow.id FROM tvshow WHERE tvshow.name=?), ?, ?)` + err = database.Edit(query, name, season, "", ShowName, episode, path) + if err != nil { + fmt.Println(err.Error()) + } +} + +// difference returns the elements in `a` that aren't in `b`. +func difference(a, b []string) []string { + mb := make(map[string]struct{}, len(b)) + for _, x := range b { + mb[x] = struct{}{} + } + var diff []string + for _, x := range a { + if _, found := mb[x]; !found { + diff = append(diff, x) + } + } + return diff +} + +func insertShowIfNotExisting(show Show, allShows *[]string) { + // if show already exists return + fmt.Println(*allShows) + for _, s := range *allShows { + if s == show.Name { + return + } + } + + // insert empty thubnail if tmdb fails + thubnail := "" + + // load tmdb infos + tmdbInfo := tmdb.SearchTVShow(show.Name) + if tmdbInfo != nil { + thubnail = tmdbInfo.Thumbnail + } + + // currently the foldernamme == name which mustn't necessarily be + query := "INSERT INTO tvshow (name, thumbnail, foldername) VALUES (?, ?, ?)" + err := database.Edit(query, show.Name, thubnail, show.Name) + if err != nil { + fmt.Println(err.Error()) + } +} + +func getAllTVShows() *[]string { + query := "SELECT name FROM tvshow" + rows := database.Query(query) + + var res []string + for rows.Next() { + var show string + rows.Scan(&show) + + res = append(res, show) + } + + return &res +} diff --git a/apiGo/videoparser/ReIndexTVShows_test.go b/apiGo/videoparser/ReIndexTVShows_test.go new file mode 100644 index 0000000..fc05648 --- /dev/null +++ b/apiGo/videoparser/ReIndexTVShows_test.go @@ -0,0 +1,13 @@ +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.") + } +} diff --git a/apiGo/videoparser/ReIndex.go b/apiGo/videoparser/ReIndexVideos.go similarity index 61% rename from apiGo/videoparser/ReIndex.go rename to apiGo/videoparser/ReIndexVideos.go index 26118a6..50f5626 100644 --- a/apiGo/videoparser/ReIndex.go +++ b/apiGo/videoparser/ReIndexVideos.go @@ -2,15 +2,13 @@ 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" + "strings" ) var mSettings types.SettingsType @@ -41,35 +39,71 @@ func ReIndexVideos(path []string, sett types.SettingsType) { fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) - for _, s := range path { + // 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) } - AppendMessageBuffer("reindex finished successfully!") - - contentAvailable = false + 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-", fileNameOrig) // match the file extension - r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`) + r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) fileName := r.ReplaceAllString(fileNameOrig, "") + // match the year and cut year from name 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!") - } + fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName) + addVideo(fileName, fileNameOrig, year) } // add a video to the database @@ -87,7 +121,7 @@ func addVideo(videoName string, fileName string, year int) { } if mExtDepsAvailable.FFMpeg { - ppic, err = parseFFmpegPic(fileName) + ppic, err = parseFFmpegPic(mSettings.VideoPath + fileName) if err != nil { fmt.Printf("FFmpeg error occured: %s\n", err.Error()) } else { @@ -96,7 +130,7 @@ func addVideo(videoName string, fileName string, year int) { } if mExtDepsAvailable.MediaInfo { - atr := getVideoAttributes(fileName) + atr := getVideoAttributes(mSettings.VideoPath + fileName) if atr != nil { vidAtr = atr } @@ -130,17 +164,18 @@ func addVideo(videoName string, fileName string, year int) { insertTMDBTags(tmdbData.GenreIds, insertId) } - AppendMessageBuffer(fmt.Sprintf("%s - added!", videoName)) + AppendMessage(fmt.Sprintf("%s - added!", videoName)) } func matchYear(fileName string) (int, string) { - r, _ := regexp.Compile(`\([0-9]{4}?\)`) + r := regexp.MustCompile(`\([0-9]{4}?\)`) years := r.FindAllString(fileName, -1) if len(years) == 0 { return -1, fileName } - - year, err := strconv.Atoi(years[len(years)-1]) + 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 @@ -150,91 +185,6 @@ func matchYear(fileName string) (int, string) { 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 diff --git a/apiGo/videoparser/VideoParser.go b/apiGo/videoparser/VideoParser.go index bfc5348..9175962 100644 --- a/apiGo/videoparser/VideoParser.go +++ b/apiGo/videoparser/VideoParser.go @@ -2,33 +2,32 @@ package videoparser import ( "fmt" + "io/ioutil" "openmediacenter/apiGo/database" "os" "path/filepath" "strings" ) -var messageBuffer []string -var contentAvailable = false - type StatusMessage struct { Messages []string ContentAvailable bool } func StartReindex() bool { - messageBuffer = []string{} - contentAvailable = true - fmt.Println("starting reindex..") + SendEvent("start") + AppendMessage("starting reindex..") - mSettings := database.GetSettings() + mSettings, PathPrefix, _ := database.GetSettings() // add the path prefix to videopath - mSettings.VideoPath = mSettings.PathPrefix + mSettings.VideoPath + mSettings.VideoPath = PathPrefix + mSettings.VideoPath // check if path even exists if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) { fmt.Println("Reindex path doesn't exist!") + AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.VideoPath)) + SendEvent("stop") return false } @@ -49,20 +48,77 @@ func StartReindex() bool { fmt.Println(err.Error()) } // start reindex process - AppendMessageBuffer("Starting Reindexing!") + AppendMessage("Starting Reindexing!") go ReIndexVideos(files, mSettings) return true } -func GetStatusMessage() *StatusMessage { - msg := StatusMessage{ - Messages: messageBuffer, - ContentAvailable: contentAvailable, +type Show struct { + Name string + files []string +} + +// 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 += "/" } - messageBuffer = []string{} + // check if path even exists + 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 + } - return &msg + var files []Show + + filess, err := ioutil.ReadDir(mSettings.EpisodePath) + if err != nil { + fmt.Println(err.Error()) + } + + for _, file := range filess { + if file.IsDir() { + elem := Show{ + Name: file.Name(), + files: nil, + } + + fmt.Println(file.Name()) + + episodefiles, err := ioutil.ReadDir(mSettings.EpisodePath + file.Name()) + if err != nil { + fmt.Println(err.Error()) + } + + for _, epfile := range episodefiles { + if strings.HasSuffix(epfile.Name(), ".mp4") { + elem.files = append(elem.files, epfile.Name()) + } + } + files = append(files, elem) + } + } + + if err != nil { + fmt.Println(err.Error()) + } + + // start reindex process + AppendMessage("Starting Reindexing!") + go startTVShowReindex(files) } func StartCleanup() { diff --git a/apiGo/videoparser/WebSocketConnector.go b/apiGo/videoparser/WebSocketConnector.go new file mode 100644 index 0000000..7aaac6f --- /dev/null +++ b/apiGo/videoparser/WebSocketConnector.go @@ -0,0 +1,135 @@ +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) +} diff --git a/apiGo/videoparser/tmdb/TMDBApi.go b/apiGo/videoparser/tmdb/TMDBApi.go index 5e9c28a..f5461ef 100644 --- a/apiGo/videoparser/tmdb/TMDBApi.go +++ b/apiGo/videoparser/tmdb/TMDBApi.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "regexp" ) @@ -20,21 +21,43 @@ type VideoTMDB struct { GenreIds []int } +type TVShowTMDB struct { + Thumbnail string + Overview string + GenreIds []int +} + type tmdbVidResult struct { - Poster_path string - Adult bool - Overview string - Release_date string - Genre_ids []int - Id int - Original_title string - Original_language string - Title string - Backdrop_path string - Popularity int - Vote_count int - Video bool - Vote_average int + PosterPath *string `json:"poster_path"` + Adult bool `json:"adult"` + Overview string `json:"overview"` + ReleaseDate string `json:"release_date"` + GenreIds []int `json:"genre_ids"` + Id int `json:"id"` + OriginalTitle string `json:"original_title"` + OriginalLanguage string `json:"original_language"` + Title string `json:"title"` + BackdropPath *string `json:"backdrop_path"` + Popularity int `json:"popularity"` + VoteCount int `json:"vote_count"` + Video bool `json:"video"` + VoteAverage int `json:"vote_average"` +} + +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 { @@ -43,8 +66,9 @@ type TMDBGenre struct { } func SearchVideo(MovieName string, year int) *VideoTMDB { - url := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, MovieName) - resp, err := http.Get(url) + fmt.Printf("Searching TMDB for: Moviename: %s, year:%d \n", MovieName, year) + queryURL := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, url.QueryEscape(MovieName)) + resp, err := http.Get(queryURL) if err != nil { fmt.Println(err.Error()) return nil @@ -63,11 +87,16 @@ func SearchVideo(MovieName string, year int) *VideoTMDB { fmt.Println(len(t.Results)) + // if there was no match with tmdb return 0 + if len(t.Results) == 0 { + return nil + } + var tmdbVid tmdbVidResult if year != -1 { for _, result := range t.Results { r, _ := regexp.Compile(fmt.Sprintf(`^%d-[0-9]{2}?-[0-9]{2}?$`, year)) - if r.MatchString(result.Release_date) { + if r.MatchString(result.ReleaseDate) { tmdbVid = result // continue parsing goto cont @@ -82,22 +111,69 @@ func SearchVideo(MovieName string, year int) *VideoTMDB { // continue label cont: - thumbnail := fetchPoster(tmdbVid) + var thumbnail = "" + if tmdbVid.PosterPath != nil { + pic := fetchPoster(*tmdbVid.PosterPath) + if pic != nil { + thumbnail = *pic + } + } result := VideoTMDB{ - Thumbnail: *thumbnail, + Thumbnail: thumbnail, Overview: tmdbVid.Overview, Title: tmdbVid.Title, - GenreIds: tmdbVid.Genre_ids, + GenreIds: tmdbVid.GenreIds, } return &result } -func fetchPoster(vid tmdbVidResult) *string { - url := fmt.Sprintf("%s%s", pictureBase, vid.Poster_path) +func SearchTVShow(Name string) *TVShowTMDB { + fmt.Printf("Searching TMDB for: TVShow: %s\n", Name) + 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 + } - resp, err := http.Get(url) + body, err := ioutil.ReadAll(resp.Body) + 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 { fmt.Println(err.Error()) return nil @@ -116,8 +192,8 @@ func fetchPoster(vid tmdbVidResult) *string { var tmdbGenres *[]TMDBGenre func fetchGenres() *[]TMDBGenre { - url := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey) - resp, err := http.Get(url) + posterURL := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey) + resp, err := http.Get(posterURL) if err != nil { fmt.Println(err.Error()) return nil diff --git a/database.sql b/database.sql index ca4dd14..295fe42 100644 --- a/database.sql +++ b/database.sql @@ -9,12 +9,12 @@ create table if not exists actors create table if not exists settings ( - video_path varchar(255) null, - episode_path varchar(255) null, - password varchar(32) null, - mediacenter_name varchar(32) null, - TMDB_grabbing tinyint null, - DarkMode tinyint default 0 null + 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 ); create table if not exists tags @@ -24,18 +24,41 @@ create table if not exists tags 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 ( movie_id int auto_increment primary key, - movie_name varchar(200) null, - movie_url varchar(250) null, - thumbnail mediumblob null, - likes int default 0 null, - create_date datetime default CURRENT_TIMESTAMP null, - quality int null, - length int null comment 'in seconds', - poster mediumblob null + movie_name varchar(200) null, + movie_url varchar(250) null, + thumbnail mediumblob null, + poster mediumblob null, + likes int default 0 null, + quality int null, + length int null comment 'in seconds', + create_date datetime default current_timestamp() null ); create table if not exists actors_videos @@ -48,10 +71,10 @@ create table if not exists actors_videos foreign key (video_id) references videos (movie_id) ); -create index actors_videos_actor_id_index +create index if not exists actors_videos_actor_id_index on actors_videos (actor_id); -create index actors_videos_video_id_index +create index if not exists actors_videos_video_id_index on actors_videos (video_id); create table if not exists video_tags diff --git a/deb/OpenMediaCenter/DEBIAN/postinst b/deb/OpenMediaCenter/DEBIAN/postinst index ac61ee2..697899b 100755 --- a/deb/OpenMediaCenter/DEBIAN/postinst +++ b/deb/OpenMediaCenter/DEBIAN/postinst @@ -18,4 +18,4 @@ chown -R www-data:www-data /var/www/openmediacenter systemctl restart nginx systemctl enable OpenMediaCenter.service -systemctl start OpenMediaCenter.service +systemctl restart OpenMediaCenter.service diff --git a/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf b/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf index 0600958..cbdb815 100755 --- a/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf +++ b/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf @@ -13,7 +13,14 @@ server { try_files $uri /index.html; } - location /api/ { + location ~* ^/(api/|token) { 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; + } } diff --git a/package.json b/package.json index 8dbc062..6bc2499 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "openmediacenter", - "version": "0.1.2", + "version": "0.1.3", "private": true, - "main": "public/electron.js", "author": { "email": "lukas.heiligenbrunner@gmail.com", "name": "Lukas Heiligenbrunner", @@ -13,19 +12,21 @@ "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.13", - "bootstrap": "^4.5.3", + "bootstrap": "^5.0.2", "plyr-react": "^3.0.7", "react": "^17.0.1", "react-bootstrap": "^1.4.0", "react-dom": "^17.0.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "typescript": "^4.1.3" + "typescript": "^4.3.5" }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build", - "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default" + "build": "CI=false react-scripts build", + "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default", + "lint": "eslint --format gitlab src/", + "apidoc": "apidoc -i apiGo/ -o doc/" }, "jest": { "collectCoverageFrom": [ @@ -39,23 +40,6 @@ }, "proxy": "http://127.0.0.1:8081", "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": { "production": [ ">0.2%", @@ -69,18 +53,37 @@ ] }, "devDependencies": { - "@testing-library/jest-dom": "^5.11.6", - "@testing-library/react": "^11.2.2", - "@testing-library/user-event": "^12.6.0", - "@types/jest": "^26.0.19", - "@types/node": "^12.19.9", - "@types/react": "^16.14.2", - "@types/react-dom": "^16.9.10", - "@types/react-router": "5.1.8", - "@types/react-router-dom": "^5.1.6", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.0.0", + "@testing-library/user-event": "^13.2.1", + "@types/jest": "^26.0.24", + "@types/node": "^16.4.7", + "@types/react": "^17.0.15", + "@types/react-dom": "^17.0.9", + "@types/react-router": "5.1.16", + "@types/react-router-dom": "^5.1.8", + "@typescript-eslint/eslint-plugin": "^4.28.5", + "@typescript-eslint/parser": "^4.28.5", + "apidoc": "^0.28.1", "enzyme": "^3.11.0", "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", - "react-scripts": "4.0.1" + "prettier": "^2.3.2", + "prettier-config": "^1.0.0", + "react-scripts": "4.0.3" + }, + "apidoc": { + "name": "OpenMediaCenter", + "description": "API Documentation of OpenMediaCenter", + "title": "OpenMediaCenter Doc", + "sampleUrl": null } } diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index bcd5dfd..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html index 92fbfdd..880a2f1 100644 --- a/public/index.html +++ b/public/index.html @@ -2,14 +2,14 @@
- + - + 200 is bottom offset to trigger load + if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) { + this.loadPreviewBlock(8); + } + }; +} + +export default DynamicContentContainer; diff --git a/src/elements/FilterButton/FilterButton.test.js b/src/elements/FilterButton/FilterButton.test.js new file mode 100644 index 0000000..d0b64fa --- /dev/null +++ b/src/elements/FilterButton/FilterButton.test.js @@ -0,0 +1,64 @@ +import {shallow} from 'enzyme'; +import React from 'react'; +import FilterButton from './FilterButton'; +import RandomPage from "../../pages/RandomPage/RandomPage"; +import {callAPI} from "../../utils/Api"; + +describe('