10 Commits

Author SHA1 Message Date
c5920d060b first attempts to remember scroll position 2021-03-10 17:45:20 +01:00
be40475615 use correct regex for the reverse proxy 2021-03-09 19:31:56 +01:00
b625a4d958 Merge branch 'Oauth_auth' into 'master'
OAuth2

See merge request lukas/openmediacenter!37
2021-03-09 12:56:54 +00:00
c24c2ac2d8 add secure requests with tokens.
generate new token on every new page load
2021-03-09 12:56:53 +00:00
162b4efd0e Merge branch 'addtag_filterablesearch' into 'master'
Filterbutton on addtag pupup

See merge request lukas/openmediacenter!38
2021-03-08 14:11:26 +00:00
db0edf7a80 outsource filterbutton to new file
make addtag popup filterable
2021-03-08 14:11:26 +00:00
488354bc28 fix lukas/openmediacenter#64 2021-03-07 19:49:40 +01:00
f42f2d4915 update @type dependencies 2021-03-05 21:10:10 +01:00
8d97ab85a9 Merge branch 'golang_backend' into 'master'
Fully use GO for backend

See merge request lukas/openmediacenter!35
2021-02-23 16:01:30 +00:00
f2b5fb6587 implement full load of videos and startdata
modify api where necessary
2021-02-23 16:01:29 +00:00
75 changed files with 14627 additions and 21137 deletions

View File

@ -1,48 +1,72 @@
image: node:14
stages:
- prepare
- build
- test
- packaging
- deploy
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
- node_modules/
include:
- template: Code-Quality.gitlab-ci.yml
variables:
SAST_DISABLE_DIND: "true"
Node_dependencies:
stage: prepare
script:
- npm ci --cache .npm --prefer-offline
Minimize:
Minimize_Frontend:
stage: build
before_script:
- yarn install --cache-folder .yarn
script:
- npm run build
- yarn run build
artifacts:
expire_in: 7 days
expire_in: 2 days
paths:
- build/
needs: ["Node_dependencies"]
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .yarn/
- node_modules/
Build_Backend:
image: golang:latest
stage: build
script:
- cd apiGo
- go build -v -o openmediacenter
- env GOOS=windows GOARCH=amd64 go build -v -o openmediacenter.exe
artifacts:
expire_in: 2 days
paths:
- "./apiGo/openmediacenter*"
Frontend_Tests:
stage: test
before_script:
- yarn install --cache-folder .yarn
script:
- npm run test
- yarn run test
artifacts:
reports:
junit:
- ./junit.xml
needs: ["Node_dependencies"]
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .yarn/
- node_modules/
Backend_Tests:
image: golang:latest
stage: test
script:
- cd apiGo
- go get -u github.com/jstemmer/go-junit-report
- go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml
artifacts:
when: always
reports:
junit: ./apiGo/report.xml
code_quality:
tags:
@ -56,23 +80,28 @@ Debian_Server:
- cd deb
- mkdir -p "./OpenMediaCenter/var/www/openmediacenter/videos/"
- mkdir -p "./OpenMediaCenter/tmp/"
- mkdir -p "./OpenMediaCenter/usr/bin/"
- cp -r ../build/* ./OpenMediaCenter/var/www/openmediacenter/
- cp -r ../api ./OpenMediaCenter/var/www/openmediacenter/
- cp ../apiGo/openmediacenter ./OpenMediaCenter/usr/bin/
- cp ../database.sql ./OpenMediaCenter/tmp/openmediacenter.sql
- 'echo "Version: ${vers}" >> ./OpenMediaCenter/DEBIAN/control'
- chmod -R 0775 *
- dpkg-deb --build OpenMediaCenter
- mv OpenMediaCenter.deb OpenMediaCenter-${vers}_amd64.deb
artifacts:
expire_in: 7 days
paths:
- deb/OpenMediaCenter-*.deb
needs: ["Minimize"]
needs:
- Minimize_Frontend
- Build_Backend
Test_Server:
stage: deploy
image: luki42/alpineopenssh:latest
needs:
- Frontend_Tests
- Backend_Tests
- Debian_Server
only:
- master

View File

@ -8,7 +8,7 @@ Feel free to contribute or open an issue here: https://gitlab.heili.eu/lukas/ope
## What is this?
Open Media Center is an open source solution for a mediacenter in your home network.
Transform your webserver into a mediaserver.
It's based on Reactjs and PHP is used for backend.
It's based on Reactjs and golang is used for backend.
It is optimized for general videos as well as for movies.
For grabbing movie data TMDB is used.
With the help of tags you can organize your video gravity.

View File

@ -1,5 +0,0 @@
<?php
include_once __DIR__ . '/src/handlers/Actor.php';
$actor = new Actor();
$actor->handleAction();

View File

@ -1,18 +0,0 @@
<?php
require_once __DIR__ . '/src/Database.php';
require_once __DIR__ . '/src/TMDBMovie.php';
require_once __DIR__ . '/src/SSettings.php';
require_once __DIR__ . '/src/VideoParser.php';
// allow UTF8 characters
setlocale(LC_ALL, 'en_US.UTF-8');
set_time_limit(3600);
$vp = new VideoParser();
$vp->writeLog("starting extraction!!\n");
$sett = new SSettings();
// load video path from settings
$scandir = __DIR__ . "/../" . $sett->getVideoPath();
$vp->extractVideos($scandir);

View File

@ -1,5 +0,0 @@
<?php
require_once __DIR__ . '/src/handlers/Settings.php';
$sett = new Settings();
$sett->handleAction();

View File

@ -1,57 +0,0 @@
<?php
/**
* Class Database
*
* Class with all neccessary stuff for the Database connections.
*/
class Database {
private static $instance = null;
private $conn;
private $servername = "127.0.0.1";
private $username = "mediacenteruser";
private $password = "mediapassword";
private $dbname = "mediacenter";
// The db connection is established in the private constructor.
private function __construct() {
// Create connection
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
if ($this->conn->connect_errno) {
echo "connecton failed... nr: " . $this->conn->connect_errno . " -- " . $this->conn->connect_error;
}
}
/**
* get an instance of this database class
* (only possible way to retrieve an object)
*
* @return Database dbobject
*/
public static function getInstance() {
if (!self::$instance) {
self::$instance = new Database();
}
return self::$instance;
}
/**
* get a connection instance of the database
*
* @return mysqli mysqli instance
*/
public function getConnection() {
return $this->conn;
}
/**
* get name of current active database
* @return string name
*/
public function getDatabaseName() {
return $this->dbname;
}
}

View File

@ -1,45 +0,0 @@
<?php
/**
* Class SSettings
* class handling all Settings used by php scripts
*/
class SSettings {
private $database;
/**
* SSettings constructor.
*/
public function __construct() {
$this->database = Database::getInstance();
}
/**
* get the videopath saved in db
* @return string videopath
*/
public function getVideoPath() {
$query = "SELECT video_path from settings";
$result = $this->database->getConnection()->query($query);
$r = mysqli_fetch_assoc($result);
return $r['video_path'];
}
/**
* check if TMDB is enableds
* @return bool isenabled?
*/
public function isTMDBGrabbingEnabled(): bool {
$query = "SELECT TMDB_grabbing from settings WHERE 1";
$result = $this->database->getConnection()->query($query);
if (!$result) {
return true; // if undefined in db --> default true
} else {
$r = mysqli_fetch_assoc($result);
return $r['TMDB_grabbing'] == '1';
}
}
}

View File

@ -1,51 +0,0 @@
<?php
/**
* Class TMDBMovie
* class to handle all interactions with the tmdb api
*/
class TMDBMovie {
public $picturebase = "https://image.tmdb.org/t/p/w500";
private $apikey = "9fd90530b11447f5646f8e6fb4733fb4";
private $baseurl = "https://api.themoviedb.org/3/";
/**
* search for a specific movie
*
* @param string $moviename moviename
* @return object movie object or null if not found
*/
public function searchMovie(string $moviename, string $year = null) {
$reply = json_decode(file_get_contents($this->baseurl . "search/movie?api_key=" . $this->apikey . "&query=" . urlencode($moviename)));
if ($reply->total_results == 0) {
// no results found
return null;
} elseif ($year != null) {
// if year is defined check year
$regex = '/[0-9]{4}?/'; // matches year of string
for ($i = 0; $i < count($reply->results); $i++) {
$releasedate = $reply->results[$i]->release_date;
preg_match($regex, $releasedate, $matches);
if (count($matches) > 0) {
$curryear = $matches[0];
if ($curryear == $year)
return $reply->results[$i];
}
}
} else {
return $reply->results[0];
}
}
/**
* query all available genres from tmdb
*
* @return array of all available genres
*/
public function getAllGenres() {
$reply = json_decode(file_get_contents($this->baseurl . "genre/movie/list?api_key=" . $this->apikey));
return $reply->genres;
}
}

View File

@ -1,332 +0,0 @@
<?php
require_once 'Database.php';
require_once 'TMDBMovie.php';
require_once 'SSettings.php';
/**
* Class VideoParser
* handling the parsing of all videos of a folder and adding
* all videos with tags and thumbnails to the database
*/
class VideoParser {
/// ffmpeg installation binary
private string $ffmpeg = 'ffmpeg';
private TMDBMovie $tmdb;
/// initial load of all available movie genres
private array $tmdbgenres;
private string $videopath;
/// db connection instance
private mysqli $conn;
/// settings object instance
private SSettings $settings;
private bool $TMDBenabled;
/// videos added in this run
private int $added = 0;
/// all videos in this run
private int $all = 0;
/// failed videos in this run
private int $failed = 0;
/// deleted videos in this run
private int $deleted = 0;
/**
* VideoParser constructor.
*/
public function __construct() {
$this->tmdb = new TMDBMovie();
$this->tmdbgenres = $this->tmdb->getAllGenres();
$this->conn = Database::getInstance()->getConnection();
$this->settings = new SSettings();
$this->TMDBenabled = $this->settings->isTMDBGrabbingEnabled();
$this->videopath = $this->settings->getVideoPath();
}
/**
* searches a folder for mp4 videos and adds them to video gravity
* @param $foldername string the folder where to search (relative to the webserver root)
*/
public function extractVideos(string $foldername) {
echo("TMDB grabbing is " . ($this->TMDBenabled ? "" : "not") . " enabled \n");
$arr = scandir($foldername);
foreach ($arr as $elem) {
if ($elem == '.' || $elem == '..') continue;
$ext = pathinfo($elem, PATHINFO_EXTENSION);
if ($ext == "mp4") {
$this->processVideo($elem);
} else {
echo($elem . " does not contain a valid .mp4 extension! - skipping \n");
$this->writeLog($elem . " does not contain a valid .mp4 extension! - skipping \n");
}
}
// cleanup gravity
$this->cleanUpGravity();
// calculate size of databse here
$size = -1;
$query = "SELECT table_schema AS \"Database\",
ROUND(SUM(data_length + index_length) / 1024 / 1024, 3) AS \"Size\"
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='" . Database::getInstance()->getDatabaseName() . "'
GROUP BY table_schema;";
$result = $this->conn->query($query);
if ($result->num_rows == 1) {
$row = $result->fetch_assoc();
$size = $row["Size"];
}
echo "Total gravity: " . $this->all . "\n";
$this->writeLog("Total gravity: " . $this->all . "\n");
echo "Size of Databse is: " . $size . "MB\n";
$this->writeLog("Size of Databse is: " . $size . "MB\n");
echo "added in this run: " . $this->added . "\n";
$this->writeLog("added in this run: " . $this->added . "\n");
echo "deleted in this run: " . $this->deleted . "\n";
$this->writeLog("deleted in this run: " . $this->deleted . "\n");
echo "errored in this run: " . $this->failed . "\n";
$this->writeLog("errored in this run: " . $this->failed . "\n");
$this->writeLog("-42"); // terminating characters to stop webui requesting infos
}
/**
* processes one mp4 video, extracts tags and adds it to the database
* @param $filename string filename of the video to process
*/
private function processVideo(string $filename) {
$moviename = substr($filename, 0, -4);
$regex = '/\([0-9]{4}?\)/'; //match year pattern
preg_match($regex, $moviename, $matches);
preg_replace($regex, '', $moviename);
$year = null;
if (count($matches) > 0) {
$year = substr($matches[count($matches) - 1], 1, 4);
$moviename = substr($moviename, 0, -6);
}
$query = "SELECT * FROM videos WHERE movie_name = '" . mysqli_real_escape_string($this->conn, $moviename) . "'";
$result = $this->conn->query($query);
// insert if not available in db
if (!mysqli_fetch_assoc($result)) {
$genres = -1;
$insert_query = "";
// extract other video attributes
$video_attributes = $this->_get_video_attributes($filename);
$duration = 0;
$size = 0;
$width = 0;
if ($video_attributes) {
$duration = $video_attributes->media->track[0]->Duration; // in seconds
$size = $video_attributes->media->track[0]->FileSize; // in Bytes
$width = $video_attributes->media->track[1]->Width; // width
}
// extract poster from video
$backpic = shell_exec("$this->ffmpeg -hide_banner -loglevel panic -ss 00:04:00 -i \"../$this->videopath$filename\" -vframes 1 -q:v 2 -f singlejpeg pipe:1 2>/dev/null");
// convert video to base64
$backpic64 = 'data:image/jpeg;base64,' . base64_encode($backpic);
// set default insert query without tmdb poster
$insert_query = "INSERT INTO videos(movie_name,movie_url,thumbnail,quality,length)
VALUES ('" . mysqli_real_escape_string($this->conn, $moviename) . "',
'" . mysqli_real_escape_string($this->conn, $this->videopath . $filename) . "',
'$backpic64',
'$width',
'$duration')";
// check if tmdb grabbing is enabled
if ($this->TMDBenabled) {
// search in tmdb api
if (!is_null($dta = $this->tmdb->searchMovie($moviename, $year))) {
$poster = file_get_contents($this->tmdb->picturebase . $dta->poster_path);
// error handling for download error
if ($poster) {
$poster_base64 = 'data:image/jpeg;base64,' . base64_encode($poster);
// override insert query if pic loaded correctly
$insert_query = "INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length)
VALUES ('" . mysqli_real_escape_string($this->conn, $moviename) . "',
'" . mysqli_real_escape_string($this->conn, $this->videopath . $filename) . "',
'$backpic64',
'$poster_base64',
'$width',
'$duration')";
}
// store genre ids for parsing later
$genres = $dta->genre_ids;
} else {
// nothing found with tmdb
echo "my moviename: " . $moviename;
$this->writeLog("nothing found with TMDB! -- $moviename\n");
}
}
if ($this->conn->query($insert_query) === TRUE) {
echo('successfully added ' . $filename . " to video gravity\n");
$this->writeLog('successfully added ' . $filename . " to video gravity\n");
// add this entry to the default tags
$last_id = $this->conn->insert_id;
$this->insertSizeTag($width, $last_id);
// handle tmdb genres here!
if ($genres != -1) {
// transform genre ids in valid names
foreach ($genres as $genreid) {
// check if genre is already a tag in db if not insert it
$tagname = array_column($this->tmdbgenres, 'name', 'id')[$genreid];
$tagid = $this->tagExists($tagname);
$query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($last_id,$tagid)";
if ($this->conn->query($query) !== TRUE) {
echo "failed to add $genreid tag here.\n";
$this->writeLog("failed to add $genreid tag here.\n");
}
}
}
$this->added++;
$this->all++;
} else {
echo('errored item: ' . $filename . "\n");
$this->writeLog('errored item: ' . $filename . "\n");
echo('{"data":"' . $this->conn->error . '"}\n');
$this->writeLog('{"data":"' . $this->conn->error . '"}\n');
$this->failed++;
}
} else {
$this->all++;
}
}
/**
* get all videoinfos of a video file
*
* @param $video string name including extension
* @return object all infos as object
*/
private function _get_video_attributes(string $video) {
$command = "mediainfo \"../$this->videopath$video\" --Output=JSON";
$output = shell_exec($command);
return json_decode($output);
}
/**
* write a line to the output log file
*
* @param string $message message to write
*/
public function writeLog(string $message) {
file_put_contents("/tmp/output.log", $message, FILE_APPEND);
flush();
}
/**
* insert the corresponding videosize tag to a specific videoid
* @param $width int video width
* @param $videoid int id of video
*/
private function insertSizeTag(int $width, int $videoid) {
// full hd
if ($width >= 1900) {
$query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($videoid,2)";
if ($this->conn->query($query) !== TRUE) {
echo "failed to add default tag here.\n";
$this->writeLog("failed to add default tag here.\n");
}
}
// HD
if ($width >= 1250 && $width < 1900) {
$query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($videoid,4)";
if ($this->conn->query($query) !== TRUE) {
echo "failed to add default tag here.\n";
$this->writeLog("failed to add default tag here.\n");
}
}
// SD
if ($width < 1250 && $width > 0) {
$query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($videoid,3)";
if ($this->conn->query($query) !== TRUE) {
echo "failed to add default tag here.\n";
$this->writeLog("failed to add default tag here.\n");
}
}
}
/**
* ckecks if tag exists -- if not creates it
* @param string $tagname the name of the tag
* @return integer the id of the inserted tag
*/
private function tagExists(string $tagname) {
$query = "SELECT * FROM tags WHERE tag_name='$tagname'";
$result = $this->conn->query($query);
if ($result->num_rows == 0) {
// tag does not exist --> create it
$query = "INSERT INTO tags (tag_name) VALUES ('$tagname')";
if ($this->conn->query($query) !== TRUE) {
echo "failed to create $tagname tag in database\n";
$this->writeLog("failed to create $tagname tag in database\n");
}
return $this->conn->insert_id;
} else {
return $result->fetch_assoc()['tag_id'];
}
}
/**
* cleans up the video gravity and removes non existent videos
*/
public function cleanUpGravity() {
// auto cleanup db entries
$query = "SELECT COUNT(*) as count FROM videos";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
if ($this->all < $r['count']) {
echo "\n\nshould be in gravity: " . $this->all . "\n";
$this->writeLog("should be in gravity: " . $this->all . "\n");
echo "really in gravity: " . $r['count'] . "\n";
$this->writeLog("really in gravity: " . $r['count'] . "\n");
echo "cleaning up gravity\n";
$this->writeLog("cleaning up gravity\n");
$query = "SELECT movie_id,movie_url FROM videos";
$result = $this->conn->query($query);
while ($r = mysqli_fetch_assoc($result)) {
$movie_id = $r['movie_id'];
$url = $r['movie_url'];
// todo ORDER BY movie_url and erase duplicates also
if (!file_exists("../$url")) {
$query = "DELETE FROM videos WHERE movie_id=$movie_id";
if ($this->conn->query($query) === TRUE) {
echo("successfully deleted $url from video gravity\n");
$this->writeLog("successfully deleted $url from video gravity\n");
$this->deleted++;
} else {
echo "failed to delete $url from gravity: $this->conn->error \n";
$this->writeLog("failed to delete $url from gravity: $this->conn->error \n");
}
}
}
}
}
}

View File

@ -1,75 +0,0 @@
<?php
require_once __DIR__ . '/../SSettings.php';
require_once 'RequestBase.php';
class Actor extends RequestBase {
function initHandlers() {
$this->databaseAdds();
$this->databaseRequests();
}
function databaseAdds() {
$this->addActionHandler("createActor", function () {
// skip tag create if already existing
$actorname = $_POST["actorname"];
$query = "INSERT IGNORE INTO actors (name) VALUES ('$actorname')";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
$this->addActionHandler("addActorToVideo", function () {
// skip tag create if already existing
$actorid = $_POST["actorid"];
$videoid = $_POST["videoid"];
$query = "INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES ($actorid,$videoid)";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
}
function databaseRequests() {
$this->addActionHandler("getAllActors", function () {
// query the actors corresponding to video
$query = "SELECT * FROM actors";
$result = $this->conn->query($query);
$this->commitMessage(json_encode(mysqli_fetch_all($result, MYSQLI_ASSOC)));
});
$this->addActionHandler("getActorsOfVideo", function () {
// query the actors corresponding to video
$video_id = $_POST["videoid"];
$query = "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=$video_id";
$result = $this->conn->query($query);
$this->commitMessage(json_encode(mysqli_fetch_all($result, MYSQLI_ASSOC)));
});
$this->addActionHandler("getActorInfo", function (){
$actorid = $_POST["actorid"];
$query = "SELECT movie_id, movie_name FROM actors_videos
JOIN videos v on v.movie_id = actors_videos.video_id
WHERE actors_videos.actor_id=$actorid";
$result = $this->conn->query($query);
$actorinfo = $this->conn->query("SELECT name, thumbnail, actor_id FROM actors WHERE actor_id=$actorid");
$reply = array("videos" => mysqli_fetch_all($result, MYSQLI_ASSOC), "info" => mysqli_fetch_assoc($actorinfo));
$this->commitMessage(json_encode($reply));
});
}
}

View File

@ -1,50 +0,0 @@
<?php
require_once __DIR__ . '/../Database.php';
abstract class RequestBase {
protected $conn;
private $actions = array();
/**
* adds a new action handler to the current api file
*
* @param $action string name of the action variable
* @param $callback Closure callback function to be called
*/
function addActionHandler($action, $callback) {
$this->actions[$action] = $callback;
}
/**
* runs the correct handler
* should be called once within the api request
*/
function handleAction() {
$this->conn = Database::getInstance()->getConnection();
if (isset($_POST['action'])) {
$this->initHandlers();
$action = $_POST['action'];
// call the right handler
$this->actions[$action]();
} else {
$this->commitMessage('{"data": "error"}');
}
}
/**
* add the action handlers in this abstract method
*/
abstract function initHandlers();
/**
* Send response message and exit script
* @param $message string the response message
*/
function commitMessage($message) {
echo $message;
exit(0);
}
}

View File

@ -1,160 +0,0 @@
<?php
require_once 'RequestBase.php';
require_once __DIR__ . '/../VideoParser.php';
/**
* Class Settings
* Backend for the Settings page
*/
class Settings extends RequestBase {
function initHandlers() {
$this->getFromDB();
$this->saveToDB();
$this->reIndexHandling();
}
/**
* handle settings stuff to load from db
*/
private function getFromDB() {
/**
* load currently set settings form db for init of settings page
*/
$this->addActionHandler("loadGeneralSettings", function () {
// query settings and infotile values
$query = "
SELECT (
SELECT COUNT(*)
FROM videos
) AS videonr,
(
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS Size
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = '" . Database::getInstance()->getDatabaseName() . "'
GROUP BY table_schema
) AS dbsize,
(
SELECT COUNT(*)
FROM tags
) AS difftagnr,
(
SELECT COUNT(*)
FROM video_tags
) AS tagsadded,
settings.*
FROM settings
LIMIT 1
";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
// booleans need to be set manually
$r['passwordEnabled'] = $r['password'] != "-1";
$r['TMDB_grabbing'] = ($r['TMDB_grabbing'] != '0');
echo json_encode($r);
});
/**
* load initial data for home page load to check if pwd is set
*/
$this->addActionHandler("loadInitialData", function () {
$query = "SELECT * from settings";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$r['passwordEnabled'] = $r['password'] != "-1";
unset($r['password']);
$r['DarkMode'] = (bool)($r['DarkMode'] != '0');
$this->commitMessage(json_encode($r));
});
}
/**
* handle setting stuff to save to db
*/
private function saveToDB() {
/**
* save changed settings to db
*/
$this->addActionHandler("saveGeneralSettings", function () {
$mediacentername = $_POST['mediacentername'];
$password = $_POST['password'];
$videopath = $_POST['videopath'];
$tvshowpath = $_POST['tvshowpath'];
$tmdbsupport = $_POST['tmdbsupport'];
$darkmodeenabled = $_POST['darkmodeenabled'];
$query = "UPDATE settings SET
video_path='$videopath',
episode_path='$tvshowpath',
password='$password',
mediacenter_name='$mediacentername',
TMDB_grabbing=$tmdbsupport,
DarkMode=$darkmodeenabled
WHERE 1";
if ($this->conn->query($query) === true) {
$this->commitMessage('{"result": "success"}');
} else {
$this->commitMessage('{"result": "success"}');
}
});
}
/**
* methods for handling reindexing and cleanup of db gravity
*/
private function reIndexHandling() {
$this->addActionHandler("startReindex", function () {
$indexrunning = false;
if (file_exists("/tmp/output.log")) {
$out = file_get_contents("/tmp/output.log");
if (substr($out, -strlen("-42")) == "-42") {
unlink("/tmp/output.log");
} else {
$indexrunning = true;
}
}
if (!$indexrunning) {
// start extraction of video previews in background
$cmd = 'php extractvideopreviews.php';
exec(sprintf("%s > %s 2>&1 & echo $! >> %s", $cmd, '/dev/zero', '/tmp/openmediacenterpid'));
$this->commitMessage('{"result": "success"}');
} else {
$this->commitMessage('{"result": "success"}');
}
});
$this->addActionHandler("cleanupGravity", function () {
$vp = new VideoParser();
$vp->cleanUpGravity();
});
$this->addActionHandler("getStatusMessage", function () {
$return = new stdClass();
if (file_exists("/tmp/output.log")) {
$out = file_get_contents("/tmp/output.log");
// clear log file
file_put_contents("/tmp/output.log", "");
$return->message = $out;
$return->contentAvailable = true;
if (substr($out, -strlen("-42")) == "-42") {
unlink("/tmp/output.log");
}
} else {
$return->contentAvailable = false;
}
$this->commitMessage(json_encode($return));
});
}
}

View File

@ -1,101 +0,0 @@
<?php
require_once 'RequestBase.php';
/**
* Class Tags
* backend to handle Tag database interactions
*/
class Tags extends RequestBase {
function initHandlers() {
$this->addToDB();
$this->getFromDB();
$this->delete();
}
private function addToDB() {
/**
* creates a new tag
* query requirements:
* * tagname -- name of the new tag
*/
$this->addActionHandler("createTag", function () {
// skip tag create if already existing
$query = "INSERT IGNORE INTO tags (tag_name) VALUES ('" . $_POST['tagname'] . "')";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
/**
* adds a new tag to an existing video
*
* query requirements:
* * movieid -- the id of the video to add the tag to
* * id -- the tag id which tag to add
*/
$this->addActionHandler("addTag", function () {
$movieid = $_POST['movieid'];
$tagid = $_POST['id'];
// skip tag add if already assigned
$query = "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES ('$tagid','$movieid')";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
}
private function getFromDB() {
/**
* returns all available tags from database
*/
$this->addActionHandler("getAllTags", function () {
$query = "SELECT tag_name,tag_id from tags";
$result = $this->conn->query($query);
$rows = array();
while ($r = mysqli_fetch_assoc($result)) {
array_push($rows, $r);
}
$this->commitMessage(json_encode($rows));
});
}
private function delete() {
/**
* delete a Tag with specified id
*/
$this->addActionHandler("deleteTag", function () {
$tag_id = $_POST['tagId'];
$force = $_POST['force'];
// delete key constraints first
if ($force === "true") {
$query = "DELETE FROM video_tags WHERE tag_id=$tag_id";
if ($this->conn->query($query) !== TRUE) {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
}
$query = "DELETE FROM tags WHERE tag_id=$tag_id";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
// check if error is a constraint error
if (preg_match('/^.*a foreign key constraint fails.*$/i', $this->conn->error)) {
$this->commitMessage('{"result":"not empty tag"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->eror . '"}');
}
}
});
}
}

View File

@ -1,245 +0,0 @@
<?php
require_once __DIR__ . '/../SSettings.php';
require_once 'RequestBase.php';
/**
* Class Video
* backend for all interactions with videoloads and receiving of video infos
*/
class Video extends RequestBase {
private $videopath;
public function __construct() {
$settings = new SSettings();
// load video path from settings
$this->videopath = $settings->getVideoPath();
}
function initHandlers() {
$this->getVideos();
$this->loadVideos();
$this->addToVideo();
}
/**
* function handles load of all videos and search for videos
*/
private function getVideos() {
$this->addActionHandler("getMovies", function () {
$query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name";
if (isset($_POST['tag'])) {
$tag = $_POST['tag'];
// if not all tags allowed filter for specific one
if (strtolower($_POST['tag']) != "all") {
$query = "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 = '$tag'
ORDER BY likes DESC, create_date, movie_name";
}
}
$result = $this->conn->query($query);
$rows = array();
while ($r = mysqli_fetch_assoc($result)) {
array_push($rows, $r);
}
$this->commitMessage(json_encode($rows));
});
$this->addActionHandler("getRandomMovies", function () {
$return = new stdClass();
$query = "SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT " . $_POST['number'];
$result = $this->conn->query($query);
$return->rows = array();
// get tags of random videos
$ids = [];
while ($r = mysqli_fetch_assoc($result)) {
array_push($return->rows, $r);
array_push($ids, "video_tags.video_id=" . $r['movie_id']);
}
$idstring = implode(" OR ", $ids);
$return->tags = array();
$query = "SELECT t.tag_name,t.tag_id FROM video_tags
INNER JOIN tags t on video_tags.tag_id = t.tag_id
WHERE $idstring
GROUP BY t.tag_id";
$result = $this->conn->query($query);
while ($r = mysqli_fetch_assoc($result)) {
array_push($return->tags, array('tag_name' => $r['tag_name'], 'tag_id' => $r['tag_id']));
}
$this->commitMessage(json_encode($return));
});
$this->addActionHandler("getSearchKeyWord", function () {
$search = $_POST['keyword'];
$query = "SELECT movie_id,movie_name FROM videos
WHERE movie_name LIKE '%$search%'
ORDER BY likes DESC, create_date DESC, movie_name";
$result = $this->conn->query($query);
$rows = array();
while ($r = mysqli_fetch_assoc($result)) {
array_push($rows, $r);
}
$this->commitMessage(json_encode($rows));
});
}
/**
* function to handle stuff for loading specific videos and startdata
*/
private function loadVideos() {
$this->addActionHandler("loadVideo", function () {
$video_id = $_POST['movieid'];
// todo join with actor db and add actors of movieid
$query = " SELECT movie_name,movie_id,movie_url,thumbnail,poster,likes,quality,length
FROM videos WHERE movie_id=$video_id";
$result = $this->conn->query($query);
$row = $result->fetch_assoc();
$arr = array();
if ($row["poster"] == null) {
$arr["thumbnail"] = $row["thumbnail"];
} else {
$arr["thumbnail"] = $row["poster"];
}
$arr["movie_id"] = $row["movie_id"];
$arr["movie_name"] = $row["movie_name"];
// todo drop video url from db -- maybe one with and one without extension
// extension hardcoded here!!!
$arr["movie_url"] = str_replace("?", "%3F", $this->videopath . $row["movie_name"] . ".mp4");
$arr["likes"] = (int)$row["likes"];
$arr["quality"] = $row["quality"];
$arr["length"] = $row["length"];
// load tags of this video
$arr['tags'] = array();
$query = "SELECT t.tag_name, t.tag_id FROM video_tags
INNER JOIN tags t on video_tags.tag_id = t.tag_id
WHERE video_tags.video_id=$video_id
GROUP BY t.tag_id";
$result = $this->conn->query($query);
while ($r = mysqli_fetch_assoc($result)) {
array_push($arr['tags'], $r);
}
// get the random predict tags
$arr['suggesttag'] = array();
// select 5 random tags which are not selected for current video
$query = "SELECT * FROM tags
WHERE tag_id NOT IN (
SELECT video_tags.tag_id FROM video_tags
WHERE video_id=$video_id)
ORDER BY rand()
LIMIT 5";
$result = $this->conn->query($query);
while ($r = mysqli_fetch_assoc($result)) {
array_push($arr['suggesttag'], $r);
}
// query the actors corresponding to video
$query = "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=$video_id";
$result = $this->conn->query($query);
$arr['actors'] = mysqli_fetch_all($result, MYSQLI_ASSOC);
$this->commitMessage(json_encode($arr));
});
$this->addActionHandler("readThumbnail", function () {
$query = "SELECT thumbnail FROM videos WHERE movie_id='" . $_POST['movieid'] . "'";
$result = $this->conn->query($query);
$row = $result->fetch_assoc();
$this->commitMessage($row["thumbnail"]);
});
$this->addActionHandler("getStartData", function () {
$query = "SELECT COUNT(*) as nr FROM videos";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr = array();
$arr['total'] = $r['nr'];
$query = "SELECT COUNT(*) as nr 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";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr['tagged'] = $r['nr'];
$query = "SELECT COUNT(*) as nr 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_name='hd'";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr['hd'] = $r['nr'];
$query = "SELECT COUNT(*) as nr 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_name='fullhd'";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr['fullhd'] = $r['nr'];
$query = "SELECT COUNT(*) as nr 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_name='lowquality'";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr['sd'] = $r['nr'];
$query = "SELECT COUNT(*) as nr FROM tags";
$result = $this->conn->query($query);
$r = mysqli_fetch_assoc($result);
$arr['tags'] = $r['nr'];
$this->commitMessage(json_encode($arr));
});
}
/**
* function to handle api handlers for stuff to add to video or database
*/
private function addToVideo() {
$this->addActionHandler("addLike", function () {
$movieid = $_POST['movieid'];
$query = "update videos set likes = likes + 1 where movie_id = '$movieid'";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
$this->addActionHandler("deleteVideo", function () {
$movieid = $_POST['movieid'];
// delete video entry and corresponding tag infos
$query = "DELETE FROM videos WHERE movie_id=$movieid";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
});
}
}

View File

@ -1,5 +0,0 @@
<?php
include_once __DIR__ . '/src/handlers/Tags.php';
$tags = new Tags();
$tags->handleAction();

View File

@ -1,5 +0,0 @@
<?php
include_once __DIR__ . '/src/handlers/Video.php';
$video = new Video();
$video->handleAction();

72
apiGo/api/Actors.go Normal file
View File

@ -0,0 +1,72 @@
package api
import (
"fmt"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
)
func AddActorsHandlers() {
saveActorsToDB()
getActorsFromDB()
}
func getActorsFromDB() {
AddHandler("getAllActors", ActorNode, nil, func() []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 {
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)
return jsonify(readActorsFromResultset(database.Query(query)))
})
var gai struct {
ActorId int
}
AddHandler("getActorInfo", ActorNode, &gai, func() []byte {
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)
videos := readVideosFromResultset(database.Query(query))
query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", gai.ActorId)
actor := readActorsFromResultset(database.Query(query))[0]
var result = struct {
Videos []types.VideoUnloadedType
Info types.Actor
}{
Videos: videos,
Info: actor,
}
return jsonify(result)
})
}
func saveActorsToDB() {
var ca struct {
ActorName string
}
AddHandler("createActor", ActorNode, &ca, func() []byte {
query := "INSERT IGNORE INTO actors (name) VALUES (?)"
return database.SuccessQuery(query, ca.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)
return database.SuccessQuery(query)
})
}

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

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

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

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

71
apiGo/api/Helpers.go Normal file
View File

@ -0,0 +1,71 @@
package api
import (
"database/sql"
"encoding/json"
"fmt"
"openmediacenter/apiGo/api/types"
)
// MovieId - MovieName : pay attention to the order!
func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType {
result := []types.VideoUnloadedType{}
for rows.Next() {
var vid types.VideoUnloadedType
err := rows.Scan(&vid.MovieId, &vid.MovieName)
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
result = append(result, vid)
}
rows.Close()
return result
}
// TagID - TagName : pay attention to the order!
func readTagsFromResultset(rows *sql.Rows) []types.Tag {
// initialize with empty array!
result := []types.Tag{}
for rows.Next() {
var tag types.Tag
err := rows.Scan(&tag.TagId, &tag.TagName)
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
result = append(result, tag)
}
rows.Close()
return result
}
// ActorId - ActorName - Thumbnail : pay attention to the order!
func readActorsFromResultset(rows *sql.Rows) []types.Actor {
var result []types.Actor
for rows.Next() {
var actor types.Actor
var thumbnail []byte
err := rows.Scan(&actor.ActorId, &actor.Name, &thumbnail)
if len(thumbnail) != 0 {
actor.Thumbnail = string(thumbnail)
}
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
result = append(result, actor)
}
rows.Close()
return result
}
func jsonify(v interface{}) []byte {
// jsonify results
str, err := json.Marshal(v)
if err != nil {
fmt.Println("Error while Jsonifying return object: " + err.Error())
}
return str
}

94
apiGo/api/Settings.go Normal file
View File

@ -0,0 +1,94 @@
package api
import (
"encoding/json"
"fmt"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser"
)
func AddSettingsHandlers() {
saveSettingsToDB()
getSettingsFromDB()
reIndexHandling()
}
func getSettingsFromDB() {
AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
type InitialDataType struct {
DarkMode int
Pasword int
Mediacenter_name string
VideoPath string
}
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())
}
type InitialDataTypeResponse struct {
DarkMode bool
Pasword bool
Mediacenter_name string
VideoPath string
}
res := InitialDataTypeResponse{
DarkMode: result.DarkMode != 0,
Pasword: result.Pasword != -1,
Mediacenter_name: result.Mediacenter_name,
VideoPath: result.VideoPath,
}
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 {
query := `
UPDATE settings SET
video_path=?,
episode_path=?,
password=?,
mediacenter_name=?,
TMDB_grabbing=?,
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)
})
}
// methods for handling reindexing and cleanup of db gravity
func reIndexHandling() {
AddHandler("startReindex", SettingsNode, nil, func() []byte {
videoparser.StartReindex()
return database.ManualSuccessResponse(nil)
})
AddHandler("cleanupGravity", SettingsNode, nil, func() []byte {
videoparser.StartCleanup()
return nil
})
AddHandler("getStatusMessage", SettingsNode, nil, func() []byte {
return jsonify(videoparser.GetStatusMessage())
})
}

74
apiGo/api/Tags.go Normal file
View File

@ -0,0 +1,74 @@
package api
import (
"fmt"
"openmediacenter/apiGo/database"
"regexp"
)
func AddTagHandlers() {
getFromDB()
addToDB()
deleteFromDB()
}
func deleteFromDB() {
var dT struct {
TagId int
Force bool
}
AddHandler("deleteTag", TagNode, &dT, func() []byte {
// delete key constraints first
if dT.Force {
query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", dT.TagId)
err := database.Edit(query)
// respond only if result not successful
if err != nil {
return database.ManualSuccessResponse(err)
}
}
query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", dT.TagId)
err := database.Edit(query)
if err == nil {
// return if successful
return database.ManualSuccessResponse(err)
} else {
// check with regex if its the key constraint error
r, _ := regexp.Compile("^.*a foreign key constraint fails.*$")
if r.MatchString(err.Error()) {
return []byte(`{"result":"not empty tag"}`)
} else {
return database.ManualSuccessResponse(err)
}
}
})
}
func getFromDB() {
AddHandler("getAllTags", TagNode, nil, func() []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 {
query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)"
return database.SuccessQuery(query, ct.TagName)
})
var at struct {
MovieId int
TagId int
}
AddHandler("addTag", TagNode, &at, func() []byte {
query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)"
return database.SuccessQuery(query, at.TagId, at.MovieId)
})
}

233
apiGo/api/Video.go Normal file
View File

@ -0,0 +1,233 @@
package api
import (
"encoding/json"
"fmt"
"net/url"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
"strconv"
)
func AddVideoHandlers() {
getVideoHandlers()
loadVideosHandlers()
addToVideoHandlers()
}
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"
}
result := readVideosFromResultset(database.Query(query))
// jsonify results
str, _ := json.Marshal(result)
return str
})
var rtn struct {
Movieid int
}
AddHandler("readThumbnail", VideoNode, &rtn, func() []byte {
var pic []byte
query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id='%d'", rtn.Movieid)
err := database.QueryRow(query).Scan(&pic)
if err != nil {
fmt.Printf("the thumbnail of movie id %d couldn't be found", rtn.Movieid)
return nil
}
return pic
})
var grm struct {
Number int
}
AddHandler("getRandomMovies", VideoNode, &grm, func() []byte {
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)
result.Videos = readVideosFromResultset(database.Query(query))
var ids string
for i := range result.Videos {
ids += "video_tags.video_id=" + strconv.Itoa(result.Videos[i].MovieId)
if i < len(result.Videos)-1 {
ids += " OR "
}
}
// add the corresponding tags
query = fmt.Sprintf(`SELECT t.tag_name,t.tag_id FROM video_tags
INNER JOIN tags t on video_tags.tag_id = t.tag_id
WHERE %s
GROUP BY t.tag_id`, ids)
rows := database.Query(query)
for rows.Next() {
var tag types.Tag
err := rows.Scan(&tag.TagName, &tag.TagId)
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
// append to final array
result.Tags = append(result.Tags, tag)
}
// jsonify results
str, _ := json.Marshal(result)
return str
})
var gsk struct {
KeyWord string
}
AddHandler("getSearchKeyWord", VideoNode, &gsk, func() []byte {
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)
result := readVideosFromResultset(database.Query(query))
// jsonify results
str, _ := json.Marshal(result)
return str
})
}
// function to handle stuff for loading specific videos and startdata
func loadVideosHandlers() {
var lv struct {
MovieId int
}
AddHandler("loadVideo", VideoNode, &lv, func() []byte {
query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length
FROM videos WHERE movie_id=%d`, lv.MovieId)
var res types.FullVideoType
var poster []byte
var thumbnail []byte
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.Println(err.Error())
return nil
}
// we ned to urlencode the movieurl
res.MovieUrl = url.PathEscape(res.MovieUrl)
// we need to stringify the pic byte array
res.Poster = string(poster)
// if poster in db is empty we use the thumbnail
if res.Poster == "" {
res.Poster = string(thumbnail)
}
// now add the tags of this video
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)
res.Tags = readTagsFromResultset(database.Query(query))
query = fmt.Sprintf(`SELECT * FROM tags
WHERE tag_id NOT IN (
SELECT video_tags.tag_id FROM video_tags
WHERE video_id=%d)
ORDER BY rand()
LIMIT 5`, lv.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)
res.Actors = readActorsFromResultset(database.Query(query))
// jsonify results
str, _ := json.Marshal(res)
return str
})
AddHandler("getStartData", VideoNode, nil, func() []byte {
var result types.StartData
// query settings and infotile values
query := `
SELECT (
SELECT COUNT(*) FROM videos
) AS videonr,
(
SELECT COUNT(*) 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
) AS tagged,
(
SELECT COUNT(*) FROM video_tags as vt
INNER JOIN tags t on vt.tag_id = t.tag_id
WHERE t.tag_name='hd'
) AS hd,
(
SELECT COUNT(*) FROM video_tags as vt
INNER JOIN tags t on vt.tag_id = t.tag_id
WHERE t.tag_name='fullhd'
) AS fullhd,
(
SELECT COUNT(*) FROM video_tags as vt
INNER JOIN tags t on vt.tag_id = t.tag_id
WHERE t.tag_name='lowquality'
) AS lq,
(
SELECT COUNT(*) as nr FROM tags
) as tags
LIMIT 1`
_ = database.QueryRow(query).Scan(&result.VideoNr, &result.Tagged, &result.HDNr, &result.FullHdNr, &result.SDNr, &result.DifferentTags)
// jsonify results
str, _ := json.Marshal(result)
return str
})
}
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)
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)
return database.SuccessQuery(query)
})
}

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

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

56
apiGo/api/types/Types.go Normal file
View File

@ -0,0 +1,56 @@
package types
type VideoUnloadedType struct {
MovieId int
MovieName string
}
type FullVideoType struct {
MovieName string
MovieId int
MovieUrl string
Poster string
Likes int
Quality int
Length int
Tags []Tag
SuggestedTag []Tag
Actors []Actor
}
type Tag struct {
TagName string
TagId int
}
type Actor struct {
ActorId int
Name string
Thumbnail string
}
type StartData struct {
VideoNr int
FullHdNr int
HDNr int
SDNr int
DifferentTags int
Tagged int
}
type SettingsType struct {
VideoPath string
EpisodePath string
MediacenterName string
Password string
PasswordEnabled bool
TMDBGrabbing bool
DarkMode bool
VideoNr int
DBSize float32
DifferentTags int
TagsAdded int
PathPrefix string
}

136
apiGo/database/Database.go Normal file
View File

@ -0,0 +1,136 @@
package database
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"openmediacenter/apiGo/api/types"
)
var db *sql.DB
var DBName string
// store the command line parameter for Videoprefix
var SettingsVideoPrefix = ""
type DatabaseConfig struct {
DBHost string
DBPort int
DBUser string
DBPassword string
DBName string
}
func InitDB(dbconf *DatabaseConfig) {
DBName = dbconf.DBName
// Open up our database connection.
var err error
db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbconf.DBUser, dbconf.DBPassword, dbconf.DBHost, dbconf.DBPort, dbconf.DBName))
// if there is an error opening the connection, handle it
if err != nil {
fmt.Printf("Error while connecting to database! - %s\n", err.Error())
}
if db != nil {
ping := db.Ping()
if ping != nil {
fmt.Printf("Error while connecting to database! - %s\n", ping.Error())
}
}
}
func Query(query string, args ...interface{}) *sql.Rows {
// perform a db.Query insert
res, err := db.Query(query, args...)
// if there is an error inserting, handle it
if err != nil {
fmt.Printf("Error while requesting data! - %s\n", err.Error())
}
return res
}
func QueryRow(SQL string, args ...interface{}) *sql.Row {
return db.QueryRow(SQL, args...)
}
// edit something in the DB and give only an error response
func Edit(query string, args ...interface{}) error {
_, err := db.Exec(query, args...)
return err
}
// insert/edit a query and return last insert id
func Insert(query string, args ...interface{}) (error, int64) {
resp, err := db.Exec(query, args...)
var id int64 = 0
if err == nil {
id, err = resp.LastInsertId()
}
return err, id
}
func SuccessQuery(query string, args ...interface{}) []byte {
return ManualSuccessResponse(Edit(query, args...))
}
func ManualSuccessResponse(err error) []byte {
if err == nil {
return []byte(`{"result":"success"}`)
} else {
return []byte(fmt.Sprintf(`{"result":"%s"}`, err.Error()))
}
}
func Close() {
db.Close()
}
func GetSettings() types.SettingsType {
var result types.SettingsType
// query settings and infotile values
query := fmt.Sprintf(`
SELECT (
SELECT COUNT(*)
FROM videos
) AS videonr,
(
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS Size
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = '%s'
GROUP BY table_schema
) AS dbsize,
(
SELECT COUNT(*)
FROM tags
) AS difftagnr,
(
SELECT COUNT(*)
FROM video_tags
) AS tagsadded,
video_path, episode_path, password, mediacenter_name, TMDB_grabbing, DarkMode
FROM settings
LIMIT 1`, DBName)
var DarkMode int
var TMDBGrabbing int
err := QueryRow(query).Scan(&result.VideoNr, &result.DBSize, &result.DifferentTags, &result.TagsAdded,
&result.VideoPath, &result.EpisodePath, &result.Password, &result.MediacenterName, &TMDBGrabbing, &DarkMode)
if err != nil {
fmt.Println(err.Error())
}
result.TMDBGrabbing = TMDBGrabbing != 0
result.PasswordEnabled = result.Password != "-1"
result.DarkMode = DarkMode != 0
result.PathPrefix = SettingsVideoPrefix
return result
}

9
apiGo/go.mod Normal file
View File

@ -0,0 +1,9 @@
module openmediacenter/apiGo
go 1.16
require (
github.com/go-session/session v3.1.2+incompatible
github.com/go-sql-driver/mysql v1.5.0
gopkg.in/oauth2.v3 v3.12.0
)

111
apiGo/go.sum Normal file
View File

@ -0,0 +1,111 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
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 h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
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/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

53
apiGo/main.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"flag"
"fmt"
"openmediacenter/apiGo/api"
"openmediacenter/apiGo/database"
)
func main() {
fmt.Println("init OpenMediaCenter server")
db, verbose, pathPrefix := handleCommandLineArguments()
// todo some verbosity logger or sth
fmt.Printf("Use verbose output: %t\n", verbose)
fmt.Printf("Videopath prefix: %s\n", *pathPrefix)
// set pathprefix in database settings object
database.SettingsVideoPrefix = *pathPrefix
database.InitDB(db)
defer database.Close()
api.AddVideoHandlers()
api.AddSettingsHandlers()
api.AddTagHandlers()
api.AddActorsHandlers()
api.ServerInit(8081)
}
func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {
dbhostPtr := flag.String("DBHost", "127.0.0.1", "database host name")
dbPortPtr := flag.Int("DBPort", 3306, "database port")
dbUserPtr := flag.String("DBUser", "mediacenteruser", "database username")
dbPassPtr := flag.String("DBPassword", "mediapassword", "database username")
dbNamePtr := flag.String("DBName", "mediacenter", "database name")
verbosePtr := flag.Bool("v", true, "Verbose log output")
pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex")
flag.Parse()
return &database.DatabaseConfig{
DBHost: *dbhostPtr,
DBPort: *dbPortPtr,
DBUser: *dbUserPtr,
DBPassword: *dbPassPtr,
DBName: *dbNamePtr,
}, *verbosePtr, pathPrefix
}

View File

@ -0,0 +1 @@
package videoparser

View File

@ -0,0 +1,294 @@
package videoparser
import (
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/tmdb"
"os/exec"
"regexp"
"strconv"
)
var mSettings types.SettingsType
var mExtDepsAvailable *ExtDependencySupport
// default Tag ids
const (
FullHd = 2
Hd = 4
LowQuality = 3
)
type ExtDependencySupport struct {
FFMpeg bool
MediaInfo bool
}
type VideoAttributes struct {
Duration float32
FileSize uint
Width uint
}
func ReIndexVideos(path []string, sett types.SettingsType) {
mSettings = sett
// check if the extern dependencies are available
mExtDepsAvailable = checkExtDependencySupport()
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
for _, s := range path {
processVideo(s)
}
AppendMessageBuffer("reindex finished successfully!")
contentAvailable = false
fmt.Println("Reindexing finished!")
}
func processVideo(fileNameOrig string) {
fmt.Printf("Processing %s video-", fileNameOrig)
// match the file extension
r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`)
fileName := r.ReplaceAllString(fileNameOrig, "")
year, fileName := matchYear(fileName)
// now we should look if this video already exists in db
query := "SELECT * FROM videos WHERE movie_name = ?"
err := database.QueryRow(query, fileName).Scan()
if err == sql.ErrNoRows {
fmt.Printf("The Video %s does't exist! Adding it to database.\n", fileName)
addVideo(fileName, fileNameOrig, year)
} else {
fmt.Println(" :existing!")
}
}
// add a video to the database
func addVideo(videoName string, fileName string, year int) {
var ppic *string
var poster *string
var tmdbData *tmdb.VideoTMDB
var err error
// initialize defaults
vidAtr := &VideoAttributes{
Duration: 0,
FileSize: 0,
Width: 0,
}
if mExtDepsAvailable.FFMpeg {
ppic, err = parseFFmpegPic(fileName)
if err != nil {
fmt.Printf("FFmpeg error occured: %s\n", err.Error())
} else {
fmt.Println("successfully extracted thumbnail!!")
}
}
if mExtDepsAvailable.MediaInfo {
atr := getVideoAttributes(fileName)
if atr != nil {
vidAtr = atr
}
}
// if TMDB grabbing is enabled serach in api for video...
if mSettings.TMDBGrabbing {
tmdbData = tmdb.SearchVideo(videoName, year)
if tmdbData != nil {
// reassign parsed pic as poster
poster = ppic
// and tmdb pic as thumbnail
ppic = &tmdbData.Thumbnail
}
}
query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) VALUES (?,?,?,?,?,?)`
err, insertId := database.Insert(query, videoName, fileName, poster, ppic, vidAtr.Width, vidAtr.Duration)
if err != nil {
fmt.Printf("Failed to insert video into db: %s\n", err.Error())
return
}
// add default tags
if vidAtr.Width != 0 {
insertSizeTag(vidAtr.Width, uint(insertId))
}
// add tmdb tags
if mSettings.TMDBGrabbing && tmdbData != nil {
insertTMDBTags(tmdbData.GenreIds, insertId)
}
AppendMessageBuffer(fmt.Sprintf("%s - added!", videoName))
}
func matchYear(fileName string) (int, string) {
r, _ := regexp.Compile(`\([0-9]{4}?\)`)
years := r.FindAllString(fileName, -1)
if len(years) == 0 {
return -1, fileName
}
year, err := strconv.Atoi(years[len(years)-1])
if err != nil {
return -1, fileName
}
// cut out year from filename
return year, r.ReplaceAllString(fileName, "")
}
// parse the thumbail picture from video file
func parseFFmpegPic(fileName string) (*string, error) {
app := "ffmpeg"
cmd := exec.Command(app,
"-hide_banner",
"-loglevel", "panic",
"-ss", "00:04:00",
"-i", mSettings.VideoPath+fileName,
"-vframes", "1",
"-q:v", "2",
"-f", "singlejpeg",
"pipe:1")
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
fmt.Println(string(err.(*exec.ExitError).Stderr))
return nil, err
}
backpic64 := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(stdout)
return &backpic64, nil
}
func getVideoAttributes(fileName string) *VideoAttributes {
app := "mediainfo"
arg0 := mSettings.VideoPath + fileName
arg1 := "--Output=JSON"
cmd := exec.Command(app, arg1, "-f", arg0)
stdout, err := cmd.Output()
var t struct {
Media struct {
Track []struct {
Duration string
FileSize string
Width string
}
}
}
err = json.Unmarshal(stdout, &t)
if err != nil {
fmt.Println(err.Error())
return nil
}
duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32)
filesize, err := strconv.Atoi(t.Media.Track[0].FileSize)
width, err := strconv.Atoi(t.Media.Track[1].Width)
ret := VideoAttributes{
Duration: float32(duration),
FileSize: uint(filesize),
Width: uint(width),
}
return &ret
}
func AppendMessageBuffer(message string) {
messageBuffer = append(messageBuffer, message)
}
// ext dependency support check
func checkExtDependencySupport() *ExtDependencySupport {
var extDepsAvailable ExtDependencySupport
extDepsAvailable.FFMpeg = commandExists("ffmpeg")
extDepsAvailable.MediaInfo = commandExists("mediainfo")
return &extDepsAvailable
}
// check if a specific system command is available
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// insert the default size tags to corresponding video
func insertSizeTag(width uint, videoId uint) {
var tagType uint
if width >= 1080 {
tagType = FullHd
} else if width >= 720 {
tagType = Hd
} else {
tagType = LowQuality
}
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagType)
err := database.Edit(query)
if err != nil {
fmt.Printf("Eror occured while adding default Tag: %s\n", err.Error())
}
}
// insert id array of tmdb geners to database
func insertTMDBTags(ids []int, videoId int64) {
genres := tmdb.GetGenres()
for _, id := range ids {
var idGenre *tmdb.TMDBGenre
for _, genre := range *genres {
if genre.Id == id {
idGenre = &genre
break
}
}
// skip tag if name couldn't be found
if idGenre == nil {
continue
}
// now we check if the tag we want to add already exists
tagId := createTagToDB(idGenre.Name)
// now we add the tag
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagId)
_ = database.Edit(query)
}
}
// returns id of tag or creates it if not existing
func createTagToDB(tagName string) int64 {
query := fmt.Sprintf("SELECT tag_id FROM tags WHERE tag_name = %s", tagName)
var id int64
err := database.QueryRow(query).Scan(&id)
if err == sql.ErrNoRows {
// tag doesn't exist -- add it
query = fmt.Sprintf("INSERT INTO tags (tag_name) VALUES (%s)", tagName)
err, id = database.Insert(query)
}
return id
}

View File

@ -0,0 +1,70 @@
package videoparser
import (
"fmt"
"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..")
mSettings := database.GetSettings()
// add the path prefix to videopath
mSettings.VideoPath = mSettings.PathPrefix + mSettings.VideoPath
// check if path even exists
if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) {
fmt.Println("Reindex path doesn't exist!")
return false
}
var files []string
err := filepath.Walk(mSettings.VideoPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err.Error())
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".mp4") {
files = append(files, info.Name())
}
return nil
})
if err != nil {
fmt.Println(err.Error())
}
// start reindex process
AppendMessageBuffer("Starting Reindexing!")
go ReIndexVideos(files, mSettings)
return true
}
func GetStatusMessage() *StatusMessage {
msg := StatusMessage{
Messages: messageBuffer,
ContentAvailable: contentAvailable,
}
messageBuffer = []string{}
return &msg
}
func StartCleanup() {
// todo start cleanup
}

View File

@ -0,0 +1,144 @@
package tmdb
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
const apiKey = "9fd90530b11447f5646f8e6fb4733fb4"
const baseUrl = "https://api.themoviedb.org/3/"
const pictureBase = "https://image.tmdb.org/t/p/w500"
type VideoTMDB struct {
Thumbnail string
Overview string
Title 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
}
type TMDBGenre struct {
Id int
Name string
}
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)
if err != nil {
fmt.Println(err.Error())
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return nil
}
var t struct {
Results []tmdbVidResult
}
err = json.Unmarshal(body, &t)
fmt.Println(len(t.Results))
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) {
tmdbVid = result
// continue parsing
goto cont
}
}
// if there is no match use first one
tmdbVid = t.Results[0]
} else {
tmdbVid = t.Results[0]
}
// continue label
cont:
thumbnail := fetchPoster(tmdbVid)
result := VideoTMDB{
Thumbnail: *thumbnail,
Overview: tmdbVid.Overview,
Title: tmdbVid.Title,
GenreIds: tmdbVid.Genre_ids,
}
return &result
}
func fetchPoster(vid tmdbVidResult) *string {
url := fmt.Sprintf("%s%s", pictureBase, vid.Poster_path)
resp, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return nil
}
backpic64 := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(body)
return &backpic64
}
var tmdbGenres *[]TMDBGenre
func fetchGenres() *[]TMDBGenre {
url := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey)
resp, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return nil
}
var t []TMDBGenre
err = json.Unmarshal(body, &t)
return &t
}
func GetGenres() *[]TMDBGenre {
// if generes are nil fetch them once
if tmdbGenres == nil {
tmdbGenres = fetchGenres()
}
return tmdbGenres
}

View File

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

View File

@ -2,20 +2,6 @@
# enable nginx site
ln -sf /etc/nginx/sites-available/OpenMediaCenter.conf /etc/nginx/sites-enabled/OpenMediaCenter.conf
# link general socket to current one
phpsymlink="/var/run/php/php-fpm.sock";
# create a gneral symlink to the php socket if not already existing
if [ -L ${phpsymlink} ] ; then
if [ -e ${phpsymlink} ] ; then
echo "general php symlink already exists."
else
ln -sf /var/run/php/php*.*-fpm.sock /var/run/php/php-fpm.sock
fi
else
ln -sf /var/run/php/php*.*-fpm.sock /var/run/php/php-fpm.sock
fi
# setup database
mysql -uroot -pPASS -e "CREATE DATABASE IF NOT EXISTS mediacenter;"
mysql -uroot -pPASS -e "CREATE USER IF NOT EXISTS 'mediacenteruser'@'localhost' IDENTIFIED BY 'mediapassword';"
@ -31,6 +17,5 @@ chown -R www-data:www-data /var/www/openmediacenter
# restart services
systemctl restart nginx
# trigger a movie reindex
php /var/www/openmediacenter/api/extractvideopreviews.php
rm /tmp/output.log
systemctl enable OpenMediaCenter.service
systemctl start OpenMediaCenter.service

View File

@ -13,9 +13,7 @@ server {
try_files $uri /index.html;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
location ~* ^/(api/|token) {
proxy_pass http://127.0.0.1:8081;
}
}

View File

@ -0,0 +1,12 @@
[Unit]
Description=OpenMediaCenter start job
After=network.target
[Service]
Type=simple
ExecStart=openmediacenter
Restart=always
User=root
[Install]
WantedBy=default.target

19522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@
"text-summary"
]
},
"proxy": "http://192.168.0.209",
"proxy": "http://127.0.0.1:8081",
"homepage": "/",
"eslintConfig": {
"extends": [
@ -72,15 +72,15 @@
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0",
"@types/react-router-dom": "^5.1.6",
"@types/react-router": "5.1.8",
"@types/jest": "^26.0.19",
"@types/node": "^12.19.9",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/node": "^14.14.31",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
"@types/react-router": "5.1.12",
"@types/react-router-dom": "^5.1.6",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"jest-junit": "^12.0.0",
"react-scripts": "4.0.1"
"react-scripts": "4.0.3"
}
}

View File

@ -45,14 +45,16 @@ class App extends React.Component<{}, state> {
// set theme
GlobalInfos.enableDarkTheme(result.DarkMode);
GlobalInfos.setVideoPath(result.VideoPath);
this.setState({
generalSettingsLoaded: true,
passwordsupport: result.passwordEnabled,
mediacentername: result.mediacenter_name,
passwordsupport: result.Password,
mediacentername: result.Mediacenter_name,
onapierror: false
});
// set tab title to received mediacenter name
document.title = result.mediacenter_name;
document.title = result.Mediacenter_name;
}, error => {
this.setState({onapierror: true});
});

View File

@ -4,20 +4,16 @@ import ActorTile from './ActorTile';
describe('<ActorTile/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<ActorTile actor={{thumbnail: '-1', name: 'testname', id: 3}}/>);
const wrapper = shallow(<ActorTile actor={{Thumbnail: '-1', Name: 'testname', id: 3}}/>);
wrapper.unmount();
});
it('simulate click with custom handler', function () {
const func = jest.fn((_) => {});
const wrapper = shallow(<ActorTile actor={{thumbnail: '-1', name: 'testname', id: 3}} onClick={() => func()}/>);
const func1 = jest.fn();
prepareViewBinding(func1);
const wrapper = shallow(<ActorTile actor={{Thumbnail: '-1', Name: 'testname', id: 3}} onClick={() => func()}/>);
wrapper.simulate('click');
expect(func1).toBeCalledTimes(0);
expect(func).toBeCalledTimes(1);
});
});

View File

@ -22,7 +22,7 @@ class ActorTile extends React.Component<props> {
return this.renderActorTile(this.props.onClick);
} else {
return (
<Link to={{pathname: '/actors/' + this.props.actor.actor_id}}>
<Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>
{this.renderActorTile(() => {
})}
</Link>
@ -34,11 +34,11 @@ class ActorTile extends React.Component<props> {
return (
<div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}>
<div className={style.actortile_thumbnail}>
{this.props.actor.thumbnail === null ? <FontAwesomeIcon style={{
{this.props.actor.Thumbnail === '' ? <FontAwesomeIcon style={{
lineHeight: '130px'
}} icon={faUser} size='5x'/> : 'dfdf' /* todo render picture provided here! */}
</div>
<div className={style.actortile_name}>{this.props.actor.name}</div>
<div className={style.actortile_name}>{this.props.actor.Name}</div>
</div>
);
}

View File

@ -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('<FilterButton/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.unmount();
});
it('test initial render ', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
expect(wrapper.find('input')).toHaveLength(0);
});
it('test clicking', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.simulate('click');
expect(wrapper.find('input')).toHaveLength(1);
});
it('test call of callback on textfield change', function () {
let val = '';
const func = jest.fn((vali => {val = vali}));
const wrapper = shallow(<FilterButton onFilterChange={func}/>);
wrapper.simulate('click');
wrapper.find('input').simulate('change', {target: {value: 'test'}});
expect(func).toHaveBeenCalledTimes(1);
expect(val).toBe('test')
});
it('test closing on x button click', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.simulate('click');
expect(wrapper.find('input')).toHaveLength(1);
wrapper.find('Button').simulate('click');
expect(wrapper.find('input')).toHaveLength(0);
});
it('test shortkey press', function () {
let events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
shallow(<RandomPage/>);
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
expect(wrapper.find('input')).toHaveLength(0);
// trigger the keypress event
events.keyup({key: 'f'});
expect(wrapper.find('input')).toHaveLength(1);
});
});

View File

@ -0,0 +1,99 @@
import React from "react";
import style from "../Popups/AddActorPopup/AddActorPopup.module.css";
import {Button} from "../GPElements/Button";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilter, faTimes} from "@fortawesome/free-solid-svg-icons";
import {addKeyHandler, removeKeyHandler} from "../../utils/ShortkeyHandler";
interface props {
onFilterChange: (filter: string) => void
}
interface state {
filtervisible: boolean;
filter: string;
}
class FilterButton extends React.Component<props, state> {
// filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined;
constructor(props: props) {
super(props);
this.state = {
filtervisible: false,
filter: ''
}
this.keypress = this.keypress.bind(this);
this.enableFilterField = this.enableFilterField.bind(this);
}
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
componentDidMount(): void {
addKeyHandler(this.keypress);
}
render(): JSX.Element {
if (this.state.filtervisible) {
return (
<>
<input className={'form-control mr-sm-2 ' + style.searchinput}
type='text' placeholder='Filter' value={this.state.filter}
onChange={(e): void => {
this.props.onFilterChange(e.target.value);
this.setState({filter: e.target.value});
}}
ref={(input): void => {
this.filterfield = input;
}}/>
<Button title={<FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
this.setState({filter: '', filtervisible: false});
}}/>
</>
);
} else {
return (<Button
title={<span>Filter <FontAwesomeIcon
style={{
verticalAlign: 'middle',
lineHeight: '130px'
}}
icon={faFilter}
size='1x'/></span>}
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={this.enableFilterField}/>)
}
}
/**
* enable filterfield and focus into searchbar
*/
private enableFilterField(): void {
this.setState({filtervisible: true}, () => {
// focus filterfield after state update
this.filterfield?.focus();
});
}
/**
* key event handling
* @param event keyevent
*/
private keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'f') {
this.enableFilterField();
}
}
}
export default FilterButton;

View File

@ -31,7 +31,7 @@ describe('<AddActorPopup/>', function () {
});
it('test api call and insertion of actor tiles', function () {
global.callAPIMock([{id: 1, name: 'test'}, {id: 2, name: 'test2'}]);
global.callAPIMock([{Id: 1, Name: 'test'}, {Id: 2, Name: 'test2'}]);
const wrapper = shallow(<AddActorPopup/>);
@ -44,7 +44,7 @@ describe('<AddActorPopup/>', function () {
global.callAPIMock({result: 'success'});
wrapper.setState({actors: [{actor_id: 1, name: 'test'}]}, () => {
wrapper.setState({actors: [{ActorId: 1, Name: 'test'}]}, () => {
wrapper.find('ActorTile').dive().simulate('click');
expect(callAPI).toHaveBeenCalledTimes(1);
@ -59,7 +59,7 @@ describe('<AddActorPopup/>', function () {
global.callAPIMock({result: 'nosuccess'});
wrapper.setState({actors: [{actor_id: 1, name: 'test'}]}, () => {
wrapper.setState({actors: [{ActorId: 1, Name: 'test'}]}, () => {
wrapper.find('ActorTile').dive().simulate('click');
expect(callAPI).toHaveBeenCalledTimes(1);
@ -80,7 +80,7 @@ describe('<AddActorPopup/>', function () {
callAPIMock({});
wrapper.setState({actors: [{name: 'test', actor_id: 1}]});
wrapper.setState({actors: [{Name: 'test', ActorId: 1}]});
wrapper.find('PopupBase').props().ParentSubmit();

View File

@ -6,10 +6,7 @@ import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
import {APINode, callAPI} from '../../../utils/Api';
import {ActorType} from '../../../types/VideoTypes';
import {GeneralSuccess} from '../../../types/GeneralTypes';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faFilter, faTimes} from '@fortawesome/free-solid-svg-icons';
import {Button} from '../../GPElements/Button';
import {addKeyHandler, removeKeyHandler} from '../../../utils/ShortkeyHandler';
import FilterButton from "../../FilterButton/FilterButton";
interface props {
onHide: () => void;
@ -20,7 +17,6 @@ interface state {
contentDefault: boolean;
actors: ActorType[];
filter: string;
filtervisible: boolean;
}
/**
@ -36,23 +32,14 @@ class AddActorPopup extends React.Component<props, state> {
this.state = {
contentDefault: true,
actors: [],
filter: '',
filtervisible: false
filter: ''
};
this.tileClickHandler = this.tileClickHandler.bind(this);
this.filterSearch = this.filterSearch.bind(this);
this.parentSubmit = this.parentSubmit.bind(this);
this.keypress = this.keypress.bind(this);
}
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
componentDidMount(): void {
addKeyHandler(this.keypress);
// fetch the available actors
this.loadActors();
}
@ -94,30 +81,9 @@ class AddActorPopup extends React.Component<props, state> {
return (
<>
<div className={style.searchbar}>
{
this.state.filtervisible ?
<>
<input className={'form-control mr-sm-2 ' + style.searchinput}
type='text' placeholder='Filter' value={this.state.filter}
onChange={(e): void => {
this.setState({filter: e.target.value});
}}
ref={(input): void => {this.filterfield = input;}}/>
<Button title={<FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
this.setState({filter: '', filtervisible: false});
}}/>
</> :
<Button
title={<span>Filter <FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faFilter} size='1x'/></span>}
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={(): void => this.enableFilterField()}/>
}
<FilterButton onFilterChange={(filter): void => {
this.setState({filter: filter})
}}/>
</div>
{this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))}
</>
@ -134,8 +100,8 @@ class AddActorPopup extends React.Component<props, state> {
// fetch the available actors
callAPI<GeneralSuccess>(APINode.Actor, {
action: 'addActorToVideo',
actorid: actor.actor_id,
videoid: this.props.movie_id
ActorId: actor.ActorId,
MovieId: this.props.movie_id
}, result => {
if (result.result === 'success') {
// return back to player page
@ -155,22 +121,12 @@ class AddActorPopup extends React.Component<props, state> {
});
}
/**
* enable filterfield and focus into searchbar
*/
private enableFilterField(): void {
this.setState({filtervisible: true}, () => {
// focus filterfield after state update
this.filterfield?.focus();
});
}
/**
* filter the actor array for search matches
* @param actor
*/
private filterSearch(actor: ActorType): boolean {
return actor.name.toLowerCase().includes(this.state.filter.toLowerCase());
return actor.Name.toLowerCase().includes(this.state.filter.toLowerCase());
}
/**
@ -185,17 +141,6 @@ class AddActorPopup extends React.Component<props, state> {
this.tileClickHandler(filteredList[0]);
}
}
/**
* key event handling
* @param event keyevent
*/
private keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'f') {
this.enableFilterField();
}
}
}
export default AddActorPopup;

View File

@ -1,26 +1,3 @@
.popup {
border: 3px #3574fe solid;
border-radius: 18px;
height: 80%;
left: 20%;
opacity: 0.95;
position: absolute;
top: 10%;
width: 60%;
z-index: 2;
}
.header {
cursor: move;
font-size: x-large;
margin-left: 15px;
margin-top: 10px;
opacity: 1;
}
.content {
margin-left: 20px;
margin-right: 20px;
margin-top: 10px;
opacity: 1;
.actionbar{
margin-bottom: 15px;
}

View File

@ -14,7 +14,7 @@ describe('<AddTagPopup/>', function () {
it('test tag insertion', function () {
const wrapper = shallow(<AddTagPopup/>);
wrapper.setState({
items: [{tag_id: 1, tag_name: 'test'}, {tag_id: 2, tag_name: 'ee'}]
items: [{TagId: 1, TagName: 'test'}, {TagId: 2, TagName: 'ee'}]
}, () => {
expect(wrapper.find('Tag')).toHaveLength(2);
expect(wrapper.find('Tag').first().dive().text()).toBe('test');
@ -25,11 +25,37 @@ describe('<AddTagPopup/>', function () {
const wrapper = shallow(<AddTagPopup submit={jest.fn()} onHide={jest.fn()}/>);
wrapper.setState({
items: [{tag_id: 1, tag_name: 'test'}]
items: [{TagId: 1, TagName: 'test'}]
}, () => {
wrapper.find('Tag').first().dive().simulate('click');
expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
});
});
it('test parent submit if one item left', function () {
const onhide = jest.fn();
const submit = jest.fn();
const wrapper = shallow(<AddTagPopup submit={submit} onHide={onhide}/>);
wrapper.setState({
items: [{TagId: 1, TagName: 'test'}]
}, () => {
wrapper.instance().parentSubmit();
expect(onhide).toHaveBeenCalledTimes(1);
expect(submit).toHaveBeenCalledTimes(1);
wrapper.setState({
items: [{TagId: 1, TagName: 'test'}, {TagId: 3, TagName: 'test3'}]
}, () => {
wrapper.instance().parentSubmit();
// expect no submit if there are more than 1 item left...
expect(onhide).toHaveBeenCalledTimes(1);
expect(submit).toHaveBeenCalledTimes(1);
})
});
});
});

View File

@ -3,15 +3,17 @@ import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase';
import {APINode, callAPI} from '../../../utils/Api';
import {TagType} from '../../../types/VideoTypes';
import FilterButton from "../../FilterButton/FilterButton";
import styles from './AddTagPopup.module.css'
interface props {
onHide: () => void;
submit: (tagId: number, tagName: string) => void;
movie_id: number;
}
interface state {
items: TagType[];
filter: string;
}
/**
@ -21,7 +23,11 @@ class AddTagPopup extends React.Component<props, state> {
constructor(props: props) {
super(props);
this.state = {items: []};
this.state = {items: [], filter: ''};
this.tagFilter = this.tagFilter.bind(this);
this.parentSubmit = this.parentSubmit.bind(this);
this.onItemClick = this.onItemClick.bind(this);
}
componentDidMount(): void {
@ -34,18 +40,37 @@ class AddTagPopup extends React.Component<props, state> {
render(): JSX.Element {
return (
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide}>
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide} ParentSubmit={this.parentSubmit}>
<div className={styles.actionbar}>
<FilterButton onFilterChange={(filter): void => this.setState({filter: filter})}/>
</div>
{this.state.items ?
this.state.items.map((i) => (
this.state.items.filter(this.tagFilter).map((i) => (
<Tag tagInfo={i}
onclick={(): void => {
this.props.submit(i.tag_id, i.tag_name);
this.props.onHide();
}}/>
onclick={(): void => this.onItemClick(i)}/>
)) : null}
</PopupBase>
);
}
private onItemClick(tag: TagType): void {
this.props.submit(tag.TagId, tag.TagName);
this.props.onHide();
}
private tagFilter(tag: TagType): boolean {
return tag.TagName.toLowerCase().includes(this.state.filter.toLowerCase());
}
private parentSubmit(): void {
// allow submit only if one item is left in selection
const filteredList = this.state.items.filter(this.tagFilter);
if (filteredList.length === 1) {
// simulate click if parent submit
this.onItemClick(filteredList[0]);
}
}
}
export default AddTagPopup;

View File

@ -29,7 +29,7 @@ class NewTagPopup extends React.Component<props> {
* store the filled in form to the backend
*/
storeselection(): void {
callAPI(APINode.Tags, {action: 'createTag', tagname: this.value}, (result: GeneralSuccess) => {
callAPI(APINode.Tags, {action: 'createTag', TagName: this.value}, (result: GeneralSuccess) => {
if (result.result !== 'success') {
console.log('error occured while writing to db -- todo error handling');
console.log(result.result);

View File

@ -8,6 +8,7 @@ import {APINode, callAPIPlain} from '../../utils/Api';
interface PreviewProps {
name: string;
movie_id: number;
onClick?: () => void;
}
interface PreviewState {
@ -38,7 +39,7 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle();
return (
<Link to={'/player/' + this.props.movie_id}>
<Link to={'/player/' + this.props.movie_id} onClick={this.props.onClick}>
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
<div className={style.previewpic}>

View File

@ -6,12 +6,12 @@ import {shallow} from 'enzyme';
describe('<Tag/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<Tag tagInfo={{tag_name: 'testname', tag_id: 1}}/>);
const wrapper = shallow(<Tag tagInfo={{TagName: 'testname', TagId: 1}}/>);
wrapper.unmount();
});
it('renders childs correctly', function () {
const wrapper = shallow(<Tag tagInfo={{tag_name: 'test', tag_id: 1}}/>);
const wrapper = shallow(<Tag tagInfo={{TagName: 'test', TagId: 1}}/>);
expect(wrapper.children().text()).toBe('test');
});
@ -19,7 +19,7 @@ describe('<Tag/>', function () {
const func = jest.fn();
const wrapper = shallow(<Tag
tagInfo={{tag_name: 'test', tag_id: 1}}
tagInfo={{TagName: 'test', TagId: 1}}
onclick={() => {func();}}>test</Tag>);
expect(func).toBeCalledTimes(0);

View File

@ -18,7 +18,7 @@ class Tag extends React.Component<props> {
return this.renderButton();
} else {
return (
<Link to={'/categories/' + this.props.tagInfo.tag_id}>
<Link to={'/categories/' + this.props.tagInfo.TagId}>
{this.renderButton()}
</Link>
);
@ -28,7 +28,7 @@ class Tag extends React.Component<props> {
renderButton(): JSX.Element {
return (
<button className={styles.tagbtn} onClick={(): void => this.TagClick()}
data-testid='Test-Tag'>{this.props.tagInfo.tag_name}</button>
data-testid='Test-Tag'>{this.props.tagInfo.TagName}</button>
);
}
@ -38,7 +38,7 @@ class Tag extends React.Component<props> {
TagClick(): void {
if (this.props.onclick) {
// call custom onclick handling
this.props.onclick(this.props.tagInfo.tag_name); // todo check if param is neccessary
this.props.onclick(this.props.tagInfo.TagName); // todo check if param is neccessary
return;
}
}

View File

@ -4,7 +4,9 @@ import style from './VideoContainer.module.css';
import {VideoTypes} from '../../types/ApiTypes';
interface props {
data: VideoTypes.VideoUnloadedType[]
data: VideoTypes.VideoUnloadedType[];
onScrollPositionChange?: (scrollPos: number, loadedTiles: number) => void;
initialScrollPosition?: {scrollPos: number, loadedTiles: number};
}
interface state {
@ -32,7 +34,16 @@ class VideoContainer extends React.Component<props, state> {
componentDidMount(): void {
document.addEventListener('scroll', this.trackScrolling);
this.loadPreviewBlock(16);
console.log(this.props.initialScrollPosition)
if(this.props.initialScrollPosition !== undefined){
this.loadPreviewBlock(this.props.initialScrollPosition.loadedTiles, () => {
if(this.props.initialScrollPosition !== undefined)
window.scrollTo(0, this.props.initialScrollPosition.scrollPos);
});
}else{
this.loadPreviewBlock(16);
}
}
render(): JSX.Element {
@ -40,9 +51,13 @@ class VideoContainer extends React.Component<props, state> {
<div className={style.maincontent}>
{this.state.loadeditems.map(elem => (
<Preview
key={elem.movie_id}
name={elem.movie_name}
movie_id={elem.movie_id}/>
key={elem.MovieId}
name={elem.MovieName}
movie_id={elem.MovieId}
onClick={(): void => {
if (this.props.onScrollPositionChange !== undefined)
this.props.onScrollPositionChange(window.pageYOffset - document.documentElement.clientHeight, this.loadindex)
}}/>
))}
{/*todo css for no items to show*/}
{this.state.loadeditems.length === 0 ?
@ -62,7 +77,7 @@ class VideoContainer extends React.Component<props, state> {
* load previews to the container
* @param nr number of previews to load
*/
loadPreviewBlock(nr: number): void {
loadPreviewBlock(nr: number, callback? : () => void): void {
console.log('loadpreviewblock called ...');
let ret = [];
for (let i = 0; i < nr; i++) {
@ -77,7 +92,7 @@ class VideoContainer extends React.Component<props, state> {
...this.state.loadeditems,
...ret
]
});
}, callback);
this.loadindex += nr;
@ -88,8 +103,10 @@ class VideoContainer extends React.Component<props, state> {
*/
trackScrolling = (): void => {
// comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
if (document.documentElement.clientHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
this.loadPreviewBlock(8);
if (this.props.onScrollPositionChange !== undefined)
this.props.onScrollPositionChange(document.documentElement.clientHeight + window.pageYOffset, this.loadindex)
}
};
}

View File

@ -38,7 +38,7 @@ class ActorOverviewPage extends React.Component<props, state> {
<Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})}/>
</SideBar>
<div className={style.container}>
{this.state.actors.map((el) => (<ActorTile key={el.actor_id} actor={el}/>))}
{this.state.actors.map((el) => (<ActorTile key={el.ActorId} actor={el}/>))}
</div>
{this.state.NActorPopupVisible ?
<NewActorPopup onHide={(): void => {

View File

@ -10,13 +10,13 @@ describe('<ActorPage/>', function () {
it('fetch infos', function () {
callAPIMock({
videos: [{
movie_id: 0,
movie_name: 'test'
}], info: {
thumbnail: '',
name: '',
actor_id: 0
Videos: [{
MovieId: 0,
MovieName: 'test'
}], Info: {
Thumbnail: '',
Name: '',
ActorId: 0
}
});

View File

@ -31,13 +31,13 @@ export class ActorPage extends React.Component<props, state> {
constructor(props: props) {
super(props);
this.state = {data: [], actor: {actor_id: 0, name: '', thumbnail: ''}};
this.state = {data: [], actor: {ActorId: 0, Name: '', Thumbnail: ''}};
}
render(): JSX.Element {
return (
<>
<PageTitle title={this.state.actor.name} subtitle={this.state.data ? this.state.data.length + ' videos' : null}>
<PageTitle title={this.state.actor.Name} subtitle={this.state.data ? this.state.data.length + ' videos' : null}>
<span className={style.overviewbutton}>
<Link to='/actors'>
<Button onClick={(): void => {}} title='Go to Actor overview'/>
@ -68,11 +68,11 @@ export class ActorPage extends React.Component<props, state> {
getActorInfo(): void {
callAPI(APINode.Actor, {
action: 'getActorInfo',
actorid: this.props.match.params.id
ActorId: parseInt(this.props.match.params.id)
}, (result: ActorTypes.videofetchresult) => {
this.setState({
data: result.videos ? result.videos : [],
actor: result.info
data: result.Videos ? result.Videos : [],
actor: result.Info
});
});
}

View File

@ -103,8 +103,8 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
private deleteTag(force: boolean): void {
callAPI<GeneralSuccess>(APINode.Tags, {
action: 'deleteTag',
tagId: parseInt(this.props.match.params.id),
force: force
TagId: parseInt(this.props.match.params.id),
Force: force
}, result => {
console.log(result.result);
if (result.result === 'success') {

View File

@ -54,8 +54,8 @@ class TagView extends React.Component<props, TagViewState> {
<div className={videocontainerstyle.maincontent}>
{this.state.loadedtags ?
this.state.loadedtags.map((m) => (
<Link to={'/categories/' + m.tag_id} key={m.tag_id}>
<TagPreview name={m.tag_name}/></Link>
<Link to={'/categories/' + m.TagId} key={m.TagId}>
<TagPreview name={m.TagName}/></Link>
)) :
'loading'}
</div>

View File

@ -10,17 +10,13 @@ import {Route, Switch, withRouter} from 'react-router-dom';
import {RouteComponentProps} from 'react-router';
import SearchHandling from './SearchHandling';
import {VideoTypes} from '../../types/ApiTypes';
import {DefaultTags} from "../../types/GeneralTypes";
interface props extends RouteComponentProps {}
interface props extends RouteComponentProps {
}
interface state {
sideinfo: {
videonr: number,
fullhdvideonr: number,
hdvideonr: number,
sdvideonr: number,
tagnr: number
},
sideinfo: VideoTypes.startDataType
subtitle: string,
data: VideoTypes.VideoUnloadedType[],
selectionnr: number
@ -38,11 +34,12 @@ export class HomePage extends React.Component<props, state> {
this.state = {
sideinfo: {
videonr: 0,
fullhdvideonr: 0,
hdvideonr: 0,
sdvideonr: 0,
tagnr: 0
VideoNr: 0,
FullHdNr: 0,
HDNr: 0,
SDNr: 0,
DifferentTags: 0,
Tagged: 0,
},
subtitle: 'All Videos',
data: [],
@ -52,8 +49,10 @@ export class HomePage extends React.Component<props, state> {
componentDidMount(): void {
// initial get of all videos
this.fetchVideoData('All');
this.fetchVideoData(DefaultTags.all.TagId);
this.fetchStartData();
console.log(this.props)
}
/**
@ -62,7 +61,7 @@ export class HomePage extends React.Component<props, state> {
*
* @param tag tag to fetch videos
*/
fetchVideoData(tag: string): void {
fetchVideoData(tag: number): void {
callAPI(APINode.Video, {action: 'getMovies', tag: tag}, (result: VideoTypes.VideoUnloadedType[]) => {
this.setState({
data: []
@ -70,7 +69,6 @@ export class HomePage extends React.Component<props, state> {
this.setState({
data: result,
selectionnr: result.length,
subtitle: `${tag} Videos`
});
});
}
@ -80,15 +78,7 @@ export class HomePage extends React.Component<props, state> {
*/
fetchStartData(): void {
callAPI(APINode.Video, {action: 'getStartData'}, (result: VideoTypes.startDataType) => {
this.setState({
sideinfo: {
videonr: result['total'],
fullhdvideonr: result['fullhd'],
hdvideonr: result['hd'],
sdvideonr: result['sd'],
tagnr: result['tags']
}
});
this.setState({sideinfo: result});
});
}
@ -113,27 +103,53 @@ export class HomePage extends React.Component<props, state> {
onChange={(e): void => {
this.keyword = e.target.value;
}}/>
<button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>Search</button>
<button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>Search
</button>
</form>
</PageTitle>
<SideBar>
<SideBarTitle>Infos:</SideBarTitle>
<Line/>
<SideBarItem><b>{this.state.sideinfo.videonr}</b> Videos Total!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.fullhdvideonr}</b> FULL-HD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.hdvideonr}</b> HD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.sdvideonr}</b> SD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.tagnr}</b> different Tags!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.VideoNr}</b> Videos Total!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.HDNr}</b> HD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.SDNr}</b> SD Videos!</SideBarItem>
<SideBarItem><b>{this.state.sideinfo.DifferentTags}</b> different Tags!</SideBarItem>
<Line/>
<SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={{tag_name: 'All', tag_id: -1}} onclick={(): void => this.fetchVideoData('All')}/>
<Tag tagInfo={{tag_name: 'FullHd', tag_id: -1}} onclick={(): void => this.fetchVideoData('FullHd')}/>
<Tag tagInfo={{tag_name: 'LowQuality', tag_id: -1}} onclick={(): void => this.fetchVideoData('LowQuality')}/>
<Tag tagInfo={{tag_name: 'HD', tag_id: -1}} onclick={(): void => this.fetchVideoData('HD')}/>
<Tag tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} onclick={(): void => {
this.fetchVideoData(DefaultTags.all.TagId);
this.setState({subtitle: `All Videos`});
}}/>
<Tag tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} onclick={(): void => {
this.fetchVideoData(DefaultTags.fullhd.TagId);
this.setState({subtitle: `Full Hd Videos`});
}}/>
<Tag tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}}
onclick={(): void => {
this.fetchVideoData(DefaultTags.lowq.TagId);
this.setState({subtitle: `Low Quality Videos`});
}}/>
<Tag tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}} onclick={(): void => {
this.fetchVideoData(DefaultTags.hd.TagId);
this.setState({subtitle: `HD Videos`});
}}/>
</SideBar>
{this.state.data.length !== 0 ?
<VideoContainer
data={this.state.data}/> :
data={this.state.data}
onScrollPositionChange={(pos, loadedTiles): void => {
this.props.location.state = {pos: pos, loaded: loadedTiles};
console.log("history state update called...")
const {state} = this.props.location;
const stateCopy: { pos: number, loaded: number } = {...state as { pos: number, loaded: number }};
stateCopy.loaded = loadedTiles;
stateCopy.pos = pos;
this.props.history.replace({state: stateCopy});
console.log(this.props)
}}
initialScrollPosition={this.props.location.state !== null ? {scrollPos: (this.props.location.state as { pos: number, loaded: number }).pos, loadedTiles: (this.props.location.state as { pos: number, loaded: number }).loaded} : undefined}/> :
<div>No Data found!</div>}
<div className={style.rightinfo}>

View File

@ -190,15 +190,15 @@ describe('<Player/>', function () {
const wrapper = instance();
global.callAPIMock({result: 'success'});
wrapper.setState({suggesttag: [{tag_name: 'test', tag_id: 1}]}, () => {
wrapper.setState({suggesttag: [{TagName: 'test', TagId: 1}]}, () => {
// mock funtion should have not been called
expect(callAPI).toBeCalledTimes(0);
wrapper.find('Tag').findWhere(p => p.props().tagInfo.tag_name === 'test').dive().simulate('click');
wrapper.find('Tag').findWhere(p => p.props().tagInfo.TagName === 'test').dive().simulate('click');
// mock function should have been called once
expect(callAPI).toBeCalledTimes(1);
// expect tag added to video tags
expect(wrapper.state().tags).toMatchObject([{tag_name: 'test'}]);
expect(wrapper.state().tags).toMatchObject([{TagName: 'test'}]);
// expect tag to be removed from tag suggestions
expect(wrapper.state().suggesttag).toHaveLength(0);
});

View File

@ -20,6 +20,7 @@ import {ActorType, TagType} from '../../types/VideoTypes';
import PlyrJS from 'plyr';
import {Button} from '../../elements/GPElements/Button';
import {VideoTypes} from '../../types/ApiTypes';
import GlobalInfos from "../../utils/GlobalInfos";
interface myprops extends RouteComponentProps<{ id: string }> {}
@ -131,16 +132,16 @@ export class Player extends React.Component<myprops, mystate> {
<Line/>
<SideBarTitle>Tags:</SideBarTitle>
{this.state.tags.map((m: TagType) => (
<Tag key={m.tag_id} tagInfo={m}/>
<Tag key={m.TagId} tagInfo={m}/>
))}
<Line/>
<SideBarTitle>Tag Quickadd:</SideBarTitle>
{this.state.suggesttag.map((m: TagType) => (
<Tag
tagInfo={m}
key={m.tag_name}
key={m.TagName}
onclick={(): void => {
this.quickAddTag(m.tag_id, m.tag_name);
this.quickAddTag(m.TagId, m.TagName);
}}/>
))}
</SideBar>
@ -155,7 +156,7 @@ export class Player extends React.Component<myprops, mystate> {
<div className={style.actorcontainer}>
{this.state.actors ?
this.state.actors.map((actr: ActorType) => (
<ActorTile key={actr.actor_id} actor={actr}/>
<ActorTile key={actr.ActorId} actor={actr}/>
)) : <></>
}
<div className={style.actorAddTile} onClick={(): void => {
@ -180,13 +181,10 @@ export class Player extends React.Component<myprops, mystate> {
handlePopOvers(): JSX.Element {
return (
<>
{this.state.popupvisible ?
<AddTagPopup onHide={(): void => {
this.setState({popupvisible: false});
}}
submit={this.quickAddTag}
movie_id={this.state.movie_id}/> :
null
{
this.state.popupvisible ?
<AddTagPopup onHide={(): void => this.setState({popupvisible: false})}
submit={this.quickAddTag}/> : null
}
{
this.state.actorpopupvisible ?
@ -207,8 +205,8 @@ export class Player extends React.Component<myprops, mystate> {
quickAddTag(tagId: number, tagName: string): void {
callAPI(APINode.Tags, {
action: 'addTag',
id: tagId,
movieid: this.props.match.params.id
TagId: tagId,
MovieId: parseInt(this.props.match.params.id)
}, (result: GeneralSuccess) => {
if (result.result !== 'success') {
console.error('error occured while writing to db -- todo error handling');
@ -216,7 +214,7 @@ export class Player extends React.Component<myprops, mystate> {
} else {
// check if tag has already been added
const tagIndex = this.state.tags.map(function (e: TagType) {
return e.tag_name;
return e.TagName;
}).indexOf(tagName);
// only add tag if it isn't already there
@ -224,7 +222,7 @@ export class Player extends React.Component<myprops, mystate> {
// update tags if successful
let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState)
const quickaddindex = this.state.suggesttag.map(function (e: TagType) {
return e.tag_id;
return e.TagId;
}).indexOf(tagId);
// check if tag is available in quickadds
@ -232,12 +230,12 @@ export class Player extends React.Component<myprops, mystate> {
array.splice(quickaddindex, 1);
this.setState({
tags: [...this.state.tags, {tag_name: tagName, tag_id: tagId}],
tags: [...this.state.tags, {TagName: tagName, TagId: tagId}],
suggesttag: array
});
} else {
this.setState({
tags: [...this.state.tags, {tag_name: tagName, tag_id: tagId}]
tags: [...this.state.tags, {TagName: tagName, TagId: tagId}]
});
}
}
@ -249,27 +247,28 @@ export class Player extends React.Component<myprops, mystate> {
* fetch all the required infos of a video from backend
*/
fetchMovieData(): void {
callAPI(APINode.Video, {action: 'loadVideo', movieid: this.props.match.params.id}, (result: VideoTypes.loadVideoType) => {
callAPI(APINode.Video, {action: 'loadVideo', MovieId: parseInt(this.props.match.params.id)}, (result: VideoTypes.loadVideoType) => {
console.log(result)
this.setState({
sources: {
type: 'video',
sources: [
{
src: getBackendDomain() + result.movie_url,
src: getBackendDomain() + GlobalInfos.getVideoPath() + result.MovieUrl,
type: 'video/mp4',
size: 1080
}
],
poster: result.thumbnail
poster: result.Poster
},
movie_id: result.movie_id,
movie_name: result.movie_name,
likes: result.likes,
quality: result.quality,
length: result.length,
tags: result.tags,
suggesttag: result.suggesttag,
actors: result.actors
movie_id: result.MovieId,
movie_name: result.MovieName,
likes: result.Likes,
quality: result.Quality,
length: result.Length,
tags: result.Tags,
suggesttag: result.SuggestedTag,
actors: result.Actors
});
});
}
@ -279,7 +278,7 @@ export class Player extends React.Component<myprops, mystate> {
* click handler for the like btn
*/
likebtn(): void {
callAPI(APINode.Video, {action: 'addLike', movieid: this.props.match.params.id}, (result: GeneralSuccess) => {
callAPI(APINode.Video, {action: 'addLike', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => {
if (result.result === 'success') {
// likes +1 --> avoid reload of all data
this.setState({likes: this.state.likes + 1});
@ -302,7 +301,7 @@ export class Player extends React.Component<myprops, mystate> {
* delete the current video and return to last page
*/
deleteVideo(): void {
callAPI(APINode.Video, {action: 'deleteVideo', movieid: this.props.match.params.id}, (result: GeneralSuccess) => {
callAPI(APINode.Video, {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => {
if (result.result === 'success') {
// return to last element if successful
this.props.history.goBack();
@ -324,7 +323,7 @@ export class Player extends React.Component<myprops, mystate> {
* fetch the available video actors again
*/
refetchActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', videoid: this.props.match.params.id}, result => {
callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id)}, result => {
this.setState({actors: result});
});
}

View File

@ -55,7 +55,7 @@ describe('<RandomPage/>', function () {
shallow(<RandomPage/>);
callAPIMock({rows: [], tags: []});
callAPIMock({Videos: [], Tags: []});
// trigger the keypress event
events.keyup({key: 's'});

View File

@ -15,8 +15,8 @@ interface state {
}
interface GetRandomMoviesType {
rows: VideoTypes.VideoUnloadedType[];
tags: TagType[];
Videos: VideoTypes.VideoUnloadedType[];
Tags: TagType[];
}
/**
@ -53,7 +53,7 @@ class RandomPage extends React.Component<{}, state> {
<SideBar>
<SideBarTitle>Visible Tags:</SideBarTitle>
{this.state.tags.map((m) => (
<Tag key={m.tag_id} tagInfo={m}/>
<Tag key={m.TagId} tagInfo={m}/>
))}
</SideBar>
@ -84,10 +84,11 @@ class RandomPage extends React.Component<{}, state> {
*/
loadShuffledvideos(nr: number): void {
callAPI<GetRandomMoviesType>(APINode.Video, {action: 'getRandomMovies', number: nr}, result => {
console.log(result)
this.setState({videos: []}); // needed to trigger rerender of main videoview
this.setState({
videos: result.rows,
tags: result.tags
videos: result.Videos,
tags: result.Tags
});
});
}

View File

@ -80,48 +80,48 @@ describe('<GeneralSettings/>', function () {
it('test videopath change event', function () {
const wrapper = shallow(<GeneralSettings/>);
expect(wrapper.state().videopath).not.toBe('test');
expect(wrapper.state().generalSettings.VideoPath).not.toBe('test');
const event = {target: {name: 'pollName', value: 'test'}};
wrapper.find('[data-testid=\'videpathform\']').find('FormControl').simulate('change', event);
expect(wrapper.state().videopath).toBe('test');
expect(wrapper.state().generalSettings.VideoPath).toBe('test');
});
it('test tvshowpath change event', function () {
const wrapper = shallow(<GeneralSettings/>);
const event = {target: {name: 'pollName', value: 'test'}};
expect(wrapper.state().tvshowpath).not.toBe('test');
expect(wrapper.state().generalSettings.EpisodePath).not.toBe('test');
wrapper.find('[data-testid=\'tvshowpath\']').find('FormControl').simulate('change', event);
expect(wrapper.state().tvshowpath).toBe('test');
expect(wrapper.state().generalSettings.EpisodePath).toBe('test');
});
it('test mediacentername-form change event', function () {
const wrapper = shallow(<GeneralSettings/>);
const event = {target: {name: 'pollName', value: 'test'}};
expect(wrapper.state().mediacentername).not.toBe('test');
expect(wrapper.state().generalSettings.MediacenterName).not.toBe('test');
wrapper.find('[data-testid=\'nameform\']').find('FormControl').simulate('change', event);
expect(wrapper.state().mediacentername).toBe('test');
expect(wrapper.state().generalSettings.MediacenterName).toBe('test');
});
it('test password-form change event', function () {
const wrapper = shallow(<GeneralSettings/>);
wrapper.setState({passwordsupport: true});
wrapper.setState({generalSettings : {PasswordEnabled: true}});
const event = {target: {name: 'pollName', value: 'test'}};
expect(wrapper.state().password).not.toBe('test');
expect(wrapper.state().generalSettings.Password).not.toBe('test');
wrapper.find('[data-testid=\'passwordfield\']').find('FormControl').simulate('change', event);
expect(wrapper.state().password).toBe('test');
expect(wrapper.state().generalSettings.Password).toBe('test');
});
it('test tmdbsupport change event', function () {
const wrapper = shallow(<GeneralSettings/>);
wrapper.setState({tmdbsupport: true});
wrapper.setState({generalSettings : {TMDBGrabbing: true}});
expect(wrapper.state().tmdbsupport).toBe(true);
expect(wrapper.state().generalSettings.TMDBGrabbing).toBe(true);
wrapper.find('[data-testid=\'tmdb-switch\']').simulate('change');
expect(wrapper.state().tmdbsupport).toBe(false);
expect(wrapper.state().generalSettings.TMDBGrabbing).toBe(false);
});
it('test insertion of 4 infoheaderitems', function () {

View File

@ -11,23 +11,13 @@ import {SettingsTypes} from '../../types/ApiTypes';
import {GeneralSuccess} from '../../types/GeneralTypes';
interface state {
passwordsupport: boolean,
tmdbsupport: boolean,
customapi: boolean,
videopath: string,
tvshowpath: string,
mediacentername: string,
password: string,
apipath: string,
videonr: number,
dbsize: number,
difftagnr: number,
tagsadded: number
customapi: boolean
apipath: string
generalSettings: SettingsTypes.loadGeneralSettingsType
}
interface props {}
interface props {
}
/**
* Component for Generalsettings tag on Settingspage
@ -38,20 +28,21 @@ class GeneralSettings extends React.Component<props, state> {
super(props);
this.state = {
passwordsupport: false,
tmdbsupport: false,
customapi: false,
videopath: '',
tvshowpath: '',
mediacentername: '',
password: '',
apipath: '',
videonr: 0,
dbsize: 0,
difftagnr: 0,
tagsadded: 0
generalSettings: {
DarkMode: true,
DBSize: 0,
DifferentTags: 0,
EpisodePath: "",
MediacenterName: "",
Password: "",
PasswordEnabled: false,
TagsAdded: 0,
TMDBGrabbing: false,
VideoNr: 0,
VideoPath: ""
}
};
}
@ -65,19 +56,19 @@ class GeneralSettings extends React.Component<props, state> {
<>
<div className={style.infoheader}>
<InfoHeaderItem backColor='lightblue'
text={this.state.videonr}
text={this.state.generalSettings.VideoNr}
subtext='Videos in Gravity'
icon={faArchive}/>
<InfoHeaderItem backColor='yellow'
text={this.state.dbsize !== undefined ? this.state.dbsize + ' MB' : ''}
text={this.state.generalSettings.DBSize + ' MB'}
subtext='Database size'
icon={faRulerVertical}/>
<InfoHeaderItem backColor='green'
text={this.state.difftagnr}
text={this.state.generalSettings.DifferentTags}
subtext='different Tags'
icon={faAddressCard}/>
<InfoHeaderItem backColor='orange'
text={this.state.tagsadded}
text={this.state.generalSettings.TagsAdded}
subtext='tags added'
icon={faBalanceScaleLeft}/>
</div>
@ -89,15 +80,26 @@ class GeneralSettings extends React.Component<props, state> {
<Form.Row>
<Form.Group as={Col} data-testid='videpathform'>
<Form.Label>Video Path</Form.Label>
<Form.Control type='text' placeholder='/var/www/html/video' value={this.state.videopath}
onChange={(ee): void => this.setState({videopath: ee.target.value})}/>
<Form.Control type='text' placeholder='/var/www/html/video'
value={this.state.generalSettings.VideoPath}
onChange={(ee): void => this.setState({
generalSettings: {
...this.state.generalSettings,
VideoPath: ee.target.value
}
})}/>
</Form.Group>
<Form.Group as={Col} data-testid='tvshowpath'>
<Form.Label>TV Show Path</Form.Label>
<Form.Control type='text' placeholder='/var/www/html/tvshow'
value={this.state.tvshowpath}
onChange={(e): void => this.setState({tvshowpath: e.target.value})}/>
value={this.state.generalSettings.EpisodePath}
onChange={(e): void => this.setState({
generalSettings: {
...this.state.generalSettings,
EpisodePath: e.target.value
}
})}/>
</Form.Group>
</Form.Row>
@ -131,17 +133,28 @@ class GeneralSettings extends React.Component<props, state> {
id='custom-switch'
data-testid='passwordswitch'
label='Enable Password support'
checked={this.state.passwordsupport}
checked={this.state.generalSettings.PasswordEnabled}
onChange={(): void => {
this.setState({passwordsupport: !this.state.passwordsupport});
this.setState({
generalSettings: {
...this.state.generalSettings,
PasswordEnabled: !this.state.generalSettings.PasswordEnabled
}
});
}}
/>
{this.state.passwordsupport ?
{this.state.generalSettings.PasswordEnabled ?
<Form.Group data-testid='passwordfield'>
<Form.Label>Password</Form.Label>
<Form.Control type='password' placeholder='**********' value={this.state.password}
onChange={(e): void => this.setState({password: e.target.value})}/>
<Form.Control type='password' placeholder='**********'
value={this.state.generalSettings.Password}
onChange={(e): void => this.setState({
generalSettings: {
...this.state.generalSettings,
Password: e.target.value
}
})}/>
</Form.Group> : null
}
@ -150,9 +163,14 @@ class GeneralSettings extends React.Component<props, state> {
id='custom-switch-2'
data-testid='tmdb-switch'
label='Enable TMDB video grabbing support'
checked={this.state.tmdbsupport}
checked={this.state.generalSettings.TMDBGrabbing}
onChange={(): void => {
this.setState({tmdbsupport: !this.state.tmdbsupport});
this.setState({
generalSettings: {
...this.state.generalSettings,
TMDBGrabbing: !this.state.generalSettings.TMDBGrabbing
}
});
}}
/>
@ -171,8 +189,14 @@ class GeneralSettings extends React.Component<props, state> {
<Form.Group className={style.mediacenternameform} data-testid='nameform'>
<Form.Label>The name of the Mediacenter</Form.Label>
<Form.Control type='text' placeholder='Mediacentername' value={this.state.mediacentername}
onChange={(e): void => this.setState({mediacentername: e.target.value})}/>
<Form.Control type='text' placeholder='Mediacentername'
value={this.state.generalSettings.MediacenterName}
onChange={(e): void => this.setState({
generalSettings: {
...this.state.generalSettings,
MediacenterName: e.target.value
}
})}/>
</Form.Group>
<Button variant='primary' type='submit'>
@ -192,19 +216,7 @@ class GeneralSettings extends React.Component<props, state> {
*/
loadSettings(): void {
callAPI(APINode.Settings, {action: 'loadGeneralSettings'}, (result: SettingsTypes.loadGeneralSettingsType) => {
this.setState({
videopath: result.video_path,
tvshowpath: result.episode_path,
mediacentername: result.mediacenter_name,
password: result.password,
passwordsupport: result.passwordEnabled,
tmdbsupport: result.TMDB_grabbing,
videonr: result.videonr,
dbsize: result.dbsize,
difftagnr: result.difftagnr,
tagsadded: result.tagsadded
});
this.setState({generalSettings: result});
});
}
@ -212,14 +224,15 @@ class GeneralSettings extends React.Component<props, state> {
* save the selected and typed settings to the backend
*/
saveSettings(): void {
let settings = this.state.generalSettings;
if(!this.state.generalSettings.PasswordEnabled){
settings.Password = '-1';
}
settings.DarkMode = GlobalInfos.isDarkTheme()
callAPI(APINode.Settings, {
action: 'saveGeneralSettings',
password: this.state.passwordsupport ? this.state.password : '-1',
videopath: this.state.videopath,
tvshowpath: this.state.tvshowpath,
mediacentername: this.state.mediacentername,
tmdbsupport: this.state.tmdbsupport,
darkmodeenabled: GlobalInfos.isDarkTheme().toString()
Settings: settings
}, (result: GeneralSuccess) => {
if (result.result) {
console.log('successfully saved settings');

View File

@ -52,10 +52,9 @@ describe('<MovieSettings/>', function () {
it('content available received and in state', () => {
const wrapper = shallow(<MovieSettings/>);
callAPIMock({
contentAvailable: true,
message: 'firstline\nsecondline'
ContentAvailable: true,
Messages: ['firstline', 'secondline']
})
wrapper.instance().updateStatus();
@ -70,7 +69,8 @@ describe('<MovieSettings/>', function () {
it('test reindex with no content available', () => {
callAPIMock({
contentAvailable: false
Messages: [],
ContentAvailable: false
})
global.clearInterval = jest.fn();
@ -99,12 +99,12 @@ describe('<MovieSettings/>', function () {
expect(wrapper.instance().setState).toBeCalledTimes(1);
});
it('test no textDiv insertion if string is empty', function () {
it('expect insertion before existing ones', function () {
const wrapper = shallow(<MovieSettings/>);
callAPIMock({
contentAvailable: true,
message: 'test'
ContentAvailable: true,
Messages: ['test']
})
wrapper.instance().updateStatus();
@ -115,14 +115,14 @@ describe('<MovieSettings/>', function () {
// expect an untouched state if we try to add an empty string...
callAPIMock({
contentAvailable: true,
message: ''
ContentAvailable: true,
Messages: ['']
})
wrapper.instance().updateStatus();
expect(wrapper.state()).toMatchObject({
text: ['test']
text: ['', 'test']
});
});
});

View File

@ -58,11 +58,7 @@ class MovieSettings extends React.Component<props, state> {
*/
startReindex(): void {
// clear output text before start
this.setState({text: []});
this.setState({startbtnDisabled: true});
console.log('starting');
this.setState({text: [], startbtnDisabled: true});
callAPI(APINode.Settings, {action: 'startReindex'}, (result: GeneralSuccess): void => {
console.log(result);
@ -85,18 +81,12 @@ class MovieSettings extends React.Component<props, state> {
*/
updateStatus = (): void => {
callAPI(APINode.Settings, {action: 'getStatusMessage'}, (result: SettingsTypes.getStatusMessageType) => {
if (result.contentAvailable) {
// ignore if message is empty
console.log(result);
if(result.message === '') return;
// todo 2020-07-4: scroll to bottom of div here
this.setState({
// insert a string for each line
text: [...result.message.split('\n'),
...this.state.text]
});
} else {
this.setState({
// insert a string for each line
text: [...result.Messages, ...this.state.text]
});
// todo 2020-07-4: scroll to bottom of div here
if (!result.ContentAvailable) {
// clear refresh interval if no content available
clearInterval(this.myinterval);

View File

@ -19,7 +19,8 @@ global.prepareFetchApi = (response) => {
const mockJsonPromise = Promise.resolve(response);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
text: () => mockJsonPromise
text: () => mockJsonPromise,
status: 200
});
return (jest.fn().mockImplementation(() => mockFetchPromise));
};
@ -33,19 +34,6 @@ global.prepareFailingFetchApi = () => {
return (jest.fn().mockImplementation(() => mockFetchPromise));
};
/**
* prepares a viewbinding instance
* @param func a mock function to be called
*/
global.prepareViewBinding = (func) => {
GlobalInfos.getViewBinding = () => {
return {
changeRootElement: func,
returnToLastElement: func
};
};
};
global.callAPIMock = (resonse) => {
const helpers = require('./utils/Api');
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});

View File

@ -2,56 +2,59 @@ import {ActorType, TagType} from './VideoTypes';
export namespace VideoTypes {
export interface loadVideoType {
movie_url: string
thumbnail: string
movie_id: number
movie_name: string
likes: number
quality: number
length: number
tags: TagType[]
suggesttag: TagType[]
actors: ActorType[]
MovieUrl: string
Poster: string
MovieId: number
MovieName: string
Likes: number
Quality: number
Length: number
Tags: TagType[]
SuggestedTag: TagType[]
Actors: ActorType[]
}
export interface startDataType {
total: number;
fullhd: number;
hd: number;
sd: number;
tags: number;
VideoNr: number;
FullHdNr: number;
HDNr: number;
SDNr: number;
DifferentTags: number;
Tagged: number;
}
export interface VideoUnloadedType {
movie_id: number;
movie_name: string
MovieId: number;
MovieName: string
}
}
export namespace SettingsTypes {
export interface initialApiCallData {
DarkMode: boolean;
passwordEnabled: boolean;
mediacenter_name: string;
Password: boolean;
Mediacenter_name: string;
VideoPath: string;
}
export interface loadGeneralSettingsType {
video_path: string,
episode_path: string,
mediacenter_name: string,
password: string,
passwordEnabled: boolean,
TMDB_grabbing: boolean,
VideoPath: string,
EpisodePath: string,
MediacenterName: string,
Password: string,
PasswordEnabled: boolean,
TMDBGrabbing: boolean,
DarkMode: boolean,
videonr: number,
dbsize: number,
difftagnr: number,
tagsadded: number
VideoNr: number,
DBSize: number,
DifferentTags: number,
TagsAdded: number
}
export interface getStatusMessageType {
contentAvailable: boolean;
message: string;
ContentAvailable: boolean;
Messages: string[];
}
}
@ -60,7 +63,7 @@ export namespace ActorTypes {
* result of actor fetch
*/
export interface videofetchresult {
videos: VideoTypes.VideoUnloadedType[];
info: ActorType;
Videos: VideoTypes.VideoUnloadedType[];
Info: ActorType;
}
}

View File

@ -9,8 +9,8 @@ interface TagarrayType {
}
export const DefaultTags: TagarrayType = {
all: {tag_id: 1, tag_name: 'all'},
fullhd: {tag_id: 2, tag_name: 'fullhd'},
lowq: {tag_id: 3, tag_name: 'lowquality'},
hd: {tag_id: 4, tag_name: 'hd'}
all: {TagId: 1, TagName: 'all'},
fullhd: {TagId: 2, TagName: 'fullhd'},
lowq: {TagId: 3, TagName: 'lowquality'},
hd: {TagId: 4, TagName: 'hd'}
};

View File

@ -2,12 +2,12 @@
* type accepted by Tag component
*/
export interface TagType {
tag_name: string
tag_id: number
TagName: string
TagId: number
}
export interface ActorType {
thumbnail: string;
name: string;
actor_id: number;
Thumbnail: string;
Name: string;
ActorId: number;
}

View File

@ -40,20 +40,154 @@ function getAPIDomain(): string {
interface ApiBaseRequest {
action: string | number,
[_: string]: string | number | boolean
[_: string]: string | number | boolean | object
}
// store api token - empty if not set
let apiToken = ''
// a callback que to be called after api token refresh
let callQue: (() => void)[] = []
// flag to check wheter a api refresh is currently pending
let refreshInProcess = false;
// store the expire seconds of token
let expireSeconds = -1;
interface APIToken {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/**
* helper function to build a formdata for requesting post data correctly
* @param args api request object
* refresh the api token or use that one in cookie if still valid
* @param callback to be called after successful refresh
*/
function buildFormData(args: ApiBaseRequest): FormData {
const req = new FormData();
export function refreshAPIToken(callback: () => void): void {
callQue.push(callback);
for (const i in args) {
req.append(i, (args[i].toString()));
// check if already is a token refresh is in process
if (refreshInProcess) {
// if yes return
return;
} else {
// if not set flat
refreshInProcess = true;
}
// check if a cookie with token is available
const token = getTokenCookie();
if (token !== null) {
// check if token is at least valid for the next minute
if (token.expire > (new Date().getTime() / 1000) + 60) {
apiToken = token.token;
expireSeconds = token.expire;
callback();
console.log("token still valid...")
callFuncQue();
return;
}
}
const formData = new FormData();
formData.append("grant_type", "client_credentials");
formData.append("client_id", "openmediacenter");
formData.append("client_secret", 'openmediacenter');
formData.append("scope", 'all');
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData})
.then((response) => response.json()
.then((result: APIToken) => {
console.log(result)
// set api token
apiToken = result.access_token;
// set expire time
expireSeconds = (new Date().getTime() / 1000) + result.expires_in;
setTokenCookie(apiToken, expireSeconds);
// call all handlers and release flag
callFuncQue();
}));
}
/**
* call all qued callbacks
*/
function callFuncQue(): void {
// call all pending handlers
callQue.map(func => {
return func();
})
// reset pending que
callQue = []
// release flag to be able to start new refresh
refreshInProcess = false;
}
/**
* set the cookie for the currently gotten token
* @param token token string
* @param validSec second time when the token will be invalid
*/
function setTokenCookie(token: string, validSec: number): void {
let d = new Date();
d.setTime(validSec * 1000);
console.log("token set" + d.toUTCString())
let expires = "expires=" + d.toUTCString();
document.cookie = "token=" + token + ";" + expires + ";path=/";
document.cookie = "token_expire=" + validSec + ";" + expires + ";path=/";
}
/**
* get all required cookies for the token
*/
function getTokenCookie(): { token: string, expire: number } | null {
const token = decodeCookie('token');
const expireInString = decodeCookie('token_expire');
const expireIn = parseInt(expireInString, 10) | 0;
if (expireIn !== 0 && token !== '') {
return {token: token, expire: expireIn};
} else {
return null
}
}
/**
* decode a simple cookie with key specified
* @param key cookie key
*/
function decodeCookie(key: string): string {
let name = key + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
/**
* check if api token is valid -- if not request new one
* when finished call callback
* @param callback function to be called afterwards
*/
function checkAPITokenValid(callback: () => void): void {
// check if token is valid and set
if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) {
refreshAPIToken(() => {
callback()
})
} else {
callback()
}
return req;
}
/**
@ -63,12 +197,28 @@ function buildFormData(args: ApiBaseRequest): FormData {
* @param callback the callback with json reply from backend
* @param errorcallback a optional callback if an error occured
*/
export function callAPI<T>(apinode: APINode, fd: ApiBaseRequest, callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {}): void {
fetch(getAPIDomain() + apinode, {method: 'POST', body: buildFormData(fd)})
.then((response) => response.json()
.then((result) => {
callback(result);
})).catch(reason => errorcallback(reason));
export function callAPI<T>(apinode: APINode,
fd: ApiBaseRequest,
callback: (_: T) => void,
errorcallback: (_: string) => void = (_: string): void => {
}): void {
checkAPITokenValid(() => {
fetch(getAPIDomain() + apinode, {
method: 'POST', body: JSON.stringify(fd), headers: new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiToken,
}),
}).then((response) => {
if (response.status !== 200) {
console.log('Error: ' + response.statusText);
// todo place error popup here
} else {
response.json().then((result: T) => {
callback(result);
})
}
}).catch(reason => errorcallback(reason));
})
}
/**
@ -78,20 +228,26 @@ export function callAPI<T>(apinode: APINode, fd: ApiBaseRequest, callback: (_: T
* @param callback the callback with PLAIN text reply from backend
*/
export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void {
fetch(getAPIDomain() + apinode, {method: 'POST', body: buildFormData(fd)})
.then((response) => response.text()
.then((result) => {
callback(result);
}));
checkAPITokenValid(() => {
fetch(getAPIDomain() + apinode, {
method: 'POST', body: JSON.stringify(fd), headers: new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiToken,
})
})
.then((response) => response.text()
.then((result) => {
callback(result);
}));
});
}
/**
* API nodes definitions
*/
export enum APINode {
Settings = 'settings.php',
Tags = 'tags.php',
Actor = 'actor.php',
Video = 'video.php'
Settings = 'settings',
Tags = 'tags',
Actor = 'actor',
Video = 'video'
}

View File

@ -6,31 +6,47 @@ import lighttheme from '../AppLightTheme.module.css';
* it contains general infos about app - like theme
*/
class StaticInfos {
#darktheme = true;
private darktheme: boolean = true;
private videopath: string = ""
/**
* check if the current theme is the dark theme
* @returns {boolean} is dark theme?
*/
isDarkTheme() {
return this.#darktheme;
isDarkTheme(): boolean {
return this.darktheme;
};
/**
* setter to enable or disable the dark or light theme
* @param enable enable the dark theme?
*/
enableDarkTheme(enable = true) {
this.#darktheme = enable;
enableDarkTheme(enable = true): void {
this.darktheme = enable;
}
/**
* get the currently selected theme stylesheet
* @returns {*} the style object of the current active theme
*/
getThemeStyle() {
getThemeStyle(): { [_: string]: string } {
return this.isDarkTheme() ? darktheme : lighttheme;
}
/**
* set the current videopath
* @param vidpath videopath with beginning and ending slash
*/
setVideoPath(vidpath: string): void {
this.videopath = vidpath;
}
/**
* return the current videopath
*/
getVideoPath(): string {
return this.videopath;
}
}
const GlobalInfos = new StaticInfos();

12152
yarn.lock Normal file

File diff suppressed because it is too large Load Diff