Merge branch 'documentation' into 'master'

Documentation and minor reformattings

Closes #27 and #35

See merge request lukas/openmediacenter!11
This commit is contained in:
Lukas Heiligenbrunner 2020-08-12 17:50:26 +00:00
commit f0902d29b7
34 changed files with 457 additions and 238 deletions

View File

@ -8,14 +8,18 @@ Feel free to contribute or open an issue here: https://gitlab.heili.eu/lukas/ope
## What is this? ## What is this?
Open Media Center is an open source solution for a mediacenter in your home network. Open Media Center is an open source solution for a mediacenter in your home network.
Transform your webserver into a mediaserver. Transform your webserver into a mediaserver.
It's based on Reactjs and PHP is used as backend. It's based on Reactjs and PHP is used for backend.
It is optimized for general videos as well as for movies. It is optimized for general videos as well as for movies.
For grabbing movie data TMDB is used. For grabbing movie data TMDB is used.
For organizing videos tags are used. With the help of tags you can organize your video gravity.
Here you can see an example main page: Here you can see an example main page in light mode:
![Image of OpenMediaCenter](https://i.ibb.co/2PC3fmk/Screenshot-20200604-163448.png) ![Image of OpenMediaCenter](https://i.ibb.co/pnDjgNT/Screenshot-20200812-172945.png)
and in dark mode:
![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png)
## Installation ## Installation
First of all clone the repository. First of all clone the repository.
@ -32,9 +36,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. The login data to this database needs to be specified in the `api/Database.php` file.
## Usage ## Usage
To index Videos run on your server: `php extractvideopreviews.php`. Now you can access your MediaCenter via your servers global ip (:
Now you can access your MediaCenter via the servers global ip (: At the settings tab you can set the correct videopath on server and click reindex afterwards.
## Contact ## Contact
Any contribution is appreciated. Any contribution is appreciated.

View File

@ -1,30 +0,0 @@
<?php
require_once 'RequestBase.php';
class Tags extends RequestBase {
function initHandlers() {
$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);
}
echo json_encode($rows);
});
$this->addActionHandler("createTag", function (){
$query = "INSERT INTO tags (tag_name) VALUES ('" . $_POST['tagname'] . "')";
if ($this->conn->query($query) === TRUE) {
echo('{"result":"success"}');
} else {
echo('{"result":"' . $this->conn->error . '"}');
}
});
}
}
$tags = new Tags();
$tags->handleAction();

View File

@ -1,7 +1,7 @@
<?php <?php
require 'Database.php'; require_once './src/Database.php';
require 'TMDBMovie.php'; require_once './src/TMDBMovie.php';
require 'SSettings.php'; require_once './src/SSettings.php';
// allow UTF8 characters // allow UTF8 characters
setlocale(LC_ALL, 'en_US.UTF-8'); setlocale(LC_ALL, 'en_US.UTF-8');
@ -226,8 +226,7 @@ writeLog("-42"); // terminating characters to stop webui requesting infos
* @param $video string name including extension * @param $video string name including extension
* @return object all infos as object * @return object all infos as object
*/ */
function _get_video_attributes($video) function _get_video_attributes($video) {
{
$command = "mediainfo \"../videos/prn/$video\" --Output=JSON"; $command = "mediainfo \"../videos/prn/$video\" --Output=JSON";
$output = shell_exec($command); $output = shell_exec($command);
return json_decode($output); return json_decode($output);
@ -238,8 +237,7 @@ function _get_video_attributes($video)
* *
* @param string $message message to write * @param string $message message to write
*/ */
function writeLog(string $message) function writeLog(string $message) {
{
file_put_contents("/tmp/output.log", $message, FILE_APPEND); file_put_contents("/tmp/output.log", $message, FILE_APPEND);
flush(); flush();
} }
@ -249,8 +247,7 @@ function writeLog(string $message)
* @param string $tagname the name of the tag * @param string $tagname the name of the tag
* @return integer the id of the inserted tag * @return integer the id of the inserted tag
*/ */
function tagExists(string $tagname) function tagExists(string $tagname) {
{
global $conn; global $conn;
$query = "SELECT * FROM tags WHERE tag_name='$tagname'"; $query = "SELECT * FROM tags WHERE tag_name='$tagname'";

5
api/settings.php Normal file
View File

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

View File

@ -5,8 +5,7 @@
* *
* Class with all neccessary stuff for the Database connections. * Class with all neccessary stuff for the Database connections.
*/ */
class Database class Database {
{
private static ?Database $instance = null; private static ?Database $instance = null;
private mysqli $conn; private mysqli $conn;
@ -16,8 +15,7 @@ class Database
private string $dbname = "mediacenter"; private string $dbname = "mediacenter";
// The db connection is established in the private constructor. // The db connection is established in the private constructor.
private function __construct() private function __construct() {
{
// Create connection // Create connection
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname); $this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
@ -32,8 +30,7 @@ class Database
* *
* @return Database dbobject * @return Database dbobject
*/ */
public static function getInstance() public static function getInstance() {
{
if (!self::$instance) { if (!self::$instance) {
self::$instance = new Database(); self::$instance = new Database();
} }
@ -46,8 +43,7 @@ class Database
* *
* @return mysqli mysqli instance * @return mysqli mysqli instance
*/ */
public function getConnection() public function getConnection() {
{
return $this->conn; return $this->conn;
} }

View File

@ -1,7 +1,10 @@
<?php <?php
class SSettings /**
{ * Class SSettings
* class handling all Settings used by php scripts
*/
class SSettings {
private ?Database $database; private ?Database $database;
/** /**
@ -11,6 +14,10 @@ class SSettings
$this->database = Database::getInstance(); $this->database = Database::getInstance();
} }
/**
* get the videopath saved in db
* @return string videopath
*/
public function getVideoPath() { public function getVideoPath() {
$query = "SELECT video_path from settings"; $query = "SELECT video_path from settings";
@ -24,8 +31,7 @@ class SSettings
* check if TMDB is enableds * check if TMDB is enableds
* @return bool isenabled? * @return bool isenabled?
*/ */
public function isTMDBGrabbingEnabled(): bool public function isTMDBGrabbingEnabled(): bool {
{
$query = "SELECT TMDB_grabbing from settings"; $query = "SELECT TMDB_grabbing from settings";
$result = $this->database->getConnection()->query($query); $result = $this->database->getConnection()->query($query);

View File

@ -1,10 +1,13 @@
<?php <?php
class TMDBMovie /**
{ * Class TMDBMovie
* class to handle all interactions with the tmdb api
*/
class TMDBMovie {
public $picturebase = "https://image.tmdb.org/t/p/w500";
private $apikey = "9fd90530b11447f5646f8e6fb4733fb4"; private $apikey = "9fd90530b11447f5646f8e6fb4733fb4";
private $baseurl = "https://api.themoviedb.org/3/"; private $baseurl = "https://api.themoviedb.org/3/";
public $picturebase = "https://image.tmdb.org/t/p/w500";
/** /**
* search for a specific movie * search for a specific movie
@ -12,8 +15,7 @@ class TMDBMovie
* @param string $moviename moviename * @param string $moviename moviename
* @return object movie object or null if not found * @return object movie object or null if not found
*/ */
public function searchMovie(string $moviename) public function searchMovie(string $moviename) {
{
$reply = json_decode(file_get_contents($this->baseurl . "search/movie?api_key=" . $this->apikey . "&query=" . urlencode($moviename))); $reply = json_decode(file_get_contents($this->baseurl . "search/movie?api_key=" . $this->apikey . "&query=" . urlencode($moviename)));
if ($reply->total_results == 0) { if ($reply->total_results == 0) {
// no results found // no results found
@ -29,8 +31,7 @@ class TMDBMovie
* *
* @return array of all available genres * @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)); $reply = json_decode(file_get_contents($this->baseurl . "genre/movie/list?api_key=" . $this->apikey));
return $reply->genres; return $reply->genres;
} }

View File

@ -1,14 +1,9 @@
<?php <?php
require_once 'Database.php'; require_once 'src/Database.php';
abstract class RequestBase { abstract class RequestBase {
private array $actions = array();
protected mysqli $conn; protected mysqli $conn;
private array $actions = array();
/**
* add the action handlers in this abstract method
*/
abstract function initHandlers();
/** /**
* adds a new action handler to the current api file * adds a new action handler to the current api file
@ -38,4 +33,18 @@ abstract class RequestBase {
echo('{data:"error"}'); 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();
} }

View File

@ -1,8 +1,23 @@
<?php <?php
require 'RequestBase.php'; require_once 'RequestBase.php';
/**
* Class Settings
* Backend for the Settings page
*/
class Settings extends RequestBase { class Settings extends RequestBase {
function initHandlers() { 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 () { $this->addActionHandler("loadGeneralSettings", function () {
$query = "SELECT * from settings"; $query = "SELECT * from settings";
@ -16,6 +31,30 @@ class Settings extends RequestBase {
echo json_encode($r); 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 () { $this->addActionHandler("saveGeneralSettings", function () {
$mediacentername = $_POST['mediacentername']; $mediacentername = $_POST['mediacentername'];
$password = $_POST['password']; $password = $_POST['password'];
@ -34,26 +73,10 @@ class Settings extends RequestBase {
WHERE 1"; WHERE 1";
if ($this->conn->query($query) === true) { if ($this->conn->query($query) === true) {
echo '{"success": true}'; $this->commitMessage('{"success": true}');
} else { } else {
echo '{"success": true}'; $this->commitMessage('{"success": true}');
} }
}); });
$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');
echo json_encode($r);
});
} }
} }
$sett = new Settings();
$sett->handleAction();

66
api/src/handlers/Tags.php Normal file
View File

@ -0,0 +1,66 @@
<?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 . '"}');
}
});
}
}

View File

@ -1,8 +1,11 @@
<?php <?php
require_once 'Database.php'; require_once 'src/SSettings.php';
require_once 'SSettings.php';
require_once 'RequestBase.php'; require_once 'RequestBase.php';
/**
* Class Video
* backend for all interactions with videoloads and receiving of video infos
*/
class Video extends RequestBase { class Video extends RequestBase {
private string $videopath; private string $videopath;
@ -13,6 +16,15 @@ class Video extends RequestBase {
} }
function initHandlers() { 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 () { $this->addActionHandler("getMovies", function () {
$query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name"; $query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name";
if (isset($_POST['tag'])) { if (isset($_POST['tag'])) {
@ -31,7 +43,7 @@ class Video extends RequestBase {
array_push($rows, $r); array_push($rows, $r);
} }
echo(json_encode($rows)); $this->commitMessage(json_encode($rows));
}); });
$this->addActionHandler("getRandomMovies", function () { $this->addActionHandler("getRandomMovies", function () {
@ -59,7 +71,7 @@ class Video extends RequestBase {
array_push($return->tags, $r); array_push($return->tags, $r);
} }
echo(json_encode($return)); $this->commitMessage(json_encode($return));
}); });
$this->addActionHandler("getSearchKeyWord", function () { $this->addActionHandler("getSearchKeyWord", function () {
@ -74,9 +86,14 @@ class Video extends RequestBase {
array_push($rows, $r); array_push($rows, $r);
} }
echo(json_encode($rows)); $this->commitMessage(json_encode($rows));
}); });
}
/**
* function to handle stuff for loading specific videos and startdata
*/
private function loadVideos() {
$this->addActionHandler("loadVideo", function () { $this->addActionHandler("loadVideo", function () {
$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='" . $_POST['movieid'] . "'";
@ -110,23 +127,7 @@ class Video extends RequestBase {
array_push($arr['tags'], $r); array_push($arr['tags'], $r);
} }
echo(json_encode($arr)); $this->commitMessage(json_encode($arr));
});
$this->addActionHandler("getDbSize", function () {
$dbname = Database::getInstance()->getDatabaseName();
$query = "SELECT table_schema AS \"Database\",
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS \"Size\"
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='$dbname'
GROUP BY table_schema;";
$result = $this->conn->query($query);
if ($result->num_rows == 1) {
$row = $result->fetch_assoc();
echo '{"data":"' . $row["Size"] . 'MB"}';
}
}); });
$this->addActionHandler("readThumbnail", function () { $this->addActionHandler("readThumbnail", function () {
@ -135,38 +136,7 @@ class Video extends RequestBase {
$result = $this->conn->query($query); $result = $this->conn->query($query);
$row = $result->fetch_assoc(); $row = $result->fetch_assoc();
echo($row["thumbnail"]); $this->commitMessage($row["thumbnail"]);
});
$this->addActionHandler("getTags", function () {
// 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 = $this->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));
});
$this->addActionHandler("addLike", function () {
$movieid = $_POST['movieid'];
$query = "update videos set likes = likes + 1 where movie_id = '$movieid'";
if ($this->conn->query($query) === TRUE) {
echo('{"result":"success"}');
} else {
echo('{"result":"' . $this->conn->error . '"}');
}
}); });
$this->addActionHandler("getStartData", function () { $this->addActionHandler("getStartData", function () {
@ -213,34 +183,24 @@ class Video extends RequestBase {
$r = mysqli_fetch_assoc($result); $r = mysqli_fetch_assoc($result);
$arr['tags'] = $r['nr']; $arr['tags'] = $r['nr'];
echo(json_encode($arr)); $this->commitMessage(json_encode($arr));
}); });
$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);
} }
echo(json_encode($rows));
});
$this->addActionHandler("addTag", function () { /**
* function to handle api handlers for stuff to add to video or database
*/
private function addToVideo() {
$this->addActionHandler("addLike", function () {
$movieid = $_POST['movieid']; $movieid = $_POST['movieid'];
$tagid = $_POST['id'];
$query = "INSERT INTO video_tags(tag_id, video_id) VALUES ('$tagid','$movieid')"; $query = "update videos set likes = likes + 1 where movie_id = '$movieid'";
if ($this->conn->query($query) === TRUE) { if ($this->conn->query($query) === TRUE) {
echo('{"result":"success"}'); $this->commitMessage('{"result":"success"}');
} else { } else {
echo('{"result":"' . $this->conn->error . '"}'); $this->commitMessage('{"result":"' . $this->conn->error . '"}');
} }
}); });
} }
} }
$video = new Video();
$video->handleAction();

5
api/tags.php Normal file
View File

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

5
api/video.php Normal file
View File

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

View File

@ -10,6 +10,9 @@ import style from './App.module.css'
import SettingsPage from "./pages/SettingsPage/SettingsPage"; import SettingsPage from "./pages/SettingsPage/SettingsPage";
import CategoryPage from "./pages/CategoryPage/CategoryPage"; import CategoryPage from "./pages/CategoryPage/CategoryPage";
/**
* The main App handles the main tabs and which content to show
*/
class App extends React.Component { class App extends React.Component {
newElement = null; newElement = null;
@ -31,7 +34,7 @@ class App extends React.Component {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'loadInitialData'); updateRequest.append('action', 'loadInitialData');
fetch('/api/Settings.php', {method: 'POST', body: updateRequest}) fetch('/api/settings.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
// set theme // set theme
@ -47,6 +50,10 @@ class App extends React.Component {
})); }));
} }
/**
* create a viewbinding to call APP functions from child elements
* @returns a set of callback functions
*/
constructViewBinding() { constructViewBinding() {
return { return {
changeRootElement: this.changeRootElement, changeRootElement: this.changeRootElement,
@ -54,6 +61,10 @@ class App extends React.Component {
}; };
} }
/**
* load the selected component into the main view
* @returns {JSX.Element} body element of selected page
*/
MainBody() { MainBody() {
let page; let page;
if (this.state.page === "default") { if (this.state.page === "default") {
@ -109,6 +120,9 @@ class App extends React.Component {
); );
} }
/**
* render a new root element into the main body
*/
changeRootElement(element) { changeRootElement(element) {
this.newElement = element; this.newElement = element;
@ -117,6 +131,9 @@ class App extends React.Component {
}); });
} }
/**
* return from page to the previous page before a change
*/
returnToLastElement() { returnToLastElement() {
this.setState({ this.setState({
page: "lastpage" page: "lastpage"

View File

@ -1,12 +1,12 @@
.navitem { .navitem {
float: left;
margin-left: 20px;
cursor: pointer; cursor: pointer;
opacity: 0.6; float: left;
font-size: large; font-size: large;
font-weight: bold; font-weight: bold;
margin-left: 20px;
opacity: 0.6;
text-transform: capitalize; text-transform: capitalize;
} }
@ -18,9 +18,9 @@
.navitem::after { .navitem::after {
content: ''; content: '';
display: block; display: block;
width: 0;
height: 2px; height: 2px;
transition: width .3s; transition: width .3s;
width: 0;
} }
.navitem:hover::after { .navitem:hover::after {
@ -33,22 +33,22 @@
} }
.navcontainer { .navcontainer {
border-bottom-width: 2px;
border-style: dotted;
border-width: 0;
padding-bottom: 40px;
padding-top: 20px; padding-top: 20px;
width: 100%; width: 100%;
padding-bottom: 40px;
border-width: 0;
border-style: dotted;
border-bottom-width: 2px;
} }
.navbrand { .navbrand {
margin-left: 20px; float: left;
margin-right: 20px;
font-size: large; font-size: large;
font-weight: bold; font-weight: bold;
margin-left: 20px;
margin-right: 20px;
text-transform: capitalize; text-transform: capitalize;
float: left;
} }

View File

@ -1,23 +1,37 @@
import darktheme from "./AppDarkTheme.module.css"; import darktheme from "./AppDarkTheme.module.css";
import lighttheme from "./AppLightTheme.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 { class StaticInfos {
#darktheme = true; #darktheme = true;
/**
* check if the current theme is the dark theme
* @returns {boolean} is dark theme?
*/
isDarkTheme() { isDarkTheme() {
return this.#darktheme; return this.#darktheme;
}; };
/**
* setter to enable or disable the dark or light theme
* @param enable enable the dark theme?
*/
enableDarkTheme(enable = true) { enableDarkTheme(enable = true) {
this.#darktheme = enable; this.#darktheme = enable;
} }
/**
* get the currently selected theme stylesheet
* @returns {*} the style object of the current active theme
*/
getThemeStyle() { getThemeStyle() {
return this.isDarkTheme() ? darktheme : lighttheme; return this.isDarkTheme() ? darktheme : lighttheme;
} }
} }
const GlobalInfos = new StaticInfos(); const GlobalInfos = new StaticInfos();
//Object.freeze(StaticInfos);
export default GlobalInfos; export default GlobalInfos;

View File

@ -3,6 +3,9 @@ import Modal from 'react-bootstrap/Modal'
import Dropdown from "react-bootstrap/Dropdown"; import Dropdown from "react-bootstrap/Dropdown";
import DropdownButton from "react-bootstrap/DropdownButton"; import DropdownButton from "react-bootstrap/DropdownButton";
/**
* component creates overlay to add a new tag to a video
*/
class AddTagPopup extends React.Component { class AddTagPopup extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -22,7 +25,7 @@ class AddTagPopup extends React.Component {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'getAllTags'); updateRequest.append('action', 'getAllTags');
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/tags.php', {method: 'POST', body: updateRequest})
.then((response) => response.json()) .then((response) => response.json())
.then((result) => { .then((result) => {
this.setState({ this.setState({
@ -68,13 +71,16 @@ class AddTagPopup extends React.Component {
); );
} }
/**
* store the filled in form to the backend
*/
storeselection() { storeselection() {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'addTag'); updateRequest.append('action', 'addTag');
updateRequest.append('id', this.state.selection.id); updateRequest.append('id', this.state.selection.id);
updateRequest.append('movieid', this.props.movie_id); updateRequest.append('movieid', this.props.movie_id);
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/tags.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
if (result.result !== "success") { if (result.result !== "success") {

View File

@ -2,6 +2,9 @@ import React from "react";
import Modal from 'react-bootstrap/Modal' import Modal from 'react-bootstrap/Modal'
import {Form} from "react-bootstrap"; import {Form} from "react-bootstrap";
/**
* creates modal overlay to define a new Tag
*/
class NewTagPopup extends React.Component { class NewTagPopup extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -45,12 +48,15 @@ class NewTagPopup extends React.Component {
); );
} }
/**
* store the filled in form to the backend
*/
storeselection() { storeselection() {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'createTag'); updateRequest.append('action', 'createTag');
updateRequest.append('tagname', this.value); 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((response) => response.json())
.then((result) => { .then((result) => {
if (result.result !== "success") { if (result.result !== "success") {

View File

@ -2,14 +2,10 @@ import React from "react";
import style from "./PageTitle.module.css" import style from "./PageTitle.module.css"
import GlobalInfos from "../../GlobalInfos"; import GlobalInfos from "../../GlobalInfos";
/**
* Component for generating PageTitle with bottom Line
*/
class PageTitle extends React.Component { class PageTitle extends React.Component {
constructor(props) {
super(props);
this.props = props;
}
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (

View File

@ -8,7 +8,7 @@
} }
.pageheadersubtitle { .pageheadersubtitle {
margin-left: 20px;
font-size: 23pt; font-size: 23pt;
margin-left: 20px;
opacity: 0.6; opacity: 0.6;
} }

View File

@ -4,6 +4,10 @@ import Player from "../../pages/Player/Player";
import {Spinner} from "react-bootstrap"; import {Spinner} from "react-bootstrap";
import GlobalInfos from "../../GlobalInfos"; import GlobalInfos from "../../GlobalInfos";
/**
* Component for single preview tile
* floating side by side
*/
class Preview extends React.Component { class Preview extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -24,7 +28,7 @@ class Preview extends React.Component {
updateRequest.append('action', 'readThumbnail'); updateRequest.append('action', 'readThumbnail');
updateRequest.append('movieid', this.props.movie_id); updateRequest.append('movieid', this.props.movie_id);
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.text() .then((response) => response.text()
.then((result) => { .then((result) => {
this.setState({ this.setState({
@ -53,6 +57,9 @@ class Preview extends React.Component {
); );
} }
/**
* handle the click event of a tile
*/
itemClick() { itemClick() {
console.log("item clicked!" + this.state.name); console.log("item clicked!" + this.state.name);
@ -63,6 +70,9 @@ class Preview extends React.Component {
} }
} }
/**
* Component for a Tag-name tile (used in category page)
*/
export class TagPreview extends React.Component { export class TagPreview extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
@ -75,6 +85,9 @@ export class TagPreview extends React.Component {
); );
} }
/**
* handle the click event of a Tag tile
*/
itemClick() { itemClick() {
this.props.categorybinding(this.props.name); this.props.categorybinding(this.props.name);
} }

View File

@ -1,11 +1,11 @@
.previewtitle { .previewtitle {
height: 20px;
text-align: center;
font-size: smaller; font-size: smaller;
font-weight: bold; font-weight: bold;
height: 20px; height: 20px;
height: 20px;
max-width: 266px; max-width: 266px;
text-align: center; text-align: center;
text-align: center;
} }
.previewpic { .previewpic {

View File

@ -2,6 +2,9 @@ import React from "react";
import style from "./SideBar.module.css" import style from "./SideBar.module.css"
import GlobalInfos from "../../GlobalInfos"; import GlobalInfos from "../../GlobalInfos";
/**
* component for sidebar-info
*/
class SideBar extends React.Component { class SideBar extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
@ -11,6 +14,9 @@ class SideBar extends React.Component {
} }
} }
/**
* The title of the sidebar
*/
export class SideBarTitle extends React.Component { export class SideBarTitle extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
@ -20,6 +26,9 @@ export class SideBarTitle extends React.Component {
} }
} }
/**
* An item of the sidebar
*/
export class SideBarItem extends React.Component { export class SideBarItem extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();

View File

@ -3,6 +3,9 @@ import React from "react";
import styles from "./Tag.module.css" import styles from "./Tag.module.css"
import CategoryPage from "../../pages/CategoryPage/CategoryPage"; import CategoryPage from "../../pages/CategoryPage/CategoryPage";
/**
* A Component representing a single Category tag
*/
class Tag extends React.Component { class Tag extends React.Component {
render() { render() {
return ( return (
@ -11,9 +14,13 @@ class Tag extends React.Component {
); );
} }
/**
* click handling for a Tag
*/
TagClick() { TagClick() {
const tag = this.props.children.toString().toLowerCase(); const tag = this.props.children.toString().toLowerCase();
// call callback functin to switch to category page with specified tag
this.props.viewbinding.changeRootElement( this.props.viewbinding.changeRootElement(
<CategoryPage <CategoryPage
category={tag} category={tag}

View File

@ -2,6 +2,10 @@ import React from "react";
import Preview from "../Preview/Preview"; import Preview from "../Preview/Preview";
import style from "./VideoContainer.module.css" 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 { class VideoContainer extends React.Component {
// stores current index of loaded elements // stores current index of loaded elements
loadindex = 0; loadindex = 0;
@ -43,9 +47,14 @@ class VideoContainer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.setState({}); this.setState({});
// unbind scroll listener when unmounting component
document.removeEventListener('scroll', this.trackScrolling); document.removeEventListener('scroll', this.trackScrolling);
} }
/**
* load previews to the container
* @param nr number of previews to load
*/
loadPreviewBlock(nr) { loadPreviewBlock(nr) {
console.log("loadpreviewblock called ...") console.log("loadpreviewblock called ...")
let ret = []; let ret = [];
@ -67,9 +76,11 @@ class VideoContainer extends React.Component {
this.loadindex += nr; this.loadindex += nr;
} }
/**
* scroll event handler -> load new previews if on bottom
*/
trackScrolling = () => { trackScrolling = () => {
// comparison if current scroll position is on bottom // comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
// 200 stands for bottom offset to trigger load
if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) { if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
this.loadPreviewBlock(8); this.loadPreviewBlock(8);
} }

View File

@ -8,6 +8,10 @@ import NewTagPopup from "../../elements/NewTagPopup/NewTagPopup";
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle"; import PageTitle, {Line} from "../../elements/PageTitle/PageTitle";
import VideoContainer from "../../elements/VideoContainer/VideoContainer"; 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 { class CategoryPage extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -27,7 +31,11 @@ class CategoryPage extends React.Component {
} }
} }
render() { /**
* render the Title and SideBar component for the Category page
* @returns {JSX.Element} corresponding jsx element for Title and Sidebar
*/
renderSideBarATitle() {
return ( return (
<> <>
<PageTitle <PageTitle
@ -61,7 +69,14 @@ class CategoryPage extends React.Component {
this.setState({popupvisible: true}) this.setState({popupvisible: true})
}}>Add a new Tag! }}>Add a new Tag!
</button> </button>
</SideBar> </SideBar></>
);
}
render() {
return (
<>
{this.renderSideBarATitle()}
{this.state.selected ? {this.state.selected ?
<> <>
@ -100,10 +115,18 @@ class CategoryPage extends React.Component {
); );
} }
/**
* load a specific tag into a new previewcontainer
* @param tagname
*/
loadTag = (tagname) => { loadTag = (tagname) => {
this.fetchVideoData(tagname); this.fetchVideoData(tagname);
}; };
/**
* fetch data for a specific tag from backend
* @param tag tagname
*/
fetchVideoData(tag) { fetchVideoData(tag) {
console.log(tag); console.log(tag);
const updateRequest = new FormData(); const updateRequest = new FormData();
@ -113,7 +136,7 @@ class CategoryPage extends React.Component {
console.log("fetching data"); console.log("fetching data");
// fetch all videos available // fetch all videos available
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
this.videodata = result; this.videodata = result;
@ -125,6 +148,9 @@ class CategoryPage extends React.Component {
}); });
} }
/**
* go back to the default category overview
*/
loadCategoryPageDefault = () => { loadCategoryPageDefault = () => {
this.setState({selected: null}); this.setState({selected: null});
this.loadTags(); this.loadTags();
@ -138,7 +164,7 @@ class CategoryPage extends React.Component {
updateRequest.append('action', 'getAllTags'); updateRequest.append('action', 'getAllTags');
// fetch all videos available // 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((response) => response.json()
.then((result) => { .then((result) => {
this.setState({loadedtags: result}); this.setState({loadedtags: result});

View File

@ -6,6 +6,9 @@ import VideoContainer from "../../elements/VideoContainer/VideoContainer";
import style from "./HomePage.module.css" import style from "./HomePage.module.css"
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle"; import PageTitle, {Line} from "../../elements/PageTitle/PageTitle";
/**
* The home page component showing on the initial pageload
*/
class HomePage extends React.Component { class HomePage extends React.Component {
/** keyword variable needed temporary store search keyword */ /** keyword variable needed temporary store search keyword */
keyword = ""; keyword = "";
@ -47,7 +50,7 @@ class HomePage extends React.Component {
console.log("fetching data"); console.log("fetching data");
// fetch all videos available // fetch all videos available
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
this.setState({ this.setState({
@ -71,7 +74,7 @@ class HomePage extends React.Component {
updateRequest.append('action', 'getStartData'); updateRequest.append('action', 'getStartData');
// fetch all videos available // fetch all videos available
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
this.setState({ this.setState({
@ -102,7 +105,7 @@ class HomePage extends React.Component {
updateRequest.append('keyword', keyword); updateRequest.append('keyword', keyword);
// fetch all videos available // fetch all videos available
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
this.setState({ this.setState({

View File

@ -8,6 +8,10 @@ import AddTagPopup from "../../elements/AddTagPopup/AddTagPopup";
import PageTitle, {Line} from "../../elements/PageTitle/PageTitle"; 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 { class Player extends React.Component {
options = { options = {
controls: [ controls: [
@ -59,7 +63,8 @@ class Player extends React.Component {
{this.state.quality !== 0 ? {this.state.quality !== 0 ?
<SideBarItem><b>{this.state.quality}p</b> Quality!</SideBarItem> : null} <SideBarItem><b>{this.state.quality}p</b> Quality!</SideBarItem> : null}
{this.state.length !== 0 ? {this.state.length !== 0 ?
<SideBarItem><b>{Math.round(this.state.length / 60)}</b> Minutes of length!</SideBarItem>: null} <SideBarItem><b>{Math.round(this.state.length / 60)}</b> Minutes of
length!</SideBarItem> : null}
<Line/> <Line/>
<SideBarTitle>Tags:</SideBarTitle> <SideBarTitle>Tags:</SideBarTitle>
{this.state.tags.map((m) => ( {this.state.tags.map((m) => (
@ -98,12 +103,15 @@ class Player extends React.Component {
); );
} }
/**
* fetch all the required infos of a video from backend
*/
fetchMovieData() { fetchMovieData() {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'loadVideo'); updateRequest.append('action', 'loadVideo');
updateRequest.append('movieid', this.props.movie_id); updateRequest.append('movieid', this.props.movie_id);
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json()) .then((response) => response.json())
.then((result) => { .then((result) => {
this.setState({ this.setState({
@ -129,13 +137,15 @@ class Player extends React.Component {
} }
/* Click Listener */ /**
* click handler for the like btn
*/
likebtn() { likebtn() {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'addLike'); updateRequest.append('action', 'addLike');
updateRequest.append('movieid', this.props.movie_id); updateRequest.append('movieid', this.props.movie_id);
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
if (result.result === "success") { if (result.result === "success") {
@ -147,6 +157,10 @@ class Player extends React.Component {
})); }));
} }
/**
* closebtn click handler
* calls callback to viewbinding to show previous page agains
*/
closebtn() { closebtn() {
this.props.viewbinding.returnToLastElement(); this.props.viewbinding.returnToLastElement();
} }

View File

@ -13,8 +13,8 @@
float: left; float: left;
margin-left: 20px; margin-left: 20px;
margin-top: 25px; margin-top: 25px;
width: 60%;
margin-top: 20px; margin-top: 20px;
width: 60%;
} }
.videoactions { .videoactions {

View File

@ -5,6 +5,9 @@ import Tag from "../../elements/Tag/Tag";
import PageTitle from "../../elements/PageTitle/PageTitle"; import PageTitle from "../../elements/PageTitle/PageTitle";
import VideoContainer from "../../elements/VideoContainer/VideoContainer"; import VideoContainer from "../../elements/VideoContainer/VideoContainer";
/**
* Randompage shuffles random viedeopreviews and provides a shuffle btn
*/
class RandomPage extends React.Component { class RandomPage extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -50,17 +53,24 @@ class RandomPage extends React.Component {
); );
} }
/**
* click handler for shuffle btn
*/
shuffleclick() { shuffleclick() {
this.loadShuffledvideos(4); this.loadShuffledvideos(4);
} }
/**
* load random videos from backend
* @param nr number of videos to load
*/
loadShuffledvideos(nr) { loadShuffledvideos(nr) {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'getRandomMovies'); updateRequest.append('action', 'getRandomMovies');
updateRequest.append('number', nr); updateRequest.append('number', nr);
// fetch all videos available // fetch all videos available
fetch('/api/videoload.php', {method: 'POST', body: updateRequest}) fetch('/api/video.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
console.log(result); console.log(result);

View File

@ -3,6 +3,10 @@ import {Button, Col, Form} from "react-bootstrap";
import style from "./GeneralSettings.module.css" import style from "./GeneralSettings.module.css"
import GlobalInfos from "../../GlobalInfos"; 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 { class GeneralSettings extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -19,22 +23,7 @@ class GeneralSettings extends React.Component {
} }
componentDidMount() { componentDidMount() {
const updateRequest = new FormData(); this.loadSettings();
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
});
}));
} }
render() { render() {
@ -119,6 +108,31 @@ class GeneralSettings extends React.Component {
); );
} }
/**
* 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() { saveSettings() {
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'saveGeneralSettings'); updateRequest.append('action', 'saveGeneralSettings');
@ -130,7 +144,7 @@ class GeneralSettings extends React.Component {
updateRequest.append("tmdbsupport", this.state.tmdbsupport); updateRequest.append("tmdbsupport", this.state.tmdbsupport);
updateRequest.append("darkmodeenabled", GlobalInfos.isDarkTheme()); updateRequest.append("darkmodeenabled", GlobalInfos.isDarkTheme());
fetch('/api/Settings.php', {method: 'POST', body: updateRequest}) fetch('/api/settings.php', {method: 'POST', body: updateRequest})
.then((response) => response.json() .then((response) => response.json()
.then((result) => { .then((result) => {
if (result.success) { if (result.success) {

View File

@ -1,6 +1,10 @@
import React from "react"; import React from "react";
import style from "./MovieSettings.module.css" import style from "./MovieSettings.module.css"
/**
* Component for MovieSettings on Settingspage
* handles settings concerning to movies in general
*/
class MovieSettings extends React.Component { class MovieSettings extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -36,6 +40,9 @@ class MovieSettings extends React.Component {
); );
} }
/**
* starts the reindex process of the videos in the specified folder
*/
startReindex() { startReindex() {
// clear output text before start // clear output text before start
this.setState({text: []}); this.setState({text: []});
@ -60,6 +67,9 @@ class MovieSettings extends React.Component {
this.myinterval = setInterval(this.updateStatus, 1000); this.myinterval = setInterval(this.updateStatus, 1000);
} }
/**
* This interval function reloads the current status of reindexing from backend
*/
updateStatus = () => { updateStatus = () => {
const updateRequest = new FormData(); const updateRequest = new FormData();
fetch('/api/extractionData.php', {method: 'POST', body: updateRequest}) fetch('/api/extractionData.php', {method: 'POST', body: updateRequest})

View File

@ -4,7 +4,10 @@ import GeneralSettings from "./GeneralSettings";
import style from "./SettingsPage.module.css" import style from "./SettingsPage.module.css"
import GlobalInfos from "../../GlobalInfos"; import GlobalInfos from "../../GlobalInfos";
/**
* The Settingspage handles all kinds of settings for the mediacenter
* and is basically a wrapper for child-tabs
*/
class SettingsPage extends React.Component { class SettingsPage extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -14,6 +17,10 @@ class SettingsPage extends React.Component {
}; };
} }
/**
* load the selected tab
* @returns {JSX.Element|string} the jsx element of the selected tab
*/
getContent() { getContent() {
switch (this.state.currentpage) { switch (this.state.currentpage) {
case "general": case "general":

View File

@ -9,6 +9,11 @@ import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()}); 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) => { global.prepareFetchApi = (response) => {
const mockJsonPromise = Promise.resolve(response); const mockJsonPromise = Promise.resolve(response);
const mockFetchPromise = Promise.resolve({ const mockFetchPromise = Promise.resolve({
@ -17,6 +22,10 @@ global.prepareFetchApi = (response) => {
return (jest.fn().mockImplementation(() => mockFetchPromise)); 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 = () => { global.prepareFailingFetchApi = () => {
const mockFetchPromise = Promise.reject("myreason"); const mockFetchPromise = Promise.reject("myreason");
return (jest.fn().mockImplementation(() => mockFetchPromise)); return (jest.fn().mockImplementation(() => mockFetchPromise));