From b8901caca25f2497003fd2e0457ee677b57ab483 Mon Sep 17 00:00:00 2001 From: aunefyren Date: Sun, 12 Dec 2021 17:31:15 +0100 Subject: [PATCH] Squashed commit of the following: commit 84caa7b85f9036c7ca4cf2535f80bdf96aa120f6 Author: aunefyren Date: Sun Dec 12 17:26:23 2021 +0100 Disable pre-made links when changing config If you disable links in the config, already made links will stop working commit 4ae03627594a821323fc20c1ad8bf1b8d5f8f1cb Author: aunefyren Date: Sun Dec 12 17:08:49 2021 +0100 Button icons and button disabling Displays that a function is working in the background by disabling the button. commit cc43d00fbc122cc681a008e6a736273540c10b3e Author: aunefyren Date: Sun Dec 12 16:43:33 2021 +0100 Added version control - Release now dynamically displayed on pages - Configs are tagged with version and deleted when not compatible commit a18c2515904a978cb0a85edbeecfb5baab81c2b6 Author: aunefyren Date: Sat Dec 11 13:56:18 2021 +0100 Option to disable links commit d8f1b0ab09f5ed1cfbdc9a753be2bebd63c51e2c Author: aunefyren Date: Sat Dec 11 12:49:58 2021 +0100 Plex Auth, shareable links, fancy colors v2.1.0 testing - Moved to class-system within PHP. - All wrapped requests now gathered and validated using Plex Auth #9 - Session stored as browser cookie - Button icons - Colors fade between wrapped categories - Minutes count added for music above a certain sum #7 - Ability to create shareable links valid for 7 days #14 - HTML Input placeholders removed #10 - Ability to specify Plex libraries #18 commit 23022d0090c267c46e95a818cd57639360e542bc Author: aunefyren Date: Sat Dec 4 23:02:16 2021 +0100 Fixed caching Forgot to add global and variable to function commit b5765f3094206406c8d2f20ffd36b0bad5ea0200 Author: aunefyren Date: Sat Dec 4 22:58:48 2021 +0100 Re-added caching mode Whops commit 20ccd23ed9a04fa4b00e2994ab89a931d79f5a78 Author: aunefyren Date: Sat Dec 4 22:52:38 2021 +0100 Stats API mostly moved to new class format commit a594447ffcd917d149078dd5c57d7e3f5a88bf1a Author: aunefyren Date: Sat Dec 4 16:17:48 2021 +0100 Fixed login bug on caching commit 9d303514ef080e9483f4753b7fc488da314d1b7a Author: aunefyren Date: Sat Dec 4 16:06:09 2021 +0100 Added root option, fixed PHP relative folders commit 08f21ad4132f92a502cd83521ba51dd9ab9a2dfb Author: aunefyren Date: Sat Dec 4 13:57:18 2021 +0100 FIxed major login issue from last commit commit 84d767ac99946016dca6ef92991f12f661de266c Author: aunefyren Date: Sat Dec 4 13:12:42 2021 +0100 Made PHP classes, added client_id variable PHP now uses classes on everything but the main stat API call. This should improve error messages. Added new client_id variable for Plex Auth Tried to improve caching description Tautulli test button now alerts error messages New code has comments commit e14ad8b5ccd937eea639eb6fc8c6c7e93ee9df3a Author: aunefyren Date: Fri Dec 3 17:25:00 2021 +0100 Changed UI for admin & Trying to alert permission issues --- .gitignore | 1 + README.md | 128 +++++++-- admin.js | 138 ++++++--- admin/index.html | 12 +- api/create_link.php | 96 +++++++ api/get_config.php | 80 +++--- api/get_connection.php | 111 ++++---- api/get_functions.php | 80 ++---- api/get_link.php | 130 +++++++++ api/get_login_cookie.php | 42 +++ api/get_login_url.php | 54 ++++ api/get_plex_wrapped_version.php | 29 ++ api/get_stats.php | 468 +++++++++++++++++++------------ api/objects/auth.php | 226 +++++++++++++++ api/objects/cache.php | 61 ++++ api/objects/config.php | 207 ++++++++++++++ api/objects/link.php | 63 +++++ api/objects/log.php | 55 ++++ api/set_config.php | 116 ++++---- api/validate_login_cookie.php | 52 ++++ assets/close.svg | 1 + assets/config.svg | 1 + assets/css/wrapped.css | 78 +++++- assets/done.svg | 1 + assets/external-link.svg | 1 + assets/for-you.svg | 1 + assets/restart.svg | 1 + assets/share.svg | 1 + assets/synchronize.svg | 1 + caching.js | 42 ++- caching/index.html | 7 +- docker/Dockerfile | 2 +- get_config.js | 18 +- get_functions.js | 16 +- get_stats.js | 186 ++++++++++-- index.html | 68 +++-- index.js | 235 +++++++++++++++- set_config.js | 8 +- 38 files changed, 2283 insertions(+), 534 deletions(-) create mode 100644 api/create_link.php create mode 100644 api/get_link.php create mode 100644 api/get_login_cookie.php create mode 100644 api/get_login_url.php create mode 100644 api/get_plex_wrapped_version.php create mode 100644 api/objects/auth.php create mode 100644 api/objects/cache.php create mode 100644 api/objects/config.php create mode 100644 api/objects/link.php create mode 100644 api/objects/log.php create mode 100644 api/validate_login_cookie.php create mode 100644 assets/close.svg create mode 100644 assets/config.svg create mode 100644 assets/done.svg create mode 100644 assets/external-link.svg create mode 100644 assets/for-you.svg create mode 100644 assets/restart.svg create mode 100644 assets/share.svg create mode 100644 assets/synchronize.svg diff --git a/.gitignore b/.gitignore index 1ab3909..301be58 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config/wrapped.log config/config.json config/cache.json +config/links \ No newline at end of file diff --git a/README.md b/README.md index b7984e0..68a20ed 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # Plex Wrapped -## Introduction +## Introduction - What is this? -A website-based platform and API for collecting Plex user stats within a set timeframe using [Tautulli](https://github.com/Tautulli/Tautulli). The data is displayed as a stat-summary, sort of like Spotify Wrapped. Yes, you need Tautulli to have been running beforehand and currently for this to work. +A website-based platform and API for collecting Plex user stats within a set timeframe using [Tautulli](https://github.com/Tautulli/Tautulli). The data is displayed as a statistics-summary, sort of like Spotify Wrapped. Yes, you need Tautulli to have been running beforehand and currently for this to work. + +
+
![alt text](https://raw.githubusercontent.com/aunefyren/Plex-Wrapped/main/assets/img/example_01.PNG?raw=true) +
+
+ ### Features - Custom timeframes +- Plex Auth - Custom introduction - Movies, shows & music - Caching of results @@ -15,50 +22,79 @@ A website-based platform and API for collecting Plex user stats within a set tim - Admin page with authentication for settings - Pre-caching of data +
+
+ ![alt text](https://raw.githubusercontent.com/aunefyren/Plex-Wrapped/main/assets/img/example_02.PNG?raw=true) +
+
+ ### Credit -- Beautiful illustrations downloaded from [FreeWebIllustrations](https://freewebillustrations.com) +- Beautiful illustrations from [FreeWebIllustrations](https://freewebillustrations.com) - Amazing statistics gathered using [Tautulli](https://github.com/Tautulli/Tautulli) - Wonderful loading icon from [icons8](https://icons8.com/preloaders/en/miscellaneous/hourglass) +- Splendid web icons from [icons8](https://icons8.com/icon/set/popular/material-rounded) + +
+
![alt text](https://raw.githubusercontent.com/aunefyren/Plex-Wrapped/main/assets/img/example_03.PNG?raw=true) -## Instructions -This is a web-based platform. Place it in a webserver like Apache or Nginx and make sure it processes PHP content. +
+
+## Instructions - How do I use this? +This is a web-based platform. It is a website hosted on a web-server and it gathers and displays statitics using an API (application programming interface) that interacts with Tautulli's API. Place the files included in this GitHub repository in a web-server, like Apache or Nginx, and make sure it processes PHP scripts, as this is the language the Wrapped API is written in. + +There are instructions for this further down. +
+
+### How does it work? There are things to know when you are up and running: -- The configuration is stored in config/config.json, but can be configured using the admin menu, located at: your-domain-or-ip/admin +- Head to the front page you should see a small navigation menu at the bottom. This will take you between the few pages you need. +- The configuration is stored in config/config.json, but can be configured using the admin menu, located at: ```your-domain-or-ip/admin``` or by clicking admin in the navigation menu. - The cache is stored in config/cache.json, but can be cleared using the admin menu previously mentioned. -- Your password is hashed and stored in the config/config.json. This is a sensetive directory! -- If you visit your-domain-or-ip/caching you can do a pre-caching. This is very useful if you want to prepare for a lot of traffic and have a large timeframe with entries. I recommend setting up the platform at the admin page and then running a pre-cache immediately. The cache is updated automatically if new data in the timeframe becomes available. +- Your password and encryption token is hashed and stored in the config/config.json. This is a sensetive directory! There is an ```.htaccess``` file included that blocks traffic to the folder, but this is only effective with Apache, but if you are using Nginx you must add a directory deny in your Nginx configuration! +- If you visit ```your-domain-or-ip/caching```, or click caching in the navigation menu, you can do a pre-caching. This is very useful if you want to prepare for traffic and reduce PHP errors. PHP scripts will exit if they run longer then a certain timeframe, giving the user an error. +- It is recommended to set up the platform at the admin page and then running a pre-cache immediately. The cache is updated automatically if new data in the timeframe becomes available. -### Manual setup +
+
+ +## Manual setup - Example of setting up a local web-server Here is an example of running this platform. This is a general approach, as there are multiple ways to host a webserver with PHP installed. -#### XAMPP +### XAMPP XAMPP is a completely free, easy to install Apache distribution containing MariaDB, PHP, and Perl. The XAMPP open source package has been set up to be incredibly easy to install and to use. This is their [website](https://www.apachefriends.org/). It works on Windows, Linux and MacOs. -Install XAMPP thorugh the installer and open up the GUI. From there you can start the Apache webserver with a single button. We don't need any of the other tools included, but make sure the status of the module is green. PHP should be pre-configured. +Install XAMPP thorugh the installer and open up the GUI. From there you can start the Apache webserver with a single button. We don't need any of the other tools included, but make sure the status of the module is green. PHP should be pre-configured by XAMPP. -#### Install Plex-Wrapped -Download this repo and place the files in a folder inside the documentroot of XAMPPs apache server. This is typically 'C:\xampp\htdocs', but this will change depending on your system and configuration of XAMPP during installation. For instance, for my location of XAMPP and the repo files is 'C:\xampp\htdocs\plex-wrapped', which in turn makes the files accessable on http://localhost/plex-wrapped. +### Install Plex-Wrapped +Download this repository and place the files inside the document-root of XAMPPs apache server. This is typically ```C:\xampp\htdocs``` on Windows, but this will change depending on your system and configuration of XAMPP during installation. -#### Config folder configuration -You need to give PHP permission to read and write to files in the directory called config. This is where the API saves the cache, configuration and writes the log. +For instance, I placed this repository in a folder inside the document-root, so my location of XAMPP, with that folder, makes the location: ```C:\xampp\htdocs\plex-wrapped```, which in turn makes the files accessable on ```http://localhost/plex-wrapped```. Notice how my folder inside the document-root altered the URL. If I placed the files directly into ```C:\xampp\htdocs``` the URL would be: ```http://localhost```. -The directory contains sensitive information that must be only accessed by the PHP scripts! There is an .htaccess file included that blocks traffic to the folder, but that is effective with Apache. If you are using Nginx you must add a directory deny in your Nginx configuration! +### Config folder configuration +You need to give PHP permission to read and write to files in the directory called ```config```. This is where the API saves the cache, configuration and writes the log. -In Windows I never had to change permissions for the folder, PHP could access it by defult. In Linux I had go give read/write access by using chmod. In the example below I change the config directory folder permissions recursively. +The directory contains sensitive information that must be only accessed by the PHP scripts! There is an ```.htaccess``` file included that blocks traffic to the folder, but this is only effective with Apache (which XAMPP uses). If you are using Nginx you must add a directory deny in your Nginx configuration! + +In Windows I never had to change permissions for the folder, PHP could access it by defult. In Linux I had give read/write access by using the ```chmod``` command. In the example below I change the config directory folder permissions recursively on Linux. This will allow PHP to read/write in the directory. ``` $ sudo chmod -R 0777 /var/www/html/config ``` -#### Test -Everything should now work, and the rest of the setup should be done on the admin page, followed up by a pre-caching on the caching page. You might have to refer to PHP configuration section below if PHP is acting up. +### Test +Go to ```http://localhost```, or your variation as discussed earlier, and you should see the front page. -### Docker +Everything should now be prepared, and the rest of the setup should be done on the admin page, followed up by a pre-caching on the caching page. You might have to refer to PHP configuration section below if PHP is acting up. + +
+
+ +## Docker Docker sets up the environment, but I recommend reading the start of the 'Instructions' section for an explanation of functionality! You might have to refer to the 'PHP Configuration' section below if PHP is acting up. Docker makes it easy, but you might want to change the setup. The pre-configured Dockerfile is in the docker folder of this repo. It's a really simple configuration, so modify it if you want and then build it. If you just want to launch the [pre-built image](https://hub.docker.com/r/aunefyren/plex-wrapped) of Plex-Wrapped, simply execute this docker command, pulling the image from Docker Hub and exposing it on port 80: @@ -67,7 +103,7 @@ Docker makes it easy, but you might want to change the setup. The pre-configured $ docker run -p '80:80' --name 'plex-wrapped' aunefyren/plex-wrapped:latest ``` -It should now be accessable on: http://localhost +It should now be accessable on: ```http://localhost``` If you use Docker Compose you could do something like this in your docker-compose.yml: @@ -78,24 +114,56 @@ services: ports: - '80:80' container_name: plex-wrapped - image: 'aunefyren/plex-wrapped:latest' + image: 'aunefyren/plex-wrapped' ``` -And launch the file with +And launch the file with: + ``` $ docker-compose up ``` -### PHP Configuration -PHP will have issues with this API based on the data available in Tautulli and your settings. If you have a large time frame for your wrapped period (like a full year), and there are a huge amount of Tautulli entries, you can have multiple issues. Pre-caching deals with a lot of these problems, so make sure you have it enabled, but configuring PHP might still be necessary. Test out the platform and if you have issues, check this list for possible solutions. +If you want to mount a volume for the config folder, you can do something like this: -In your php.ini file you may have to change: -- max_execution_time=enough seconds for the script to finish. The longer the timeframe and the less info that is cached, the more execution time. Every unique date in your timeframe is a new Tautulli API call. -- memory_limit=enough M for the script to handle JSON data. If there is a lot of data, PHP needs to have enough memory to manage it without crashing. This still applies if caching is on, as PHP needs to be able to read the cache without crashing. -- max_input_time=enough seconds for the script to parse JSON data. You might not need to change this, depending on Tautulli connection speed. +``` +version: '3.3' +services: + plex-wrapped: + ports: + - '80:80' + container_name: plex-wrapped + image: 'aunefyren/plex-wrapped' + volumes: + - './my-folder:/var/www/html/config' +``` + +Afterwards, remember to chmod the mounted folder on the host so the container can write to it: + + +``` +$ sudo chmod -R 0777 ./my-folder +``` + +
+
+ +## PHP Configuration +PHP will have issues with this API based on the data available in Tautulli and your settings on the admin page. If you have a large time frame for your wrapped period (like a full year), and there are a huge amount of Tautulli entries, you can have multiple issues. The PHP API can, for example, exit because the runtime exceeds the PHP configured runtime, because it takes a long time to interact with your Tautulli server. + +Pre-caching deals with a lot of these problems, so make sure you have it enabled and done to avoid these issues. Go to the caching page found in the navigation meny at the bottom. + +If you performed pre-caching and you still have issues, check the list below for possible alterations to PHP. These are changes to the ```php.ini``` file found in the PHP installation directory. Do some research or ask for help if you don't know how to do this. + +In your ```php.ini``` file you may have to change: +- max_execution_time=enough seconds for the script to finish.
The longer the timeframe, the more execution time. Every unique date in your timeframe is a new Tautulli API call. +- memory_limit=enough M for the script to handle JSON data.
If there is a lot of data, PHP needs to have enough memory to manage it without crashing. This still applies if caching is on, as PHP needs to be able to read the cache without crashing. +- max_input_time=enough seconds for the script to parse JSON data.
You might not need to change this, depending on Tautulli connection speed. + +
+
## Need help? If you have any issues feel free to contact me. I am always trying to improve the project. If I can't, many people on several forums (including [/r/plex](https://www.reddit.com/r/plex)) might be able to assist you. -Goodybye. +Have fun. diff --git a/admin.js b/admin.js index 02cef3a..a8aa72f 100644 --- a/admin.js +++ b/admin.js @@ -71,12 +71,12 @@ function login_menu() { var html = '
' html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; - html += ''; + html += ''; html += ''; html += '
'; @@ -93,18 +93,18 @@ function set_password(back) { var html = '' html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; @@ -132,43 +132,50 @@ function set_tautulli(back) { } var html = '
'; - html += ''; + html += ''; html += '
'; + html += '
'; + html += '' html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; - html += '
'; + html += ''; + html += '
'; html += '
'; html += '
'; - html += ''; + html += ''; + html += '
'; + html += '
'; + + html += '
'; + html += ''; html += 'Root for Plex-Wrapped: (Optional)'; + html += '
'; + html += '
'; + var temp_date = wrapped_start.toLocaleDateString("en-GB", { // you can skip the first argument year: "numeric", month: "2-digit", @@ -220,7 +235,7 @@ function set_tautulli_details(back) { var temp_date_second = temp_date[1].split(':'); html += '
'; html += '
!
Load time for long wrapped periods are extensive. Consider enabling caching and performing pre-caching once.
'; - html += ''; + html += ''; html += '
'; html += '
'; @@ -237,19 +252,28 @@ function set_tautulli_details(back) { var temp_date_first = temp_date[0].split('/'); var temp_date_second = temp_date[1].split(':'); html += '
'; - html += '
'; html += '
'; - html += '
'; + html += '
'; + html += '
@@ -64,11 +64,14 @@ var root = "../"; var first_time = false; +var plex_wrapped_version = ""; + var tautulli_apikey = ""; var tautulli_ip = ""; var tautulli_port = ""; var tautulli_length = 5000; var tautulli_root = ""; +var tautulli_libraries = ""; var ssl = false; @@ -83,6 +86,8 @@ wrapped_start.setUTCSeconds(1609455600); var wrapped_end = new Date(0); wrapped_end.setUTCSeconds(1640991540); +var create_share_links = true; + var get_user_movie_stats = true; var get_user_show_stats = true; var get_user_show_buddy = true; @@ -102,8 +107,13 @@ var current_username = ""; var stats_intro = "New year, new page of statistics..."; +var client_id = ""; +var plex_wrapped_root = ""; + $(document).ready(function() { + get_plex_wrapped_version(); + get_config_initial("../"); }); diff --git a/api/create_link.php b/api/create_link.php new file mode 100644 index 0000000..0933c89 --- /dev/null +++ b/api/create_link.php @@ -0,0 +1,96 @@ +log_activity('create_link.php', 'unknown', 'No input provided.'); + + echo json_encode(array("error" => true, "message" => "No input provided.")); + exit(0); + +} + +// Check if confgiured +if(!$config->is_configured()) { + + // Log use + $log->log_activity('create_link.php', 'unknown', 'Plex-Wrapped is not configured.'); + + echo json_encode(array("error" => true, "message" => "Plex-Wrapped is not configured.", "password" => false, "data" => array())); + exit(0); + +} + +// Check if link creation is allowed +if(!$config->create_share_links) { + + // Log use + $log->log_activity('create_link.php', 'unknown', 'Plex-Wrapped does not allow link creation in config.'); + + echo json_encode(array("error" => true, "message" => "Plex-Wrapped option for link creation not enabled.")); + exit(0); + +} + +// Remove potential harmfull input +$cookie = htmlspecialchars($data->cookie); +$wrapped_data = $data->data; + +// Get Plex Token +$token_object = json_decode($auth->validate_token($cookie)); + +// Validate Plex ID +if(empty($token_object) || !isset($token_object->data->id)) { + + // Log use + $log->log_activity('create_link.php', 'unknown', 'Plex Token from cookie not valid.'); + + echo json_encode(array("error" => true, "message" => "Login not accepted. Log in again.")); + exit(0); + +} + +// Assign values from Plex Token +$id = $token_object->data->id; + +// Get the current date +$now = new DateTime('NOW'); + +// Create random URL value +$random = md5(rand(0,1000)); + +$url_hash = $id . '-' . $random; + +//Create link content +$link_content = array("url_hash" => $url_hash, "id" => $id, "date" => $now->format('Y-m-d'), "data" => $wrapped_data); + +// Save the content to file +$link->save_link($link_content, $id); + +// Log use +$log->log_activity('create_link.php', $id, 'Created Wrapped link.'); + +// Return URL generated +echo json_encode(array("error" => false, "message" => "Link created.", "url" => "?hash=" . $url_hash)); +exit(0); +?> \ No newline at end of file diff --git a/api/get_config.php b/api/get_config.php index 88bd8a9..f1ea7d6 100644 --- a/api/get_config.php +++ b/api/get_config.php @@ -1,59 +1,61 @@ log_activity('get_config.php', 'unknown', 'No admin login input provided.'); + echo json_encode(array("error" => true, "message" => "No input provided.")); exit(0); + } -if(empty($config->password) || empty($config->username)) { - echo json_encode(array("error" => false, "message" => "Password and/or username not set.", "password" => false, "data" => array())); - exit(0); -} - +// Remove potential harmfull input $password = htmlspecialchars($data->password); $username = htmlspecialchars($data->username); -if(password_verify($password, $config->password) && $username == $config->username) { - - // Log API request if enabled - if($config->use_logs) { - if(!log_activity()) { - echo json_encode(array("message" => "Failed to log event.", "error" => true)); - exit(0); - } - } +// Check if confgiured +if(!$config->is_configured()) { + + // Log use + $log->log_activity('get_config.php', 'unknown', 'Plex-Wrapped is not configured.'); + + echo json_encode(array("error" => true, "message" => "Plex-Wrapped is not configured.", "password" => false, "data" => array())); + exit(0); + +// Verify password and username combination +} else if($config->verify_wrapped_admin($username, $password)) { + // Log use + $log->log_activity('get_config.php', 'unknown', 'Retrieved Plex-Wrapped configuraton.'); + echo json_encode(array("error" => false, "message" => "Login successful.", "password" => true, "data" => $config)); exit(0); + +// If input was given, but is empty } else { + + // Log use + $log->log_activity('get_config.php', 'unknown', 'Wrong admin password/username combination.'); + echo json_encode(array("error" => true, "message" => "Username and password combination not accepted.", "password" => true, "data" => array())); exit(0); -} -function log_activity() { - $date = date('Y-m-d H:i:s'); - - $path = "../config/wrapped.log"; - if(!file_exists($path)) { - $temp = fopen($path, "w"); - fwrite($temp, 'Plex Wrapped'); - fclose($temp); - } - - $log_file = fopen($path, 'a'); - fwrite($log_file, PHP_EOL . $date . ' - get_config.php'); - - if(fclose($log_file)) { - return True; - } - - return False; } ?> \ No newline at end of file diff --git a/api/get_connection.php b/api/get_connection.php index da9f221..5830ab7 100644 --- a/api/get_connection.php +++ b/api/get_connection.php @@ -1,67 +1,78 @@ log_activity('get_config.php', 'unknown', 'No connection input provided.'); + echo json_encode(array("error" => true, "message" => "No input provided.")); exit(0); } -// Log API request if enabled -if(empty($config) || $config->use_logs) { - if(!log_activity()) { - echo json_encode(array("message" => "Failed to log event.", "error" => true)); - exit(0); - } -} - -$arrContextOptions= [ - 'ssl' => [ - 'verify_peer'=> false, - 'verify_peer_name'=> false, - ], -]; - +// Remove potential harmfull input $url = htmlspecialchars($data->url); $apikey = htmlspecialchars($data->apikey); -$ssl = htmlspecialchars($data->ssl); +// Create URL $url = $url . '?apikey=' . $apikey . '&cmd=status'; -try { - if($ssl) { - @$response = json_decode(file_get_contents($url, false, stream_context_create($arrContextOptions))); - } else { - @$response = json_decode(file_get_contents($url)); - } - if(!isset($response)) { - throw new Exception('Could not reach Tautulli.'); - } + +// Attempt to call Tautulli API +try { + + // Call Tautulli status API + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + echo json_encode(array("error" => true, "message" => "Tautulli did not respond.", "data" => array())); + exit(0); + } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + $decoded = json_decode($result, true); + + // Check reponse for success + if($decoded["response"]["result"] == "success") { + echo json_encode(array("error" => false, "message" => "Tautulli reached and accepted.", "data" => $decoded)); + exit(0); + } + + // Check reponse for error + if($decoded["response"]["result"] == "error") { + $message = $decoded["response"]["message"]; + echo json_encode(array("error" => true, "message" => "Tautulli error. Reply: $message", "data" => $decoded)); + exit(0); + } + + echo json_encode(array("error" => true, "message" => "Parsing Tautulli reponse failed. It could be working.", "data" => $decoded)); + exit(0); + +// Catch errors } catch (Exception $e) { echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array())); exit(0); } - -echo json_encode(array("error" => false, "message" => "Tautulli reached.", "data" => $response)); -exit(0); - -function log_activity() { - $date = date('Y-m-d H:i:s'); - - $path = "../config/wrapped.log"; - if(!file_exists($path)) { - $temp = fopen($path, "w"); - fwrite($temp, 'Plex Wrapped'); - fclose($temp); - } - - $log_file = fopen($path, 'a'); - fwrite($log_file, PHP_EOL . $date . ' - get_connection.php'); - - if(fclose($log_file)) { - return True; - } - - return False; -} ?> \ No newline at end of file diff --git a/api/get_functions.php b/api/get_functions.php index 9d3da31..28ff677 100644 --- a/api/get_functions.php +++ b/api/get_functions.php @@ -1,56 +1,38 @@ log_activity('get_functions.php', 'unknown', 'Retrieved Plex-Wrapped functions.'); -if (empty($config)) { - echo json_encode(array("message" => "Plex Wrapped is not configured.", "error" => true)); - exit(0); -} +// Create JSON from functions +$functions_json = array("plex_wrapped_version" => $config->plex_wrapped_version, + "get_user_movie_stats" => $config->get_user_movie_stats, + "get_user_show_stats" => $config->get_user_show_stats, + "get_user_show_buddy" => $config->get_user_show_buddy, + "get_user_music_stats" => $config->get_user_music_stats, + "get_year_stats_movies" => $config->get_year_stats_movies, + "get_year_stats_shows" => $config->get_year_stats_shows, + "get_year_stats_music" => $config->get_year_stats_music, + "get_year_stats_leaderboard" => $config->get_year_stats_leaderboard, + "stats_intro" => $config->stats_intro, + "create_share_links" => $config->create_share_links + ); -$functions = array("get_user_movie_stats" => $config->get_user_movie_stats, - "get_user_show_stats" => $config->get_user_show_stats, - "get_user_show_buddy" => $config->get_user_show_buddy, - "get_user_music_stats" => $config->get_user_music_stats, - "get_year_stats_movies" => $config->get_year_stats_movies, - "get_year_stats_shows" => $config->get_year_stats_shows, - "get_year_stats_music" => $config->get_year_stats_music, - "get_year_stats_leaderboard" => $config->get_year_stats_leaderboard, - "stats_intro" => $config->stats_intro - ); - -// Log API request if enabled -if($config->use_logs) { - if(!log_activity()) { - echo json_encode(array("message" => "Failed to log event.", "error" => true)); - exit(0); - } -} - -echo json_encode($functions); +// Encode JSON and print it +echo json_encode($functions_json); exit(0); - -function log_activity() { - $date = date('Y-m-d H:i:s'); - - $path = "../config/wrapped.log"; - if(!file_exists($path)) { - $temp = fopen($path, "w"); - fwrite($temp, 'Plex Wrapped'); - fclose($temp); - } - - $log_file = fopen($path, 'a'); - fwrite($log_file, PHP_EOL . $date . ' - get_functions.php'); - - if(fclose($log_file)) { - return True; - } - - return False; -} ?> \ No newline at end of file diff --git a/api/get_link.php b/api/get_link.php new file mode 100644 index 0000000..7aae0b6 --- /dev/null +++ b/api/get_link.php @@ -0,0 +1,130 @@ +hash)) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'No input provided.'); + + echo json_encode(array("error" => true, "message" => "No input provided.")); + exit(0); + +} + +// Check if confgiured +if(!$config->is_configured()) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Plex-Wrapped is not configured.'); + + echo json_encode(array("error" => true, "message" => "Plex-Wrapped is not configured.", "password" => false, "data" => array())); + exit(0); + +} + +// Check if link creation is allowed +if(!$config->create_share_links) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Plex-Wrapped does not allow link creation in config.'); + + echo json_encode(array("error" => true, "message" => "Plex-Wrapped option for link creation not enabled.")); + exit(0); + +} + +// Remove potential harmfull input and seperate ID from hash +$hash_input = explode('-', htmlspecialchars($data->hash)); + +if(count($hash_input) !== 2) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Failed to get Wrapped link. Can\'t parse input.'); + + echo json_encode(array("error" => true, "message" => "Wrapped link is either wrong or expired.")); + exit(0); + +} + +$id = $hash_input[0]; +$hash = $hash_input[1]; + +// Get the current date +$now = new DateTime('NOW'); + +// Create random URL value +$random = md5(rand(0,1000)); + +$url_hash = $id . '-' . $random; + +// Save the content to file +$content = $link->open_link($id); + +if(!$content) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Failed to get Wrapped link. File not found.'); + + echo json_encode(array("error" => true, "message" => "There was an error fetching this Wrapped page. Could the link have expired?")); + exit(0); + +} + +$link_data = json_decode($content); + +// Validate hash +if($link_data->url_hash !== $data->hash) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Failed to get Wrapped link. Hash did not match.'); + + echo json_encode(array("error" => true, "message" => "There was an error fetching this Wrapped page. Could the link have expired?")); + exit(0); + +} + +$now = new DateTime('NOW'); +$then = date_create_from_format('Y-m-d', $link_data->date); +$diff = (array) date_diff($now, $then); + +if($diff['days'] > 7) { + + // Log use + $log->log_activity('get_link.php', 'unknown', 'Failed to get Wrapped link for ID: ' . $id . '. It has expired. Deleting file.'); + + // Delete expired content + if(!$link->delete_link($id)) { + $log->log_activity('get_link.php', 'unknown', 'Failed to delete link for ID: ' . $id . '.'); + } + + echo json_encode(array("error" => true, "message" => "This Wrapped link has expired.")); + exit(0); + +} + +// Log use +$log->log_activity('get_link.php', 'unknown', 'Retrieved Wrapped link for ID: ' . $id . '.'); + +// Return URL generated +echo json_encode(array("error" => false, "message" => "Link retrieved.", "data" => $link_data->data)); +exit(0); +?> \ No newline at end of file diff --git a/api/get_login_cookie.php b/api/get_login_cookie.php new file mode 100644 index 0000000..1936aba --- /dev/null +++ b/api/get_login_cookie.php @@ -0,0 +1,42 @@ +code) || !isset($data->id)) { + + // Log use + $log->log_activity('get_login_cookie.php', 'unknown', 'Input error from user.'); + + echo json_encode(array("error" => true, "message" => "Input error.")); + exit(0); + +} + +// Remove potential harmfull input +$id = htmlspecialchars($data->id); +$code = htmlspecialchars($data->code); + +// Get cookie +$cookie = $auth->get_cookie($id, $code); + +// Log use +$log->log_activity('get_login_cookie.php', 'unknown', 'Plex-Wrapped login cookie created.'); + +// Print cookie and exit +echo json_encode(array("error" => false, "message" => "Cookie created.", "cookie" => $cookie)); +exit(0); +?> \ No newline at end of file diff --git a/api/get_login_url.php b/api/get_login_url.php new file mode 100644 index 0000000..b15d265 --- /dev/null +++ b/api/get_login_url.php @@ -0,0 +1,54 @@ +is_configured()) { + + // Log activity + $log->log_activity('get_login_url.php', 'unknown', 'Plex-Wrapped is not confgured..'); + + echo json_encode(array("message" => "Plex-Wrapped is not confgured.", "error" => true)); + exit(0); +} + +// If POST data is empty or wrong +if(empty($data) || !isset($data->home_url)) { + + // Log use + $log->log_activity('get_login_url.php', 'unknown', 'Input error from user.'); + + echo json_encode(array("error" => true, "message" => "Input error.")); + exit(0); + +} + +// Get code and pin from Plex +$pin_object = json_decode($auth->get_pin(), true); + +// Get URL using pin and code +$url = $auth->get_login_url($pin_object['code'], $pin_object['id'], $data->home_url); + +// Log URL creation +$log->log_activity('get_login_url.php', 'unknown', 'Login URL returned.'); + +// Return URL for login +echo json_encode(array("message" => 'Plex login URL created.', "error" => false, "url" => $url, "code" => $pin_object['code'], "id" => $pin_object['id'])); +exit(0); + +?> \ No newline at end of file diff --git a/api/get_plex_wrapped_version.php b/api/get_plex_wrapped_version.php new file mode 100644 index 0000000..2c433e6 --- /dev/null +++ b/api/get_plex_wrapped_version.php @@ -0,0 +1,29 @@ +log_activity('get_plex_wrapped_version.php', 'unknown', 'Retrieved Plex-Wrapped version.'); + +// Create JSON from functions +$version_json = array("plex_wrapped_version" => $config->plex_wrapped_version, + "message" => "Retrieved Plex-Wrapped verison.", + "error" => false + ); + +// Encode JSON and print it +echo json_encode($version_json); +exit(0); +?> \ No newline at end of file diff --git a/api/get_stats.php b/api/get_stats.php index cc80c2c..32223a6 100644 --- a/api/get_stats.php +++ b/api/get_stats.php @@ -1,37 +1,48 @@ is_configured()) { -$arrContextOptions= [ - 'ssl' => [ - 'verify_peer'=> false, - 'verify_peer_name'=> false, - ], -]; + // Log activity + $log->log_activity('get_stats.php', 'unknown', 'Plex-Wrapped is not confgured..'); -//Check if the config is configured. If not, exit the API call. -if (empty($config)) { - http_response_code(400); - echo json_encode(array("message" => "Plex Wrapped is not configured.", "error" => true)); + echo json_encode(array("message" => "Plex-Wrapped is not confgured.", "error" => true)); exit(0); } // Set time-zone to the one configured date_default_timezone_set($config->timezone); -//Base-URL for connections to Tautulli API. +// Set maximum run-time +set_time_limit(300); + +// Base-URL for connections to Tautulli API. $connection = create_url(); -//Declare given inputs. GET and POST. -if(!empty($data)){ - $p_identity = htmlspecialchars(trim($data->p_identity)); -} else if(isset($_GET["p_identity"])) { - $p_identity = htmlspecialchars(trim($_GET["p_identity"])); +// Declare given inputs. GET and POST. +if(!empty($data) && isset($data->cookie)){ + $cookie = htmlspecialchars(trim($data->cookie)); +} else if(isset($_GET["cookie"])) { + $cookie = htmlspecialchars(trim($_GET["cookie"])); } else { http_response_code(400); echo json_encode(array("message" => "Input error.", "error" => true)); @@ -39,7 +50,7 @@ if(!empty($data)){ } //Check if Caching parameter was supplied through GET or POST -if(!empty($data) && !empty($data->caching)) { +if(!empty($data) && isset($data->caching)) { $caching = filter_var(htmlspecialchars(trim($data->caching)), FILTER_VALIDATE_BOOLEAN); } else if(isset($_GET["caching"])) { $caching = filter_var(htmlspecialchars(trim($_GET["caching"])), FILTER_VALIDATE_BOOLEAN); @@ -49,78 +60,71 @@ if(!empty($data) && !empty($data->caching)) { // Confirm input variables if($caching) { - if(!empty($data->cache_limit)) { + if(!empty($data) && isset($data->cache_limit)) { $cache_limit = htmlspecialchars(trim($data->cache_limit)); } else if(isset($_GET["cache_limit"])) { $cache_limit = htmlspecialchars(trim($_GET["cache_limit"])); } else { http_response_code(400); - echo json_encode(array("message" => "Caching enabled. No 'cache_limit' input.", "error" => true)); + echo json_encode(array("message" => "Caching enabled, but no 'cache_limit' input retrieved.", "error" => true)); exit(0); } } -// IF CACHING IS TRUE DO THIS +// Check caching mode if($caching) { - $id = "Caching mode"; - log_activity($id, "Caching mode enabled"); - - if(!$config->use_cache) { - http_response_code(400); - echo json_encode(array("message" => "Caching is disabled.", "error" => true)); - exit(0); - } - - // Log checking cache - log_activity($id, "Starting cache loop"); - + + // Log activity + $log->log_activity('get_stats.php', 'unknown', 'Starting caching mode.'); - // Log checking cache - log_activity($id, "Checking data-cache"); + caching_mode($cache_limit); - // GET WRAPPED DATES CACHE - if($config->use_cache) { - if($cache = check_cache()) { - $tautulli_data = $cache; - } else { - $tautulli_data = array(); - } - } else { - $tautulli_data = array(); - } - - // Log refresh cache - log_activity($id, "Refreshing data-cache of missing/incomplete days, maximum " . $cache_limit . " days"); - - // REFRESH THE CACHE - $tautulli_data = tautulli_get_wrapped_dates($id, $tautulli_data, $cache_limit); - $complete_date_loop = $tautulli_data["complete"]; - $tautulli_data = $tautulli_data["data"]; - - // Log updating cache - log_activity($id, "Saving data-cache"); - - // SAVE WRAPPED DATES CACHE - if($config->use_cache) { - update_cache($tautulli_data); - } - - http_response_code(200); - echo json_encode(array("message" => "Caching complete.", "caching_complete" => $complete_date_loop, "error" => False)); - exit(0); - } -// Get user ID -$id = tautulli_get_user($p_identity); -if (!$id) { - http_response_code(400); - echo json_encode(array("message" => "No user found.", "error" => true)); +// Test Tautulli connection +if(!tautulli_test_connection()) { + + // Log activity + $log->log_activity('get_stats.php', 'unknown', 'Tautulli connection test was not successful.'); + + echo json_encode(array("message" => "Tautulli connection test was not successful.", "error" => true)); exit(0); + +} else { + + // Log activity + $log->log_activity('get_stats.php', 'unknown', 'Tautulli connection test was successful.'); + } -// Log user found -log_activity($id, "User found"); +// Get Plex Token +$token_object = json_decode($auth->validate_token($cookie)); + +// Validate Plex ID +if(empty($token_object) || !isset($token_object->data->id)) { + + // Log use + $log->log_activity('get_stats.php', 'unknown', 'Plex Token from cookie not valid.'); + + echo json_encode(array("error" => true, "message" => "Login not accepted. Log in again.")); + exit(0); + +} + +// Assign values from Plex Token +$id = $token_object->data->id; +if(isset($token_object->data->friendlyName)) { + $name = $token_object->data->friendlyName; +} else if(isset($token_object->data->title)) { + $name = $token_object->data->title; +} else if(isset($token_object->data->username)) { + $name = $token_object->data->username; +} else if(isset($token_object->data->email)) { + $name = $token_object->data->email; +} + +// Log use +$log->log_activity('get_stats.php', $token_object->data->id, 'Plex-Wrapped login cookie accepted.'); // Get user name $name = tautulli_get_name($id); @@ -131,12 +135,12 @@ if(!$name) { } // Log checking cache -log_activity($id, "Checking data-cache"); +$log->log_activity('get_stats.php', $id, 'Checking data-cache.'); // GET WRAPPED DATES CACHE if($config->use_cache) { - if($cache = check_cache()) { - $tautulli_data = $cache; + if($cache_data = $cache->check_cache()) { + $tautulli_data = $cache_data; } else { $tautulli_data = array(); } @@ -145,22 +149,22 @@ if($config->use_cache) { } // Log refresh cache -log_activity($id, "Refreshing data-cache of missing/incomplete days"); +$log->log_activity('get_stats.php', $id, 'Refreshing data-cache of missing/incomplete days'); // Refresh the cache $tautulli_data = tautulli_get_wrapped_dates($id, $tautulli_data, False); $tautulli_data = $tautulli_data["data"]; // Log updating cache -log_activity($id, "Saving data-cache"); +$log->log_activity('get_stats.php', $id, 'Saving data-cache'); // Save the wrapped cache date if($config->use_cache) { - update_cache($tautulli_data); + $cache->update_cache($tautulli_data); } // Log wrapped create -log_activity($id, "Creating wrapped data"); +$log->log_activity('get_stats.php', $id, 'Creating wrapped data'); // Get stats based on configured options. if($config->get_user_movie_stats || $config->get_user_show_stats || $config->get_user_music_stats || $config->get_year_stats_movies || $config->get_year_stats_shows || $config->get_year_stats_music) { @@ -218,8 +222,8 @@ if($config->get_user_movie_stats || $config->get_user_show_stats || $config->get } } else { - // Log updating cache - log_activity($id, "No options, creating empty dataset."); + // Log creating empty datasets + $log->log_activity('get_stats.php', $id, 'No options, creating empty dataset.'); // No options selected, empty datasets being configured $user_movies = array("error" => True, "message" => "Disabled in config.", "data" => array()); @@ -234,12 +238,12 @@ if($config->get_user_movie_stats || $config->get_user_show_stats || $config->get // Get show buddy if enabled, shows are not empty, and shows is enabled. if($config->get_year_stats_shows && $config->get_user_show_buddy && count($user_shows["data"]["shows"]) > 0) { // Log show-buddy action - log_activity($id, "Getting show-buddy."); + $log->log_activity('get_stats.php', $id, 'Getting show-buddy.'); $user_shows["data"] = $user_shows["data"] + array("show_buddy" => data_get_user_show_buddy($id, $user_shows["data"]["shows"][0]["title"], $tautulli_data)); } else { // Log show-buddy action - log_activity($id, "Show-buddy disabled."); + $log->log_activity('get_stats.php', $id, 'Show-buddy disabled.'); $user_shows["data"] = $user_shows["data"] + array("show_buddy" => array("message" => "Disabled in config.", "error" => True)); } @@ -248,7 +252,7 @@ if($config->get_year_stats_shows && $config->get_user_show_buddy && count($user_ $now = new DateTime('NOW'); // Log wrapped create -log_activity($id, "Printing wrapped data"); +$log->log_activity('get_stats.php', $id, 'Printing wrapped data'); // Print results on page $result = json_encode(array("error" => False, @@ -305,6 +309,49 @@ function create_url() { return $base . $ip . $port . $root; } +function tautulli_test_connection() { + global $connection; + global $config; + $url = $connection . "/api/v2?apikey=" . $config->tautulli_apikey . "&cmd=status"; + + try { + + // Call Tautulli status API + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + return false; + } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + $decoded = json_decode($result, true); + + // Check reponse for success + if($decoded["response"]["result"] == "success") { + return true; + } + + return false; + + // Catch errors + } catch (Exception $e) { + return false; + } +} + function tautulli_get_user($input) { global $connection; global $config; @@ -359,62 +406,106 @@ function tautulli_get_name($id) { } } -function check_cache() { +function caching_mode($cache_limit) { + global $config; + global $cache; + global $log; + global $cache_data; + $id = "Caching mode"; + + // Log caching mode + $log->log_activity('get_stats.php', $id, 'Tautulli connection test was successful.'); + + if(!$config->use_cache) { + http_response_code(400); + echo json_encode(array("message" => "Caching is disabled.", "error" => true)); + exit(0); + } + + // Log checking cache + $log->log_activity('get_stats.php', $id, 'Starting cache loop.'); + + + // Log checking cache + $log->log_activity('get_stats.php', $id, 'Checking data-cache.'); + + // GET WRAPPED DATES CACHE + if($config->use_cache) { + if($cache_data = $cache->check_cache()) { + $tautulli_data = $cache_data; + } else { + $tautulli_data = array(); + } + } else { + $tautulli_data = array(); + } + + // Log refresh cache + $log->log_activity('get_stats.php', $id, 'Refreshing data-cache of missing/incomplete days, maximum ' . $cache_limit . ' days'); + + // Refresh the cache + $tautulli_data = tautulli_get_wrapped_dates($id, $tautulli_data, $cache_limit); + $complete_date_loop = $tautulli_data["complete"]; + $tautulli_data = $tautulli_data["data"]; + + // Log saving cache + $log->log_activity('get_stats.php', $id, 'Saving data-cache.'); + + // Save the wrapped cache + if($config->use_cache) { + $cache->update_cache($tautulli_data); + } + + http_response_code(200); + echo json_encode(array("message" => "Caching complete.", "caching_complete" => $complete_date_loop, "error" => False)); + exit(0); + +} + +function call_tautulli_url($url) { + global $connection; global $config; global $id; - - $path = "../config/cache.json"; + global $log; - if(!file_exists($path)) { - fopen($path, "w"); - } + try { - $cache = json_decode(file_get_contents($path), True); - - if(!empty($cache)) { - return $cache; - } - - return False; -} - -function update_cache($result) { - global $config; - $save = json_encode($result); - file_put_contents("../config/cache.json", $save); - return True; -} - -function log_activity($id, $message) { - global $config; - - if($config->use_logs) { - try { - $date = date('Y-m-d H:i:s'); - - $path = "../config/wrapped.log"; - if(@!file_exists($path)) { - $temp = @fopen($path, "w"); - fwrite($temp, 'Plex Wrapped'); - fclose($temp); - } - - $log_file = @fopen($path, 'a'); - @fwrite($log_file, PHP_EOL . $date . ' - get_stats.php - ID: ' . $id . ' - ' . $message); - - if(@fclose($log_file)) { - return True; - } - - } catch(Error $e) { - http_response_code(500); - echo json_encode(array("error" => True, "message" => "Failed to log event.")); - exit(); + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + throw new Exception('Tautulli API call was not successful.'); } - - } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + $result = json_decode($result, true); - return True; + if($result) { + return $result; + } else { + throw new Exception('Tautulli API call was not successful.'); + } + + // Catch errors + } catch (Exception $e) { + // Log activity + $log->log_activity('get_stats.php', $id, $e->getMessage()); + + echo json_encode(array("message" => $e->getMessage(), "error" => true)); + exit(0); + } } function tautulli_get_wrapped_dates($id, $array, $loop_interval) { @@ -423,6 +514,7 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) { global $connection; global $config; + global $log; global $arrContextOptions; $end_loop_date = $config->wrapped_end; @@ -433,12 +525,20 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) { $complete_date_loop = True; + $libraries = explode(',',$config->tautulli_libraries); + + if($libraries < 1) { + $libraries[0] = null; + } + + for ($loop_time = $config->wrapped_start; $loop_time <= $end_loop_date; $loop_time += 86400) { $current_loop_date = date('Y-m-d', $loop_time); $now = new DateTime('NOW'); $then = new DateTime($current_loop_date); + // Stop if($then > $now) { break; } @@ -456,26 +556,38 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) { } // Log that we are downoading a new day - log_activity($id, "Downloading day: " . $current_loop_date); + $log->log_activity('get_stats.php', $id, 'Downloading day: ' . $current_loop_date); - $url = $connection . "/api/v2?apikey=" . $config->tautulli_apikey . "&cmd=get_history&order_column=date&order_dir=desc&include_activity=0&length=" . $config->tautulli_length . "&start_date=" . $current_loop_date; - - if($config->ssl) { - $response = json_decode(file_get_contents($url, false, stream_context_create($arrContextOptions)), True); - } else { - $response = json_decode(file_get_contents($url), True); - } - - // Filter data by content type. Movie, episode or track. - $temp = $response["response"]["data"]["data"]; + // Clean array to populate with results $temp_clean = array(); - for($j = 0; $j < count($temp); $j++) { - if($temp[$j]["media_type"] == "movie" || $temp[$j]["media_type"] == "episode" || $temp[$j]["media_type"] == "track") { - $temp2 = array("date" => $temp[$j]["date"], "duration" => $temp[$j]["duration"], "friendly_name" => $temp[$j]["friendly_name"], "full_title" => $temp[$j]["full_title"], "grandparent_rating_key" => $temp[$j]["grandparent_rating_key"], "grandparent_title" => $temp[$j]["grandparent_title"], "original_title" => $temp[$j]["original_title"], "media_type" => $temp[$j]["media_type"], "parent_rating_key" => $temp[$j]["parent_rating_key"], "parent_title" => $temp[$j]["parent_title"], "paused_counter" => $temp[$j]["paused_counter"], "percent_complete" => $temp[$j]["percent_complete"], "rating_key" => $temp[$j]["rating_key"], "title" => $temp[$j]["title"], "user" => $temp[$j]["user"], "user_id" => $temp[$j]["user_id"], "year" => $temp[$j]["year"]); - array_push($temp_clean, $temp2); - } - } + + // Loop through selected libraries + for ($library_loop = 0; $library_loop < count($libraries); $library_loop++) { + + // If no libraries are selected do not specify one in API call to Tautulli + if($libraries[$library_loop] === null) { + $library_str = ''; + } else { + $library_str = '§ion_id=' . trim($libraries[$library_loop]); + } + + // Create URL for API call + $url = $connection . "/api/v2?apikey=" . $config->tautulli_apikey . "&cmd=get_history" . $library_str . "&order_column=date&order_dir=desc&include_activity=0&length=" . $config->tautulli_length . "&start_date=" . $current_loop_date; + + // Call URL for data + $response = call_tautulli_url($url); + + // Filter data by content type. Movie, episode or track. + $temp = $response["response"]["data"]["data"]; + for($j = 0; $j < count($temp); $j++) { + if($temp[$j]["media_type"] == "movie" || $temp[$j]["media_type"] == "episode" || $temp[$j]["media_type"] == "track") { + $temp2 = array("date" => $temp[$j]["date"], "duration" => $temp[$j]["duration"], "friendly_name" => $temp[$j]["friendly_name"], "full_title" => $temp[$j]["full_title"], "grandparent_rating_key" => $temp[$j]["grandparent_rating_key"], "grandparent_title" => $temp[$j]["grandparent_title"], "original_title" => $temp[$j]["original_title"], "media_type" => $temp[$j]["media_type"], "parent_rating_key" => $temp[$j]["parent_rating_key"], "parent_title" => $temp[$j]["parent_title"], "paused_counter" => $temp[$j]["paused_counter"], "percent_complete" => $temp[$j]["percent_complete"], "rating_key" => $temp[$j]["rating_key"], "title" => $temp[$j]["title"], "user" => $temp[$j]["user"], "user_id" => $temp[$j]["user_id"], "year" => $temp[$j]["year"]); + array_push($temp_clean, $temp2); + } + } + } + // If the date is today, then the data might not be complete. if($now->format('Y-m-d') == $then->format('Y-m-d')) { $complete = False; @@ -488,13 +600,14 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) { } else { array_push($array, array("date" => $current_loop_date, "data" => $temp_clean, "complete" => $complete)); } - - if($loop_interval > 0) { - $loop_interval -= 1; - } else if($loop_interval === 0) { - $complete_date_loop = False; - break; - } + + if($loop_interval > 0) { + $loop_interval -= 1; + } else if($loop_interval === 0) { + $complete_date_loop = False; + break; + } + } // Sort data by date @@ -504,19 +617,20 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) { // End time and calcualte total duration. $time_end = microtime(true); $execution_time = ($time_end - $time_start); - log_activity($id, 'Refresh execution: '.$execution_time.' Seconds'); + $log->log_activity('get_stats.php', $id, 'Refresh execution: '.$execution_time.' Seconds'); return array("data" => $array, "complete" => $complete_date_loop); } function data_get_user_stats_loop($id, $array) { - + // Start execution timer $time_start = microtime(true); // Define global values needed global $connection; global $config; + global $log; global $arrContextOptions; // Pre-define variables as empty @@ -1035,7 +1149,7 @@ function data_get_user_stats_loop($id, $array) { // Calculate and log execution time $time_end = microtime(true); $execution_time = ($time_end - $time_start); - log_activity($id, 'Wrapping execution: '.$execution_time.' seconds'); + $log->log_activity('get_stats.php', $id, 'Wrapping execution: '.$execution_time.' seconds'); return array("movies" => $return_movies, "shows" => $return_shows, "music" => $return_music, "year_movies" => $return_year_movies, "year_shows" => $return_year_shows, "year_music" => $return_year_music, "year_users" => $return_year_users); } @@ -1046,6 +1160,7 @@ function data_get_user_show_buddy($id, $show, $array) { global $connection; global $config; + global $log; global $name; global $arrContextOptions; @@ -1105,27 +1220,8 @@ function data_get_user_show_buddy($id, $show, $array) { $time_end = microtime(true); $execution_time = ($time_end - $time_start); - log_activity($id, 'Buddy execution: '.$execution_time.' seconds'); + $log->log_activity('get_stats.php', $id, 'Buddy execution: '.$execution_time.' seconds'); return $buddy; } - -function tautulli_get_year_stats_cache($id) { - $cache = json_decode(file_get_contents("../config/cache.json")); - global $config; - - if(!empty($cache)) { - for($i = 0; $i < count($cache); $i++) { - $now = new DateTime('NOW'); - $then = new DateTime($cache[$i]->year_stats->data->origin_date); - $diff = $then->diff($now); - - if(($diff->format('%a') < $config->cache_age_limit || $config->cache_age_limit == "" || $config->cache_age_limit == 0) && !$cache[$i]->year_stats->error) { - return $cache[$i]->year_stats->data; - } - } - } - - return tautulli_get_year_stats($id); -} ?> diff --git a/api/objects/auth.php b/api/objects/auth.php new file mode 100644 index 0000000..89a9fa1 --- /dev/null +++ b/api/objects/auth.php @@ -0,0 +1,226 @@ +client_id = $config_file->client_id; + $this->token_encrypter = $config_file->token_encrypter; + + } + + // Get pin from Plex + function get_pin() { + + // Create URL + $url = 'https://plex.tv/api/v2/pins'; + + // Attempt to call Plex Auth + try { + + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Add payload + $payload = array( "strong"=> $this->strong, "X-Plex-Product" => $this->x_plex_product, "X-Plex-Client-Identifier" => $this->client_id ); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + + // Add headers + $headers = [ + "accept: $this->header" + ]; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + throw new Exception('Plex Auth did not respond.'); + } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + return $result; + + + // Catch errors + } catch (Exception $e) { + echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array())); + exit(0); + } + } + + // Create URL for Plex login window + function get_login_url($code, $id, $home_url) { + + $base = 'https://app.plex.tv/auth#?'; + $forwardUrl = $home_url . '?close_me=true'; + + return $base . 'clientID=' . urlencode($this->client_id) . '&code=' . urlencode($code) . '&context%5Bdevice%5D%5Bproduct%5D=' . urlencode($this->x_plex_product) . '&forwardUrl=' . urlencode($forwardUrl); + + } + + // Check if pin has been accepted + function get_cookie($id, $code) { + + // Create URL + $url = 'https://plex.tv/api/v2/pins/' . $id; + + // Attempt to call Plex Auth + try { + + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Add payload + $payload = array( "code"=> $code, "X-Plex-Client-Identifier" => $this->client_id ); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + + // Add headers + $headers = [ + "accept: $this->header" + ]; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + throw new Exception('Plex Auth did not respond.'); + } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + $pin_object = json_decode($result, true); + + if(!isset($pin_object['authToken']) || $pin_object['authToken'] === '') { + + throw new Exception('Plex Auth didn\'t confirm login.'); + + } else { + + $nonceSize = openssl_cipher_iv_length(self::METHOD); + $nonce = openssl_random_pseudo_bytes($nonceSize); + + $token = openssl_encrypt( + $pin_object['authToken'], + self::METHOD, + $this->token_encrypter, + OPENSSL_RAW_DATA, + $nonce + ); + + return base64_encode($nonce.$token); + + } + + + // Catch errors + } catch (Exception $e) { + echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array())); + exit(0); + } + } + + // Validate Plex Token + function validate_token($token) { + + // Attempt to call Plex Auth + try { + + // Create URL + $url = 'https://plex.tv/api/v2/user'; + + // Debase token-cookie + $token_debased = base64_decode($token, true); + if ($token_debased === false) { + throw new Exception('Encryption failure.'); + } + + // Assign variables + $nonceSize = openssl_cipher_iv_length(self::METHOD); + $nonce = mb_substr($token_debased, 0, $nonceSize, '8bit'); + $ciphertext = mb_substr($token_debased, $nonceSize, null, '8bit'); + + // Decrypt token-cookie + $x_plex_token = openssl_decrypt( + $ciphertext, + self::METHOD, + $this->token_encrypter, + OPENSSL_RAW_DATA, + $nonce + ); + + // Initiate curl + $ch = curl_init(); + + // Set the options for curl + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Add payload + $payload = array( "X-Plex-Token"=> $x_plex_token, "X-Plex-Client-Identifier" => $this->client_id, "X-Plex-Product" => $this->x_plex_product ); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + + // Add headers + $headers = [ + "accept: $this->header" + ]; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Execute curl + $result = curl_exec($ch); + + // Check if an error occurred + if(curl_errno($ch)) { + throw new Exception('Plex Auth did not respond.'); + } + + // Closing curl + curl_close($ch); + + // Decode the JSON response + $token_result = json_decode($result, true); + + // Return Plex token + return json_encode(array("message" => 'Login accepted.', "error" => false, "data" => $token_result)); + + + // Catch errors + } catch (Exception $e) { + echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array())); + exit(0); + } + } + +} \ No newline at end of file diff --git a/api/objects/cache.php b/api/objects/cache.php new file mode 100644 index 0000000..e15884c --- /dev/null +++ b/api/objects/cache.php @@ -0,0 +1,61 @@ +path = dirname(__FILE__, 3) . '/config/cache.json'; + + // Check if cache file exists, if not, create it + if(!file_exists($this->path)) { + @$create_cache = fopen($this->path, "w"); + if(!$create_cache) { + echo json_encode(array("message" => "Failed to create cache.json. Is the 'config' directory writable?", "error" => true)); + exit(); + } + fclose($create_cache); + } + + } + + public function clear_cache() { + + // Try to open cache + @$cache = fopen($this->path, "w"); + + if(!$cache) { + return false; + } else { + fwrite($cache, ""); + fclose($cache); + return true; + } + } + + public function check_cache() { + + $cache = json_decode(file_get_contents($this->path), True); + + if(!empty($cache)) { + return $cache; + } + + return false; + } + + public function update_cache($result) { + + $save = json_encode($result); + if(file_put_contents($this->path, $save)) { + return true; + } + + return false; + } + +} \ No newline at end of file diff --git a/api/objects/config.php b/api/objects/config.php new file mode 100644 index 0000000..ab891e8 --- /dev/null +++ b/api/objects/config.php @@ -0,0 +1,207 @@ +path = dirname(__FILE__, 3) . '/config/config.json'; + + // Check if config file exists, if not, create it + if(!file_exists($this->path)) { + $create_config = fopen($this->path, "w"); + if(!$create_config) { + echo json_encode(array("message" => "Failed to create config.json. Is the 'config' directory writable?", "error" => true)); + exit(); + } + fclose($create_config); + } + + // Parse JSON from config + $json = json_decode(file_get_contents($this->path)); + + if(!empty($json)) { + + // Assign values from config file + $this->tautulli_apikey = $json->tautulli_apikey; + $this->tautulli_ip = $json->tautulli_ip; + $this->tautulli_port = $json->tautulli_port; + $this->tautulli_length = $json->tautulli_length; + $this->tautulli_root = $json->tautulli_root; + $this->tautulli_libraries = $json->tautulli_libraries; + $this->ssl = $json->ssl; + $this->password = $json->password; + $this->username = $json->username; + $this->timezone = $json->timezone; + $this->use_cache = $json->use_cache; + $this->use_logs = $json->use_logs; + $this->client_id = $json->client_id; + $this->token_encrypter = $json->token_encrypter; + $this->plex_wrapped_root = $json->plex_wrapped_root; + $this->wrapped_start = $json->wrapped_start; + $this->wrapped_end = $json->wrapped_end; + $this->stats_intro = $json->stats_intro; + $this->create_share_links = $json->create_share_links; + $this->get_user_movie_stats = $json->get_user_movie_stats; + $this->get_user_show_stats = $json->get_user_show_stats; + $this->get_user_show_buddy = $json->get_user_show_buddy; + $this->get_user_music_stats = $json->get_user_music_stats; + $this->get_year_stats_movies = $json->get_year_stats_movies; + $this->get_year_stats_shows = $json->get_year_stats_shows; + $this->get_year_stats_music = $json->get_year_stats_music; + $this->get_year_stats_leaderboard = $json->get_year_stats_leaderboard; + + if($this->plex_wrapped_version !== $json->plex_wrapped_version) { + if(!$this->delete_config()) { + echo json_encode(array( + "message" => "Plex Wrapped configuration is made with version: " . $json->plex_wrapped_version . ", but you are using: " . $this->plex_wrapped_version . ". Delete config.json and re-configure Plex Wrapped again.", + "error" => true)); + exit(); + } + } + } + + // If token encrypter is not set, generate one + if($this->token_encrypter == '') { + $this->token_encrypter = md5(rand(0,1000)); + } + + } + + public function delete_config() { + + // Check if config exists + if (!file_exists($this->path)) { + return false; + } + + if (!unlink($this->path)) { + return false; + } + else { + return true; + } + + return false; + } + + public function verify_wrapped_admin($username, $password) { + return password_verify($password, $this->password) && $username == $this->username; + } + + public function is_configured() { + return !empty(file_get_contents($this->path)); + } + + public function save_config($data, $clear_cache) { + + // If clear cache is enabled, clear the cache + if($clear_cache) { + include_once dirname(__FILE__, 3) . '/api/objects/cache.php'; + $cache = new Cache(); + + if(!$cache->clear_cache()) { + echo json_encode(array("message" => "Failed to clear the cache. Is the 'config' directory writable?", "error" => true)); + exit(); + } + } + + // Hash the new password if changed + if($data->password !== "") { + $hash = password_hash($data->password, PASSWORD_DEFAULT); + $this->password = $hash; + } + + // Save new username if it has changed + if($data->username !== "") { + $this->username = $data->username; + } + + // Assign new variables from recieved config + $this->tautulli_apikey = $data->tautulli_apikey; + $this->tautulli_ip = $data->tautulli_ip; + $this->tautulli_port = $data->tautulli_port; + $this->tautulli_length = $data->tautulli_length; + $this->tautulli_root = $data->tautulli_root; + $this->tautulli_libraries = $data->tautulli_libraries; + $this->ssl = $data->ssl; + $this->timezone = $data->timezone; + $this->use_cache = $data->use_cache; + $this->use_logs = $data->use_logs; + $this->client_id = $data->client_id; + $this->plex_wrapped_root = $data->plex_wrapped_root; + $this->wrapped_start = $data->wrapped_start; + $this->wrapped_end = $data->wrapped_end; + $this->stats_intro = $data->stats_intro; + $this->create_share_links = $data->create_share_links; + $this->get_user_movie_stats = $data->get_user_movie_stats; + $this->get_user_show_stats = $data->get_user_show_stats; + $this->get_user_show_buddy = $data->get_user_show_buddy; + $this->get_user_music_stats = $data->get_user_music_stats; + $this->get_year_stats_movies = $data->get_year_stats_movies; + $this->get_year_stats_shows = $data->get_year_stats_shows; + $this->get_year_stats_music = $data->get_year_stats_music; + $this->get_year_stats_leaderboard = $data->get_year_stats_leaderboard; + + // Generate new random client ID if empty + if($this->client_id === "") { + $this->client_id = md5(rand(0,1000)); + } + + // If token encrypter is not set, generate one + if($this->token_encrypter == '') { + $this->token_encrypter = md5(rand(0,1000)); + } + + // Save new variables to file + if(file_put_contents($this->path, json_encode($this))) { + return true; + } else { + return false; + } + + } + + +} +?> \ No newline at end of file diff --git a/api/objects/link.php b/api/objects/link.php new file mode 100644 index 0000000..afffe69 --- /dev/null +++ b/api/objects/link.php @@ -0,0 +1,63 @@ +path = dirname(__FILE__, 3) . '/config/links/'; + + // Create link folder + if (!file_exists($this->path)) { + mkdir($this->path, 0777, true); + } + + } + + public function save_link($content, $id) { + + $save = json_encode($content); + if(file_put_contents($this->path . $id . '.json', $save)) { + return true; + } + + return false; + } + + public function open_link($id) { + + // Check if link exists + if (!file_exists($this->path . $id . '.json')) { + return false; + } + + // Try to read link + if($content = file_get_contents($this->path . $id . '.json')) { + return $content; + } + + return false; + } + + public function delete_link($id) { + + // Check if link exists + if (!file_exists($this->path . $id . '.json')) { + return false; + } + + if (!unlink($this->path . $id . '.json')) { + return false; + } + else { + return true; + } + + return false; + } + +} \ No newline at end of file diff --git a/api/objects/log.php b/api/objects/log.php new file mode 100644 index 0000000..58b3dfa --- /dev/null +++ b/api/objects/log.php @@ -0,0 +1,55 @@ +path = dirname(__FILE__, 3) . '/config/wrapped.log'; + + // Check if log file exists, if not, create it + if(!file_exists($this->path)) { + @$create_log = fopen($this->path, "w"); + if(!$create_log) { + echo json_encode(array("message" => "Failed to create wrapped.log. Is the 'config' directory writable?", "error" => true)); + exit(); + } + fwrite($create_log, 'Plex Wrapped'); + fclose($create_log); + } + + // Assign use_logs configuration from config + include_once(dirname(__FILE__, 3) . '/api/objects/config.php'); + $config = new Config(); + $this->use_logs = $config->use_logs; + } + + public function log_activity($function, $id, $message) { + + if($this->use_logs) { + try { + $date = date('Y-m-d H:i:s'); + + $log_file = @fopen($this->path, 'a'); + fwrite($log_file, PHP_EOL . $date . ' - ' . $function . ' - ID: ' . $id . ' - ' . $message); + fclose($log_file); + + } catch(Error $e) { + http_response_code(500); + echo json_encode(array("error" => True, "message" => "Failed to log event.")); + exit(); + } + + } + + return True; + } +} +?> \ No newline at end of file diff --git a/api/set_config.php b/api/set_config.php index 37b78a8..7469c58 100644 --- a/api/set_config.php +++ b/api/set_config.php @@ -1,90 +1,84 @@ log_activity('set_config.php', 'unknown', 'No admin login input provided.'); -if(!empty($data)) { - $config_data = $data->data; -} else { echo json_encode(array("error" => true, "message" => "No input provided.")); exit(0); } + +// Remove potential harmfull input $password = htmlspecialchars($data->password); $username = htmlspecialchars($data->username); -if(empty($config->password)) { - save_config(); - exit(0); -} +// Verify password and username combination +if(!$config->is_configured()) { -if(password_verify($password, $config->password) && $username == $config->username) { - // Log API request if enabled - if($config->use_logs) { - if(!log_activity()) { - echo json_encode(array("message" => "Failed to log event.", "error" => true)); - exit(0); - } - } + // Log use + $log->log_activity('set_config.php', 'unknown', 'Not configured before, saving first-time configuration.'); + + // Call save function + save_config($data->data, $data->clear_cache); + +} else if($config->verify_wrapped_admin($username, $password)) { - save_config(); - exit(0); + // Log use + $log->log_activity('set_config.php', 'unknown', 'Admin login verified.'); + + // Call save function + save_config($data->data, $data->clear_cache); + +// If input was given, but is empty } else { - echo json_encode(array("error" => true, "message" => "Password and username combination not accepted.", "password" => true)); + + // Log use + $log->log_activity('set_config.php', 'unknown', 'Wrong admin password/username combination.'); + + echo json_encode(array("error" => true, "message" => "Username and password combination not accepted.", "password" => true, "data" => array())); exit(0); + } -function save_config() { - global $data; - global $config_data; +// Retrieve data and save it +function save_config($data, $clear_cache) { + global $config; - global $path; - global $path2; + global $log; - if(!empty($config_data->password)) { - $hash = password_hash($config_data->password, PASSWORD_DEFAULT); - $config_data->password = $hash; - } else { - $config_data->password = $config->password; - } + // Call function to save data + if($config->save_config($data, $clear_cache)) { - if(file_put_contents($path, json_encode($config_data))) { - - if($data->clear_cache) { - file_put_contents($path2, ""); - } + // Log use + $log->log_activity('set_config.php', 'unknown', 'New config was saved.'); echo json_encode(array("error" => false, "message" => "Changes saved.")); exit(0); } else { - echo json_encode(array("error" => true, "message" => "Changes were not saved.")); + + // Log use + $log->log_activity('set_config.php', 'unknown', 'New login was not saved.'); + + echo json_encode(array("error" => true, "message" => "Changes were not saved. Is the directory 'config' writable?")); exit(0); } } - -function log_activity() { - $date = date('Y-m-d H:i:s'); - - $path = "../config/wrapped.log"; - if(!file_exists($path)) { - $temp = fopen($path, "w"); - fwrite($temp, 'Plex Wrapped'); - fclose($temp); - } - - $log_file = fopen($path, 'a'); - fwrite($log_file, PHP_EOL . $date . ' - set_config.php'); - - if(fclose($log_file)) { - return True; - } - - return False; -} ?> \ No newline at end of file diff --git a/api/validate_login_cookie.php b/api/validate_login_cookie.php new file mode 100644 index 0000000..52eed97 --- /dev/null +++ b/api/validate_login_cookie.php @@ -0,0 +1,52 @@ +cookie)) { + + // Log use + $log->log_activity('validate_login_cookie.php', 'unknown', 'Input error from user.'); + + echo json_encode(array("error" => true, "message" => "Input error.")); + exit(0); + +} + +// Remove potential harmfull input +$cookie = htmlspecialchars($data->cookie); + +// Get Plex token +$token_object = json_decode($auth->validate_token($cookie)); + +// Validate Plex ID +if(empty($token_object) || !isset($token_object->data->id)) { + + // Log use + $log->log_activity('validate_login_cookie.php', 'unknown', 'Plex Token from cookie not valid.'); + + echo json_encode(array("error" => true, "message" => "Login not accepted. Log in again.")); + exit(0); + +} + +// Log use +$log->log_activity('validate_login_cookie.php', $token_object->data->id, 'Plex-Wrapped login cookie accepted.'); + +// Print cookie and exit +echo json_encode(array("error" => false, "message" => "Cookie is valid.")); +exit(0); +?> \ No newline at end of file diff --git a/assets/close.svg b/assets/close.svg new file mode 100644 index 0000000..c978abf --- /dev/null +++ b/assets/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/config.svg b/assets/config.svg new file mode 100644 index 0000000..3bc2d37 --- /dev/null +++ b/assets/config.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/css/wrapped.css b/assets/css/wrapped.css index ec322e9..95dc6ca 100644 --- a/assets/css/wrapped.css +++ b/assets/css/wrapped.css @@ -22,7 +22,7 @@ h1{ text-shadow: 1px 1px 2px #555; line-height: 110%; text-align: center; - padding: 0; + padding: 0.25em 0; margin: 0; } @@ -63,6 +63,13 @@ p{ margin: 0; } +p2{ + font-size: 1em; + color: black; + text-align: center; + margin: 0; +} + a { color: inherit; text-decoration: underline; @@ -222,7 +229,7 @@ img { .form { font-family: 'Roboto', serif; font-size: 1em; - margin: 0.5em; + margin: 0; } .form_button { @@ -347,10 +354,6 @@ img { padding: 0.5em; } -form { - padding: 0; -} - .form-control { font-size: 1em; padding: 0.5em; @@ -367,7 +370,7 @@ form { display: inline-block; margin: auto; float: left; - padding: 1em; + padding: 0.5em; width: 100%; box-sizing: border-box; } @@ -375,7 +378,13 @@ form { .btn { background-color: #ffbd55; border: none; - cursor: pointer; + cursor: pointer; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: center; + justify-content: center; + align-items: center; } label { @@ -400,6 +409,29 @@ input[type="checkbox" i] { text-align: center; } +.sign_out { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: center; + justify-content: flex-start; + align-items: center; + background-color: #ffbd55; + border: none; + cursor: pointer; + border-radius: 0.25em; + font-family: 'Roboto', serif; + padding: 0.25em 0.5em; + margin: auto; +} + +.btn_logo { + width: 1em; + height: 1em; + margin: 0; + padding: 0 0.25em; +} + /*Snowflakes*/ .snowflake { color: #fff; @@ -409,3 +441,33 @@ input[type="checkbox" i] { } @-webkit-keyframes snowflakes-fall{0%{top:-10%}100%{top:100%}}@-webkit-keyframes snowflakes-shake{0%{-webkit-transform:translateX(0px);transform:translateX(0px)}50%{-webkit-transform:translateX(80px);transform:translateX(80px)}100%{-webkit-transform:translateX(0px);transform:translateX(0px)}}@keyframes snowflakes-fall{0%{top:-10%}100%{top:100%}}@keyframes snowflakes-shake{0%{transform:translateX(0px)}50%{transform:translateX(80px)}100%{transform:translateX(0px)}}.snowflake{position:fixed;top:-10%;z-index:9999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;-webkit-animation-name:snowflakes-fall,snowflakes-shake;-webkit-animation-duration:10s,3s;-webkit-animation-timing-function:linear,ease-in-out;-webkit-animation-iteration-count:infinite,infinite;-webkit-animation-play-state:running,running;animation-name:snowflakes-fall,snowflakes-shake;animation-duration:10s,3s;animation-timing-function:linear,ease-in-out;animation-iteration-count:infinite,infinite;animation-play-state:running,running}.snowflake:nth-of-type(0){left:1%;-webkit-animation-delay:0s,0s;animation-delay:0s,0s}.snowflake:nth-of-type(1){left:10%;-webkit-animation-delay:1s,1s;animation-delay:1s,1s}.snowflake:nth-of-type(2){left:20%;-webkit-animation-delay:6s,.5s;animation-delay:6s,.5s}.snowflake:nth-of-type(3){left:30%;-webkit-animation-delay:4s,2s;animation-delay:4s,2s}.snowflake:nth-of-type(4){left:40%;-webkit-animation-delay:2s,2s;animation-delay:2s,2s}.snowflake:nth-of-type(5){left:50%;-webkit-animation-delay:8s,3s;animation-delay:8s,3s}.snowflake:nth-of-type(6){left:60%;-webkit-animation-delay:6s,2s;animation-delay:6s,2s}.snowflake:nth-of-type(7){left:70%;-webkit-animation-delay:2.5s,1s;animation-delay:2.5s,1s}.snowflake:nth-of-type(8){left:80%;-webkit-animation-delay:1s,0s;animation-delay:1s,0s}.snowflake:nth-of-type(9){left:90%;-webkit-animation-delay:3s,1.5s;animation-delay:3s,1.5s} + +.color-purple { + background-color: #B9A3D2; + transition-duration: 1s; +} + +.color-pink { + background-color: #D2A3A4; + transition-duration: 1s; +} + +.color-green { + background-color: #BBD2A3; + transition-duration: 1s; +} + +.color-brown { + background-color: #CFA38C; + transition-duration: 1s; +} + +.color-blue { + background-color: #a2d1d0; + transition-duration: 1s; +} + +.color-black { + background-color: #39393A; + transition-duration: 1s; +} diff --git a/assets/done.svg b/assets/done.svg new file mode 100644 index 0000000..6574f68 --- /dev/null +++ b/assets/done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/external-link.svg b/assets/external-link.svg new file mode 100644 index 0000000..ae6c2ae --- /dev/null +++ b/assets/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/for-you.svg b/assets/for-you.svg new file mode 100644 index 0000000..34a393e --- /dev/null +++ b/assets/for-you.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/restart.svg b/assets/restart.svg new file mode 100644 index 0000000..d37b53b --- /dev/null +++ b/assets/restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/share.svg b/assets/share.svg new file mode 100644 index 0000000..6001dc1 --- /dev/null +++ b/assets/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/synchronize.svg b/assets/synchronize.svg new file mode 100644 index 0000000..a697f1a --- /dev/null +++ b/assets/synchronize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/caching.js b/caching.js index 4b7fe49..67903d7 100644 --- a/caching.js +++ b/caching.js @@ -6,13 +6,19 @@ function search_box() {

- This will call the API in a loop until all the entires in the set period are cached. The entry below allows you to choose the maximum amount of days to be cached each time you call the API to attempt pre-caching. Having a higher amount of days requires the PHP script to run longer meaning you might have to configure the php.ini file. + When you configured a wrapped period, it included the amount of days you want to analyze. Each unique day in that period is a new API request to Tautulli. +
+
+ Here you can perform a full caching of all those days, by calling the Plex-Wrapped API in a loop, downloading all the data needed. This makes the site fast to load. +
+
+ Plex-Wrapped uses PHP, which has script run-time limits. The input below allows you to reduce the number of days cached each time the cacher loops, preventing the PHP script from running for more than the allowed runtime (typically 120 seconds).

- - + +
@@ -79,7 +85,7 @@ function cache_log(days, result, complete) { function get_stats(days) { stats_form = { - "p_identity" : '0', + "cookie" : '', "caching" : true, "cache_limit" : days }; @@ -137,7 +143,6 @@ function get_config_cache() { login_menu(); } else { alert(result.message); - window.location.href = "../admin"; } } }; @@ -182,12 +187,12 @@ function login_menu() { var html = '' html += '
'; - html += ''; + html += ''; html += ''; html += '
'; html += '
'; - html += ''; + html += ''; html += ''; html += '
'; @@ -197,4 +202,27 @@ function login_menu() { html += ''; document.getElementById("cache").innerHTML = html; +} + +function get_plex_wrapped_version() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + + try { + var result= JSON.parse(this.responseText); + } catch(error) { + console.log('Failed to parse Plex-Wrapped version. Response: ' + this.responseText) + } + + if(!result.error) { + document.getElementById('github_link').innerHTML = 'GitHub (' + result.plex_wrapped_version + ')'; + } + + } + }; + xhttp.withCredentials = true; + xhttp.open("post", "../api/get_plex_wrapped_version.php"); + xhttp.send(); + return; } \ No newline at end of file diff --git a/caching/index.html b/caching/index.html index e46b130..a4aaa38 100644 --- a/caching/index.html +++ b/caching/index.html @@ -31,7 +31,7 @@

- If you want to decrease loadtime for your users you can pre-cache results. + Caching decreases loadtime for users and reduces PHP induced errors.

Remember to configure the system first.

@@ -59,15 +59,18 @@ Wrapped | Admin | Caching | - GitHub + GitHub (v2.1.0)
- +
@@ -31,7 +31,7 @@
-
+

Did you get that thing from Spotify and wondered what your Plex statistics looked like?

Well, have a look... @@ -46,31 +46,45 @@

@@ -118,7 +132,7 @@ Wrapped | Admin | Caching | - GitHub + GitHub
@@ -128,9 +142,31 @@ diff --git a/index.js b/index.js index c5c0ad3..bb4b0ac 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,238 @@ -function search_button(string) { - $('#search_get').html(string); +function cookie_login_actions() { + cookie = get_cookie('plex-wrapped'); + + if(cookie) { + document.getElementById('search_wrapped_form').style.display = 'block'; + document.getElementById('plex_login_form').style.display = 'none'; + document.getElementById('sign_out_div').style.display = 'block'; + + validate_cookie(cookie); + } } -$(document).on('submit', '#stats_form', function(){ +function wrapped_link_actions(hash) { - search_button("SEARCHING..."); + hash_form = { + "hash" : hash + }; + + var hash_data = JSON.stringify(hash_form); + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) { + try { + var result= JSON.parse(this.responseText); + } catch(error) { + document.getElementById('stats').innerHTML = "API response can't be parsed."; + reset_button(); + } + + if(result.error) { + document.getElementById('stats').innerHTML = '

' + result.message + '

'; + } else { + results = result.data; + get_functions(); + } + } + }; + xhttp.withCredentials = true; + xhttp.open("post", "api/get_link.php"); + xhttp.send(hash_data); +} + +function sign_out() { + set_cookie("plex-wrapped", "", 1); + document.getElementById('search_wrapped_form').style.display = 'none'; + document.getElementById('plex_login_form').style.display = 'block'; + document.getElementById("plex_login_button").disabled = false; + document.getElementById("plex_login_button").style.opacity = '1'; + document.getElementById("search_wrapped_button").disabled = true; + document.getElementById("search_wrapped_button").style.opacity = '0.5'; + document.getElementById('sign_out_div').style.display = 'none'; +} + +$(document).on('submit', '#search_wrapped_form', function(){ + + document.getElementById("search_wrapped_button").disabled = true; + document.getElementById("search_wrapped_button").style.opacity = '0.5'; + document.getElementById("plex_signout_button").disabled = true; + document.getElementById("plex_signout_button").style.opacity = '0.5'; document.getElementById('results_error').innerHTML = ""; get_functions(); }); + +$(document).on('submit', '#plex_login_form', function(){ + + window_url = window.location.href.split('?')[0]; + + auth_form = {"home_url" : window_url}; + + var auth_data = JSON.stringify(auth_form); + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) { + try { + var result= JSON.parse(this.responseText); + document.getElementById('snowflakes').style.display = 'none'; + } catch(error) { + document.getElementById('results_error').innerHTML = "API response can't be parsed."; + } + if(result.error) { + document.getElementById('results_error').innerHTML = result.message; + + } else { + pop_up_login(result.url, result.code, result.id); + } + } + }; + xhttp.withCredentials = true; + xhttp.open("post", "api/get_login_url.php"); + xhttp.send(auth_data); + +}); + +function pop_up_login(url, code, id) { + + document.getElementById('plex_login_button_text').innerHTML = 'Loading...'; + document.getElementById("plex_login_button").disabled = true; + document.getElementById("plex_login_button").style.opacity = '0.5'; + + const openedWindow = window.open( + url, + "Plex Login", + "width=500,height=750,resizable,scrollbars" + ); + + var timer = setInterval(function() { + if(openedWindow.closed) { + + check_token(code, id); + + clearInterval(timer); + + } + }, 1000); + +} + +function check_token(code, id) { + + auth_form = { + "code" : code, + "id" : id + }; + + var auth_data = JSON.stringify(auth_form); + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) { + try { + var result= JSON.parse(this.responseText); + } catch(error) { + document.getElementById('results_error').innerHTML = "API response can't be parsed."; + reset_button(); + } + + if(result.error) { + reset_button(); + } else { + set_cookie("plex-wrapped", result.cookie, 1); + location.reload(); + } + } + }; + xhttp.withCredentials = true; + xhttp.open("post", "api/get_login_cookie.php"); + xhttp.send(auth_data); + +} + +function reset_button() { + document.getElementById('plex_login_button_text').innerHTML = 'Sign in using Plex'; + document.getElementById("plex_login_button").disabled = false; + document.getElementById("plex_login_button").style.opacity = '1'; +} + +function set_cookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays*24*60*60*1000)); + var expires = "expires="+ d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function get_cookie(cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for(var i = 0; i