Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			passwordfi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8c9f3aecd8 | 
@@ -1,26 +0,0 @@
 | 
			
		||||
version: "2"
 | 
			
		||||
checks:
 | 
			
		||||
  argument-count:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 5
 | 
			
		||||
  complex-logic:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 4
 | 
			
		||||
  file-lines:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 350
 | 
			
		||||
  method-complexity:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 5
 | 
			
		||||
  method-count:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 20
 | 
			
		||||
  method-lines:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 60
 | 
			
		||||
  nested-control-flow:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 4
 | 
			
		||||
  return-statements:
 | 
			
		||||
    config:
 | 
			
		||||
      threshold: 4
 | 
			
		||||
@@ -1,88 +1,34 @@
 | 
			
		||||
image: node:latest
 | 
			
		||||
 | 
			
		||||
stages:
 | 
			
		||||
  - prepare
 | 
			
		||||
  - build
 | 
			
		||||
  - test
 | 
			
		||||
  - packaging
 | 
			
		||||
  - deploy
 | 
			
		||||
  - coverage
 | 
			
		||||
  - build
 | 
			
		||||
 | 
			
		||||
cache:
 | 
			
		||||
  paths:
 | 
			
		||||
    - node_modules/
 | 
			
		||||
 | 
			
		||||
include:
 | 
			
		||||
  - template: Code-Quality.gitlab-ci.yml
 | 
			
		||||
 | 
			
		||||
variables:
 | 
			
		||||
  SAST_DISABLE_DIND: "true"
 | 
			
		||||
 | 
			
		||||
prepare:
 | 
			
		||||
  stage: prepare
 | 
			
		||||
  script:
 | 
			
		||||
    - npm install --progress=false
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
  stage: build
 | 
			
		||||
  script:
 | 
			
		||||
    - npm run build
 | 
			
		||||
  artifacts:
 | 
			
		||||
    expire_in: 7 days
 | 
			
		||||
    paths:
 | 
			
		||||
      - build/
 | 
			
		||||
  needs: ["prepare"]
 | 
			
		||||
 | 
			
		||||
test:
 | 
			
		||||
  stage: test
 | 
			
		||||
  script:
 | 
			
		||||
    - npm install
 | 
			
		||||
    - CI=true npm run test
 | 
			
		||||
  artifacts:
 | 
			
		||||
    reports:
 | 
			
		||||
      junit:
 | 
			
		||||
        - ./junit.xml
 | 
			
		||||
  needs: ["prepare"]
 | 
			
		||||
 | 
			
		||||
coverage:
 | 
			
		||||
  stage: test
 | 
			
		||||
  stage: coverage
 | 
			
		||||
  script:
 | 
			
		||||
    - CI=true npm run coverage
 | 
			
		||||
  artifacts:
 | 
			
		||||
    reports:
 | 
			
		||||
      cobertura:
 | 
			
		||||
        - ./coverage/cobertura-coverage.xml
 | 
			
		||||
  needs: ["prepare"]
 | 
			
		||||
 | 
			
		||||
package_debian:
 | 
			
		||||
  stage: packaging
 | 
			
		||||
  image: debian
 | 
			
		||||
build:
 | 
			
		||||
  stage: build
 | 
			
		||||
  script:
 | 
			
		||||
    - cd deb
 | 
			
		||||
    - mkdir -p "./OpenMediaCenter/var/www/openmediacenter/videos/"
 | 
			
		||||
    - mkdir -p "./OpenMediaCenter/tmp/"
 | 
			
		||||
    - cp -r ../build/* ./OpenMediaCenter/var/www/openmediacenter/
 | 
			
		||||
    - cp -r ../api ./OpenMediaCenter/var/www/openmediacenter/
 | 
			
		||||
    - cp ../database.sql ./OpenMediaCenter/tmp/openmediacenter.sql
 | 
			
		||||
    - chmod -R 0775 *
 | 
			
		||||
    - dpkg-deb --build OpenMediaCenter
 | 
			
		||||
    - mv OpenMediaCenter.deb OpenMediaCenter-0.1_amd64.deb
 | 
			
		||||
  artifacts:
 | 
			
		||||
    paths:
 | 
			
		||||
      - deb/OpenMediaCenter-0.1_amd64.deb
 | 
			
		||||
  needs: ["build"]
 | 
			
		||||
 | 
			
		||||
deploy_test1:
 | 
			
		||||
  stage: deploy
 | 
			
		||||
  image: luki42/alpineopenssh:latest
 | 
			
		||||
  needs:
 | 
			
		||||
    - test
 | 
			
		||||
    - build
 | 
			
		||||
  only:
 | 
			
		||||
    - master
 | 
			
		||||
  script:
 | 
			
		||||
    - eval $(ssh-agent -s)
 | 
			
		||||
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
 | 
			
		||||
    - mkdir -p ~/.ssh
 | 
			
		||||
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
 | 
			
		||||
    - scp -r build/* root@192.168.0.42:/var/www/html/
 | 
			
		||||
    - scp -r api/ root@192.168.0.42:/var/www/html/
 | 
			
		||||
 | 
			
		||||
    - npm run build
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							@@ -7,19 +7,14 @@ 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  uses PHP 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.
 | 
			
		||||
For organizing videos tags are used.
 | 
			
		||||
 | 
			
		||||
Here you can see an example main page in light mode:
 | 
			
		||||
Here you can see an example main page:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
and in dark mode:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
First of all clone the repository.
 | 
			
		||||
@@ -36,9 +31,9 @@ You need also to setup a Database with the structure described in [SQL Style Ref
 | 
			
		||||
The login data to this database needs to be specified in the `api/Database.php` file.
 | 
			
		||||
 
 | 
			
		||||
## Usage
 | 
			
		||||
Now you can access your MediaCenter via your servers global ip (:
 | 
			
		||||
To index Videos run on your server: `php extractvideopreviews.php`.
 | 
			
		||||
 | 
			
		||||
At the settings tab you can set the correct videopath on server and click reindex afterwards. 
 | 
			
		||||
Now you can access your MediaCenter via the servers global ip (:
 | 
			
		||||
 | 
			
		||||
## Contact
 | 
			
		||||
Any contribution is appreciated. 
 | 
			
		||||
 
 | 
			
		||||
@@ -5,17 +5,19 @@
 | 
			
		||||
 *
 | 
			
		||||
 * Class with all neccessary stuff for the Database connections.
 | 
			
		||||
 */
 | 
			
		||||
class Database {
 | 
			
		||||
    private static $instance = null;
 | 
			
		||||
    private $conn;
 | 
			
		||||
class Database
 | 
			
		||||
{
 | 
			
		||||
    private static ?Database $instance = null;
 | 
			
		||||
    private mysqli $conn;
 | 
			
		||||
 | 
			
		||||
    private $servername = "127.0.0.1";
 | 
			
		||||
    private $username = "mediacenteruser";
 | 
			
		||||
    private $password = "mediapassword";
 | 
			
		||||
    private $dbname = "mediacenter";
 | 
			
		||||
    private string $servername = "192.168.0.30";
 | 
			
		||||
    private string $username = "root";
 | 
			
		||||
    private string $password = "1qayxsw2";
 | 
			
		||||
    private string $dbname = "mediacenter";
 | 
			
		||||
 | 
			
		||||
    // The db connection is established in the private constructor.
 | 
			
		||||
    private function __construct() {
 | 
			
		||||
    private function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        // Create connection
 | 
			
		||||
        $this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +32,8 @@ class Database {
 | 
			
		||||
     *
 | 
			
		||||
     * @return Database dbobject
 | 
			
		||||
     */
 | 
			
		||||
    public static function getInstance() {
 | 
			
		||||
    public static function getInstance()
 | 
			
		||||
    {
 | 
			
		||||
        if (!self::$instance) {
 | 
			
		||||
            self::$instance = new Database();
 | 
			
		||||
        }
 | 
			
		||||
@@ -43,7 +46,8 @@ class Database {
 | 
			
		||||
     *
 | 
			
		||||
     * @return mysqli mysqli instance
 | 
			
		||||
     */
 | 
			
		||||
    public function getConnection() {
 | 
			
		||||
    public function getConnection()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->conn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +55,7 @@ class Database {
 | 
			
		||||
     * get name of current active database
 | 
			
		||||
     * @return string name
 | 
			
		||||
     */
 | 
			
		||||
    public function getDatabaseName() {
 | 
			
		||||
    public function getDatabaseName(){
 | 
			
		||||
        return $this->dbname;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,10 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class TMDBMovie
 | 
			
		||||
 * class to handle all interactions with the tmdb api
 | 
			
		||||
 */
 | 
			
		||||
class TMDBMovie {
 | 
			
		||||
    public $picturebase = "https://image.tmdb.org/t/p/w500";
 | 
			
		||||
class TMDBMovie
 | 
			
		||||
{
 | 
			
		||||
    private $apikey = "9fd90530b11447f5646f8e6fb4733fb4";
 | 
			
		||||
    private $baseurl = "https://api.themoviedb.org/3/";
 | 
			
		||||
    public $picturebase = "https://image.tmdb.org/t/p/w500";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * search for a specific movie
 | 
			
		||||
@@ -15,25 +12,13 @@ class TMDBMovie {
 | 
			
		||||
     * @param string $moviename moviename
 | 
			
		||||
     * @return object movie object or null if not found
 | 
			
		||||
     */
 | 
			
		||||
    public function searchMovie(string $moviename, string $year = null) {
 | 
			
		||||
    public function searchMovie(string $moviename)
 | 
			
		||||
    {
 | 
			
		||||
        $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
 | 
			
		||||
            // todo maybe parse first pictures somehow
 | 
			
		||||
            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];
 | 
			
		||||
        }
 | 
			
		||||
@@ -44,7 +29,8 @@ class TMDBMovie {
 | 
			
		||||
     *
 | 
			
		||||
     * @return array of all available genres
 | 
			
		||||
     */
 | 
			
		||||
    public function getAllGenres() {
 | 
			
		||||
    public function getAllGenres()
 | 
			
		||||
    {
 | 
			
		||||
        $reply = json_decode(file_get_contents($this->baseurl . "genre/movie/list?api_key=" . $this->apikey));
 | 
			
		||||
        return $reply->genres;
 | 
			
		||||
    }
 | 
			
		||||
							
								
								
									
										31
									
								
								api/Tags.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/Tags.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
<?php
 | 
			
		||||
require 'Database.php';
 | 
			
		||||
 | 
			
		||||
$conn = Database::getInstance()->getConnection();
 | 
			
		||||
 | 
			
		||||
if (isset($_POST['action'])) {
 | 
			
		||||
    $action = $_POST['action'];
 | 
			
		||||
    switch ($action) {
 | 
			
		||||
        case "getAllTags":
 | 
			
		||||
            $query = "SELECT tag_name,tag_id from tags";
 | 
			
		||||
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $rows = array();
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($rows, $r);
 | 
			
		||||
            }
 | 
			
		||||
            echo json_encode($rows);
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "createTag":
 | 
			
		||||
            $query = "INSERT INTO tags (tag_name) VALUES ('" . $_POST['tagname'] . "')";
 | 
			
		||||
 | 
			
		||||
            if ($conn->query($query) === TRUE) {
 | 
			
		||||
                echo('{"result":"success"}');
 | 
			
		||||
            } else {
 | 
			
		||||
                echo('{"result":"' . $conn->error . '"}');
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +1,265 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once './src/Database.php';
 | 
			
		||||
require_once './src/TMDBMovie.php';
 | 
			
		||||
require_once './src/SSettings.php';
 | 
			
		||||
require_once './src/VideoParser.php';
 | 
			
		||||
require 'Database.php';
 | 
			
		||||
require 'TMDBMovie.php';
 | 
			
		||||
 | 
			
		||||
// allow UTF8 characters
 | 
			
		||||
setlocale(LC_ALL, 'en_US.UTF-8');
 | 
			
		||||
set_time_limit(3600);
 | 
			
		||||
writeLog("starting extraction!\n");
 | 
			
		||||
 | 
			
		||||
$vp = new VideoParser();
 | 
			
		||||
$vp->writeLog("starting extraction!!\n");
 | 
			
		||||
$ffmpeg = 'ffmpeg'; //or: /usr/bin/ffmpeg , or /usr/local/bin/ffmpeg - depends on your installation (type which ffmpeg into a console to find the install path)
 | 
			
		||||
$tmdb = new TMDBMovie();
 | 
			
		||||
// initial load of all available movie genres
 | 
			
		||||
$tmdbgenres = $tmdb->getAllGenres();
 | 
			
		||||
 | 
			
		||||
$sett = new SSettings();
 | 
			
		||||
$conn = Database::getInstance()->getConnection();
 | 
			
		||||
 | 
			
		||||
// load video path from settings
 | 
			
		||||
$scandir = "../" . $sett->getVideoPath();
 | 
			
		||||
$vp->extractVideos($scandir);
 | 
			
		||||
$scandir = "../videos/prn/";
 | 
			
		||||
$arr = scandir($scandir);
 | 
			
		||||
 | 
			
		||||
$all = 0;
 | 
			
		||||
$added = 0;
 | 
			
		||||
$deleted = 0;
 | 
			
		||||
$failed = 0;
 | 
			
		||||
 | 
			
		||||
foreach ($arr as $elem) {
 | 
			
		||||
    if ($elem != "." && $elem != "..") {
 | 
			
		||||
        if (strpos($elem, '.mp4') !== false) {
 | 
			
		||||
            $moviename = substr($elem, 0, -4);
 | 
			
		||||
 | 
			
		||||
            $query = "SELECT * FROM videos WHERE movie_name = '" . mysqli_real_escape_string($conn, $moviename) . "'";
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
 | 
			
		||||
            // insert if not available in db
 | 
			
		||||
            if (!mysqli_fetch_assoc($result)) {
 | 
			
		||||
                // try to fetch data from TMDB
 | 
			
		||||
                $poster = -1;
 | 
			
		||||
                $genres = -1;
 | 
			
		||||
                if (!is_null($dta = $tmdb->searchMovie($moviename))) {
 | 
			
		||||
                    $pic = file_get_contents($tmdb->picturebase . $dta->poster_path);
 | 
			
		||||
                    $poster = shell_exec("ffmpeg -hide_banner -loglevel panic -ss 00:04:00 -i \"../videos/prn/$elem\" -vframes 1 -q:v 2 -f singlejpeg pipe:1 2>/dev/null");
 | 
			
		||||
 | 
			
		||||
                    // error handling for download error
 | 
			
		||||
                    if (!$pic) {
 | 
			
		||||
                        $pic = $poster;
 | 
			
		||||
                        $poster = -1;
 | 
			
		||||
                        echo "Failed to load Picture from TMDB!  \n";
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $genres = $dta->genre_ids;
 | 
			
		||||
                } else {
 | 
			
		||||
                    echo "nothing found with TMDB!\n";
 | 
			
		||||
                    writeLog("nothing found with TMDB!\n");
 | 
			
		||||
                    $pic = shell_exec("ffmpeg -hide_banner -loglevel panic -ss 00:04:00 -i \"../videos/prn/$elem\" -vframes 1 -q:v 2 -f singlejpeg pipe:1 2>/dev/null");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //convert video to base64
 | 
			
		||||
                $image_base64 = base64_encode($pic);
 | 
			
		||||
                // add base64 fileinfo
 | 
			
		||||
                $image = 'data:image/jpeg;base64,' . $image_base64;
 | 
			
		||||
 | 
			
		||||
                // extract other video attributes
 | 
			
		||||
                $video_attributes = _get_video_attributes($elem);
 | 
			
		||||
                $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
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if ($poster != -1) {
 | 
			
		||||
                    $poster_base64 = 'data:image/jpeg;base64,' . base64_encode($poster);
 | 
			
		||||
 | 
			
		||||
                    $query = "INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) 
 | 
			
		||||
                            VALUES ('" . mysqli_real_escape_string($conn, $moviename) . "',
 | 
			
		||||
                            '" . mysqli_real_escape_string($conn, 'videos/prn/' . $elem) . "',
 | 
			
		||||
                            '$poster_base64',
 | 
			
		||||
                            '$image',
 | 
			
		||||
                            '$width',
 | 
			
		||||
                            '$duration')";
 | 
			
		||||
                } else {
 | 
			
		||||
                    $query = "INSERT INTO videos(movie_name,movie_url,thumbnail,quality,length) 
 | 
			
		||||
                            VALUES ('" . mysqli_real_escape_string($conn, $moviename) . "',
 | 
			
		||||
                            '" . mysqli_real_escape_string($conn, 'videos/prn/' . $elem) . "',
 | 
			
		||||
                            '$image',
 | 
			
		||||
                            '$width',
 | 
			
		||||
                            '$duration')";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if ($conn->query($query) === TRUE) {
 | 
			
		||||
                    echo('successfully added ' . $elem . " to video gravity\n");
 | 
			
		||||
                    writeLog('successfully added ' . $elem . " to video gravity\n");
 | 
			
		||||
 | 
			
		||||
                    // add this entry to the default tags
 | 
			
		||||
                    $last_id = $conn->insert_id;
 | 
			
		||||
 | 
			
		||||
                    // full hd
 | 
			
		||||
                    if ($width >= 1900) {
 | 
			
		||||
                        $query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($last_id,2)";
 | 
			
		||||
                        if ($conn->query($query) !== TRUE) {
 | 
			
		||||
                            echo "failed to add default tag here.\n";
 | 
			
		||||
                            writeLog("failed to add default tag here.\n");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // HD
 | 
			
		||||
                    if ($width >= 1250 && $width < 1900) {
 | 
			
		||||
                        $query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($last_id,4)";
 | 
			
		||||
                        if ($conn->query($query) !== TRUE) {
 | 
			
		||||
                            echo "failed to add default tag here.\n";
 | 
			
		||||
                            writeLog("failed to add default tag here.\n");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // SD
 | 
			
		||||
                    if ($width < 1250 && $width > 0) {
 | 
			
		||||
                        $query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($last_id,3)";
 | 
			
		||||
                        if ($conn->query($query) !== TRUE) {
 | 
			
		||||
                            echo "failed to add default tag here.\n";
 | 
			
		||||
                            writeLog("failed to add default tag here.\n");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // 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($tmdbgenres, 'name', 'id')[$genreid];
 | 
			
		||||
                            $tagid = tagExists($tagname);
 | 
			
		||||
 | 
			
		||||
                            $query = "INSERT INTO video_tags(video_id,tag_id) VALUES ($last_id,$tagid)";
 | 
			
		||||
                            if ($conn->query($query) !== TRUE) {
 | 
			
		||||
                                echo "failed to add $genreid tag here.\n";
 | 
			
		||||
                                writeLog("failed to add $genreid tag here.\n");
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    $added++;
 | 
			
		||||
                    $all++;
 | 
			
		||||
                } else {
 | 
			
		||||
                    echo('errored item: ' . $elem . "\n");
 | 
			
		||||
                    writeLog('errored item: ' . $elem . "\n");
 | 
			
		||||
                    echo('{"data":"' . $conn->error . '"}\n');
 | 
			
		||||
                    writeLog('{"data":"' . $conn->error . '"}\n');
 | 
			
		||||
                    $failed++;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                $all++;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            echo($elem . " does not contain a .mp4 extension! - skipping \n");
 | 
			
		||||
            writeLog($elem . " does not contain a .mp4 extension! - skipping \n");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// auto cleanup db entries
 | 
			
		||||
$query = "SELECT COUNT(*) as count FROM videos";
 | 
			
		||||
$result = $conn->query($query);
 | 
			
		||||
$r = mysqli_fetch_assoc($result);
 | 
			
		||||
 | 
			
		||||
if ($all < $r['count']) {
 | 
			
		||||
    echo "should be in gravity: " . $all . "\n";
 | 
			
		||||
    writeLog("should be in gravity: " . $all . "\n");
 | 
			
		||||
    echo "really in gravity: " . $r['count'] . "\n";
 | 
			
		||||
    writeLog("really in gravity: " . $r['count'] . "\n");
 | 
			
		||||
    echo "cleaning up gravity\n";
 | 
			
		||||
    writeLog("cleaning up gravity\n");
 | 
			
		||||
 | 
			
		||||
    $query = "SELECT movie_id,movie_url FROM videos";
 | 
			
		||||
    $result = $conn->query($query);
 | 
			
		||||
 | 
			
		||||
    while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
        if (!file_exists("../" . $r['movie_url'])) {
 | 
			
		||||
            $query = "DELETE FROM videos WHERE movie_id='" . $r['movie_id'] . "'";
 | 
			
		||||
            if ($conn->query($query) === TRUE) {
 | 
			
		||||
                echo('successfully deleted ' . $r['movie_url'] . " from video gravity\n");
 | 
			
		||||
                writeLog('successfully deleted ' . $r['movie_url'] . " from video gravity\n");
 | 
			
		||||
                $deleted++;
 | 
			
		||||
            } else {
 | 
			
		||||
                echo "failed to delete " . $r['movie_url'] . " from gravity: " . $conn->error . "\n";
 | 
			
		||||
                writeLog("failed to delete " . $r['movie_url'] . " from gravity: " . $conn->error . "\n");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 = $conn->query($query);
 | 
			
		||||
if ($result->num_rows == 1) {
 | 
			
		||||
    $row = $result->fetch_assoc();
 | 
			
		||||
    $size = $row["Size"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
echo "Total gravity: " . $all . "\n";
 | 
			
		||||
writeLog("Total gravity: " . $all . "\n");
 | 
			
		||||
echo "Size of Databse is: " . $size . "MB\n";
 | 
			
		||||
writeLog("Size of Databse is: " . $size . "MB\n");
 | 
			
		||||
echo "added in this run: " . $added . "\n";
 | 
			
		||||
writeLog("added in this run: " . $added . "\n");
 | 
			
		||||
echo "deleted in this run: " . $deleted . "\n";
 | 
			
		||||
writeLog("deleted in this run: " . $deleted . "\n");
 | 
			
		||||
echo "errored in this run: " . $failed . "\n";
 | 
			
		||||
writeLog("errored in this run: " . $failed . "\n");
 | 
			
		||||
 | 
			
		||||
writeLog("-42"); // terminating characters to stop webui requesting infos
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * get all videoinfos of a video file
 | 
			
		||||
 *
 | 
			
		||||
 * @param $video string name including extension
 | 
			
		||||
 * @return object all infos as object
 | 
			
		||||
 */
 | 
			
		||||
function _get_video_attributes($video)
 | 
			
		||||
{
 | 
			
		||||
    $command = "mediainfo \"../videos/prn/$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
 | 
			
		||||
 */
 | 
			
		||||
function writeLog(string $message)
 | 
			
		||||
{
 | 
			
		||||
    file_put_contents("/tmp/output.log", $message, FILE_APPEND);
 | 
			
		||||
    flush();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ckecks if tag exists -- if not creates it
 | 
			
		||||
 * @param string $tagname the name of the tag
 | 
			
		||||
 * @return integer the id of the inserted tag
 | 
			
		||||
 */
 | 
			
		||||
function tagExists(string $tagname)
 | 
			
		||||
{
 | 
			
		||||
    global $conn;
 | 
			
		||||
 | 
			
		||||
    $query = "SELECT * FROM tags WHERE tag_name='$tagname'";
 | 
			
		||||
 | 
			
		||||
    $result = $conn->query($query);
 | 
			
		||||
    if ($result->num_rows == 0) {
 | 
			
		||||
        // tag does not exist --> create it
 | 
			
		||||
        $query = "INSERT INTO tags (tag_name) VALUES ('$tagname')";
 | 
			
		||||
        if ($conn->query($query) !== TRUE) {
 | 
			
		||||
            echo "failed to create $tagname tag in database\n";
 | 
			
		||||
            writeLog("failed to create $tagname tag in database\n");
 | 
			
		||||
        }
 | 
			
		||||
        return $conn->insert_id;
 | 
			
		||||
    } else {
 | 
			
		||||
        return $result->fetch_assoc()['tag_id'];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,17 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once './src/handlers/Settings.php';
 | 
			
		||||
require 'Database.php';
 | 
			
		||||
 | 
			
		||||
$sett = new Settings();
 | 
			
		||||
$sett->handleAction();
 | 
			
		||||
$conn = Database::getInstance()->getConnection();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if (isset($_POST['action'])) {
 | 
			
		||||
    $action = $_POST['action'];
 | 
			
		||||
    switch ($action) {
 | 
			
		||||
        case "isPasswordNeeded":
 | 
			
		||||
            echo '{"password": true}';
 | 
			
		||||
            break;
 | 
			
		||||
        case "checkPassword":
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,332 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once './src/Database.php';
 | 
			
		||||
require_once './src/TMDBMovie.php';
 | 
			
		||||
require_once './src/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++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once 'src/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 {
 | 
			
		||||
            echo('{data:"error"}');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send response message and exit script
 | 
			
		||||
     * @param $message string the response message
 | 
			
		||||
     */
 | 
			
		||||
    function commitMessage($message){
 | 
			
		||||
        echo $message;
 | 
			
		||||
        exit(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * add the action handlers in this abstract method
 | 
			
		||||
     */
 | 
			
		||||
    abstract function initHandlers();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,82 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once 'RequestBase.php';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Settings
 | 
			
		||||
 * Backend for the Settings page
 | 
			
		||||
 */
 | 
			
		||||
class Settings extends RequestBase {
 | 
			
		||||
    function initHandlers() {
 | 
			
		||||
        $this->getFromDB();
 | 
			
		||||
        $this->saveToDB();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 = "SELECT * from settings";
 | 
			
		||||
 | 
			
		||||
            $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('{"success": true}');
 | 
			
		||||
            } else {
 | 
			
		||||
                $this->commitMessage('{"success": true}');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,66 +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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 addToDB(){
 | 
			
		||||
        /**
 | 
			
		||||
         * creates a new tag
 | 
			
		||||
         * query requirements:
 | 
			
		||||
         * * tagname -- name of the new tag
 | 
			
		||||
         */
 | 
			
		||||
        $this->addActionHandler("createTag", function () {
 | 
			
		||||
            $query = "INSERT 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'];
 | 
			
		||||
 | 
			
		||||
            $query = "INSERT 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 . '"}');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
include_once './src/handlers/Tags.php';
 | 
			
		||||
 | 
			
		||||
$tags = new Tags();
 | 
			
		||||
$tags->handleAction();
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
include_once './src/handlers/Video.php';
 | 
			
		||||
 | 
			
		||||
$video = new Video();
 | 
			
		||||
$video->handleAction();
 | 
			
		||||
@@ -1,32 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
require_once 'src/SSettings.php';
 | 
			
		||||
require_once 'RequestBase.php';
 | 
			
		||||
require 'Database.php';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Video
 | 
			
		||||
 * backend for all interactions with videoloads and receiving of video infos
 | 
			
		||||
 */
 | 
			
		||||
class Video extends RequestBase {
 | 
			
		||||
    private $videopath;
 | 
			
		||||
$conn = Database::getInstance()->getConnection();
 | 
			
		||||
 | 
			
		||||
    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";
 | 
			
		||||
//$_POST['action'] = "getRandomMovies";$_POST['number'] =6;
 | 
			
		||||
if (isset($_POST['action'])) {
 | 
			
		||||
    $action = $_POST['action'];
 | 
			
		||||
    switch ($action) {
 | 
			
		||||
        case "getMovies":
 | 
			
		||||
            $query = "SELECT movie_id,movie_name FROM videos ORDER BY likes DESC, create_date DESC, movie_name ASC";
 | 
			
		||||
            if (isset($_POST['tag'])) {
 | 
			
		||||
                $tag = $_POST['tag'];
 | 
			
		||||
                if ($_POST['tag'] != "all") {
 | 
			
		||||
@@ -34,22 +16,21 @@ class Video extends RequestBase {
 | 
			
		||||
                            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 = '$tag'
 | 
			
		||||
                            ORDER BY likes DESC, create_date, movie_name";
 | 
			
		||||
                            ORDER BY likes DESC, create_date ASC, movie_name ASC";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $rows = array();
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($rows, $r);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage(json_encode($rows));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->addActionHandler("getRandomMovies", function () {
 | 
			
		||||
            echo(json_encode($rows));
 | 
			
		||||
            break;
 | 
			
		||||
        case "getRandomMovies":
 | 
			
		||||
            $return = new stdClass();
 | 
			
		||||
            $query = "SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT " . $_POST['number'];
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $return->rows = array();
 | 
			
		||||
 | 
			
		||||
            // get tags of random videos
 | 
			
		||||
@@ -66,41 +47,32 @@ class Video extends RequestBase {
 | 
			
		||||
                        INNER JOIN tags t on video_tags.tag_id = t.tag_id
 | 
			
		||||
                        WHERE $idstring
 | 
			
		||||
                        GROUP BY t.tag_name";
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($return->tags, $r);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage(json_encode($return));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->addActionHandler("getSearchKeyWord", function () {
 | 
			
		||||
            echo(json_encode($return));
 | 
			
		||||
            break;
 | 
			
		||||
        case "getSearchKeyWord":
 | 
			
		||||
            $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);
 | 
			
		||||
                        ORDER BY likes DESC, create_date DESC, movie_name ASC";
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $rows = array();
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($rows, $r);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage(json_encode($rows));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
            echo(json_encode($rows));
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * function to handle stuff for loading specific videos and startdata
 | 
			
		||||
     */
 | 
			
		||||
    private function loadVideos() {
 | 
			
		||||
        $this->addActionHandler("loadVideo", function () {
 | 
			
		||||
            $video_id = $_POST['movieid'];
 | 
			
		||||
            break;
 | 
			
		||||
        case "loadVideo":
 | 
			
		||||
            $query = "SELECT movie_name,movie_id,movie_url,thumbnail,poster,likes,quality,length FROM videos WHERE movie_id='" . $_POST['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);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $row = $result->fetch_assoc();
 | 
			
		||||
 | 
			
		||||
            $arr = array();
 | 
			
		||||
@@ -112,10 +84,8 @@ class Video extends RequestBase {
 | 
			
		||||
 | 
			
		||||
            $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["movie_url"] = str_replace("?","%3F",$row["movie_url"]);
 | 
			
		||||
            $arr["likes"] = $row["likes"];
 | 
			
		||||
            $arr["quality"] = $row["quality"];
 | 
			
		||||
            $arr["length"] = $row["length"];
 | 
			
		||||
 | 
			
		||||
@@ -123,42 +93,71 @@ class Video extends RequestBase {
 | 
			
		||||
            $arr['tags'] = array();
 | 
			
		||||
            $query = "SELECT t.tag_name FROM video_tags 
 | 
			
		||||
                        INNER JOIN tags t on video_tags.tag_id = t.tag_id
 | 
			
		||||
                        WHERE video_tags.video_id=$video_id
 | 
			
		||||
                        WHERE video_tags.video_id=" . $_POST['movieid'] . "
 | 
			
		||||
                        GROUP BY t.tag_name";
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $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);
 | 
			
		||||
            echo(json_encode($arr));
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        case "getDbSize":
 | 
			
		||||
            $query = "SELECT table_schema AS \"Database\", 
 | 
			
		||||
                        ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS \"Size\" 
 | 
			
		||||
                        FROM information_schema.TABLES 
 | 
			
		||||
                        WHERE TABLE_SCHEMA='hub'
 | 
			
		||||
                        GROUP BY table_schema;";
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
 | 
			
		||||
            if ($result->num_rows == 1) {
 | 
			
		||||
                $row = $result->fetch_assoc();
 | 
			
		||||
                echo '{"data":"' . $row["Size"] . 'MB"}';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage(json_encode($arr));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->addActionHandler("readThumbnail", function () {
 | 
			
		||||
            break;
 | 
			
		||||
        case "readThumbnail":
 | 
			
		||||
            $query = "SELECT thumbnail FROM videos WHERE movie_id='" . $_POST['movieid'] . "'";
 | 
			
		||||
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $row = $result->fetch_assoc();
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage($row["thumbnail"]);
 | 
			
		||||
        });
 | 
			
		||||
            echo($row["thumbnail"]);
 | 
			
		||||
 | 
			
		||||
        $this->addActionHandler("getStartData", function () {
 | 
			
		||||
            break;
 | 
			
		||||
        case "getTags":
 | 
			
		||||
            // todo add this to loadVideo maybe
 | 
			
		||||
            $movieid = $_POST['movieid'];
 | 
			
		||||
 | 
			
		||||
            $query = "SELECT tag_name FROM video_tags 
 | 
			
		||||
                        INNER JOIN tags t on video_tags.tag_id = t.tag_id 
 | 
			
		||||
                        WHERE video_id='$movieid'";
 | 
			
		||||
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
 | 
			
		||||
            $rows = array();
 | 
			
		||||
            $rows['tags'] = array();
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($rows['tags'], $r['tag_name']);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            echo(json_encode($rows));
 | 
			
		||||
            break;
 | 
			
		||||
        case "addLike":
 | 
			
		||||
            $movieid = $_POST['movieid'];
 | 
			
		||||
 | 
			
		||||
            $query = "update videos set likes = likes + 1 where movie_id = '$movieid'";
 | 
			
		||||
 | 
			
		||||
            if ($conn->query($query) === TRUE) {
 | 
			
		||||
                echo('{"result":"success"}');
 | 
			
		||||
            } else {
 | 
			
		||||
                echo('{"result":"' . $conn->error . '"}');
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "getStartData":
 | 
			
		||||
            $query = "SELECT COUNT(*) as nr FROM videos";
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
 | 
			
		||||
            $arr = array();
 | 
			
		||||
@@ -167,7 +166,7 @@ class Video extends RequestBase {
 | 
			
		||||
            $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);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
            $arr['tagged'] = $r['nr'];
 | 
			
		||||
 | 
			
		||||
@@ -175,7 +174,7 @@ class Video extends RequestBase {
 | 
			
		||||
                        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);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
            $arr['hd'] = $r['nr'];
 | 
			
		||||
 | 
			
		||||
@@ -183,7 +182,7 @@ class Video extends RequestBase {
 | 
			
		||||
                        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);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
            $arr['fullhd'] = $r['nr'];
 | 
			
		||||
 | 
			
		||||
@@ -191,46 +190,43 @@ class Video extends RequestBase {
 | 
			
		||||
                        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);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
            $arr['sd'] = $r['nr'];
 | 
			
		||||
 | 
			
		||||
            $query = "SELECT COUNT(*) as nr FROM tags";
 | 
			
		||||
            $result = $this->conn->query($query);
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
            $r = mysqli_fetch_assoc($result);
 | 
			
		||||
            $arr['tags'] = $r['nr'];
 | 
			
		||||
 | 
			
		||||
            $this->commitMessage(json_encode($arr));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
            echo(json_encode($arr));
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * function to handle api handlers for stuff to add to video or database
 | 
			
		||||
     */
 | 
			
		||||
    private function addToVideo() {
 | 
			
		||||
        $this->addActionHandler("addLike", function () {
 | 
			
		||||
        case "getAllTags":
 | 
			
		||||
            $query = "SELECT tag_name,tag_id from tags";
 | 
			
		||||
            $result = $conn->query($query);
 | 
			
		||||
 | 
			
		||||
            $rows = array();
 | 
			
		||||
            while ($r = mysqli_fetch_assoc($result)) {
 | 
			
		||||
                array_push($rows, $r);
 | 
			
		||||
            }
 | 
			
		||||
            echo(json_encode($rows));
 | 
			
		||||
            break;
 | 
			
		||||
        case "addTag":
 | 
			
		||||
            $movieid = $_POST['movieid'];
 | 
			
		||||
            $tagid = $_POST['id'];
 | 
			
		||||
 | 
			
		||||
            $query = "update videos set likes = likes + 1 where movie_id = '$movieid'";
 | 
			
		||||
            $query = "INSERT INTO video_tags(tag_id, video_id) VALUES ('$tagid','$movieid')";
 | 
			
		||||
 | 
			
		||||
            if ($this->conn->query($query) === TRUE) {
 | 
			
		||||
                $this->commitMessage('{"result":"success"}');
 | 
			
		||||
            if ($conn->query($query) === TRUE) {
 | 
			
		||||
                echo('{"result":"success"}');
 | 
			
		||||
            } else {
 | 
			
		||||
                $this->commitMessage('{"result":"' . $this->conn->error . '"}');
 | 
			
		||||
                echo('{"result":"' . $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 . '"}');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
} else {
 | 
			
		||||
    echo('{data:"error"}');
 | 
			
		||||
}
 | 
			
		||||
return;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								database.sql
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								database.sql
									
									
									
									
									
								
							@@ -27,25 +27,11 @@ create table if not exists video_tags
 | 
			
		||||
        foreign key (tag_id) references tags (tag_id),
 | 
			
		||||
    constraint video_tags_videos_movie_id_fk
 | 
			
		||||
        foreign key (video_id) references videos (movie_id)
 | 
			
		||||
            on delete cascade
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create table if not exists settings
 | 
			
		||||
(
 | 
			
		||||
    video_path       varchar(255)                          null,
 | 
			
		||||
    episode_path     varchar(255)                          null,
 | 
			
		||||
    password         varchar(32) default '-1'              null,
 | 
			
		||||
    mediacenter_name varchar(32) default 'OpenMediaCenter' null,
 | 
			
		||||
    TMDB_grabbing    tinyint           null,
 | 
			
		||||
    DarkMode         tinyint default 0 null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT IGNORE INTO tags (tag_id, tag_name)
 | 
			
		||||
INSERT INTO tags (tag_id, tag_name)
 | 
			
		||||
VALUES (2, 'fullhd');
 | 
			
		||||
INSERT IGNORE INTO tags (tag_id, tag_name)
 | 
			
		||||
INSERT INTO tags (tag_id, tag_name)
 | 
			
		||||
VALUES (3, 'lowquality');
 | 
			
		||||
INSERT IGNORE INTO tags (tag_id, tag_name)
 | 
			
		||||
INSERT INTO tags (tag_id, tag_name)
 | 
			
		||||
VALUES (4, 'hd');
 | 
			
		||||
 | 
			
		||||
INSERT IGNORE INTO settings (video_path, episode_path, password, mediacenter_name)
 | 
			
		||||
VALUES ('./videos/', './tvshows/', -1, 'OpenMediaCenter');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
Package: OpenMediaCenter
 | 
			
		||||
Version: 0.1
 | 
			
		||||
Depends: nginx, php-fpm, php-mysqli, mariadb-server
 | 
			
		||||
Section: web
 | 
			
		||||
Priority: optional
 | 
			
		||||
Architecture: all
 | 
			
		||||
Essential: no
 | 
			
		||||
Installed-Size: 1024
 | 
			
		||||
Maintainer: heili.eu
 | 
			
		||||
Description: OpenMediaCenter
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
# enable nginx site
 | 
			
		||||
rm /etc/nginx/sites-enabled/OpenMediaCenter.conf
 | 
			
		||||
ln -s /etc/nginx/sites-available/OpenMediaCenter.conf /etc/nginx/sites-enabled/OpenMediaCenter.conf
 | 
			
		||||
 | 
			
		||||
# link general socket to current one
 | 
			
		||||
rm /var/run/php-fpm.sock
 | 
			
		||||
ln -s /var/run/php/php*-fpm.sock /var/run/php-fpm.sock
 | 
			
		||||
 | 
			
		||||
# 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';"
 | 
			
		||||
mysql -uroot -pPASS -e "GRANT ALL PRIVILEGES ON mediacenter . * TO 'mediacenteruser'@'localhost';"
 | 
			
		||||
mysql -u mediacenteruser -pmediapassword mediacenter < /tmp/openmediacenter.sql
 | 
			
		||||
 | 
			
		||||
# removed unused sql style file
 | 
			
		||||
rm /tmp/openmediacenter.sql
 | 
			
		||||
 | 
			
		||||
# correct user rights
 | 
			
		||||
chown -R www-data:www-data /var/www/openmediacenter
 | 
			
		||||
 | 
			
		||||
# restart services
 | 
			
		||||
systemctl restart nginx
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
#preset db password
 | 
			
		||||
debconf-set-selections <<< 'mariadb-server-10.0 mysql-server/root_password password PASS'
 | 
			
		||||
debconf-set-selections <<< 'mariadb-server-10.0 mysql-server/root_password_again password PASS'
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
server {
 | 
			
		||||
    listen 8080 default_server;
 | 
			
		||||
    listen [::]:8080 default_server;
 | 
			
		||||
 | 
			
		||||
    location ~ \.php$ {
 | 
			
		||||
        include snippets/fastcgi-php.conf;
 | 
			
		||||
 | 
			
		||||
        fastcgi_pass unix:/var/run/php-fpm.sock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    root /var/www/openmediacenter;
 | 
			
		||||
 | 
			
		||||
    index index.html;
 | 
			
		||||
 | 
			
		||||
    access_log /var/log/nginx/openmediacenter.access.log;
 | 
			
		||||
    error_log /var/log/nginx/openmediacenter.error.log;
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        try_files $uri $uri/ =404;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -27,7 +27,7 @@
 | 
			
		||||
      "text-summary"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "proxy": "http://192.168.0.42",
 | 
			
		||||
  "proxy": "http://192.168.0.248",
 | 
			
		||||
  "homepage": "/",
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": "react-app"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										147
									
								
								src/App.js
									
									
									
									
									
								
							
							
						
						
									
										147
									
								
								src/App.js
									
									
									
									
									
								
							@@ -1,143 +1,136 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import "./css/App.css"
 | 
			
		||||
import HomePage from "./pages/HomePage/HomePage";
 | 
			
		||||
import RandomPage from "./pages/RandomPage/RandomPage";
 | 
			
		||||
import GlobalInfos from "./GlobalInfos";
 | 
			
		||||
 | 
			
		||||
// include bootstraps css
 | 
			
		||||
import 'bootstrap/dist/css/bootstrap.min.css';
 | 
			
		||||
import style from './App.module.css'
 | 
			
		||||
 | 
			
		||||
import SettingsPage from "./pages/SettingsPage/SettingsPage";
 | 
			
		||||
import CategoryPage from "./pages/CategoryPage/CategoryPage";
 | 
			
		||||
import {Spinner} from "react-bootstrap";
 | 
			
		||||
import LoginPage from "./pages/LoginPage/LoginPage";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The main App handles the main tabs and which content to show
 | 
			
		||||
 */
 | 
			
		||||
class App extends React.Component {
 | 
			
		||||
    newElement = null;
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
        this.state = {
 | 
			
		||||
            page: "default",
 | 
			
		||||
            generalSettingsLoaded: false,
 | 
			
		||||
            passwordsupport: null,
 | 
			
		||||
            mediacentername: "OpenMediaCenter"
 | 
			
		||||
        };
 | 
			
		||||
        this.state = {page: "unverified"};
 | 
			
		||||
 | 
			
		||||
        // bind this to the method for being able to call methods such as this.setstate
 | 
			
		||||
        this.changeRootElement = this.changeRootElement.bind(this);
 | 
			
		||||
        this.returnToLastElement = this.returnToLastElement.bind(this);
 | 
			
		||||
        this.showVideo = this.showVideo.bind(this);
 | 
			
		||||
        this.hideVideo = this.hideVideo.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'loadInitialData');
 | 
			
		||||
    videoelement = null;
 | 
			
		||||
 | 
			
		||||
        fetch('/api/settings.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    // set theme
 | 
			
		||||
                    GlobalInfos.enableDarkTheme(result.DarkMode);
 | 
			
		||||
 | 
			
		||||
                    this.setState({
 | 
			
		||||
                        generalSettingsLoaded: true,
 | 
			
		||||
                        passwordsupport: result.passwordEnabled,
 | 
			
		||||
                        mediacentername: result.mediacenter_name
 | 
			
		||||
                    });
 | 
			
		||||
                    // set tab title to received mediacenter name
 | 
			
		||||
                    document.title = result.mediacenter_name;
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create a viewbinding to call APP functions from child elements
 | 
			
		||||
     * @returns a set of callback functions
 | 
			
		||||
     */
 | 
			
		||||
    constructViewBinding() {
 | 
			
		||||
        return {
 | 
			
		||||
            changeRootElement: this.changeRootElement,
 | 
			
		||||
            returnToLastElement: this.returnToLastElement
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * load the selected component into the main view
 | 
			
		||||
     * @returns {JSX.Element} body element of selected page
 | 
			
		||||
     */
 | 
			
		||||
    MainBody() {
 | 
			
		||||
        let page;
 | 
			
		||||
        if (this.state.page === "default") {
 | 
			
		||||
            page = <HomePage viewbinding={this.constructViewBinding()}/>;
 | 
			
		||||
            page = <HomePage viewbinding={{showVideo: this.showVideo, hideVideo: this.hideVideo}}/>;
 | 
			
		||||
            this.mypage = page;
 | 
			
		||||
        } else if (this.state.page === "random") {
 | 
			
		||||
            page = <RandomPage viewbinding={this.constructViewBinding()}/>;
 | 
			
		||||
            page = <RandomPage viewbinding={{showVideo: this.showVideo, hideVideo: this.hideVideo}}/>;
 | 
			
		||||
            this.mypage = page;
 | 
			
		||||
        } else if (this.state.page === "settings") {
 | 
			
		||||
            page = <SettingsPage/>;
 | 
			
		||||
            this.mypage = page;
 | 
			
		||||
        } else if (this.state.page === "categories") {
 | 
			
		||||
            page = <CategoryPage viewbinding={this.constructViewBinding()}/>;
 | 
			
		||||
            page = <CategoryPage viewbinding={{showVideo: this.showVideo, hideVideo: this.hideVideo}}/>;
 | 
			
		||||
            this.mypage = page;
 | 
			
		||||
        } else if (this.state.page === "video") {
 | 
			
		||||
            // show videoelement if neccessary
 | 
			
		||||
            page = this.newElement;
 | 
			
		||||
            page = this.videoelement;
 | 
			
		||||
 | 
			
		||||
            console.log(page);
 | 
			
		||||
        } else if (this.state.page === "lastpage") {
 | 
			
		||||
            // return back to last page
 | 
			
		||||
            page = this.mypage;
 | 
			
		||||
        } else if (this.state.page === "loginpage") {
 | 
			
		||||
            // return back to last page
 | 
			
		||||
            page = <LoginPage/>;
 | 
			
		||||
        } else if (this.state.page === "unverified") {
 | 
			
		||||
            // return back to last page
 | 
			
		||||
            page =
 | 
			
		||||
                <div className='loadSpinner'>
 | 
			
		||||
                    <Spinner style={{marginLeft: "40px", marginBottom: "20px"}} animation="border" role="status">
 | 
			
		||||
                        <span className="sr-only">Loading...</span>
 | 
			
		||||
                    </Spinner>
 | 
			
		||||
                    <div>Content loading...</div>
 | 
			
		||||
                </div>;
 | 
			
		||||
        } else {
 | 
			
		||||
            page = <div>unimplemented yet!</div>;
 | 
			
		||||
        }
 | 
			
		||||
        return (page);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        // add the main theme to the page body
 | 
			
		||||
        document.body.className = themeStyle.backgroundcolor;
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={style.app}>
 | 
			
		||||
                <div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}>
 | 
			
		||||
                    <div className={style.navbrand}>{this.state.mediacentername}</div>
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append("action", "isPasswordNeeded");
 | 
			
		||||
 | 
			
		||||
                    <div className={[style.navitem, themeStyle.navitem, this.state.page === "default" ? style.navitemselected : {}].join(' ')}
 | 
			
		||||
        fetch('/api/settings.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                        if (result.password === false) {
 | 
			
		||||
                            this.setState({page: "default"});
 | 
			
		||||
                        } else {
 | 
			
		||||
                            this.setState({page: "loginpage"});
 | 
			
		||||
                        }
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="App">
 | 
			
		||||
                <nav className="navbar navbar-expand-sm bg-primary navbar-dark">
 | 
			
		||||
                    <div className="navbar-brand">OpenMediaCenter</div>
 | 
			
		||||
 | 
			
		||||
                    <ul className="navbar-nav">
 | 
			
		||||
                        <li className="nav-item">
 | 
			
		||||
                            <div className="nav-link"
 | 
			
		||||
                                 style={this.state.page === "default" ? {color: "rgba(255,255,255,.75"} : {}}
 | 
			
		||||
                                 onClick={() => this.setState({page: "default"})}>Home
 | 
			
		||||
                            </div>
 | 
			
		||||
                    <div className={[style.navitem, themeStyle.navitem, this.state.page === "random" ? style.navitemselected : {}].join(' ')}
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li className="nav-item">
 | 
			
		||||
                            <div className="nav-link"
 | 
			
		||||
                                 style={this.state.page === "random" ? {color: "rgba(255,255,255,.75"} : {}}
 | 
			
		||||
                                 onClick={() => this.setState({page: "random"})}>Random Video
 | 
			
		||||
                            </div>
 | 
			
		||||
                    <div className={[style.navitem, themeStyle.navitem, this.state.page === "categories" ? style.navitemselected : {}].join(' ')}
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li className="nav-item">
 | 
			
		||||
                            <div className="nav-link"
 | 
			
		||||
                                 style={this.state.page === "categories" ? {color: "rgba(255,255,255,.75"} : {}}
 | 
			
		||||
                                 onClick={() => this.setState({page: "categories"})}>Categories
 | 
			
		||||
                            </div>
 | 
			
		||||
                    <div className={[style.navitem, themeStyle.navitem, this.state.page === "settings" ? style.navitemselected : {}].join(' ')}
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li className="nav-item">
 | 
			
		||||
                            <div className="nav-link"
 | 
			
		||||
                                 style={this.state.page === "settings" ? {color: "rgba(255,255,255,.75"} : {}}
 | 
			
		||||
                                 onClick={() => this.setState({page: "settings"})}>Settings
 | 
			
		||||
                            </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {this.state.generalSettingsLoaded ? this.MainBody() : "loading"}
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </nav>
 | 
			
		||||
                {this.MainBody()}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * render a new root element into the main body
 | 
			
		||||
     */
 | 
			
		||||
    changeRootElement(element) {
 | 
			
		||||
        this.newElement = element;
 | 
			
		||||
    showVideo(element) {
 | 
			
		||||
        this.videoelement = element;
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            page: "video"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * return from page to the previous page before a change
 | 
			
		||||
     */
 | 
			
		||||
    returnToLastElement() {
 | 
			
		||||
    hideVideo() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            page: "lastpage"
 | 
			
		||||
        });
 | 
			
		||||
        this.element = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
.app {
 | 
			
		||||
    user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navitem {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    float: left;
 | 
			
		||||
 | 
			
		||||
    font-size: large;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
    text-transform: capitalize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navitem:hover {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transition: opacity .5s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navitem::after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: 2px;
 | 
			
		||||
    transition: width .3s;
 | 
			
		||||
    width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navitem:hover::after {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.navitemselected {
 | 
			
		||||
    opacity: 0.85;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navcontainer {
 | 
			
		||||
    border-bottom-width: 2px;
 | 
			
		||||
    border-style: dotted;
 | 
			
		||||
    border-width: 0;
 | 
			
		||||
 | 
			
		||||
    padding-bottom: 40px;
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbrand {
 | 
			
		||||
    float: left;
 | 
			
		||||
    font-size: large;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
    text-transform: capitalize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -10,99 +10,69 @@ describe('<App/>', function () {
 | 
			
		||||
 | 
			
		||||
    it('renders title', () => {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter');
 | 
			
		||||
        expect(wrapper.find('.navbar-brand').text()).toBe('OpenMediaCenter');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('are navlinks correct', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        expect(wrapper.find('.navitem')).toHaveLength(4);
 | 
			
		||||
        expect(wrapper.find('nav').find('li')).toHaveLength(4);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('simulate video view change ', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        wrapper.instance().changeRootElement(<div id='testit'/>);
 | 
			
		||||
        wrapper.instance().showVideo(<div id='testit'></div>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("#testit")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test hide video again', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        wrapper.instance().changeRootElement(<div id='testit'/>);
 | 
			
		||||
        wrapper.instance().showVideo(<div id='testit'></div>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("#testit")).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        wrapper.instance().returnToLastElement();
 | 
			
		||||
        wrapper.instance().hideVideo();
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("HomePage")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test fallback to last loaded page', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        wrapper.find(".navitem").findWhere(t => t.text() === "Random Video" && t.type() === "div").simulate("click");
 | 
			
		||||
        wrapper.find(".nav-link").findWhere(t => t.text() === "Random Video" && t.type() === "div").simulate("click");
 | 
			
		||||
 | 
			
		||||
        wrapper.instance().changeRootElement(<div id='testit'/>);
 | 
			
		||||
        wrapper.instance().showVideo(<div id='testit'></div>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("#testit")).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        wrapper.instance().returnToLastElement();
 | 
			
		||||
        wrapper.instance().hideVideo();
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("RandomPage")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test home click', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        wrapper.setState({page: "wrongvalue"});
 | 
			
		||||
        expect(wrapper.find("HomePage")).toHaveLength(0);
 | 
			
		||||
        wrapper.find(".navitem").findWhere(t => t.text() === "Home" && t.type() === "div").simulate("click");
 | 
			
		||||
        wrapper.find(".nav-link").findWhere(t => t.text() === "Home" && t.type() === "div").simulate("click");
 | 
			
		||||
        expect(wrapper.find("HomePage")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test category click', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("CategoryPage")).toHaveLength(0);
 | 
			
		||||
        wrapper.find(".navitem").findWhere(t => t.text() === "Categories" && t.type() === "div").simulate("click");
 | 
			
		||||
        wrapper.find(".nav-link").findWhere(t => t.text() === "Categories" && t.type() === "div").simulate("click");
 | 
			
		||||
        expect(wrapper.find("CategoryPage")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test settings click', function () {
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
        wrapper.setState({generalSettingsLoaded: true}); // simulate fetch to have already finisheed
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("SettingsPage")).toHaveLength(0);
 | 
			
		||||
        wrapper.find(".navitem").findWhere(t => t.text() === "Settings" && t.type() === "div").simulate("click");
 | 
			
		||||
        wrapper.find(".nav-link").findWhere(t => t.text() === "Settings" && t.type() === "div").simulate("click");
 | 
			
		||||
        expect(wrapper.find("SettingsPage")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test initial fetch from api', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({
 | 
			
		||||
            generalSettingsLoaded: true,
 | 
			
		||||
            passwordsupport: true,
 | 
			
		||||
            mediacentername: "testname"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<App/>);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
        wrapper.instance().setState = func;
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(func).toBeCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The coloring elements for dark theme
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.backgroundcolor {
 | 
			
		||||
    background-color: #141520;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textcolor {
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.subtextcolor {
 | 
			
		||||
    color: #dedad6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lighttextcolor {
 | 
			
		||||
    color: #d5d5d5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navitem::after {
 | 
			
		||||
    background: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hrcolor {
 | 
			
		||||
    border-color: rgba(255, 255, 255, .1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.secbackground {
 | 
			
		||||
    background-color: #3c3d48;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thirdbackground {
 | 
			
		||||
    background-color: #141520;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview:hover {
 | 
			
		||||
    box-shadow: rgba(255, 255, 255, 0.7) 0 0 0 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The coloring elements for light theme
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.navitem::after {
 | 
			
		||||
    background: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.backgroundcolor {
 | 
			
		||||
    background-color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textcolor {
 | 
			
		||||
    color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.subtextcolor {
 | 
			
		||||
    color: #212529;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lighttextcolor {
 | 
			
		||||
    color: #3d3d3d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hrcolor {
 | 
			
		||||
    border-color: rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.secbackground {
 | 
			
		||||
    background-color: #a8c3ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thirdbackground {
 | 
			
		||||
    background-color: #8ca3fc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview:hover {
 | 
			
		||||
    box-shadow: rgba(2, 12, 27, 0.7) 0 0 0 5px;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
import darktheme from "./AppDarkTheme.module.css";
 | 
			
		||||
import lighttheme from "./AppLightTheme.module.css";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is available for all components in project
 | 
			
		||||
 * it contains general infos about app - like theme
 | 
			
		||||
 */
 | 
			
		||||
class StaticInfos {
 | 
			
		||||
    #darktheme = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the current theme is the dark theme
 | 
			
		||||
     * @returns {boolean} is dark theme?
 | 
			
		||||
     */
 | 
			
		||||
    isDarkTheme() {
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get the currently selected theme stylesheet
 | 
			
		||||
     * @returns {*} the style object of the current active theme
 | 
			
		||||
     */
 | 
			
		||||
    getThemeStyle() {
 | 
			
		||||
        return this.isDarkTheme() ? darktheme : lighttheme;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const GlobalInfos = new StaticInfos();
 | 
			
		||||
export default GlobalInfos;
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import GlobalInfos from "./GlobalInfos";
 | 
			
		||||
 | 
			
		||||
describe('<GlobalInfos/>', function () {
 | 
			
		||||
    it('always same instance ', function () {
 | 
			
		||||
        GlobalInfos.enableDarkTheme(true);
 | 
			
		||||
 | 
			
		||||
        expect(GlobalInfos.isDarkTheme()).toBe(true);
 | 
			
		||||
 | 
			
		||||
        GlobalInfos.enableDarkTheme(false);
 | 
			
		||||
 | 
			
		||||
        expect(GlobalInfos.isDarkTheme()).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test default theme', function () {
 | 
			
		||||
        expect(GlobalInfos.isDarkTheme()).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test receive of stylesheet', function () {
 | 
			
		||||
        const style = GlobalInfos.getThemeStyle();
 | 
			
		||||
 | 
			
		||||
        expect(style.navitem).not.toBeNull();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										17
									
								
								src/css/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/css/App.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
.nav-item {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link {
 | 
			
		||||
    color: rgba(255, 255, 255, .5);
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link:hover {
 | 
			
		||||
    color: rgba(255, 255, 255, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadSpinner {
 | 
			
		||||
    margin-top: 200px;
 | 
			
		||||
    margin-left: 50%;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
.pageheader {
 | 
			
		||||
    padding: 20px 12% 20px 22%;
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    padding-left: 22%;
 | 
			
		||||
    padding-right: 12%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pageheadertitle {
 | 
			
		||||
@@ -8,7 +11,8 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pageheadersubtitle {
 | 
			
		||||
    font-size: 23pt;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    font-size: 23pt;
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								src/css/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/css/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
 | 
			
		||||
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
 | 
			
		||||
    sans-serif;
 | 
			
		||||
    -webkit-font-smoothing: antialiased;
 | 
			
		||||
    -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +1,28 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import ReactDom from 'react-dom';
 | 
			
		||||
import style from './AddTagPopup.module.css'
 | 
			
		||||
import Tag from "../Tag/Tag";
 | 
			
		||||
import {Line} from "../PageTitle/PageTitle";
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
import Modal from 'react-bootstrap/Modal'
 | 
			
		||||
import Dropdown from "react-bootstrap/Dropdown";
 | 
			
		||||
import DropdownButton from "react-bootstrap/DropdownButton";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * component creates overlay to add a new tag to a video
 | 
			
		||||
 */
 | 
			
		||||
class AddTagPopup extends React.Component {
 | 
			
		||||
    /// instance of root element
 | 
			
		||||
    element;
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {items: []};
 | 
			
		||||
        this.handleClickOutside = this.handleClickOutside.bind(this);
 | 
			
		||||
        this.keypress = this.keypress.bind(this);
 | 
			
		||||
        this.state = {
 | 
			
		||||
            selection: {
 | 
			
		||||
                name: "nothing selected",
 | 
			
		||||
                id: -1
 | 
			
		||||
            },
 | 
			
		||||
            items: []
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        document.addEventListener('click', this.handleClickOutside);
 | 
			
		||||
        document.addEventListener('keyup', this.keypress);
 | 
			
		||||
 | 
			
		||||
        // add element drag drop events
 | 
			
		||||
        if (this.element != null) {
 | 
			
		||||
            this.dragElement();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'getAllTags');
 | 
			
		||||
 | 
			
		||||
        fetch('/api/tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((result) => {
 | 
			
		||||
                this.setState({
 | 
			
		||||
@@ -43,115 +31,59 @@ class AddTagPopup extends React.Component {
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        // remove the appended listeners
 | 
			
		||||
        document.removeEventListener('click', this.handleClickOutside);
 | 
			
		||||
        document.removeEventListener('keyup', this.keypress);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={[style.popup, themeStyle.thirdbackground].join(' ')} ref={el => this.element = el}>
 | 
			
		||||
                <div className={[style.header, themeStyle.textcolor].join(' ')}>Add a Tag to this Video:</div>
 | 
			
		||||
                <Line/>
 | 
			
		||||
                <div className={style.content}>
 | 
			
		||||
            <>
 | 
			
		||||
                <Modal
 | 
			
		||||
                    show={this.props.show}
 | 
			
		||||
                    onHide={this.props.onHide}
 | 
			
		||||
                    size="lg"
 | 
			
		||||
                    aria-labelledby="contained-modal-title-vcenter"
 | 
			
		||||
                    centered>
 | 
			
		||||
                    <Modal.Header closeButton>
 | 
			
		||||
                        <Modal.Title id="contained-modal-title-vcenter">
 | 
			
		||||
                            Add to Tag
 | 
			
		||||
                        </Modal.Title>
 | 
			
		||||
                    </Modal.Header>
 | 
			
		||||
                    <Modal.Body>
 | 
			
		||||
                        <h4>Select a Tag:</h4>
 | 
			
		||||
                        <DropdownButton id="dropdown-basic-button" title={this.state.selection.name}>
 | 
			
		||||
                            {this.state.items ?
 | 
			
		||||
                                this.state.items.map((i) => (
 | 
			
		||||
                            <Tag onclick={() => {
 | 
			
		||||
                                this.addTag(i.tag_id, i.tag_name);
 | 
			
		||||
                            }}>{i.tag_name}</Tag>
 | 
			
		||||
                        )) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
                                    <Dropdown.Item key={i.tag_name} onClick={() => {
 | 
			
		||||
                                        this.setState({selection: {name: i.tag_name, id: i.tag_id}})
 | 
			
		||||
                                    }}>{i.tag_name}</Dropdown.Item>
 | 
			
		||||
                                )) :
 | 
			
		||||
                                <Dropdown.Item>loading tags...</Dropdown.Item>}
 | 
			
		||||
                        </DropdownButton>
 | 
			
		||||
                    </Modal.Body>
 | 
			
		||||
                    <Modal.Footer>
 | 
			
		||||
                        <button className='btn btn-primary' onClick={() => {
 | 
			
		||||
                            this.storeselection();
 | 
			
		||||
                        }}>Add
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </Modal.Footer>
 | 
			
		||||
                </Modal>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Alert if clicked on outside of element
 | 
			
		||||
     */
 | 
			
		||||
    handleClickOutside(event) {
 | 
			
		||||
        const domNode = ReactDom.findDOMNode(this);
 | 
			
		||||
 | 
			
		||||
        if (!domNode || !domNode.contains(event.target)) {
 | 
			
		||||
            this.props.onHide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * key event handling
 | 
			
		||||
     * @param event keyevent
 | 
			
		||||
     */
 | 
			
		||||
    keypress(event) {
 | 
			
		||||
        // hide if escape is pressed
 | 
			
		||||
        if (event.key === "Escape") {
 | 
			
		||||
            this.props.onHide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * add a new tag to this video
 | 
			
		||||
     * @param tagid tag id to add
 | 
			
		||||
     * @param tagname tag name to add
 | 
			
		||||
     */
 | 
			
		||||
    addTag(tagid, tagname) {
 | 
			
		||||
        console.log(this.props)
 | 
			
		||||
    storeselection() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'addTag');
 | 
			
		||||
        updateRequest.append('id', tagid);
 | 
			
		||||
        updateRequest.append('id', this.state.selection.id);
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.result !== "success") {
 | 
			
		||||
                        console.log("error occured while writing to db -- todo error handling");
 | 
			
		||||
                        console.log(result.result);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.props.submit(tagid, tagname);
 | 
			
		||||
                    }
 | 
			
		||||
                    this.props.onHide();
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * make the element drag and droppable
 | 
			
		||||
     */
 | 
			
		||||
    dragElement() {
 | 
			
		||||
        let xOld = 0, yOld = 0;
 | 
			
		||||
 | 
			
		||||
        const elmnt = this.element;
 | 
			
		||||
        elmnt.firstChild.onmousedown = dragMouseDown;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        function dragMouseDown(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            // get the mouse cursor position at startup:
 | 
			
		||||
            xOld = e.clientX;
 | 
			
		||||
            yOld = e.clientY;
 | 
			
		||||
            document.onmouseup = closeDragElement;
 | 
			
		||||
            // call a function whenever the cursor moves:
 | 
			
		||||
            document.onmousemove = elementDrag;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function elementDrag(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            // calculate the new cursor position:
 | 
			
		||||
            const dx = xOld - e.clientX;
 | 
			
		||||
            const dy = yOld - e.clientY;
 | 
			
		||||
            xOld = e.clientX;
 | 
			
		||||
            yOld = e.clientY;
 | 
			
		||||
            // set the element's new position:
 | 
			
		||||
            elmnt.style.top = (elmnt.offsetTop - dy) + "px";
 | 
			
		||||
            elmnt.style.left = (elmnt.offsetLeft - dx) + "px";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function closeDragElement() {
 | 
			
		||||
            // stop moving when mouse button is released:
 | 
			
		||||
            document.onmouseup = null;
 | 
			
		||||
            document.onmousemove = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AddTagPopup;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
@@ -11,68 +11,47 @@ describe('<AddTagPopup/>', function () {
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test tag insertion', function () {
 | 
			
		||||
    it('test dropdown insertion', function () {
 | 
			
		||||
        const wrapper = shallow(<AddTagPopup/>);
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            items: [{tag_id: 1, tag_name: 'test'}, {tag_id: 2, tag_name: "ee"}]
 | 
			
		||||
        }, () => {
 | 
			
		||||
            expect(wrapper.find('Tag')).toHaveLength(2);
 | 
			
		||||
            expect(wrapper.find('Tag').first().dive().text()).toBe("test");
 | 
			
		||||
        });
 | 
			
		||||
        wrapper.setState({items: ["test1", "test2", "test3"]});
 | 
			
		||||
        expect(wrapper.find('DropdownItem')).toHaveLength(3);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test tag click', function () {
 | 
			
		||||
    it('test storeseletion click event', done => {
 | 
			
		||||
        const mockSuccessResponse = {};
 | 
			
		||||
        const mockJsonPromise = Promise.resolve(mockSuccessResponse);
 | 
			
		||||
        const mockFetchPromise = Promise.resolve({
 | 
			
		||||
            json: () => mockJsonPromise,
 | 
			
		||||
        });
 | 
			
		||||
        global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
 | 
			
		||||
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<AddTagPopup/>);
 | 
			
		||||
        wrapper.instance().addTag = jest.fn();
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            onHide: () => {
 | 
			
		||||
                func()
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            items: [{tag_id: 1, tag_name: 'test'}]
 | 
			
		||||
        }, () => {
 | 
			
		||||
            wrapper.find('Tag').first().dive().simulate('click');
 | 
			
		||||
            expect(wrapper.instance().addTag).toHaveBeenCalledTimes(1);
 | 
			
		||||
        });
 | 
			
		||||
            items: ["test1", "test2", "test3"],
 | 
			
		||||
            selection: {
 | 
			
		||||
                name: "test1",
 | 
			
		||||
                id: 42
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    it('test addtag', done => {
 | 
			
		||||
        const wrapper = shallow(<AddTagPopup/>);
 | 
			
		||||
 | 
			
		||||
        global.fetch = prepareFetchApi({result: "success"});
 | 
			
		||||
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            submit: jest.fn((arg1, arg2) => {}),
 | 
			
		||||
            onHide: jest.fn()
 | 
			
		||||
        }, () => {
 | 
			
		||||
            wrapper.instance().addTag(1, "test");
 | 
			
		||||
 | 
			
		||||
        // first call of fetch is getting of available tags
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
        });
 | 
			
		||||
        wrapper.find('ModalFooter').find('button').simulate('click');
 | 
			
		||||
 | 
			
		||||
        // now called 2 times
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1);
 | 
			
		||||
            expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test failing addTag', done => {
 | 
			
		||||
        const wrapper = shallow(<AddTagPopup/>);
 | 
			
		||||
 | 
			
		||||
        global.fetch = prepareFetchApi({result: "fail"});
 | 
			
		||||
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            submit: jest.fn((arg1, arg2) => {}),
 | 
			
		||||
            onHide: jest.fn()
 | 
			
		||||
        }, () => {
 | 
			
		||||
            wrapper.instance().addTag(1, "test");
 | 
			
		||||
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(0);
 | 
			
		||||
            expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
 | 
			
		||||
            //callback to close window should have called
 | 
			
		||||
            expect(func).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,6 @@ import React from "react";
 | 
			
		||||
import Modal from 'react-bootstrap/Modal'
 | 
			
		||||
import {Form} from "react-bootstrap";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * creates modal overlay to define a new Tag
 | 
			
		||||
 */
 | 
			
		||||
class NewTagPopup extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
@@ -48,15 +45,12 @@ class NewTagPopup extends React.Component {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * store the filled in form to the backend
 | 
			
		||||
     */
 | 
			
		||||
    storeselection() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'createTag');
 | 
			
		||||
        updateRequest.append('tagname', this.value);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/Tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((result) => {
 | 
			
		||||
                if (result.result !== "success") {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./PageTitle.module.css"
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for generating PageTitle with bottom Line
 | 
			
		||||
 */
 | 
			
		||||
class PageTitle extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={style.pageheader + ' ' + themeStyle.backgroundcolor}>
 | 
			
		||||
                <span className={style.pageheadertitle + ' ' + themeStyle.textcolor}>{this.props.title}</span>
 | 
			
		||||
                <span className={style.pageheadersubtitle + ' ' + themeStyle.textcolor}>{this.props.subtitle}</span>
 | 
			
		||||
                <>
 | 
			
		||||
                    {this.props.children}
 | 
			
		||||
                </>
 | 
			
		||||
                <Line/>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * class to override default <hr> color and styling
 | 
			
		||||
 * use this for horizontal lines to use the current active theming
 | 
			
		||||
 */
 | 
			
		||||
export class Line extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <hr className={themeStyle.hrcolor}/>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PageTitle;
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import {shallow} from 'enzyme'
 | 
			
		||||
 | 
			
		||||
import PageTitle from "./PageTitle";
 | 
			
		||||
 | 
			
		||||
describe('<Preview/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<PageTitle/>);
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('renders childs correctly', function () {
 | 
			
		||||
        const wrapper = shallow(<PageTitle>heyimachild</PageTitle>);
 | 
			
		||||
 | 
			
		||||
        const children = wrapper.children();
 | 
			
		||||
        expect(children.at(children.length - 2).text()).toBe("heyimachild");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('renders pagetitle prop', function () {
 | 
			
		||||
        const wrapper = shallow(<PageTitle title='testtitle'/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find(".pageheader").text()).toBe("testtitle<Line />");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('renders subtitle prop', function () {
 | 
			
		||||
        const wrapper = shallow(<PageTitle subtitle='testsubtitle'/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find(".pageheadersubtitle").text()).toBe("testsubtitle");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -1,30 +1,22 @@
 | 
			
		||||
.previewtitle {
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    max-width: 266px;
 | 
			
		||||
    color: #3d3d3d;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    max-width: 266px;
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.previewpic {
 | 
			
		||||
    height: 80%;
 | 
			
		||||
    min-height: 150px;
 | 
			
		||||
    min-width: 266px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadAnimation {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    line-height: 150px;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.previewimage {
 | 
			
		||||
    max-height: 400px;
 | 
			
		||||
    max-width: 410px;
 | 
			
		||||
    min-height: 150px;
 | 
			
		||||
    max-height: 400px;
 | 
			
		||||
    min-width: 266px;
 | 
			
		||||
    max-width: 410px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.previewbottom {
 | 
			
		||||
@@ -32,25 +24,28 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.videopreview {
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 25px;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    /*background-color: #7F7F7F;*/
 | 
			
		||||
    background-color: #a8c3ff;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    opacity: 0.85;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.videopreview:hover {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    box-shadow: rgba(2, 12, 27, 0.7) 0px 0px 0px 5px;
 | 
			
		||||
    transition: all 300ms;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tagpreview {
 | 
			
		||||
    font-size: x-large;
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
    height: 150px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
    font-size: x-large;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    height: 150px;
 | 
			
		||||
    width: 266px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1,16 +1,12 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./Preview.module.css";
 | 
			
		||||
import "./Preview.css";
 | 
			
		||||
import Player from "../../pages/Player/Player";
 | 
			
		||||
import {Spinner} from "react-bootstrap";
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
import VideoContainer from "../VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for single preview tile
 | 
			
		||||
 * floating side by side
 | 
			
		||||
 */
 | 
			
		||||
class Preview extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
        this.props = props;
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            previewpicture: null,
 | 
			
		||||
@@ -18,6 +14,10 @@ class Preview extends React.Component {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        this.setState({});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            previewpicture: null,
 | 
			
		||||
@@ -28,71 +28,84 @@ class Preview extends React.Component {
 | 
			
		||||
        updateRequest.append('action', 'readThumbnail');
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.text()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.setState({
 | 
			
		||||
                        previewpicture: result
 | 
			
		||||
                    });
 | 
			
		||||
                    this.setState(prevState => ({
 | 
			
		||||
                        ...prevState.previewpicture, previewpicture: result
 | 
			
		||||
                    }));
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}
 | 
			
		||||
                 onClick={() => this.itemClick()}>
 | 
			
		||||
                <div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.state.name}</div>
 | 
			
		||||
                <div className={style.previewpic}>
 | 
			
		||||
                    {this.state.previewpicture !== null ?
 | 
			
		||||
                        <img className={style.previewimage}
 | 
			
		||||
            <div className='videopreview' onClick={() => this.itemClick()}>
 | 
			
		||||
                <div className='previewtitle'>{this.state.name}</div>
 | 
			
		||||
                <div className='previewpic'>
 | 
			
		||||
                    <img className='previewimage'
 | 
			
		||||
                         src={this.state.previewpicture}
 | 
			
		||||
                             alt='Pic loading.'/> :
 | 
			
		||||
                        <span className={style.loadAnimation}><Spinner animation="border"/></span>}
 | 
			
		||||
 | 
			
		||||
                         alt='Pic loading.'/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={style.previewbottom}>
 | 
			
		||||
                <div className='previewbottom'>
 | 
			
		||||
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle the click event of a tile
 | 
			
		||||
     */
 | 
			
		||||
    itemClick() {
 | 
			
		||||
        console.log("item clicked!" + this.state.name);
 | 
			
		||||
 | 
			
		||||
        this.props.viewbinding.changeRootElement(
 | 
			
		||||
            <Player
 | 
			
		||||
        this.props.viewbinding.showVideo(<Player
 | 
			
		||||
            viewbinding={this.props.viewbinding}
 | 
			
		||||
            movie_id={this.props.movie_id}/>);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for a Tag-name tile (used in category page)
 | 
			
		||||
 */
 | 
			
		||||
export class TagPreview extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fetchVideoData(tag) {
 | 
			
		||||
        console.log(tag);
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'getMovies');
 | 
			
		||||
        updateRequest.append('tag', tag);
 | 
			
		||||
 | 
			
		||||
        console.log("fetching data");
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    console.log(result);
 | 
			
		||||
                    this.props.categorybinding(
 | 
			
		||||
                        <VideoContainer
 | 
			
		||||
                            data={result}
 | 
			
		||||
                            viewbinding={this.props.viewbinding}/>, tag
 | 
			
		||||
                    );
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}
 | 
			
		||||
                onClick={() => this.itemClick()}>
 | 
			
		||||
                <div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>
 | 
			
		||||
            <div className='videopreview tagpreview' onClick={() => this.itemClick()}>
 | 
			
		||||
                <div className='tagpreviewtitle'>
 | 
			
		||||
                    {this.props.name}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle the click event of a Tag tile
 | 
			
		||||
     */
 | 
			
		||||
    itemClick() {
 | 
			
		||||
        this.props.categorybinding(this.props.name);
 | 
			
		||||
        this.fetchVideoData(this.props.name);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ describe('<Preview/>', function () {
 | 
			
		||||
        const wrapper = shallow(<Preview/>);
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            viewbinding: {
 | 
			
		||||
                changeRootElement: () => {
 | 
			
		||||
                showVideo: () => {
 | 
			
		||||
                    func()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -56,13 +56,6 @@ describe('<Preview/>', function () {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('spinner loads correctly', function () {
 | 
			
		||||
        const wrapper = shallow(<Preview/>);
 | 
			
		||||
 | 
			
		||||
        // expect load animation to be visible
 | 
			
		||||
        expect(wrapper.find(".loadAnimation")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('<TagPreview/>', function () {
 | 
			
		||||
@@ -78,7 +71,14 @@ describe('<TagPreview/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    it('click event triggered', function () {
 | 
			
		||||
    it('click event triggered', done => {
 | 
			
		||||
        const mockSuccessResponse = {};
 | 
			
		||||
        const mockJsonPromise = Promise.resolve(mockSuccessResponse);
 | 
			
		||||
        const mockFetchPromise = Promise.resolve({
 | 
			
		||||
            json: () => mockJsonPromise,
 | 
			
		||||
        });
 | 
			
		||||
        global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
 | 
			
		||||
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<TagPreview/>);
 | 
			
		||||
@@ -89,11 +89,19 @@ describe('<TagPreview/>', function () {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // first call of fetch is getting of available tags
 | 
			
		||||
        expect(func).toHaveBeenCalledTimes(0);
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(0);
 | 
			
		||||
        wrapper.find('.videopreview').simulate('click');
 | 
			
		||||
 | 
			
		||||
        // now called 1 times
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            //callback to close window should have called
 | 
			
		||||
            expect(func).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,24 @@
 | 
			
		||||
.sideinfo {
 | 
			
		||||
    border: 2px #3574fe solid;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    float: left;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    background-color: #b4c7fe;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    border: 2px #3574fe solid;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebartitle {
 | 
			
		||||
    font-size: larger;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    font-size: larger;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebarinfo {
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
    background-color: #8ca3fc;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    padding: 2px 10px 2px 15px;
 | 
			
		||||
    width: 220px;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,42 +1,12 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./SideBar.module.css"
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
import "./SideBar.css"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * component for sidebar-info
 | 
			
		||||
 */
 | 
			
		||||
class SideBar extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (<div className={style.sideinfo + ' ' + themeStyle.secbackground}>
 | 
			
		||||
        return (<div className='sideinfo'>
 | 
			
		||||
            {this.props.children}
 | 
			
		||||
        </div>);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The title of the sidebar
 | 
			
		||||
 */
 | 
			
		||||
export class SideBarTitle extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={style.sidebartitle + ' ' + themeStyle.subtextcolor}>{this.props.children}</div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An item of the sidebar
 | 
			
		||||
 */
 | 
			
		||||
export class SideBarItem extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>{this.props.children}</div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SideBar;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import SideBar, {SideBarItem, SideBarTitle} from "./SideBar";
 | 
			
		||||
import SideBar from "./SideBar";
 | 
			
		||||
 | 
			
		||||
import "@testing-library/jest-dom"
 | 
			
		||||
import {shallow} from "enzyme";
 | 
			
		||||
@@ -14,14 +14,4 @@ describe('<SideBar/>', function () {
 | 
			
		||||
        const wrapper = shallow(<SideBar>test</SideBar>);
 | 
			
		||||
        expect(wrapper.children().text()).toBe("test");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('sidebar Item renders without crashing', function () {
 | 
			
		||||
        const wrapper = shallow(<SideBarItem>Test</SideBarItem>);
 | 
			
		||||
        expect(wrapper.children().text()).toBe("Test");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('renderes sidebartitle correctly', function () {
 | 
			
		||||
        const wrapper = shallow(<SideBarTitle>Test</SideBarTitle>);
 | 
			
		||||
        expect(wrapper.children().text()).toBe("Test");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
.tagbtn {
 | 
			
		||||
    color: white;
 | 
			
		||||
    margin: 10px;
 | 
			
		||||
    background-color: #3574fe;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
    margin-top: 15px;
 | 
			
		||||
    /*font-weight: bold;*/
 | 
			
		||||
    padding: 5px 15px 5px 15px;
 | 
			
		||||
    /*font-weight: bold;*/
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tagbtn:focus {
 | 
			
		||||
@@ -1,36 +1,21 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./Tag.module.css"
 | 
			
		||||
import CategoryPage from "../../pages/CategoryPage/CategoryPage";
 | 
			
		||||
import "./Tag.css"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A Component representing a single Category tag
 | 
			
		||||
 */
 | 
			
		||||
class Tag extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        // todo onclick events correctlyy
 | 
			
		||||
        return (
 | 
			
		||||
            <button className={styles.tagbtn} onClick={() => this.TagClick()}
 | 
			
		||||
            <button className='tagbtn' onClick={this.props.onClick}
 | 
			
		||||
                    data-testid="Test-Tag">{this.props.children}</button>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * click handling for a Tag
 | 
			
		||||
     */
 | 
			
		||||
    TagClick() {
 | 
			
		||||
        if (this.props.onclick) {
 | 
			
		||||
            this.props.onclick();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tag = this.props.children.toString().toLowerCase();
 | 
			
		||||
 | 
			
		||||
        // call callback functin to switch to category page with specified tag
 | 
			
		||||
        this.props.viewbinding.changeRootElement(
 | 
			
		||||
            <CategoryPage
 | 
			
		||||
                category={tag}
 | 
			
		||||
                viewbinding={this.props.viewbinding}/>);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Tag;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,34 +14,4 @@ describe('<Tag/>', function () {
 | 
			
		||||
        const wrapper = shallow(<Tag>test</Tag>);
 | 
			
		||||
        expect(wrapper.children().text()).toBe("test");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('click event triggered and setvideo callback called', function () {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({});
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
        const elem = {
 | 
			
		||||
            changeRootElement: () => func()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<Tag
 | 
			
		||||
            viewbinding={elem}>test</Tag>);
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(0);
 | 
			
		||||
 | 
			
		||||
        wrapper.simulate("click");
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test custom onclick function', function () {
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<Tag
 | 
			
		||||
            onclick={() => {func()}}>test</Tag>);
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(0);
 | 
			
		||||
 | 
			
		||||
        wrapper.simulate("click");
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,7 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import Preview from "../Preview/Preview";
 | 
			
		||||
import style from "./VideoContainer.module.css"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A videocontainer storing lots of Preview elements
 | 
			
		||||
 * includes scroll handling and loading of preview infos
 | 
			
		||||
 */
 | 
			
		||||
class VideoContainer extends React.Component {
 | 
			
		||||
    // stores current index of loaded elements
 | 
			
		||||
    loadindex = 0;
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
@@ -21,15 +13,18 @@ class VideoContainer extends React.Component {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // stores current index of loaded elements
 | 
			
		||||
    loadindex = 0;
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        document.addEventListener('scroll', this.trackScrolling);
 | 
			
		||||
 | 
			
		||||
        this.loadPreviewBlock(16);
 | 
			
		||||
        this.loadPreviewBlock(12);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={style.maincontent}>
 | 
			
		||||
            <div className='maincontent'>
 | 
			
		||||
                {this.state.loadeditems.map(elem => (
 | 
			
		||||
                    <Preview
 | 
			
		||||
                        key={elem.movie_id}
 | 
			
		||||
@@ -40,21 +35,15 @@ class VideoContainer extends React.Component {
 | 
			
		||||
                {/*todo css for no items to show*/}
 | 
			
		||||
                {this.state.loadeditems.length === 0 ?
 | 
			
		||||
                    "no items to show!" : null}
 | 
			
		||||
                {this.props.children}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        this.setState({});
 | 
			
		||||
        // unbind scroll listener when unmounting component
 | 
			
		||||
        document.removeEventListener('scroll', this.trackScrolling);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * load previews to the container
 | 
			
		||||
     * @param nr number of previews to load
 | 
			
		||||
     */
 | 
			
		||||
    loadPreviewBlock(nr) {
 | 
			
		||||
        console.log("loadpreviewblock called ...")
 | 
			
		||||
        let ret = [];
 | 
			
		||||
@@ -76,11 +65,9 @@ class VideoContainer extends React.Component {
 | 
			
		||||
        this.loadindex += nr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * scroll event handler -> load new previews if on bottom
 | 
			
		||||
     */
 | 
			
		||||
    trackScrolling = () => {
 | 
			
		||||
        // comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
 | 
			
		||||
        // comparison if current scroll position is on bottom
 | 
			
		||||
        // 200 stands for bottom offset to trigger load
 | 
			
		||||
        if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
 | 
			
		||||
            this.loadPreviewBlock(8);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
.maincontent {
 | 
			
		||||
    float: left;
 | 
			
		||||
    width: 70%;
 | 
			
		||||
}
 | 
			
		||||
@@ -8,11 +8,11 @@ describe('<VideoContainer/>', function () {
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts tiles correctly if enough available', () => {
 | 
			
		||||
    it('inserts tiles correctly', () => {
 | 
			
		||||
        const wrapper = shallow(<VideoContainer data={[
 | 
			
		||||
            {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
 | 
			
		||||
            {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
 | 
			
		||||
        ]}/>);
 | 
			
		||||
        expect(wrapper.find('Preview')).toHaveLength(16);
 | 
			
		||||
        expect(wrapper.find('Preview')).toHaveLength(12);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts tiles correctly if not enough available', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
import './css/index.css';
 | 
			
		||||
import App from './App';
 | 
			
		||||
 | 
			
		||||
ReactDOM.render(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,16 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import SideBar, {SideBarTitle} from "../../elements/SideBar/SideBar";
 | 
			
		||||
import SideBar from "../../elements/SideBar/SideBar";
 | 
			
		||||
import Tag from "../../elements/Tag/Tag";
 | 
			
		||||
import videocontainerstyle from "../../elements/VideoContainer/VideoContainer.module.css"
 | 
			
		||||
 | 
			
		||||
import {TagPreview} from "../../elements/Preview/Preview";
 | 
			
		||||
import NewTagPopup from "../../elements/NewTagPopup/NewTagPopup";
 | 
			
		||||
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle";
 | 
			
		||||
import VideoContainer from "../../elements/VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for Category Page
 | 
			
		||||
 * Contains a Tag Overview and loads specific Tag videos in VideoContainer
 | 
			
		||||
 */
 | 
			
		||||
class CategoryPage extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            loadedtags: [],
 | 
			
		||||
            selected: null
 | 
			
		||||
@@ -23,72 +18,33 @@ class CategoryPage extends React.Component {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        // check if predefined category is set
 | 
			
		||||
        if (this.props.category) {
 | 
			
		||||
            this.fetchVideoData(this.props.category);
 | 
			
		||||
        } else {
 | 
			
		||||
        this.loadTags();
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * render the Title and SideBar component for the Category page
 | 
			
		||||
     * @returns {JSX.Element} corresponding jsx element for Title and Sidebar
 | 
			
		||||
     */
 | 
			
		||||
    renderSideBarATitle() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <PageTitle
 | 
			
		||||
                    title='Categories'
 | 
			
		||||
                    subtitle={!this.state.selected ? this.state.loadedtags.length + " different Tags" : this.state.selected}/>
 | 
			
		||||
 | 
			
		||||
                <SideBar>
 | 
			
		||||
                    <SideBarTitle>Default Tags:</SideBarTitle>
 | 
			
		||||
                    <Tag viewbinding={{
 | 
			
		||||
                        changeRootElement: (e) => {
 | 
			
		||||
                            this.loadTag(e.props.category)
 | 
			
		||||
                        }
 | 
			
		||||
                    }}>All</Tag>
 | 
			
		||||
                    <Tag viewbinding={{
 | 
			
		||||
                        changeRootElement: (e) => {
 | 
			
		||||
                            this.loadTag(e.props.category)
 | 
			
		||||
                        }
 | 
			
		||||
                    }}>FullHd</Tag>
 | 
			
		||||
                    <Tag viewbinding={{
 | 
			
		||||
                        changeRootElement: (e) => {
 | 
			
		||||
                            this.loadTag(e.props.category)
 | 
			
		||||
                        }
 | 
			
		||||
                    }}>LowQuality</Tag>
 | 
			
		||||
                    <Tag viewbinding={{
 | 
			
		||||
                        changeRootElement: (e) => {
 | 
			
		||||
                            this.loadTag(e.props.category)
 | 
			
		||||
                        }
 | 
			
		||||
                    }}>HD</Tag>
 | 
			
		||||
                    <Line/>
 | 
			
		||||
                    <button data-testid='btnaddtag' className='btn btn-success' onClick={() => {
 | 
			
		||||
                        this.setState({popupvisible: true})
 | 
			
		||||
                    }}>Add a new Tag!
 | 
			
		||||
                    </button>
 | 
			
		||||
                </SideBar></>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                {this.renderSideBarATitle()}
 | 
			
		||||
 | 
			
		||||
                {this.state.selected ?
 | 
			
		||||
                    <>
 | 
			
		||||
                        {this.videodata ?
 | 
			
		||||
                            <VideoContainer
 | 
			
		||||
                                data={this.videodata}
 | 
			
		||||
                                viewbinding={this.props.viewbinding}/> : null}
 | 
			
		||||
                        <button data-testid='backbtn' className="btn btn-success"
 | 
			
		||||
                                onClick={this.loadCategoryPageDefault}>Back
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Categories</span>
 | 
			
		||||
                    <span
 | 
			
		||||
                        className='pageheadersubtitle'>{!this.state.selected ? this.state.loadedtags.length + " different Tags" : this.state.selected}</span>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <SideBar>
 | 
			
		||||
                    <div className='sidebartitle'>Default Tags:</div>
 | 
			
		||||
                    <Tag>All</Tag>
 | 
			
		||||
                    <Tag>FullHd</Tag>
 | 
			
		||||
                    <Tag>LowQuality</Tag>
 | 
			
		||||
                    <Tag>HD</Tag>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                    <button data-testid='btnaddtag' className='btn btn-success' onClick={() => {
 | 
			
		||||
                        this.setState({popupvisible: true})
 | 
			
		||||
                    }}>Add a new Tag!
 | 
			
		||||
                    </button>
 | 
			
		||||
                    </> :
 | 
			
		||||
                    <div className={videocontainerstyle.maincontent}>
 | 
			
		||||
                </SideBar>
 | 
			
		||||
 | 
			
		||||
                {!this.state.selected ?
 | 
			
		||||
                    (<div className='maincontent'>
 | 
			
		||||
                        {this.state.loadedtags ?
 | 
			
		||||
                            this.state.loadedtags.map((m) => (
 | 
			
		||||
                                <TagPreview
 | 
			
		||||
@@ -96,10 +52,16 @@ class CategoryPage extends React.Component {
 | 
			
		||||
                                    name={m.tag_name}
 | 
			
		||||
                                    tag_id={m.tag_id}
 | 
			
		||||
                                    viewbinding={this.props.viewbinding}
 | 
			
		||||
                                    categorybinding={this.loadTag}/>
 | 
			
		||||
                                    categorybinding={this.setPage}/>
 | 
			
		||||
                            )) :
 | 
			
		||||
                            "loading"}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    </div>) :
 | 
			
		||||
                    <>
 | 
			
		||||
                        {this.selectionelements}
 | 
			
		||||
                        <button data-testid='backbtn' className="btn btn-success"
 | 
			
		||||
                                onClick={this.loadCategoryPageDefault}>Back
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                {this.state.popupvisible ?
 | 
			
		||||
@@ -115,45 +77,14 @@ class CategoryPage extends React.Component {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * load a specific tag into a new previewcontainer
 | 
			
		||||
     * @param tagname
 | 
			
		||||
     */
 | 
			
		||||
    loadTag = (tagname) => {
 | 
			
		||||
        this.fetchVideoData(tagname);
 | 
			
		||||
    setPage = (element, tagname) => {
 | 
			
		||||
        this.selectionelements = element;
 | 
			
		||||
 | 
			
		||||
        this.setState({selected: tagname});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * fetch data for a specific tag from backend
 | 
			
		||||
     * @param tag tagname
 | 
			
		||||
     */
 | 
			
		||||
    fetchVideoData(tag) {
 | 
			
		||||
        console.log(tag);
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'getMovies');
 | 
			
		||||
        updateRequest.append('tag', tag);
 | 
			
		||||
 | 
			
		||||
        console.log("fetching data");
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.videodata = result;
 | 
			
		||||
                    this.setState({selected: null}); // needed to trigger the state reload correctly
 | 
			
		||||
                    this.setState({selected: tag});
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * go back to the default category overview
 | 
			
		||||
     */
 | 
			
		||||
    loadCategoryPageDefault = () => {
 | 
			
		||||
        this.setState({selected: null});
 | 
			
		||||
        this.loadTags();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -164,7 +95,7 @@ class CategoryPage extends React.Component {
 | 
			
		||||
        updateRequest.append('action', 'getAllTags');
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/Tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.setState({loadedtags: result});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,15 @@
 | 
			
		||||
import {mount, shallow} from "enzyme";
 | 
			
		||||
import {shallow, mount} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import CategoryPage from "./CategoryPage";
 | 
			
		||||
import VideoContainer from "../../elements/VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
function prepareFetchApi(response) {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('<CategoryPage/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
@@ -9,7 +18,7 @@ describe('<CategoryPage/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test tag fetch call', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi(["first", "second"]);
 | 
			
		||||
        global.fetch = prepareFetchApi(["first", "second"]);
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<CategoryPage/>);
 | 
			
		||||
 | 
			
		||||
@@ -25,14 +34,14 @@ describe('<CategoryPage/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test errored fetch call', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({});
 | 
			
		||||
        global.fetch = prepareFetchApi({});
 | 
			
		||||
 | 
			
		||||
        let message;
 | 
			
		||||
        global.console.log = jest.fn((m) => {
 | 
			
		||||
            message = m;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        shallow(<CategoryPage/>);
 | 
			
		||||
        const wrapper = shallow(<CategoryPage/>);
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +68,7 @@ describe('<CategoryPage/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test setpage callback', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi([{}, {}]);
 | 
			
		||||
        global.fetch = prepareFetchApi([{}, {}]);
 | 
			
		||||
 | 
			
		||||
        const wrapper = mount(<CategoryPage/>);
 | 
			
		||||
 | 
			
		||||
@@ -77,6 +86,8 @@ describe('<CategoryPage/>', function () {
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // expect callback to have loaded correct tag
 | 
			
		||||
            expect(wrapper.state().selected).toBe("testname");
 | 
			
		||||
            // expect to receive a videocontainer with simulated data
 | 
			
		||||
            expect(wrapper.instance().selectionelements).toMatchObject(<VideoContainer data={[{}, {}]}/>);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
@@ -93,30 +104,4 @@ describe('<CategoryPage/>', function () {
 | 
			
		||||
        wrapper.find('[data-testid="backbtn"]').simulate("click");
 | 
			
		||||
        expect(wrapper.state().selected).toBeNull();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('load categorypage with predefined tag', function () {
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
        CategoryPage.prototype.fetchVideoData = func;
 | 
			
		||||
 | 
			
		||||
        shallow(<CategoryPage category="fullhd"/>);
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test sidebar tag clicks', function () {
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        const wrapper = mount(<CategoryPage category="fullhd"/>);
 | 
			
		||||
        wrapper.instance().loadTag = func;
 | 
			
		||||
 | 
			
		||||
        console.log(wrapper.debug());
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(0);
 | 
			
		||||
        wrapper.find("SideBar").find("Tag").forEach(e => {
 | 
			
		||||
            e.simulate("click");
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        expect(func).toBeCalledTimes(4);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,14 @@
 | 
			
		||||
.maincontent {
 | 
			
		||||
    float: left;
 | 
			
		||||
    width: 70%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rightinfo {
 | 
			
		||||
    float: left;
 | 
			
		||||
    width: 10%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.searchform {
 | 
			
		||||
    float: right;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +1,12 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import SideBar, {SideBarItem, SideBarTitle} from "../../elements/SideBar/SideBar";
 | 
			
		||||
import SideBar from "../../elements/SideBar/SideBar";
 | 
			
		||||
import Tag from "../../elements/Tag/Tag";
 | 
			
		||||
import VideoContainer from "../../elements/VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
import style from "./HomePage.module.css"
 | 
			
		||||
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle";
 | 
			
		||||
import "./HomePage.css"
 | 
			
		||||
import "../../css/DefaultPage.css"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The home page component showing on the initial pageload
 | 
			
		||||
 */
 | 
			
		||||
class HomePage extends React.Component {
 | 
			
		||||
    /** keyword variable needed temporary store search keyword */
 | 
			
		||||
    keyword = "";
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +24,9 @@ class HomePage extends React.Component {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** keyword variable needed temporary store search keyword */
 | 
			
		||||
    keyword = "";
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        // initial get of all videos
 | 
			
		||||
        this.fetchVideoData("all");
 | 
			
		||||
@@ -50,7 +47,7 @@ class HomePage extends React.Component {
 | 
			
		||||
        console.log("fetching data");
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.setState({
 | 
			
		||||
@@ -74,7 +71,7 @@ class HomePage extends React.Component {
 | 
			
		||||
        updateRequest.append('action', 'getStartData');
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.setState({
 | 
			
		||||
@@ -105,7 +102,7 @@ class HomePage extends React.Component {
 | 
			
		||||
        updateRequest.append('keyword', keyword);
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    this.setState({
 | 
			
		||||
@@ -123,11 +120,11 @@ class HomePage extends React.Component {
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <PageTitle
 | 
			
		||||
                    title='Home Page'
 | 
			
		||||
                    subtitle={this.state.tag + " Videos - " + this.state.selectionnr}>
 | 
			
		||||
                    <form className={"form-inline " + style.searchform} onSubmit={(e) => {
 | 
			
		||||
            <div>
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Home Page</span>
 | 
			
		||||
                    <span className='pageheadersubtitle'>{this.state.tag} Videos - {this.state.selectionnr}</span>
 | 
			
		||||
                    <form className="form-inline searchform" onSubmit={(e) => {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        this.searchVideos(this.keyword);
 | 
			
		||||
                    }}>
 | 
			
		||||
@@ -138,32 +135,49 @@ class HomePage extends React.Component {
 | 
			
		||||
                               }}/>
 | 
			
		||||
                        <button data-testid='searchbtnsubmit' className="btn btn-success" type="submit">Search</button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </PageTitle>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <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>
 | 
			
		||||
                    <Line/>
 | 
			
		||||
                    <SideBarTitle>Default Tags:</SideBarTitle>
 | 
			
		||||
                    <Tag viewbinding={this.props.viewbinding}>All</Tag>
 | 
			
		||||
                    <Tag viewbinding={this.props.viewbinding}>FullHd</Tag>
 | 
			
		||||
                    <Tag viewbinding={this.props.viewbinding}>LowQuality</Tag>
 | 
			
		||||
                    <Tag viewbinding={this.props.viewbinding}>HD</Tag>
 | 
			
		||||
                    <div className='sidebartitle'>Infos:</div>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.sideinfo.videonr}</b> Videos Total!</div>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.sideinfo.fullhdvideonr}</b> FULL-HD Videos!</div>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.sideinfo.hdvideonr}</b> HD Videos!</div>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.sideinfo.sdvideonr}</b> SD Videos!</div>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.sideinfo.tagnr}</b> different Tags!</div>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                    <div className='sidebartitle'>Default Tags:</div>
 | 
			
		||||
                    <Tag onClick={() => {
 | 
			
		||||
                        this.setState({tag: "All"});
 | 
			
		||||
                        this.fetchVideoData("all");
 | 
			
		||||
                    }}>All
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                    <Tag onClick={() => {
 | 
			
		||||
                        this.setState({tag: "Full HD"});
 | 
			
		||||
                        this.fetchVideoData("fullhd");
 | 
			
		||||
                    }}>FullHd
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                    <Tag onClick={() => {
 | 
			
		||||
                        this.setState({tag: "Low Quality"});
 | 
			
		||||
                        this.fetchVideoData("lowquality");
 | 
			
		||||
                    }}>LowQuality
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                    <Tag onClick={() => {
 | 
			
		||||
                        this.setState({tag: "HD"});
 | 
			
		||||
                        this.fetchVideoData("hd");
 | 
			
		||||
                    }}>HD
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                </SideBar>
 | 
			
		||||
                {this.state.data.length !== 0 ?
 | 
			
		||||
                    <VideoContainer
 | 
			
		||||
                        data={this.state.data}
 | 
			
		||||
                        viewbinding={this.props.viewbinding}/> :
 | 
			
		||||
                    <div>No Data found!</div>}
 | 
			
		||||
                <div className={style.rightinfo}>
 | 
			
		||||
                <div className='rightinfo'>
 | 
			
		||||
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
            </>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,37 @@ import React from "react";
 | 
			
		||||
import HomePage from "./HomePage";
 | 
			
		||||
import VideoContainer from "../../elements/VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
function prepareFetchApi(response) {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function prepareFailingFetchApi() {
 | 
			
		||||
    const mockFetchPromise = Promise.reject("myreason");
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('<HomePage/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('ckeck default tag click events', function () {
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
        global.fetch = prepareFetchApi({});
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(0);
 | 
			
		||||
        // click every tag button
 | 
			
		||||
        wrapper.find("Tag").map((i) => {
 | 
			
		||||
            i.simulate("click");
 | 
			
		||||
        });
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(4);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test data insertion', function () {
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
 | 
			
		||||
@@ -27,18 +52,18 @@ describe('<HomePage/>', function () {
 | 
			
		||||
    it('test title and nr insertions', function () {
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("PageTitle").props().subtitle).toBe("All Videos - 0");
 | 
			
		||||
        expect(wrapper.find(".pageheadersubtitle").text()).toBe("All Videos - 0");
 | 
			
		||||
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            tag: "testtag",
 | 
			
		||||
            selectionnr: 42
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("PageTitle").props().subtitle).toBe("testtag Videos - 42");
 | 
			
		||||
        expect(wrapper.find(".pageheadersubtitle").text()).toBe("testtag Videos - 42");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test search field', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi([{}, {}]);
 | 
			
		||||
        global.fetch = prepareFetchApi([{}, {}]);
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
 | 
			
		||||
@@ -55,7 +80,7 @@ describe('<HomePage/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test form submit', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi([{}, {}]);
 | 
			
		||||
        global.fetch = prepareFetchApi([{}, {}]);
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
 | 
			
		||||
@@ -75,14 +100,14 @@ describe('<HomePage/>', function () {
 | 
			
		||||
 | 
			
		||||
    it('test no backend connection behaviour', done => {
 | 
			
		||||
        // this test assumes a console.log within every connection fail
 | 
			
		||||
        global.fetch = global.prepareFailingFetchApi();
 | 
			
		||||
        global.fetch = prepareFailingFetchApi();
 | 
			
		||||
 | 
			
		||||
        let count = 0;
 | 
			
		||||
        global.console.log = jest.fn(() => {
 | 
			
		||||
        global.console.log = jest.fn((m) => {
 | 
			
		||||
            count++;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        shallow(<HomePage/>);
 | 
			
		||||
        const wrapper = shallow(<HomePage/>);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // state to be set correctly with response
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								src/pages/LoginPage/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/pages/LoginPage/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import {Form} from "react-bootstrap";
 | 
			
		||||
 | 
			
		||||
class LoginPage extends React.Component {
 | 
			
		||||
    constructor(props) {
 | 
			
		||||
        super(props);
 | 
			
		||||
 | 
			
		||||
        this.state = {};
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Login Page</span>
 | 
			
		||||
                    <span className='pageheadersubtitle'>type correct password!</span>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div style={{marginLeft: "35%", width: "400px", marginTop: "100px"}}>
 | 
			
		||||
                    <Form.Group>
 | 
			
		||||
                        <Form.Label>Password</Form.Label>
 | 
			
		||||
                        <Form.Control id='passfield' type="password" placeholder="Enter Password" onChange={(v) => {
 | 
			
		||||
                            this.password = v.target.value
 | 
			
		||||
                        }}/>
 | 
			
		||||
                        <Form.Text className="text-muted">
 | 
			
		||||
                            You can disable/enable this feature on settingspage.
 | 
			
		||||
                        </Form.Text>
 | 
			
		||||
                        <hr/>
 | 
			
		||||
                        <button className='btn btn-success' type='submit' onClick={() => this.checkPassword()}>Submit
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </Form.Group>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkPassword() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append("action", "checkPassword");
 | 
			
		||||
        updateRequest.append("password", this.state.password);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/settings.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.correct) {
 | 
			
		||||
                        // todo 2020-06-18: call a callback to return back to right page
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // error popup
 | 
			
		||||
                    }
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default LoginPage;
 | 
			
		||||
@@ -1,20 +1,30 @@
 | 
			
		||||
.closebutton {
 | 
			
		||||
    background-color: #FF0000;
 | 
			
		||||
    color: white;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    margin-left: 25px;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    padding: 5px 15px 5px 15px;
 | 
			
		||||
    background-color: #FF0000;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    margin-left: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.likefield {
 | 
			
		||||
    margin-top: 15px;
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    margin-right: 15px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
    background-color: #9e5353;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.videowrapper {
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    width: 60%;
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.videoactions {
 | 
			
		||||
@@ -1,18 +1,29 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./Player.module.css"
 | 
			
		||||
 | 
			
		||||
import "./Player.css"
 | 
			
		||||
import {PlyrComponent} from 'plyr-react';
 | 
			
		||||
import SideBar, {SideBarItem, SideBarTitle} from "../../elements/SideBar/SideBar";
 | 
			
		||||
import SideBar from "../../elements/SideBar/SideBar";
 | 
			
		||||
import Tag from "../../elements/Tag/Tag";
 | 
			
		||||
import AddTagPopup from "../../elements/AddTagPopup/AddTagPopup";
 | 
			
		||||
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Player page loads when a video is selected to play and handles the video view
 | 
			
		||||
 * and actions such as tag adding and liking
 | 
			
		||||
 */
 | 
			
		||||
class Player extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            sources: null,
 | 
			
		||||
            movie_id: null,
 | 
			
		||||
            movie_name: null,
 | 
			
		||||
            likes: null,
 | 
			
		||||
            quality: null,
 | 
			
		||||
            length: null,
 | 
			
		||||
            tags: [],
 | 
			
		||||
            popupvisible: false
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.props = props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    options = {
 | 
			
		||||
        controls: [
 | 
			
		||||
            'play-large', // The large play button in the center
 | 
			
		||||
@@ -30,165 +41,70 @@ class Player extends React.Component {
 | 
			
		||||
        ]
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            sources: null,
 | 
			
		||||
            movie_id: null,
 | 
			
		||||
            movie_name: null,
 | 
			
		||||
            likes: null,
 | 
			
		||||
            quality: null,
 | 
			
		||||
            length: null,
 | 
			
		||||
            tags: [],
 | 
			
		||||
            suggesttag: [],
 | 
			
		||||
            popupvisible: false
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.quickAddTag = this.quickAddTag.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        this.fetchMovieData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * quick add callback to add tag to db and change gui correctly
 | 
			
		||||
     * @param tag_id id of tag to add
 | 
			
		||||
     * @param tag_name name of tag to add
 | 
			
		||||
     */
 | 
			
		||||
    quickAddTag(tag_id, tag_name) {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'addTag');
 | 
			
		||||
        updateRequest.append('id', tag_id);
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/tags.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.result !== "success") {
 | 
			
		||||
                        console.error("error occured while writing to db -- todo error handling");
 | 
			
		||||
                        console.error(result.result);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // update tags if successful
 | 
			
		||||
                        let array = [...this.state.suggesttag]; // make a separate copy of the array
 | 
			
		||||
                        const index = array.map(function (e) {
 | 
			
		||||
                            return e.tag_id;
 | 
			
		||||
                        }).indexOf(tag_id);
 | 
			
		||||
 | 
			
		||||
                        // check if tag is available in quickadds
 | 
			
		||||
                        if (index !== -1) {
 | 
			
		||||
                            array.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
                            this.setState({
 | 
			
		||||
                                tags: [...this.state.tags, {tag_name: tag_name}],
 | 
			
		||||
                                suggesttag: array
 | 
			
		||||
                            });
 | 
			
		||||
                        } else {
 | 
			
		||||
                            this.setState({
 | 
			
		||||
                                tags: [...this.state.tags, {tag_name: tag_name}]
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle the popovers generated according to state changes
 | 
			
		||||
     * @returns {JSX.Element}
 | 
			
		||||
     */
 | 
			
		||||
    handlePopOvers() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                {this.state.popupvisible ?
 | 
			
		||||
                    <AddTagPopup show={this.state.popupvisible}
 | 
			
		||||
                                 onHide={() => {
 | 
			
		||||
                                     this.setState({popupvisible: false});
 | 
			
		||||
                                 }}
 | 
			
		||||
                                 submit={this.quickAddTag}
 | 
			
		||||
                                 movie_id={this.state.movie_id}/> :
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * generate sidebar with all items
 | 
			
		||||
     */
 | 
			
		||||
    assembleSideBar() {
 | 
			
		||||
        return (
 | 
			
		||||
            <SideBar>
 | 
			
		||||
                <SideBarTitle>Infos:</SideBarTitle>
 | 
			
		||||
                <Line/>
 | 
			
		||||
                <SideBarItem><b>{this.state.likes}</b> Likes!</SideBarItem>
 | 
			
		||||
                {this.state.quality !== 0 ?
 | 
			
		||||
                    <SideBarItem><b>{this.state.quality}p</b> Quality!</SideBarItem> : null}
 | 
			
		||||
                {this.state.length !== 0 ?
 | 
			
		||||
                    <SideBarItem><b>{Math.round(this.state.length / 60)}</b> Minutes of
 | 
			
		||||
                        length!</SideBarItem> : null}
 | 
			
		||||
                <Line/>
 | 
			
		||||
                <SideBarTitle>Tags:</SideBarTitle>
 | 
			
		||||
                {this.state.tags.map((m) => (
 | 
			
		||||
                    <Tag
 | 
			
		||||
                        key={m.tag_name}
 | 
			
		||||
                        viewbinding={this.props.viewbinding}>{m.tag_name}</Tag>
 | 
			
		||||
                ))}
 | 
			
		||||
                <Line/>
 | 
			
		||||
                <SideBarTitle>Tag Quickadd:</SideBarTitle>
 | 
			
		||||
                {this.state.suggesttag.map((m) => (
 | 
			
		||||
                    <Tag
 | 
			
		||||
                        key={m.tag_name}
 | 
			
		||||
                        onclick={() => {
 | 
			
		||||
                            this.quickAddTag(m.tag_id, m.tag_name);
 | 
			
		||||
                        }}>
 | 
			
		||||
                        {m.tag_name}
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                ))}
 | 
			
		||||
            </SideBar>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <div id='videocontainer'>
 | 
			
		||||
                <PageTitle
 | 
			
		||||
                    title='Watch'
 | 
			
		||||
                    subtitle={this.state.movie_name}/>
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Watch</span>
 | 
			
		||||
                    <span className='pageheadersubtitle'>{this.state.movie_name}</span>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <SideBar>
 | 
			
		||||
                    <div className='sidebartitle'>Infos:</div>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                    <div className='sidebarinfo'><b>{this.state.likes}</b> Likes!</div>
 | 
			
		||||
                    {this.state.quality !== 0 ?
 | 
			
		||||
                        <div className='sidebarinfo'><b>{this.state.quality}p</b> Quality!
 | 
			
		||||
                        </div> : null}
 | 
			
		||||
                    {this.state.length !== 0 ?
 | 
			
		||||
                        <div className='sidebarinfo'><b>{Math.round(this.state.length / 60)}</b> Minutes of length!
 | 
			
		||||
                        </div> : null}
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                    <div className='sidebartitle'>Tags:</div>
 | 
			
		||||
                    {this.state.tags.map((m) => (
 | 
			
		||||
                        <Tag key={m.tag_name}>{m.tag_name}</Tag>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </SideBar>
 | 
			
		||||
 | 
			
		||||
                {this.assembleSideBar()}
 | 
			
		||||
 | 
			
		||||
                <div className={style.videowrapper}>
 | 
			
		||||
                <div className="videowrapper">
 | 
			
		||||
                    {/* video component is added here */}
 | 
			
		||||
                    {this.state.sources ? <PlyrComponent
 | 
			
		||||
                            className='myvideo'
 | 
			
		||||
                            sources={this.state.sources}
 | 
			
		||||
                            options={this.options}/> :
 | 
			
		||||
                        <div>not loaded yet</div>}
 | 
			
		||||
                    <div className={style.videoactions}>
 | 
			
		||||
                    <div className='videoactions'>
 | 
			
		||||
                        <button className='btn btn-primary' onClick={() => this.likebtn()}>Like this Video!</button>
 | 
			
		||||
                        <button className='btn btn-info' onClick={() => this.setState({popupvisible: true})}>Give this Video a Tag</button>
 | 
			
		||||
                        <button className='btn btn-danger' onClick={() =>{this.deleteVideo()}}>Delete Video</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <button className={style.closebutton} onClick={() => this.closebtn()}>Close</button>
 | 
			
		||||
                {
 | 
			
		||||
                    // handle the popovers switched on and off according to state changes
 | 
			
		||||
                    this.handlePopOvers()
 | 
			
		||||
                        <button className='btn btn-info' onClick={() => this.setState({popupvisible: true})}>Give this
 | 
			
		||||
                            Video a Tag
 | 
			
		||||
                        </button>
 | 
			
		||||
                        {this.state.popupvisible ?
 | 
			
		||||
                            <AddTagPopup show={this.state.popupvisible}
 | 
			
		||||
                                         onHide={() => {
 | 
			
		||||
                                             this.setState({popupvisible: false});
 | 
			
		||||
                                             this.fetchMovieData();
 | 
			
		||||
                                         }}
 | 
			
		||||
                                         movie_id={this.state.movie_id}/> :
 | 
			
		||||
                            null
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <button className="closebutton" onClick={() => this.closebtn()}>Close</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * fetch all the required infos of a video from backend
 | 
			
		||||
     */
 | 
			
		||||
    fetchMovieData() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'loadVideo');
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((result) => {
 | 
			
		||||
                this.setState({
 | 
			
		||||
@@ -208,62 +124,32 @@ class Player extends React.Component {
 | 
			
		||||
                    likes: result.likes,
 | 
			
		||||
                    quality: result.quality,
 | 
			
		||||
                    length: result.length,
 | 
			
		||||
                    tags: result.tags,
 | 
			
		||||
                    suggesttag: result.suggesttag
 | 
			
		||||
                    tags: result.tags
 | 
			
		||||
                });
 | 
			
		||||
                console.log(this.state);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * click handler for the like btn
 | 
			
		||||
     */
 | 
			
		||||
    /* Click Listener */
 | 
			
		||||
    likebtn() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'addLike');
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.result === "success") {
 | 
			
		||||
                        // likes +1 --> avoid reload of all data
 | 
			
		||||
                        this.setState({likes: this.state.likes + 1})
 | 
			
		||||
                        this.fetchMovieData();
 | 
			
		||||
                    } else {
 | 
			
		||||
                        console.error("an error occured while liking");
 | 
			
		||||
                        console.error(result);
 | 
			
		||||
                        console.log("an error occured while liking");
 | 
			
		||||
                        console.log(result);
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * closebtn click handler
 | 
			
		||||
     * calls callback to viewbinding to show previous page agains
 | 
			
		||||
     */
 | 
			
		||||
    closebtn() {
 | 
			
		||||
        this.props.viewbinding.returnToLastElement();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * delete the current video and return to last page
 | 
			
		||||
     */
 | 
			
		||||
    deleteVideo() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'deleteVideo');
 | 
			
		||||
        updateRequest.append('movieid', this.props.movie_id);
 | 
			
		||||
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.result === "success") {
 | 
			
		||||
                        // return to last element if successful
 | 
			
		||||
                        this.props.viewbinding.returnToLastElement();
 | 
			
		||||
                    } else {
 | 
			
		||||
                        console.error("an error occured while liking");
 | 
			
		||||
                        console.error(result);
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
        this.props.viewbinding.hideVideo();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,14 @@ import {shallow} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import Player from "./Player";
 | 
			
		||||
 | 
			
		||||
function prepareFetchApi(response) {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('<Player/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
@@ -23,8 +31,17 @@ describe('<Player/>', function () {
 | 
			
		||||
        expect(wrapper.find("r")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function simulateLikeButtonClick() {
 | 
			
		||||
    it('likebtn click', done => {
 | 
			
		||||
        global.fetch = prepareFetchApi({result: 'success'});
 | 
			
		||||
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            onHide: () => {
 | 
			
		||||
                func()
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // initial fetch for getting movie data
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
@@ -32,19 +49,9 @@ describe('<Player/>', function () {
 | 
			
		||||
        // fetch for liking
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
        return wrapper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    it('likebtn click', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({result: 'success'});
 | 
			
		||||
        global.console.error = jest.fn();
 | 
			
		||||
 | 
			
		||||
        simulateLikeButtonClick();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(2);
 | 
			
		||||
            expect(global.console.error).toHaveBeenCalledTimes(0);
 | 
			
		||||
            // refetch is called so fetch called 3 times
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(3);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
@@ -52,15 +59,25 @@ describe('<Player/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('errored likebtn click', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({result: 'nosuccess'});
 | 
			
		||||
        global.console.error = jest.fn();
 | 
			
		||||
        global.fetch = prepareFetchApi({result: 'nosuccess'});
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        simulateLikeButtonClick();
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            onHide: () => {
 | 
			
		||||
                func()
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // initial fetch for getting movie data
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
        wrapper.find('.videoactions').find("button").first().simulate('click');
 | 
			
		||||
        // fetch for liking
 | 
			
		||||
        expect(global.fetch).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // refetch is called so fetch called 3 times
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(2);
 | 
			
		||||
            expect(global.console.error).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
@@ -69,38 +86,20 @@ describe('<Player/>', function () {
 | 
			
		||||
 | 
			
		||||
    it('show tag popup', function () {
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("AddTagPopup")).toHaveLength(0);
 | 
			
		||||
        // todo dynamic button find without index
 | 
			
		||||
        wrapper.find('.videoactions').find("button").at(1).simulate('click');
 | 
			
		||||
        wrapper.find('.videoactions').find("button").last().simulate('click');
 | 
			
		||||
        // addtagpopup should be showing now
 | 
			
		||||
        expect(wrapper.find("AddTagPopup")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test delete button', done => {
 | 
			
		||||
        const wrapper = shallow(<Player viewbinding={{
 | 
			
		||||
            returnToLastElement: jest.fn()
 | 
			
		||||
        }}/>);
 | 
			
		||||
        global.fetch = prepareFetchApi({result: "success"});
 | 
			
		||||
 | 
			
		||||
        wrapper.find('.videoactions').find("button").at(2).simulate('click');
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // refetch is called so fetch called 3 times
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
            expect(wrapper.instance().props.viewbinding.returnToLastElement).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('hide click ', function () {
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
        const func = jest.fn();
 | 
			
		||||
 | 
			
		||||
        wrapper.setProps({
 | 
			
		||||
            viewbinding: {
 | 
			
		||||
                returnToLastElement: () => {
 | 
			
		||||
                hideVideo: () => {
 | 
			
		||||
                    func()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -126,59 +125,4 @@ describe('<Player/>', function () {
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("Tag")).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts tag quickadd correctly', function () {
 | 
			
		||||
        generatetag();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test click of quickadd tag btn', done => {
 | 
			
		||||
        const wrapper = generatetag();
 | 
			
		||||
 | 
			
		||||
        global.fetch = prepareFetchApi({result: 'success'});
 | 
			
		||||
 | 
			
		||||
        // render tag subcomponent
 | 
			
		||||
        const tag = wrapper.find("Tag").first().dive();
 | 
			
		||||
        tag.simulate('click');
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(global.fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test failing quickadd', done => {
 | 
			
		||||
        const wrapper = generatetag();
 | 
			
		||||
 | 
			
		||||
        global.fetch = prepareFetchApi({result: 'nonsuccess'});
 | 
			
		||||
        global.console.error = jest.fn();
 | 
			
		||||
 | 
			
		||||
        // render tag subcomponent
 | 
			
		||||
        const tag = wrapper.find("Tag").first().dive();
 | 
			
		||||
        tag.simulate('click');
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(global.console.error).toHaveBeenCalledTimes(2);
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function generatetag() {
 | 
			
		||||
        const wrapper = shallow(<Player/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("Tag")).toHaveLength(0);
 | 
			
		||||
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            suggesttag: [
 | 
			
		||||
                {tag_name: 'first', tag_id: 1},
 | 
			
		||||
            ]
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("Tag")).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        return wrapper;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
.Shufflebutton {
 | 
			
		||||
    align-content: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btnshuffle {
 | 
			
		||||
    background-color: #39a945;
 | 
			
		||||
 | 
			
		||||
    color: white;
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    margin-left: 45%;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    font-size: larger;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-left: 45%;
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    padding: 15px 25px 15px 25px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    font-size: larger;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btnshuffle:focus {
 | 
			
		||||
@@ -1,13 +1,9 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./RandomPage.module.css"
 | 
			
		||||
import SideBar, {SideBarTitle} from "../../elements/SideBar/SideBar";
 | 
			
		||||
import Preview from "../../elements/Preview/Preview";
 | 
			
		||||
import "./RandomPage.css"
 | 
			
		||||
import SideBar from "../../elements/SideBar/SideBar";
 | 
			
		||||
import Tag from "../../elements/Tag/Tag";
 | 
			
		||||
import PageTitle from "../../elements/PageTitle/PageTitle";
 | 
			
		||||
import VideoContainer from "../../elements/VideoContainer/VideoContainer";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Randompage shuffles random viedeopreviews and provides a shuffle btn
 | 
			
		||||
 */
 | 
			
		||||
class RandomPage extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
@@ -25,57 +21,48 @@ class RandomPage extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <div>
 | 
			
		||||
                <PageTitle
 | 
			
		||||
                    title='Random Videos'
 | 
			
		||||
                    subtitle='4pc'/>
 | 
			
		||||
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Random Videos</span>
 | 
			
		||||
                    <span className='pageheadersubtitle'>4pc</span>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
                <SideBar>
 | 
			
		||||
                    <SideBarTitle>Visible Tags:</SideBarTitle>
 | 
			
		||||
                    <div className='sidebartitle'>Visible Tags:</div>
 | 
			
		||||
                    {this.state.tags.map((m) => (
 | 
			
		||||
                        <Tag
 | 
			
		||||
                            key={m.tag_name}
 | 
			
		||||
                            viewbinding={this.props.viewbinding}>{m.tag_name}</Tag>
 | 
			
		||||
                        <Tag>{m.tag_name}</Tag>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </SideBar>
 | 
			
		||||
 | 
			
		||||
                {this.state.videos.length !== 0 ?
 | 
			
		||||
                    <VideoContainer
 | 
			
		||||
                        data={this.state.videos}
 | 
			
		||||
                        viewbinding={this.props.viewbinding}>
 | 
			
		||||
                        <div className={style.Shufflebutton}>
 | 
			
		||||
                            <button onClick={() => this.shuffleclick()} className={style.btnshuffle}>Shuffle</button>
 | 
			
		||||
                <div className='maincontent'>
 | 
			
		||||
                    {this.state.videos.map(elem => (
 | 
			
		||||
                        <Preview
 | 
			
		||||
                            key={elem.movie_id}
 | 
			
		||||
                            name={elem.movie_name}
 | 
			
		||||
                            movie_id={elem.movie_id}
 | 
			
		||||
                            viewbinding={this.props.viewbinding}/>
 | 
			
		||||
                    ))}
 | 
			
		||||
                    <div className='Shufflebutton'>
 | 
			
		||||
                        <button onClick={() => this.shuffleclick()} className='btnshuffle'>Shuffle</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                    </VideoContainer>
 | 
			
		||||
                    :
 | 
			
		||||
                    <div>No Data found!</div>}
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * click handler for shuffle btn
 | 
			
		||||
     */
 | 
			
		||||
    shuffleclick() {
 | 
			
		||||
        this.loadShuffledvideos(4);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * load random videos from backend
 | 
			
		||||
     * @param nr number of videos to load
 | 
			
		||||
     */
 | 
			
		||||
    loadShuffledvideos(nr) {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'getRandomMovies');
 | 
			
		||||
        updateRequest.append('number', nr);
 | 
			
		||||
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/video.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
        fetch('/api/videoload.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    console.log(result);
 | 
			
		||||
 | 
			
		||||
                    this.setState({videos: []}); // needed to trigger rerender of main videoview
 | 
			
		||||
                    this.setState({
 | 
			
		||||
                        videos: result.rows,
 | 
			
		||||
                        tags: result.tags
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,14 @@ import {shallow} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import RandomPage from "./RandomPage";
 | 
			
		||||
 | 
			
		||||
function prepareFetchApi(response) {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('<RandomPage/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<RandomPage/>);
 | 
			
		||||
@@ -9,25 +17,18 @@ describe('<RandomPage/>', function () {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test shuffleload fetch', function () {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({});
 | 
			
		||||
        global.fetch = prepareFetchApi({});
 | 
			
		||||
 | 
			
		||||
        shallow(<RandomPage/>);
 | 
			
		||||
        const wrapper = shallow(<RandomPage/>);
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('btnshuffle click test', function () {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({});
 | 
			
		||||
        global.fetch = prepareFetchApi({});
 | 
			
		||||
 | 
			
		||||
        const wrapper = shallow(<RandomPage/>);
 | 
			
		||||
 | 
			
		||||
        // simulate at least one existing element
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            videos: [
 | 
			
		||||
                {}
 | 
			
		||||
            ]
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        wrapper.find(".btnshuffle").simulate("click");
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(2);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,161 +0,0 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import {Button, Col, Form} from "react-bootstrap";
 | 
			
		||||
import style from "./GeneralSettings.module.css"
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for Generalsettings tag on Settingspage
 | 
			
		||||
 * handles general settings of mediacenter which concerns to all pages
 | 
			
		||||
 */
 | 
			
		||||
class GeneralSettings extends React.Component {
 | 
			
		||||
    constructor(props) {
 | 
			
		||||
        super(props);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            passwordsupport: false,
 | 
			
		||||
            tmdbsupport: null,
 | 
			
		||||
 | 
			
		||||
            videopath: "",
 | 
			
		||||
            tvshowpath: "",
 | 
			
		||||
            mediacentername: "",
 | 
			
		||||
            password: ""
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        this.loadSettings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themeStyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <div className={style.GeneralForm + ' ' + themeStyle.subtextcolor}>
 | 
			
		||||
                    <Form data-testid='mainformsettings' onSubmit={(e) => {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        this.saveSettings();
 | 
			
		||||
                    }}>
 | 
			
		||||
                        <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) => this.setState({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) => this.setState({tvshowpath: e.target.value})}/>
 | 
			
		||||
                            </Form.Group>
 | 
			
		||||
                        </Form.Row>
 | 
			
		||||
 | 
			
		||||
                        <Form.Check
 | 
			
		||||
                            type="switch"
 | 
			
		||||
                            id="custom-switch"
 | 
			
		||||
                            data-testid='passwordswitch'
 | 
			
		||||
                            label="Enable Password support"
 | 
			
		||||
                            checked={this.state.passwordsupport}
 | 
			
		||||
                            onChange={() => {
 | 
			
		||||
                                this.setState({passwordsupport: !this.state.passwordsupport})
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
 | 
			
		||||
                        {this.state.passwordsupport ?
 | 
			
		||||
                            <Form.Group data-testid="passwordfield">
 | 
			
		||||
                                <Form.Label>Password</Form.Label>
 | 
			
		||||
                                <Form.Control type="password" placeholder="**********" value={this.state.password}
 | 
			
		||||
                                              onChange={(e) => this.setState({password: e.target.value})}/>
 | 
			
		||||
                            </Form.Group> : null
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        <Form.Check
 | 
			
		||||
                            type="switch"
 | 
			
		||||
                            id="custom-switch-2"
 | 
			
		||||
                            data-testid='tmdb-switch'
 | 
			
		||||
                            label="Enable TMDB video grabbing support"
 | 
			
		||||
                            checked={this.state.tmdbsupport}
 | 
			
		||||
                            onChange={() => {
 | 
			
		||||
                                this.setState({tmdbsupport: !this.state.tmdbsupport})
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
 | 
			
		||||
                        <Form.Check
 | 
			
		||||
                            type="switch"
 | 
			
		||||
                            id="custom-switch-3"
 | 
			
		||||
                            data-testid='darktheme-switch'
 | 
			
		||||
                            label="Enable Dark-Theme"
 | 
			
		||||
                            checked={GlobalInfos.isDarkTheme()}
 | 
			
		||||
                            onChange={() => {
 | 
			
		||||
                                GlobalInfos.enableDarkTheme(!GlobalInfos.isDarkTheme());
 | 
			
		||||
                                this.forceUpdate();
 | 
			
		||||
                                // todo initiate rerender
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
 | 
			
		||||
                        <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) => this.setState({mediacentername: e.target.value})}/>
 | 
			
		||||
                        </Form.Group>
 | 
			
		||||
 | 
			
		||||
                        <Button variant="primary" type="submit">
 | 
			
		||||
                            Submit
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </Form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * inital load of already specified settings from backend
 | 
			
		||||
     */
 | 
			
		||||
    loadSettings() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'loadGeneralSettings');
 | 
			
		||||
 | 
			
		||||
        fetch('/api/settings.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    console.log(result);
 | 
			
		||||
                    this.setState({
 | 
			
		||||
                        videopath: result.video_path,
 | 
			
		||||
                        tvshowpath: result.episode_path,
 | 
			
		||||
                        mediacentername: result.mediacenter_name,
 | 
			
		||||
                        password: result.password,
 | 
			
		||||
                        passwordsupport: result.passwordEnabled,
 | 
			
		||||
                        tmdbsupport: result.TMDB_grabbing
 | 
			
		||||
                    });
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save the selected and typed settings to the backend
 | 
			
		||||
     */
 | 
			
		||||
    saveSettings() {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        updateRequest.append('action', 'saveGeneralSettings');
 | 
			
		||||
 | 
			
		||||
        updateRequest.append('password', this.state.passwordsupport ? this.state.password : "-1");
 | 
			
		||||
        updateRequest.append('videopath', this.state.videopath);
 | 
			
		||||
        updateRequest.append('tvshowpath', this.state.tvshowpath);
 | 
			
		||||
        updateRequest.append('mediacentername', this.state.mediacentername);
 | 
			
		||||
        updateRequest.append("tmdbsupport", this.state.tmdbsupport);
 | 
			
		||||
        updateRequest.append("darkmodeenabled", GlobalInfos.isDarkTheme());
 | 
			
		||||
 | 
			
		||||
        fetch('/api/settings.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.success) {
 | 
			
		||||
                        console.log("successfully saved settings");
 | 
			
		||||
                        // todo 2020-07-10: popup success
 | 
			
		||||
                    } else {
 | 
			
		||||
                        console.log("failed to save settings");
 | 
			
		||||
                        // todo 2020-07-10: popup error
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default GeneralSettings;
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
.GeneralForm {
 | 
			
		||||
    width: 60%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mediacenternameform {
 | 
			
		||||
    margin-top: 25px;
 | 
			
		||||
    width: 40%;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,112 +0,0 @@
 | 
			
		||||
import {shallow} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import GeneralSettings from "./GeneralSettings";
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
 | 
			
		||||
describe('<GeneralSettings/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test password hide/show switchbutton', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("[data-testid='passwordfield']")).toHaveLength(0);
 | 
			
		||||
        wrapper.find("FormCheck").findWhere(it => it.props().label === "Enable Password support").simulate("change");
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find("[data-testid='passwordfield']")).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test theme switchbutton', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        GlobalInfos.enableDarkTheme(false);
 | 
			
		||||
        expect(GlobalInfos.isDarkTheme()).toBe(false);
 | 
			
		||||
        wrapper.find("[data-testid='darktheme-switch']").simulate("change");
 | 
			
		||||
        expect(GlobalInfos.isDarkTheme()).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test savesettings', done => {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        global.fetch = global.prepareFetchApi({success: true});
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(0);
 | 
			
		||||
        const fakeEvent = {preventDefault: () => console.log('preventDefault')};
 | 
			
		||||
        wrapper.find("[data-testid='mainformsettings']").simulate("submit", fakeEvent);
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // todo 2020-07-13: test popup of error success here
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test failing savesettings', done => {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        global.fetch = global.prepareFetchApi({success: false});
 | 
			
		||||
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(0);
 | 
			
		||||
        const fakeEvent = {preventDefault: () => console.log('preventDefault')};
 | 
			
		||||
        wrapper.find("[data-testid='mainformsettings']").simulate("submit", fakeEvent);
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            // todo 2020-07-13: test error popup here!
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test videopath change event', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.state().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");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test tvshowpath change event', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
 | 
			
		||||
        const event = {target: {name: "pollName", value: "test"}};
 | 
			
		||||
        expect(wrapper.state().tvshowpath).not.toBe("test");
 | 
			
		||||
        wrapper.find("[data-testid='tvshowpath']").find("FormControl").simulate("change", event);
 | 
			
		||||
        expect(wrapper.state().tvshowpath).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");
 | 
			
		||||
        wrapper.find("[data-testid='nameform']").find("FormControl").simulate("change", event);
 | 
			
		||||
        expect(wrapper.state().mediacentername).toBe("test");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test password-form change event', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
        wrapper.setState({passwordsupport: true});
 | 
			
		||||
 | 
			
		||||
        const event = {target: {name: "pollName", value: "test"}};
 | 
			
		||||
        expect(wrapper.state().password).not.toBe("test");
 | 
			
		||||
        wrapper.find("[data-testid='passwordfield']").find("FormControl").simulate("change", event);
 | 
			
		||||
        expect(wrapper.state().password).toBe("test");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test tmdbsupport change event', function () {
 | 
			
		||||
        const wrapper = shallow(<GeneralSettings/>);
 | 
			
		||||
        wrapper.setState({tmdbsupport: true});
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.state().tmdbsupport).toBe(true);
 | 
			
		||||
        wrapper.find("[data-testid='tmdb-switch']").simulate("change");
 | 
			
		||||
        expect(wrapper.state().tmdbsupport).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,99 +0,0 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import style from "./MovieSettings.module.css"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component for MovieSettings on Settingspage
 | 
			
		||||
 * handles settings concerning to movies in general
 | 
			
		||||
 */
 | 
			
		||||
class MovieSettings extends React.Component {
 | 
			
		||||
    constructor(props) {
 | 
			
		||||
        super(props);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            text: [],
 | 
			
		||||
            startbtnDisabled: false
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        if (this.myinterval) {
 | 
			
		||||
            clearInterval(this.myinterval);
 | 
			
		||||
        }
 | 
			
		||||
        this.myinterval = setInterval(this.updateStatus, 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        clearInterval(this.myinterval);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <button disabled={this.state.startbtnDisabled} className='reindexbtn btn btn-success' onClick={() => {
 | 
			
		||||
                    this.startReindex()
 | 
			
		||||
                }}>Reindex Movies
 | 
			
		||||
                </button>
 | 
			
		||||
                <div className={style.indextextarea}>{this.state.text.map(m => (
 | 
			
		||||
                    <div className='textarea-element'>{m}</div>
 | 
			
		||||
                ))}</div>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * starts the reindex process of the videos in the specified folder
 | 
			
		||||
     */
 | 
			
		||||
    startReindex() {
 | 
			
		||||
        // clear output text before start
 | 
			
		||||
        this.setState({text: []});
 | 
			
		||||
 | 
			
		||||
        this.setState({startbtnDisabled: true});
 | 
			
		||||
 | 
			
		||||
        console.log("starting");
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/extractvideopreviews.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.text()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    // todo 2020-07-4: some kind of return finished handler
 | 
			
		||||
                    console.log("returned");
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
        if (this.myinterval) {
 | 
			
		||||
            clearInterval(this.myinterval);
 | 
			
		||||
        }
 | 
			
		||||
        this.myinterval = setInterval(this.updateStatus, 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This interval function reloads the current status of reindexing from backend
 | 
			
		||||
     */
 | 
			
		||||
    updateStatus = () => {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        fetch('/api/extractionData.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.contentAvailable === true) {
 | 
			
		||||
                        console.log(result);
 | 
			
		||||
                        // 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 {
 | 
			
		||||
                        // clear refresh interval if no content available
 | 
			
		||||
                        clearInterval(this.myinterval);
 | 
			
		||||
 | 
			
		||||
                        this.setState({startbtnDisabled: false});
 | 
			
		||||
                    }
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MovieSettings;
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
.indextextarea {
 | 
			
		||||
    background-color: #c2c2c2;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
 | 
			
		||||
    margin-top: 15px;
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
 | 
			
		||||
    min-height: 100px;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    width: 50%;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
import {shallow} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import MovieSettings from "./MovieSettings";
 | 
			
		||||
 | 
			
		||||
describe('<MovieSettings/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<MovieSettings/>);
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('received text renders into dom', function () {
 | 
			
		||||
        const wrapper = shallow(<MovieSettings/>);
 | 
			
		||||
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            text: [
 | 
			
		||||
                "firstline",
 | 
			
		||||
                "secline"
 | 
			
		||||
            ]
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find(".indextextarea").find(".textarea-element")).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('test simulate reindex', function () {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({});
 | 
			
		||||
        const wrapper = shallow(<MovieSettings/>);
 | 
			
		||||
 | 
			
		||||
        wrapper.find(".reindexbtn").simulate("click");
 | 
			
		||||
 | 
			
		||||
        // initial send of reindex request to server
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('content available received and in state', done => {
 | 
			
		||||
        global.fetch = global.prepareFetchApi({
 | 
			
		||||
            contentAvailable: true,
 | 
			
		||||
            message: "firstline\nsecondline"
 | 
			
		||||
        });
 | 
			
		||||
        const wrapper = shallow(<MovieSettings/>);
 | 
			
		||||
        wrapper.instance().updateStatus();
 | 
			
		||||
 | 
			
		||||
        process.nextTick(() => {
 | 
			
		||||
            expect(wrapper.state()).toMatchObject({
 | 
			
		||||
                text: [
 | 
			
		||||
                    "firstline",
 | 
			
		||||
                    "secondline"
 | 
			
		||||
                ]
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            global.fetch.mockClear();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,61 +1,87 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import MovieSettings from "./MovieSettings";
 | 
			
		||||
import GeneralSettings from "./GeneralSettings";
 | 
			
		||||
import style from "./SettingsPage.module.css"
 | 
			
		||||
import GlobalInfos from "../../GlobalInfos";
 | 
			
		||||
import "../../css/DefaultPage.css"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The Settingspage handles all kinds of settings for the mediacenter
 | 
			
		||||
 * and is basically a wrapper for child-tabs
 | 
			
		||||
 */
 | 
			
		||||
class SettingsPage extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            currentpage: "general"
 | 
			
		||||
            text: []
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * load the selected tab
 | 
			
		||||
     * @returns {JSX.Element|string} the jsx element of the selected tab
 | 
			
		||||
     */
 | 
			
		||||
    getContent() {
 | 
			
		||||
        switch (this.state.currentpage) {
 | 
			
		||||
            case "general":
 | 
			
		||||
                return <GeneralSettings/>;
 | 
			
		||||
            case "movies":
 | 
			
		||||
                return <MovieSettings/>;
 | 
			
		||||
            case "tv":
 | 
			
		||||
                return <span/>; // todo this page
 | 
			
		||||
            default:
 | 
			
		||||
                return "unknown button clicked";
 | 
			
		||||
    updateStatus = () => {
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        fetch('/api/extractionData.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    if (result.contentAvailable === true) {
 | 
			
		||||
                        console.log(result);
 | 
			
		||||
                        this.setState({
 | 
			
		||||
                            text: [...result.message.split("\n"),
 | 
			
		||||
                                ...this.state.text]
 | 
			
		||||
                        });
 | 
			
		||||
                    } else {
 | 
			
		||||
                        clearInterval(this.myinterval);
 | 
			
		||||
                    }
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        if (this.myinterval) {
 | 
			
		||||
            clearInterval(this.myinterval);
 | 
			
		||||
        }
 | 
			
		||||
        // todo 2020-06-18: maybe not start on mount
 | 
			
		||||
        this.myinterval = setInterval(this.updateStatus, 1000);
 | 
			
		||||
 | 
			
		||||
        // todo 2020-06-18: fetch path data here
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        clearInterval(this.myinterval);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const themestyle = GlobalInfos.getThemeStyle();
 | 
			
		||||
        return (
 | 
			
		||||
            <div>
 | 
			
		||||
                <div className={style.SettingsSidebar + ' ' + themestyle.secbackground}>
 | 
			
		||||
                    <div className={style.SettingsSidebarTitle + ' ' + themestyle.lighttextcolor}>Settings</div>
 | 
			
		||||
                    <div onClick={() => this.setState({currentpage: "general"})}
 | 
			
		||||
                         className={style.SettingSidebarElement}>General
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div onClick={() => this.setState({currentpage: "movies"})}
 | 
			
		||||
                         className={style.SettingSidebarElement}>Movies
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div onClick={() => this.setState({currentpage: "tv"})}
 | 
			
		||||
                         className={style.SettingSidebarElement}>TV Shows
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={style.SettingsContent}>
 | 
			
		||||
                    {this.getContent()}
 | 
			
		||||
                <div className='pageheader'>
 | 
			
		||||
                    <span className='pageheadertitle'>Settings Page</span>
 | 
			
		||||
                    <span className='pageheadersubtitle'>todo</span>
 | 
			
		||||
                    <hr/>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <button className='reindexbtn btn btn-success' onClick={() => {
 | 
			
		||||
                    this.startReindex()
 | 
			
		||||
                }}>Reindex Movies
 | 
			
		||||
                </button>
 | 
			
		||||
                <div className='indextextarea'>{this.state.text.map(m => (
 | 
			
		||||
                    <div className='textarea-element'>{m}</div>
 | 
			
		||||
                ))}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    startReindex() {
 | 
			
		||||
        console.log("starting");
 | 
			
		||||
        const updateRequest = new FormData();
 | 
			
		||||
        // fetch all videos available
 | 
			
		||||
        fetch('/api/extractvideopreviews.php', {method: 'POST', body: updateRequest})
 | 
			
		||||
            .then((response) => response.json()
 | 
			
		||||
                .then((result) => {
 | 
			
		||||
                    console.log("returned");
 | 
			
		||||
                }))
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log("no connection to backend");
 | 
			
		||||
            });
 | 
			
		||||
        if (this.myinterval) {
 | 
			
		||||
            clearInterval(this.myinterval);
 | 
			
		||||
        }
 | 
			
		||||
        this.myinterval = setInterval(this.updateStatus, 1000);
 | 
			
		||||
        console.log("sent");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SettingsPage;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
.SettingsSidebar {
 | 
			
		||||
    border-bottom-right-radius: 10px;
 | 
			
		||||
    border-top-right-radius: 10px;
 | 
			
		||||
    float: left;
 | 
			
		||||
    min-height: calc(100vh - 62px);
 | 
			
		||||
    min-width: 110px;
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
    width: 10%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.SettingsSidebarTitle {
 | 
			
		||||
    font-size: larger;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-bottom: 25px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.SettingsContent {
 | 
			
		||||
    float: left;
 | 
			
		||||
    padding-left: 30px;
 | 
			
		||||
    padding-top: 30px;
 | 
			
		||||
    width: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.SettingSidebarElement {
 | 
			
		||||
    background-color: #919fd9;
 | 
			
		||||
    border-radius: 7px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin: 10px 5px 5px;
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.SettingSidebarElement:hover {
 | 
			
		||||
    background-color: #7d8dd4;
 | 
			
		||||
    box-shadow: #7d8dd4 0 0 0 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
    transition: all 300ms;
 | 
			
		||||
}
 | 
			
		||||
@@ -2,39 +2,40 @@ import {shallow} from "enzyme";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import SettingsPage from "./SettingsPage";
 | 
			
		||||
 | 
			
		||||
function prepareFetchApi(response) {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('<RandomPage/>', function () {
 | 
			
		||||
    it('renders without crashing ', function () {
 | 
			
		||||
        const wrapper = shallow(<SettingsPage/>);
 | 
			
		||||
        wrapper.unmount();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('simulate topic clicka', function () {
 | 
			
		||||
    it('received text renders into dom', function () {
 | 
			
		||||
        const wrapper = shallow(<SettingsPage/>);
 | 
			
		||||
 | 
			
		||||
        simulateSideBarClick("General", wrapper);
 | 
			
		||||
        expect(wrapper.state().currentpage).toBe("general");
 | 
			
		||||
        expect(wrapper.find(".SettingsContent").find("GeneralSettings")).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        simulateSideBarClick("Movies", wrapper);
 | 
			
		||||
        expect(wrapper.state().currentpage).toBe("movies");
 | 
			
		||||
        expect(wrapper.find(".SettingsContent").find("MovieSettings")).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        simulateSideBarClick("TV Shows", wrapper);
 | 
			
		||||
        expect(wrapper.state().currentpage).toBe("tv");
 | 
			
		||||
        expect(wrapper.find(".SettingsContent").find("span")).toHaveLength(1);
 | 
			
		||||
        wrapper.setState({
 | 
			
		||||
            text: [
 | 
			
		||||
                "firstline",
 | 
			
		||||
                "secline"
 | 
			
		||||
            ]
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    function simulateSideBarClick(name, wrapper) {
 | 
			
		||||
        wrapper.find(".SettingSidebarElement").findWhere(it =>
 | 
			
		||||
            it.text() === name &&
 | 
			
		||||
            it.type() === "div").simulate("click");
 | 
			
		||||
    }
 | 
			
		||||
        expect(wrapper.find(".indextextarea").find(".textarea-element")).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('simulate unknown topic', function () {
 | 
			
		||||
    it('test simulate reindex', function () {
 | 
			
		||||
        global.fetch = prepareFetchApi({});
 | 
			
		||||
        const wrapper = shallow(<SettingsPage/>);
 | 
			
		||||
        wrapper.setState({currentpage: "unknown"});
 | 
			
		||||
 | 
			
		||||
        expect(wrapper.find(".SettingsContent").text()).toBe("unknown button clicked");
 | 
			
		||||
        wrapper.find(".reindexbtn").simulate("click");
 | 
			
		||||
 | 
			
		||||
        // initial send of reindex request to server
 | 
			
		||||
        expect(global.fetch).toBeCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -8,25 +8,3 @@ import {configure} from 'enzyme';
 | 
			
		||||
import Adapter from 'enzyme-adapter-react-16';
 | 
			
		||||
 | 
			
		||||
configure({adapter: new Adapter()});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * prepares fetch api for a virtual test call
 | 
			
		||||
 * @param response the response fetch should give you back
 | 
			
		||||
 * @returns {jest.Mock<any, any>} a jest mock function simulating a fetch
 | 
			
		||||
 */
 | 
			
		||||
global.prepareFetchApi = (response) => {
 | 
			
		||||
    const mockJsonPromise = Promise.resolve(response);
 | 
			
		||||
    const mockFetchPromise = Promise.resolve({
 | 
			
		||||
        json: () => mockJsonPromise,
 | 
			
		||||
    });
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * prepares a failing virtual fetch api call
 | 
			
		||||
 * @returns {jest.Mock<any, any>} a jest moch function simulating a failing fetch call
 | 
			
		||||
 */
 | 
			
		||||
global.prepareFailingFetchApi = () => {
 | 
			
		||||
    const mockFetchPromise = Promise.reject("myreason");
 | 
			
		||||
    return (jest.fn().mockImplementation(() => mockFetchPromise));
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user