Squashed commit of the following:

commit 84caa7b85f9036c7ca4cf2535f80bdf96aa120f6
Author: aunefyren <oystein.sverre@gmail.com>
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 <oystein.sverre@gmail.com>
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 <oystein.sverre@gmail.com>
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 <oystein.sverre@gmail.com>
Date:   Sat Dec 11 13:56:18 2021 +0100

    Option to disable links

commit d8f1b0ab09f5ed1cfbdc9a753be2bebd63c51e2c
Author: aunefyren <oystein.sverre@gmail.com>
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 <oystein.sverre@gmail.com>
Date:   Sat Dec 4 23:02:16 2021 +0100

    Fixed caching

    Forgot to add global and variable to function

commit b5765f3094206406c8d2f20ffd36b0bad5ea0200
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Dec 4 22:58:48 2021 +0100

    Re-added caching mode

    Whops

commit 20ccd23ed9a04fa4b00e2994ab89a931d79f5a78
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Dec 4 22:52:38 2021 +0100

    Stats API mostly moved to new class format

commit a594447ffcd917d149078dd5c57d7e3f5a88bf1a
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Dec 4 16:17:48 2021 +0100

    Fixed login bug on caching

commit 9d303514ef080e9483f4753b7fc488da314d1b7a
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Dec 4 16:06:09 2021 +0100

    Added root option, fixed PHP relative folders

commit 08f21ad4132f92a502cd83521ba51dd9ab9a2dfb
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Dec 4 13:57:18 2021 +0100

    FIxed major login issue from last  commit

commit 84d767ac99946016dca6ef92991f12f661de266c
Author: aunefyren <oystein.sverre@gmail.com>
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 <oystein.sverre@gmail.com>
Date:   Fri Dec 3 17:25:00 2021 +0100

    Changed UI for admin & Trying to alert permission issues
This commit is contained in:
aunefyren 2021-12-12 17:31:15 +01:00
parent 7556dd9ab5
commit b8901caca2
38 changed files with 2283 additions and 534 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
config/wrapped.log
config/config.json
config/cache.json
config/links

128
README.md
View file

@ -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.
<br>
<br>
![alt text](https://raw.githubusercontent.com/aunefyren/Plex-Wrapped/main/assets/img/example_01.PNG?raw=true)
<br>
<br>
### 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
<br>
<br>
![alt text](https://raw.githubusercontent.com/aunefyren/Plex-Wrapped/main/assets/img/example_02.PNG?raw=true)
<br>
<br>
### 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)
<br>
<br>
![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.
<br>
<br>
## 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.
<br>
<br>
### 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: <b>your-domain-or-ip/admin</b>
- 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 <b>your-domain-or-ip/caching</b> 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
<br>
<br>
## 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 <b>config</b>. 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.
<br>
<br>
## 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=<b>enough seconds for the script to finish.</b> 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=<b>enough M for the script to handle JSON data.</b> 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=<b>enough seconds for the script to parse JSON data.</b> 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
```
<br>
<br>
## 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.
<b>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.</b>
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=<b>enough seconds for the script to finish.</b><br>The longer the timeframe, the more execution time. Every unique date in your timeframe is a new Tautulli API call.
- memory_limit=<b>enough M for the script to handle JSON data.</b><br>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=<b>enough seconds for the script to parse JSON data.</b><br>You might not need to change this, depending on Tautulli connection speed.
<br>
<br>
## 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.
<b>Goodybye.</b>
Have fun.

138
admin.js
View file

@ -71,12 +71,12 @@ function login_menu() {
var html = '<form id="password_login_form" onsubmit="get_config();return false">'
html += '<div class="form-group">';
html += '<label for="username" title="The username chosen during first-time setup.">Username</label>';
html += '<input type="text" class="form-control" id="username" value="" minlength=4 autocomplete="on" required />';
html += '<label for="username" title="The username chosen during first-time setup.">Username:</label>';
html += '<input type="text" class="form-control" id="username" value="" placeholder="" minlength=4 autocomplete="on" required />';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="password" title="The password chosen during first-time setup.">Password</label>';
html += '<label for="password" title="The password chosen during first-time setup.">Password:</label>';
html += '<input type="password" class="form-control" id="password" value="" autocomplete="off" required />';
html += '</div>';
@ -93,18 +93,18 @@ function set_password(back) {
var html = '<form id="password_form" onsubmit="set_tautulli(false);return false">'
html += '<div class="form-group">';
html += '<label for="username" title="The username needed to change the config-file remotely.">Set an admin username</label>';
html += '<input type="text" class="form-control" id="username" value="' + username + '" minlength=4 autocomplete="on" required />';
html += '<label for="username" title="The username needed to log in as administrator and change the config-file remotely.">Set an admin username:</label>';
html += '<input type="text" class="form-control" id="username" value="' + username + '" placeholder="" minlength=4 autocomplete="on" required />';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="password" title="The password needed to change the config-file remotely.">Set an admin password</label>';
html += '<input type="password" class="form-control" id="password" value="' + password + '" autocomplete="off" required />';
html += '<label for="password" title="The password needed to change the config-file remotely.">Set an admin password:</label>';
html += '<input type="password" class="form-control" id="password" value="' + password + '" placeholder="" autocomplete="off" required />';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="password_2" title="The password needed to change the config-file remotely.">Repeat the password</label>';
html += '<input type="password" class="form-control" id="password_2" value="' + password + '" autocomplete="off" required />';
html += '<label for="password_2" title="The password needed to change the config-file remotely.">Repeat the password:</label>';
html += '<input type="password" class="form-control" id="password_2" value="' + password + '" placeholder="" autocomplete="off" required />';
html += '</div>';
html += '<div class="form-group">';
@ -132,43 +132,50 @@ function set_tautulli(back) {
}
var html = '<div class="form-group">';
html += '<button class="form-control btn" onclick="set_password(true)">Set admin password</button>';
html += '<button class="form-control btn" onclick="set_password(true)"><img src="../assets/config.svg" class="btn_logo"><p2>Change admin password</p2></button>';
html += '</div>';
html += '<hr>';
html += '<form id="tautulli_form" onsubmit="set_tautulli_details(false);return false">'
html += '<div class="form-group">';
html += '<label for="tautulli_apikey" title="The API key needed to interact with Tautulli. Commonly found at Tautulli->Settings->Web Interface->API Key.">Tautulli API key</label>';
html += '<input type="text" class="form-control" id="tautulli_apikey" value="' + tautulli_apikey + '" autocomplete="off" required placeholder="l0NgWe1rDAp1K3y..." /><br>';
html += '<label for="tautulli_apikey" title="The API key is needed for Plex-Wrapped to interact with Tautulli. Commonly found at Tautulli->Settings->Web Interface->API Key.">Tautulli API key:</label>';
html += '<input type="text" class="form-control" id="tautulli_apikey" value="' + tautulli_apikey + '" autocomplete="off" required placeholder="" /><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="tautulli_ip" title="The IP address or domain that connects to Tautulli. No subfolders, as this is another setting, but subdomains can be defined.">IP or domain for Tautulli connection</label>';
html += '<input type="text" class="form-control" id="tautulli_ip" value="' + tautulli_ip + '" required placeholder="mycooldomain.plex" /><br>';
html += '<label for="tautulli_ip" title="The IP address or domain that connects to Tautulli. No subfolders, as this is another setting, but subdomains can be defined.">IP or domain for Tautulli connection:</label>';
html += '<input type="text" class="form-control" id="tautulli_ip" value="' + tautulli_ip + '" required placeholder="" /><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="tautulli_port" title="The port Tautulli uses for connections. Typically empty if a domain is used.">Port for Tautulli (optional)</label>';
html += '<input type="text" class="form-control" id="tautulli_port" value="' + tautulli_port + '" placeholder="8181" /><br>';
html += '<label for="tautulli_port" title="The port Tautulli uses for connections. Typically empty if a domain is used.">Port for Tautulli: (Optional)</label>';
html += '<input type="text" class="form-control" id="tautulli_port" value="' + tautulli_port + '" placeholder="" /><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="tautulli_length" title="The max amount of entries Tautulli responds with during API calls. Typically doesn\'t need to be changed, but if you have more than 5000 entries in a day, they won\'t be loaded.">Tautlli item length</label>';
html += '<input type="number" min="0" class="form-control" id="tautulli_length" value="' + tautulli_length + '" autocomplete="off" placeholder="5000" required /><br>';
html += '<label for="tautulli_length" title="The max amount of entries Tautulli responds with during API calls. Typically doesn\'t need to be changed, but if you have more than 5000 entries in a day, they won\'t be loaded.">Tautlli item length:</label>';
html += '<input type="number" min="0" class="form-control" id="tautulli_length" value="' + tautulli_length + '" autocomplete="off" placeholder="" required /><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="tautulli_root" title="Subfolder for Tautulli, no slashes needed at the beginning or end. It is the folder accessed after the main IP/domain. For example: tautulli.com/subfolder.">Root for Tautulli (optional)</label>';
html += '<input type="text" class="form-control" id="tautulli_root" value="' + tautulli_root + '" autocomplete="off" placeholder="tautulli"/><br>';
html += '<label for="tautulli_root" title="Subfolder for Tautulli, no slashes needed at the beginning or end. It is the folder accessed after the main IP/domain. For example: \'tautulli.com/subfolder\' would mean you enter \'subfolder\' here.">Root for Tautulli: (Optional)</label>';
html += '<input type="text" class="form-control" id="tautulli_root" value="' + tautulli_root + '" autocomplete="off" placeholder=""/><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="timezone" title="The timezone the data is located in.">Timezone <a href="https://www.php.net/manual/en/timezones.php" target="_blank">(List)</a></label>';
html += '<input type="text" class="form-control" id="timezone" value="' + timezone + '" autocomplete="off" placeholder="Europe/Oslo" required /><br>';
html += '<label for="tautulli_libraries" title="Comma seprated list of ID\'s to use for statistics. If none are given, it will search all.">Libraries ID\'s to use: (Optional)</label>';
html += '<input type="text" class="form-control" id="tautulli_libraries" value="' + tautulli_libraries + '" autocomplete="off" placeholder=""/><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="ssl" title="Enable if your connection uses HTTPS or HTTP.">Use HTTPS</label>';
html += '<label for="timezone" title="The timezone the data is located in, like \'Europe/Oslo\'. Type it exactly as it is specified in the PHP documentation.">Timezone: <a href="https://www.php.net/manual/en/timezones.php" target="_blank">(List)</a></label>';
html += '<input type="text" class="form-control" id="timezone" value="' + timezone + '" autocomplete="off" placeholder="" required /><br>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="ssl" title="Enable if your connection uses HTTPS.">Use HTTPS:</label>';
html += '<input type="checkbox" class="form-control" id="ssl" ';
if(ssl) {
html += 'checked="' + ssl + '" ';
@ -177,7 +184,7 @@ function set_tautulli(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<input style="background-color: lightgrey;" type="button" class="form-control btn" id="test_connection" onclick="test_tautulli_connection()" value="Test Tautulli connection" />';
html += '<button style="background-color: lightgrey;" type="button" class="form-control btn" id="test_connection" onclick="test_tautulli_connection()"><img src="../assets/synchronize.svg" class="btn_logo"><p2 id="test_tautulli_text">Test Tautulli connection</p2></button>';
html += '</div>';
html += '<div class="form-group">';
@ -197,15 +204,23 @@ function set_tautulli_details(back) {
tautulli_port = document.getElementById('tautulli_port').value;
tautulli_length = document.getElementById('tautulli_length').value;
tautulli_root = document.getElementById('tautulli_root').value;
tautulli_libraries = document.getElementById('tautulli_libraries').value;
timezone = document.getElementById('timezone').value;
ssl = document.getElementById('ssl').checked;
}
var html = '<div class="form-group">';
html += '<button class="form-control btn" onclick="set_tautulli(true, false)">Tautulli settings</button>';
html += '<button class="form-control btn" onclick="set_tautulli(true, false)"><img src="../assets/config.svg" class="btn_logo"><p2>Tautulli settings</p2></button>';
html += '</div>';
html += '<hr>';
html += '<form id="tautulli_details_form" onsubmit="set_tautulli_last(false);return false">'
html += '<div class="form-group">';
html += '<label for="plex_wrapped_root" title="Subfolder for Plex-Wrapped, no slashes needed at the beginning or end. It is the folder accessed after the main IP/domain. For example: \'mycooldomain.com/wrapped\' would mean you enter \'wrapped\' here.">Root for Plex-Wrapped: (Optional)</label>';
html += '<input type="text" class="form-control" id="plex_wrapped_root" value="' + plex_wrapped_root + '" autocomplete="off" placeholder=""/><br>';
html += '</div>';
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 += '<div class="form-group">';
html += '<div class="warning">!<br>Load time for long wrapped periods are extensive. Consider enabling caching and performing pre-caching once.</div>';
html += '<label for="wrapped_start" title="The start of the period you want wrapped.">Start of wrapped period</label>';
html += '<label for="wrapped_start" title="The start of the period you want wrapped.">Start of wrapped period:</label>';
html += '<input type="datetime-local" class="form-control" id="wrapped_start" value="' + temp_date_first[2].trim() + '-' + temp_date_first[1].trim() + '-' + temp_date_first[0].trim() + 'T' + temp_date_second[0].trim() + ':' + temp_date_second[1].trim() + '" required /><br>';
html += '</div>';
@ -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 += '<div class="form-group">';
html += '<label for="wrapped_end" title="The end of your wrapped period. All data until this point is viable.">End of wrapped period<br>';
html += '<label for="wrapped_end" title="The end of your wrapped period. All data until this point is viable.">End of wrapped period:<br>';
html += '<input type="datetime-local" class="form-control" id="wrapped_end" value="' + temp_date_first[2].trim() + '-' + temp_date_first[1].trim() + '-' + temp_date_first[0].trim() + 'T' + temp_date_second[0].trim() + ':' + temp_date_second[1].trim() + '" required /></label>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="stats_intro" title="Introduction text that is shown when the statistics are done loading. Could be used to inform about chosen timeframe. HTML allowed.">Introduction for statistics page<br>';
html += '<label for="stats_intro" title="Introduction text that is shown when the statistics are done loading. Could be used to inform about chosen timeframe. HTML allowed.">Introduction for statistics page:<br>';
html += '<textarea cols="40" rows="5" class="form-control" style="overflow-x: hidden;resize:vertical;min-height: 5em;" id="stats_intro" name="stats_intro" value="" autocomplete="off" placeholder="New year, new page of statistics..."></textarea></label>';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="create_share_links" title="Grants users the ability to create a URL for sharing. Valid for 7 days.">Allow shareable URL creation:<br>';
html += '<input type="checkbox" class="form-control" id="create_share_links" ';
if(create_share_links) {
html += 'checked="' + create_share_links + '" ';
}
html += '/><br>';
html += '</div>';
html += '<hr>';
html += '<div class="form-group">';
html += '<label for="get_user_movie_stats" title="Includes movie statistics in your wrapped period.">Get users movie statistics<br>';
html += '<label for="get_user_movie_stats" title="Includes movie statistics in your wrapped period.">Get users movie statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_user_movie_stats" ';
if(get_user_movie_stats) {
html += 'checked="' + get_user_movie_stats + '" ';
@ -258,7 +282,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_user_show_stats" title="Includes show statistics in your wrapped period.">Get users show statistics<br>';
html += '<label for="get_user_show_stats" title="Includes show statistics in your wrapped period.">Get users show statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_user_show_stats" ';
if(get_user_show_stats) {
html += 'checked="' + get_user_show_stats + '" ';
@ -267,7 +291,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_user_show_buddy" title="Includes the users top show-buddy in your wrapped period. Requires show stats.">Get users show-buddy<br>';
html += '<label for="get_user_show_buddy" title="Includes the users top show-buddy in your wrapped period. Requires show stats.">Get users show-buddy:<br>';
html += '<input type="checkbox" class="form-control" id="get_user_show_buddy" ';
if(get_user_show_buddy) {
html += 'checked="' + get_user_show_buddy + '" ';
@ -276,7 +300,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_user_music_stats" title="Includes music statistics in your wrapped period.">Get users music statistics<br>';
html += '<label for="get_user_music_stats" title="Includes music statistics in your wrapped period.">Get users music statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_user_music_stats" ';
if(get_user_music_stats) {
html += 'checked="' + get_user_music_stats + '" ';
@ -287,7 +311,7 @@ function set_tautulli_details(back) {
html += '<hr>';
html += '<div class="form-group">';
html += '<label for="get_year_stats_movies" title="Includes server-wide movie statistics in your wrapped period.">Get server-wide movie statistics<br>';
html += '<label for="get_year_stats_movies" title="Includes server-wide movie statistics in your wrapped period.">Get server-wide movie statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_year_stats_movies" ';
if(get_year_stats_movies) {
html += 'checked="' + get_year_stats_movies + '" ';
@ -296,7 +320,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_year_stats_shows" title="Includes server-wide show statistics in your wrapped period.">Get server-wide show statistics<br>';
html += '<label for="get_year_stats_shows" title="Includes server-wide show statistics in your wrapped period.">Get server-wide show statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_year_stats_shows" ';
if(get_year_stats_shows) {
html += 'checked="' + get_year_stats_shows + '" ';
@ -305,7 +329,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_year_stats_music" title="Includes server-wide music statistics in your wrapped period.">Get server-wide music statistics<br>';
html += '<label for="get_year_stats_music" title="Includes server-wide music statistics in your wrapped period.">Get server-wide music statistics:<br>';
html += '<input type="checkbox" class="form-control" id="get_year_stats_music" ';
if(get_year_stats_music) {
html += 'checked="' + get_year_stats_music + '" ';
@ -314,7 +338,7 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group">';
html += '<label for="get_year_stats_leaderboard" title="Creates a user leaderboard based on the server-wide statistics in your wrapped period.">Get server-wide leaderboard rankings<br>';
html += '<label for="get_year_stats_leaderboard" title="Creates a user leaderboard based on the server-wide statistics in your wrapped period.">Get server-wide leaderboard rankings:<br>';
html += '<input type="checkbox" class="form-control" id="get_year_stats_leaderboard" ';
if(get_year_stats_leaderboard) {
html += 'checked="' + get_year_stats_leaderboard + '" ';
@ -325,7 +349,7 @@ function set_tautulli_details(back) {
html += '<hr>';
html += '<div class="form-group">';
html += '<label for="use_logs" title="Logs every API request into a log-file in the config folder. ID for Wrapped request included.">Log API calls<br>';
html += '<label for="use_logs" title="Logs every API request into a log-file in the config folder. ID for Wrapped request included.">Log API calls:<br>';
html += '<input type="checkbox" class="form-control" id="use_logs" ';
if(use_logs) {
html += 'checked="' + use_logs + '" ';
@ -335,7 +359,7 @@ function set_tautulli_details(back) {
html += '<div class="form-group">';
html += '<div class="warning">!<br>If your wrapped period is long and no results are cached, the wait time can be extensive. Using the cache feature and <a href="../caching" target="_blank">pre-caching</a> once is recommended.</div>';
html += '<label for="use_cache" title="Caches your results in cache.json for later use.">Cache results for later use<br>';
html += '<label for="use_cache" title="Caches your results in cache.json for later use.">Cache results for later use:<br>';
html += '<input type="checkbox" class="form-control" id="use_cache" ';
if(use_cache) {
html += 'checked="' + use_cache + '" ';
@ -344,12 +368,12 @@ function set_tautulli_details(back) {
html += '</div>';
html += '<div class="form-group" title="Clear the cache now to include the newest settings.">';
html += '<label for="clear_cache">Clear cache now<br>';
html += '<label for="clear_cache">Clear cache now:<br>';
html += '<input type="checkbox" class="form-control" id="clear_cache" checked /></label>';
html += '</div>';
html += '<div class="form-group">';
html += '<input type="submit" class="form-control btn" id="form_button" value="Finish" required />';
html += '<button type="submit" class="form-control btn" id="form_button"><img src="../assets/done.svg" class="btn_logo"><p2>Finish</p2></button>';
html += '</div>';
html += '</form>';
@ -358,6 +382,9 @@ function set_tautulli_details(back) {
}
function test_tautulli_connection() {
document.getElementById("test_connection").disabled = true;
document.getElementById("test_connection").style.opacity = '0.5';
var button = document.getElementById('test_connection');
button.style.backgroundColor = 'lightgrey';
@ -395,8 +422,13 @@ function test_tautulli_connection() {
var result = JSON.parse(this.responseText);
if(!result.error) {
button.style.backgroundColor = '#79A04F';
document.getElementById("test_connection").disabled = false;
document.getElementById("test_connection").style.opacity = '1';
} else {
button.style.backgroundColor = '#F1909C';
document.getElementById("test_connection").disabled = false;
document.getElementById("test_connection").style.opacity = '1';
alert(result.message);
}
}
};
@ -407,6 +439,8 @@ function test_tautulli_connection() {
function set_tautulli_last(back) {
if(!back) {
plex_wrapped_root = document.getElementById('plex_wrapped_root').value;
wrapped_start = new Date(document.getElementById('wrapped_start').value);
wrapped_end = new Date(document.getElementById('wrapped_end').value);
if(wrapped_end < wrapped_start) {
@ -414,6 +448,7 @@ function set_tautulli_last(back) {
return;
}
stats_intro = document.getElementById('stats_intro').value;
create_share_links = document.getElementById('create_share_links').checked;
get_user_movie_stats = document.getElementById('get_user_movie_stats').checked;
get_user_show_stats = document.getElementById('get_user_show_stats').checked;
get_user_show_buddy = document.getElementById('get_user_show_buddy').checked;
@ -428,3 +463,26 @@ function set_tautulli_last(back) {
set_config();
}
}
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;
}

View file

@ -52,7 +52,7 @@
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../">Wrapped</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../admin">Admin</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../caching">Caching</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub</a>
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" id="github_link" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub</a>
</div>
<div class="content" id="search_results">
@ -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("../");
});

96
api/create_link.php Normal file
View file

@ -0,0 +1,96 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
require(dirname(__FILE__) . '/objects/link.php');
// Create variables
$auth = new Auth();
$config = new Config();
$link = new Link();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data)) {
// Log use
$log->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);
?>

View file

@ -1,59 +1,61 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$config = new Config();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
$path = "../config/config.json";
if(!file_exists($path)) {
fopen($path, "w");
}
$config = json_decode(file_get_contents($path));
// If POST data is empty
if(empty($data)) {
// Log use
$log->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) {
// Check if confgiured
if(!$config->is_configured()) {
// Log API request if enabled
if($config->use_logs) {
if(!log_activity()) {
echo json_encode(array("message" => "Failed to log event.", "error" => true));
// 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;
}
?>

View file

@ -1,67 +1,78 @@
<?php
$data = json_decode(file_get_contents("php://input"));
$config = json_decode(file_get_contents("../config/config.json"));
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Create variables
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data)) {
// Log use
$log->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';
// Attempt to call Tautulli API
try {
if($ssl) {
@$response = json_decode(file_get_contents($url, false, stream_context_create($arrContextOptions)));
} else {
@$response = json_decode(file_get_contents($url));
// 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);
}
if(!isset($response)) {
throw new Exception('Could not reach Tautulli.');
// 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;
}
?>

View file

@ -1,18 +1,26 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$config = new Config();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
$path = "../config/config.json";
if(!file_exists($path)) {
fopen($path, "w");
}
$config = json_decode(file_get_contents("../config/config.json"));
// Log use
$log->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);
}
$functions = array("get_user_movie_stats" => $config->get_user_movie_stats,
// 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,
@ -20,37 +28,11 @@ $functions = array("get_user_movie_stats" => $config->get_user_movie_stats,
"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
"stats_intro" => $config->stats_intro,
"create_share_links" => $config->create_share_links
);
// Log API request if enabled
if($config->use_logs) {
if(!log_activity()) {
echo json_encode(array("message" => "Failed to log event.", "error" => true));
// Encode JSON and print it
echo json_encode($functions_json);
exit(0);
}
}
echo json_encode($functions);
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;
}
?>

130
api/get_link.php Normal file
View file

@ -0,0 +1,130 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
require(dirname(__FILE__) . '/objects/link.php');
// Create variables
$auth = new Auth();
$config = new Config();
$link = new Link();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data) || !isset($data->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);
?>

42
api/get_login_cookie.php Normal file
View file

@ -0,0 +1,42 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$auth = new Auth();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty or wrong
if(empty($data) || !isset($data->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);
?>

54
api/get_login_url.php Normal file
View file

@ -0,0 +1,54 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/log.php');
require(dirname(__FILE__) . '/objects/config.php');
$data = json_decode(file_get_contents("php://input"));
// Create variables
$auth = new Auth();
$log = new Log();
$config = new Config();
// Check if configured
if(!$config->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);
?>

View file

@ -0,0 +1,29 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$config = new Config();
$log = new Log();
// Log use
$log->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);
?>

View file

@ -1,37 +1,48 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
require(dirname(__FILE__) . '/objects/cache.php');
// Create variables
$config = new Config();
$auth = new Auth();
$log = new Log();
$cache = new Cache();
$data = json_decode(file_get_contents("php://input"));
$path = "../config/config.json";
if(!file_exists($path)) {
fopen($path, "w");
}
$config = json_decode(file_get_contents("../config/config.json"));
// Check if configured
if(!$config->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);
// 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"]));
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 activity
$log->log_activity('get_stats.php', 'unknown', 'Starting caching mode.');
caching_mode($cache_limit);
}
// Log checking cache
log_activity($id, "Starting cache loop");
// Test Tautulli connection
if(!tautulli_test_connection()) {
// Log activity
$log->log_activity('get_stats.php', 'unknown', 'Tautulli connection test was not successful.');
// Log checking cache
log_activity($id, "Checking data-cache");
echo json_encode(array("message" => "Tautulli connection test was not successful.", "error" => true));
exit(0);
// GET WRAPPED DATES CACHE
if($config->use_cache) {
if($cache = check_cache()) {
$tautulli_data = $cache;
} else {
$tautulli_data = array();
}
} else {
$tautulli_data = array();
// Log activity
$log->log_activity('get_stats.php', 'unknown', 'Tautulli connection test was successful.');
}
// Log refresh cache
log_activity($id, "Refreshing data-cache of missing/incomplete days, maximum " . $cache_limit . " days");
// Get Plex Token
$token_object = json_decode($auth->validate_token($cookie));
// 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"];
// Validate Plex ID
if(empty($token_object) || !isset($token_object->data->id)) {
// Log updating cache
log_activity($id, "Saving data-cache");
// Log use
$log->log_activity('get_stats.php', 'unknown', 'Plex Token from cookie not valid.');
// 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));
echo json_encode(array("error" => true, "message" => "Login not accepted. Log in again."));
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));
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 user found
log_activity($id, "User found");
// 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;
global $log;
$path = "../config/cache.json";
if(!file_exists($path)) {
fopen($path, "w");
}
$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);
// 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.');
}
$log_file = @fopen($path, 'a');
@fwrite($log_file, PHP_EOL . $date . ' - get_stats.php - ID: ' . $id . ' - ' . $message);
// Closing curl
curl_close($ch);
if(@fclose($log_file)) {
return True;
// Decode the JSON response
$result = json_decode($result, true);
if($result) {
return $result;
} else {
throw new Exception('Tautulli API call was not successful.');
}
} catch(Error $e) {
http_response_code(500);
echo json_encode(array("error" => True, "message" => "Failed to log event."));
exit();
}
// 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);
}
return True;
}
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,19 +556,29 @@ 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;
// Clean array to populate with results
$temp_clean = array();
if($config->ssl) {
$response = json_decode(file_get_contents($url, false, stream_context_create($arrContextOptions)), True);
// 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 {
$response = json_decode(file_get_contents($url), True);
$library_str = '&section_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"];
$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"]);
@ -476,6 +586,8 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) {
}
}
}
// 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;
@ -495,6 +607,7 @@ function tautulli_get_wrapped_dates($id, $array, $loop_interval) {
$complete_date_loop = False;
break;
}
}
// Sort data by date
@ -504,7 +617,7 @@ 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);
}
@ -517,6 +630,7 @@ function data_get_user_stats_loop($id, $array) {
// 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);
}
?>

226
api/objects/auth.php Normal file
View file

@ -0,0 +1,226 @@
<?php
class Auth {
// Object properties
private $client_id;
private $token_encrypter;
private $strong = true;
private $header = 'application/json';
private $x_plex_product = 'Plex-Wrapped';
const METHOD = 'aes-256-ctr';
// Constructor
public function __construct(){
// Get variables from config file
require_once(dirname(__FILE__) . '/config.php');
$config_file = new Config();
$this->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);
}
}
}

61
api/objects/cache.php Normal file
View file

@ -0,0 +1,61 @@
<?php
class Cache {
// Object properties
// Cache path
private $path;
// Constructor
public function __construct(){
// Delcare cache path
$this->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;
}
}

207
api/objects/config.php Normal file
View file

@ -0,0 +1,207 @@
<?php
class Config {
// Object properties
// Config path
private $path;
// Tautulli
public $tautulli_apikey;
public $tautulli_ip;
public $tautulli_port;
public $tautulli_length;
public $tautulli_root;
public $tautulli_libraries;
public $ssl;
// Admin user
public $password;
public $username;
// Plex-Wrapped config
public $plex_wrapped_version = 'v2.1.0';
public $timezone;
public $use_cache;
public $use_logs;
public $client_id;
public $plex_wrapped_root;
public $token_encrypter;
// Plex Wrapped custom
public $wrapped_start;
public $wrapped_end;
public $stats_intro;
public $create_share_links;
public $get_user_movie_stats;
public $get_user_show_stats;
public $get_user_show_buddy;
public $get_user_music_stats;
public $get_year_stats_movies;
public $get_year_stats_shows;
public $get_year_stats_music;
public $get_year_stats_leaderboard;
// Constructor
public function __construct(){
// Declare config path
$this->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;
}
}
}
?>

63
api/objects/link.php Normal file
View file

@ -0,0 +1,63 @@
<?php
class Link {
// Object properties
// Cache path
private $path;
// Constructor
public function __construct(){
// Delcare cache path
$this->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;
}
}

55
api/objects/log.php Normal file
View file

@ -0,0 +1,55 @@
<?php
class Log {
// Object properties
// Log path
private $path;
// Log enabled
public $use_logs;
// Constructor
public function __construct(){
// Delcare log path
$this->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;
}
}
?>

View file

@ -1,90 +1,84 @@
<?php
$path = "../config/config.json";
$path2 = "../config/cache.json";
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/config.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$config = new Config();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
if(!file_exists($path)) {
fopen($path, "w");
}
$config = json_decode(file_get_contents($path));
// If POST data is empty
if(empty($data)) {
// Log use
$log->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.');
save_config();
exit(0);
// Call save function
save_config($data->data, $data->clear_cache);
} else if($config->verify_wrapped_admin($username, $password)) {
// 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;
}
?>

View file

@ -0,0 +1,52 @@
<?php
// Required headers
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Files needed to use objects
require(dirname(__FILE__) . '/objects/auth.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$auth = new Auth();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty or wrong
if(empty($data) || !isset($data->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);
?>

1
assets/close.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 4.9902344 3.9902344 A 1.0001 1.0001 0 0 0 4.2929688 5.7070312 L 10.585938 12 L 4.2929688 18.292969 A 1.0001 1.0001 0 1 0 5.7070312 19.707031 L 12 13.414062 L 18.292969 19.707031 A 1.0001 1.0001 0 1 0 19.707031 18.292969 L 13.414062 12 L 19.707031 5.7070312 A 1.0001 1.0001 0 0 0 18.980469 3.9902344 A 1.0001 1.0001 0 0 0 18.292969 4.2929688 L 12 10.585938 L 5.7070312 4.2929688 A 1.0001 1.0001 0 0 0 4.9902344 3.9902344 z"/></svg>

After

Width:  |  Height:  |  Size: 544 B

1
assets/config.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 16.984375 0.98632812 A 1.0001 1.0001 0 0 0 16 2 L 16 3.1230469 C 15.238847 3.3010568 14.592763 3.6769133 14.109375 4.1777344 L 13.169922 3.6347656 A 1.0001 1.0001 0 0 0 12.595703 3.4941406 A 1.0001 1.0001 0 0 0 12.169922 5.3652344 L 13.148438 5.9296875 C 13.058779 6.2815613 13 6.6401219 13 7 C 13 7.3598781 13.058779 7.7184387 13.148438 8.0703125 L 12.169922 8.6347656 A 1.0001 1.0001 0 1 0 13.169922 10.365234 L 14.109375 9.8222656 C 14.592763 10.323087 15.238847 10.698943 16 10.876953 L 16 12 A 1.0001 1.0001 0 1 0 18 12 L 18 10.876953 C 18.761153 10.698943 19.407237 10.323087 19.890625 9.8222656 L 20.830078 10.365234 A 1.0001 1.0001 0 1 0 21.830078 8.6347656 L 20.851562 8.0703125 C 20.941221 7.7184387 21 7.3598781 21 7 C 21 6.6401219 20.941221 6.2815613 20.851562 5.9296875 L 21.830078 5.3652344 A 1.0001 1.0001 0 0 0 21.375 3.4941406 A 1.0001 1.0001 0 0 0 20.830078 3.6347656 L 19.890625 4.1777344 C 19.407237 3.6769133 18.761153 3.3010568 18 3.1230469 L 18 2 A 1.0001 1.0001 0 0 0 16.984375 0.98632812 z M 16.902344 5.0078125 A 1.0001 1.0001 0 0 0 17.101562 5.0078125 C 17.784418 5.0308603 18.201113 5.263117 18.501953 5.6015625 C 18.532495 5.6359227 18.556001 5.6776743 18.583984 5.7148438 A 1.0001 1.0001 0 0 0 18.869141 6.2363281 A 1.0001 1.0001 0 0 0 18.869141 6.2382812 C 18.95366 6.4768975 19 6.7367576 19 7 C 19 7.2888633 18.940948 7.5710174 18.839844 7.828125 A 1.0001 1.0001 0 0 0 18.621094 8.2304688 C 18.581714 8.287018 18.546923 8.3478458 18.501953 8.3984375 C 18.200202 8.7379074 17.782297 8.9703776 17.095703 8.9921875 A 1.0001 1.0001 0 0 0 16.984375 8.9863281 A 1.0001 1.0001 0 0 0 16.898438 8.9921875 C 16.215582 8.9691397 15.798887 8.736883 15.498047 8.3984375 C 15.451291 8.3458375 15.413753 8.2836315 15.373047 8.2246094 A 1.0001 1.0001 0 0 0 15.164062 7.8378906 C 15.060447 7.5782094 15 7.2925688 15 7 C 15 6.7367576 15.04634 6.4768975 15.130859 6.2382812 A 1.0001 1.0001 0 0 0 15.130859 6.2363281 A 1.0001 1.0001 0 0 0 15.416016 5.7148438 C 15.443999 5.6776743 15.467505 5.6359227 15.498047 5.6015625 C 15.799494 5.2624341 16.216997 5.0300361 16.902344 5.0078125 z M 7.984375 8.9863281 A 1.0001 1.0001 0 0 0 7 10 L 7 11.09375 C 5.8639179 11.298221 4.9151951 11.83979 4.2519531 12.585938 C 4.2330732 12.607177 4.2215633 12.632784 4.203125 12.654297 L 3.3046875 12.134766 A 1.0001 1.0001 0 0 0 2.7304688 11.994141 A 1.0001 1.0001 0 0 0 2.3046875 13.865234 L 3.25 14.412109 C 3.0913676 14.928761 3 15.463787 3 16 C 3 16.536213 3.0913676 17.071239 3.25 17.587891 L 2.3046875 18.134766 A 1.0001 1.0001 0 1 0 3.3046875 19.865234 L 4.203125 19.345703 C 4.2215633 19.367216 4.2330732 19.392823 4.2519531 19.414062 C 4.9151951 20.16021 5.8639179 20.701779 7 20.90625 L 7 22 A 1.0001 1.0001 0 1 0 9 22 L 9 20.90625 C 10.136082 20.701779 11.084805 20.16021 11.748047 19.414062 C 11.766927 19.392823 11.778437 19.367216 11.796875 19.345703 L 12.695312 19.865234 A 1.0001 1.0001 0 1 0 13.695312 18.134766 L 12.75 17.587891 C 12.908632 17.071239 13 16.536213 13 16 C 13 15.463787 12.908632 14.928761 12.75 14.412109 L 13.695312 13.865234 A 1.0001 1.0001 0 0 0 13.240234 11.994141 A 1.0001 1.0001 0 0 0 12.695312 12.134766 L 11.796875 12.654297 C 11.778437 12.632784 11.766927 12.607177 11.748047 12.585938 C 11.084805 11.83979 10.136082 11.298221 9 11.09375 L 9 10 A 1.0001 1.0001 0 0 0 7.984375 8.9863281 z M 7.9042969 13.007812 A 1.0001 1.0001 0 0 0 8.0976562 13.009766 C 9.1200164 13.033196 9.7845173 13.388197 10.251953 13.914062 C 10.325032 13.996277 10.382391 14.092015 10.445312 14.183594 A 1.0001 1.0001 0 0 0 10.753906 14.753906 C 10.909898 15.142709 11 15.567192 11 16 C 11 16.455832 10.899125 16.901726 10.726562 17.306641 A 1.0001 1.0001 0 0 0 10.478516 17.769531 C 10.406799 17.8787 10.337687 17.989487 10.251953 18.085938 C 9.7838563 18.612547 9.1185934 18.969687 8.09375 18.992188 A 1.0001 1.0001 0 0 0 7.984375 18.986328 A 1.0001 1.0001 0 0 0 7.9023438 18.990234 C 6.8799836 18.966804 6.2154828 18.611803 5.7480469 18.085938 C 5.661112 17.988136 5.5900963 17.876487 5.5175781 17.765625 A 1.0001 1.0001 0 0 0 5.2773438 17.314453 C 5.1025454 16.907472 5 16.458857 5 16 C 5 15.568705 5.0892102 15.145534 5.2441406 14.757812 A 1.0001 1.0001 0 0 0 5.5546875 14.181641 C 5.6173575 14.090541 5.6753172 13.995884 5.7480469 13.914062 C 6.2158275 13.387809 6.8806423 13.030758 7.9042969 13.007812 z M 8 15 A 1 1 0 0 0 8 17 A 1 1 0 0 0 8 15 z"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -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;
}
@ -376,6 +379,12 @@ form {
background-color: #ffbd55;
border: none;
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;
}

1
assets/done.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 19.980469 5.9902344 A 1.0001 1.0001 0 0 0 19.292969 6.2929688 L 9 16.585938 L 5.7070312 13.292969 A 1.0001 1.0001 0 1 0 4.2929688 14.707031 L 8.2929688 18.707031 A 1.0001 1.0001 0 0 0 9.7070312 18.707031 L 20.707031 7.7070312 A 1.0001 1.0001 0 0 0 19.980469 5.9902344 z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

1
assets/external-link.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 19.980469 2.9902344 A 1.0001 1.0001 0 0 0 19.869141 3 L 15 3 A 1.0001 1.0001 0 1 0 15 5 L 17.585938 5 L 8.2929688 14.292969 A 1.0001 1.0001 0 1 0 9.7070312 15.707031 L 19 6.4140625 L 19 9 A 1.0001 1.0001 0 1 0 21 9 L 21 4.1269531 A 1.0001 1.0001 0 0 0 19.980469 2.9902344 z M 5 3 C 3.9069372 3 3 3.9069372 3 5 L 3 19 C 3 20.093063 3.9069372 21 5 21 L 19 21 C 20.093063 21 21 20.093063 21 19 L 21 13 A 1.0001 1.0001 0 1 0 19 13 L 19 19 L 5 19 L 5 5 L 11 5 A 1.0001 1.0001 0 1 0 11 3 L 5 3 z"/></svg>

After

Width:  |  Height:  |  Size: 612 B

1
assets/for-you.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0"?><svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"> <path d="M19,3H5C3.897,3,3,3.897,3,5v14c0,1.103,0.897,2,2,2h14c1.103,0,2-0.897,2-2V5C21,3.897,20.103,3,19,3z M6.277,7.578 c0.278-0.367,0.699-0.596,1.158-0.577C8.121,7.029,8.5,7.522,8.5,7.522s0.379-0.493,1.064-0.521c0.46-0.019,0.881,0.21,1.158,0.577 c0.966,1.276-0.863,2.769-1.293,3.17c-0.257,0.24-0.575,0.525-0.764,0.693c-0.095,0.085-0.236,0.085-0.331,0 c-0.19-0.169-0.507-0.454-0.764-0.693C7.14,10.347,5.311,8.854,6.277,7.578z M17,17H7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1 h10c0.552,0,1,0.448,1,1v0C18,16.552,17.552,17,17,17z M17,13h-4c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h4c0.552,0,1,0.448,1,1 v0C18,12.552,17.552,13,17,13z M17,9h-2c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h2c0.552,0,1,0.448,1,1v0 C18,8.552,17.552,9,17,9z"/></svg>

After

Width:  |  Height:  |  Size: 875 B

1
assets/restart.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"><path d="M 3.5 2 C 3.372 2 3.2444844 2.0494844 3.1464844 2.1464844 C 2.9514844 2.3414844 2.9514844 2.6585156 3.1464844 2.8535156 L 5.09375 4.8007812 C 3.1950225 6.6199194 2 9.1685121 2 12 C 2 17.511334 6.4886661 22 12 22 C 17.511334 22 22 17.511334 22 12 C 22 6.864114 18.106486 2.6175896 13.109375 2.0644531 A 1.0001 1.0001 0 0 0 13.009766 2.0585938 A 1.0001 1.0001 0 0 0 12.890625 4.0527344 C 16.891514 4.4955979 20 7.871886 20 12 C 20 16.430666 16.430666 20 12 20 C 7.5693339 20 4 16.430666 4 12 C 4 9.7105359 4.967513 7.6643975 6.5039062 6.2109375 L 8.1464844 7.8535156 C 8.3414844 8.0485156 8.6585156 8.0485156 8.8535156 7.8535156 C 8.9515156 7.7565156 9 7.628 9 7.5 L 9 3 A 1 1 0 0 0 8 2 L 3.5 2 z"/></svg>

After

Width:  |  Height:  |  Size: 815 B

1
assets/share.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0"?><svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"> <path d="M 19 3 C 17.346 3 16 4.346 16 6 C 16 6.4617584 16.113553 6.8939944 16.300781 7.2851562 L 12.585938 11 L 7.8164062 11 C 7.4021391 9.8387486 6.3016094 9 5 9 C 3.346 9 2 10.346 2 12 C 2 13.654 3.346 15 5 15 C 6.3016094 15 7.4021391 14.161251 7.8164062 13 L 12.585938 13 L 16.300781 16.714844 C 16.113553 17.106006 16 17.538242 16 18 C 16 19.654 17.346 21 19 21 C 20.654 21 22 19.654 22 18 C 22 16.346 20.654 15 19 15 C 18.538242 15 18.106006 15.113553 17.714844 15.300781 L 14.414062 12 L 17.714844 8.6992188 C 18.106006 8.8864466 18.538242 9 19 9 C 20.654 9 22 7.654 22 6 C 22 4.346 20.654 3 19 3 z"/></svg>

After

Width:  |  Height:  |  Size: 742 B

1
assets/synchronize.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0"?><svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96px" height="96px"> <path d="M 11 0 C 10.74 0 10.483969 0.10196875 10.292969 0.29296875 L 7.2929688 3.2929688 C 6.9019688 3.6839688 6.9019687 4.3170313 7.2929688 4.7070312 L 10.292969 7.7070312 C 10.578969 7.9930312 11.007812 8.0778281 11.382812 7.9238281 C 11.756813 7.7688281 12 7.405 12 7 L 12 5 C 15.877484 5 19 8.1225161 19 12 C 19 13.025799 18.774981 13.99479 18.376953 14.876953 A 1.0001 1.0001 0 1 0 20.199219 15.699219 C 20.707191 14.573382 21 13.320201 21 12 C 21 7.0414839 16.958516 3 12 3 L 12 1 C 12 0.596 11.756812 0.23117187 11.382812 0.076171875 C 11.258813 0.025171875 11.129 0 11 0 z M 4.7265625 7.6992188 A 1.0001 1.0001 0 0 0 3.8007812 8.3007812 C 3.2928092 9.426618 3 10.679799 3 12 C 3 16.958516 7.0414839 21 12 21 L 12 23 C 12 23.404 12.243188 23.768828 12.617188 23.923828 C 12.741187 23.974828 12.871 24 13 24 C 13.26 24 13.516031 23.898031 13.707031 23.707031 L 16.707031 20.707031 C 17.098031 20.316031 17.098031 19.683969 16.707031 19.292969 L 13.707031 16.292969 C 13.421031 16.006969 12.992188 15.922172 12.617188 16.076172 C 12.243187 16.231172 12 16.596 12 17 L 12 19 C 8.1225161 19 5 15.877484 5 12 C 5 10.974201 5.225019 10.00521 5.6230469 9.1230469 A 1.0001 1.0001 0 0 0 4.7265625 7.6992188 z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -6,13 +6,19 @@ function search_box() {
<form id='stats_form' class='form' onsubmit='return false' action="" method="post">
<p style="font-size:1em;">
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.
<br>
<br>
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.
<br>
<br>
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).
</p>
<div class='form-group'>
<label for="days" title="">Maximum days per API call</label>
<input type="number" class='form-control' name="days" id="days" minlenght='1' value='100' autocomplete="off" required />
<label for="days" title="">Days to cache:</label>
<input type="number" class='form-control' name="days" id="days" minlenght='1' value='50' autocomplete="off" required />
</div>
@ -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 = '<form id="password_login_form" onsubmit="get_config();return false">'
html += '<div class="form-group">';
html += '<label for="username" title="The username chosen during first-time setup.">Username</label>';
html += '<label for="username" title="The administrator username chosen during first-time setup.">Username:</label>';
html += '<input type="text" class="form-control" id="username" value="" autocomplete="on" minlength=4 required />';
html += '</div>';
html += '<div class="form-group">';
html += '<label for="password" title="The password chosen during first-time setup.">Password</label>';
html += '<label for="password" title="The password chosen for the administrator during first-time setup.">Password:</label>';
html += '<input type="password" class="form-control" id="password" value="" autocomplete="off" required />';
html += '</div>';
@ -198,3 +203,26 @@ function login_menu() {
html += '</form>';
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;
}

View file

@ -31,7 +31,7 @@
<div class="stats_tekst" style="height: auto;">
<p>
If you want to decrease loadtime for your users you can pre-cache results.
Caching decreases loadtime for users and reduces PHP induced errors.
<br><br><a href="../admin">Remember to configure the system first.</a>
</p>
</div>
@ -59,16 +59,19 @@
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../">Wrapped</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../admin">Admin</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../caching">Caching</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub</a>
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub (v2.1.0)</a>
</div>
<script type="text/javascript">
var root = "../";
var cookie;
$(document).ready(function() {
get_plex_wrapped_version();
get_config_cache();
});

View file

@ -2,5 +2,5 @@ FROM php:7.4-apache
RUN apt-get update && apt-get install -y \
git
RUN rm -d -r /var/www/html
RUN git clone https://github.com/aunefyren/plex-wrapped /var/www/html
RUN git clone https://github.com/aunefyren/plex-wrapped --branch v2.1.0 /var/www/html
RUN chmod -R 0777 /var/www/html/config

View file

@ -11,12 +11,14 @@ function get_config_initial() {
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var result = JSON.parse(this.responseText);
if(result.password) {
login_menu();
} else {
if(result.error) {
if(!result.password) {
first_time = true;
set_password();
} else {
login_menu();
}
}
}
};
@ -51,6 +53,7 @@ function get_config() {
tautulli_port = result.data.tautulli_port;
tautulli_length = result.data.tautulli_length;
tautulli_root = result.data.tautulli_root;
tautulli_libraries = result.data.tautulli_libraries;
ssl = result.data.ssl;
@ -66,7 +69,7 @@ function get_config() {
wrapped_end.setUTCSeconds(result.data.wrapped_end);
stats_intro = result.data.stats_intro;
create_share_links = result.data.create_share_links;
get_user_movie_stats = result.data.get_user_movie_stats;
get_user_show_stats = result.data.get_user_show_stats;
get_user_show_buddy = result.data.get_user_show_buddy;
@ -80,6 +83,9 @@ function get_config() {
use_cache = result.data.use_cache;
use_logs = result.data.use_logs;
clientID = result.data.clientID;
plex_wrapped_root = result.data.plex_wrapped_root;
set_tautulli(true);
}
}

View file

@ -9,14 +9,18 @@ function get_functions() {
if (this.readyState == 4 && this.status == 200) {
var result = JSON.parse(this.responseText);
if(result.error) {
if(result.message == 'Plex Wrapped is not configured.') {
document.getElementById('results_error').innerHTML = '<a href="./admin"><p style="color:inherit; text-shadow: none;">' + result.message + '</p></a>';
} else {
document.getElementById('results_error').innerHTML = '<p style="color:inherit; text-shadow: none;">' + result.message + '</p>';
}
document.getElementById("search_wrapped_button").disabled = false;
document.getElementById("search_wrapped_button").style.opacity = '1';
document.getElementById("plex_signout_button").disabled = false;
document.getElementById("plex_signout_button").style.opacity = '1';
document.getElementById('results_error').innerHTML = result.message;
} else {
functions = result;
if(!link_mode) {
get_stats();
} else {
load_page(results);
}
}
}
};

View file

@ -1,12 +1,11 @@
var loaded = false;
function get_stats() {
var results;
var functions;
var loading_icon = document.getElementById("loading_icon");
var p_identity = document.getElementById("p_identity").value;
stats_form = {
"p_identity" : p_identity.trim(),
"cookie" : cookie,
"caching" : false
};
@ -17,20 +16,27 @@ function get_stats() {
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 = '<p style="color:inherit; text-shadow: none;">' + "API response can't be parsed." + '</p>';
console.log('Error: ' + error);
console.log(this.responseText);
loading_icon.style.display = "none";
document.getElementById("search_wrapped_button").disabled = false;
document.getElementById("search_wrapped_button").style.opacity = '1';
document.getElementById("plex_signout_button").disabled = false;
document.getElementById("plex_signout_button").style.opacity = '1';
document.getElementById('results_error').innerHTML = "API response can't be parsed.";
}
if(result.error) {
loading_icon.style.display = "none";
search_button("SEARCH");
document.getElementById('results_error').innerHTML = '<p style="color:inherit; text-shadow: none;">' + result.message + '</p>';
document.getElementById("search_wrapped_button").disabled = false;
document.getElementById("search_wrapped_button").style.opacity = '1';
document.getElementById("plex_signout_button").disabled = false;
document.getElementById("plex_signout_button").style.opacity = '1';
document.getElementById('results_error').innerHTML = result.message;
} else {
load_page(this.responseText);
results = result;
load_page(results);
}
}
};
@ -42,7 +48,12 @@ function get_stats() {
}
function load_page(data){
results = JSON.parse(data);
// Remove snow
document.getElementById('snowflakes').style.display = 'none';
// Enable changing background colors JS
loaded = true;
if(results.error) {
$('#results_error').html(results.message);
@ -51,6 +62,7 @@ function load_page(data){
return
}
// Find HTML elements and hide them
var search_box = document.getElementById("search_input");
var login_content = document.getElementById("login_content");
var footer = document.getElementById("footer");
@ -58,8 +70,13 @@ function load_page(data){
login_content.style.display = "none";
footer.style.display = "none";
// Set body background color to introduction
document.getElementById("body").classList.add('color-pink');
// Load the introduction
load_introduction();
// Load the segments based on configuration
if(!results.user.user_movies.error && functions.get_user_movie_stats) {
load_movies();
}
@ -76,12 +93,14 @@ function load_page(data){
load_users();
}
// Load the outro
load_outro();
}
//INTRODUCTION
function load_introduction() {
var text = "";
text += "<div class='boks' style='width: 100%; padding-bottom: 15em; padding-top: 15em; height:auto; background-color:#D2A3A4;'>";
text += "<div class='boks' data-color='pink' style='width: 100%; padding-bottom: 15em; padding-top: 15em; height:auto; background-color:none'>";
text += "<div class='boks3'>";
@ -109,7 +128,7 @@ function load_movies() {
if(results.user.user_movies.data.movie_plays > 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#B9A3D2;'>";
text += "<div class='boks' data-color='purple' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color: none;'>";
text += "<div class='boks3'>";
text += "<h1>Movies!</h1>";
@ -152,7 +171,7 @@ function load_movies() {
} else if(results.user.user_movies.data.movie_plays == 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#B9A3D2;'>";
text += "<div class='boks' data-color='purple' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<h1>Movies!</h1>";
@ -184,7 +203,7 @@ function load_movies() {
} else {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#B9A3D2;'>";
text += "<div class='boks' data-color='purple' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<div class='boks2'>";
@ -207,7 +226,7 @@ function load_shows() {
var text = "";
if(results.user.user_shows.data.show_plays > 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#BBD2A3;'>";
text += "<div class='boks' data-color='green' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<h1>Shows!</h1>";
@ -245,7 +264,7 @@ function load_shows() {
} else if(results.user.user_shows.data.show_plays == 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#BBD2A3;'>";
text += "<div class='boks' data-color='green' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<h1>Shows!</h1>";
@ -275,7 +294,7 @@ function load_shows() {
} else {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#BBD2A3;'>";
text += "<div class='boks' data-color='green' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<div class='boks2'>";
@ -299,7 +318,7 @@ function load_music() {
var text = "";
if(results.user.user_music.data.track_plays > 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#CFA38C;'>";
text += "<div class='boks' data-color='brown' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<h1>Music!</h1>";
@ -339,7 +358,7 @@ function load_music() {
} else if(results.user.user_music.data.track_plays == 1) {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#CFA38C;'>";
text += "<div class='boks' data-color='brown' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<h1>Music!</h1>";
@ -356,7 +375,7 @@ function load_music() {
} else {
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:#CFA38C;'>";
text += "<div class='boks' data-color='brown' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<div class='boks2'>";
@ -512,14 +531,18 @@ function load_longest_episode(array) {
function you_spent(time, category, verb) {
var html = "";
var time = seconds_to_time(time, false);
var time_str = seconds_to_time(time, false);
html += "<div class='status' id='list3' style='padding:1em;min-width:15em;'>";
html += "<div class='stats'>";
html += "You spent <b>" + time + "</b>";
html += "You spent <b>" + time_str + "</b>";
html += " " + verb + " ";
html += category;
if(category == 'music') {
if(time > 3600) {
var time_min = Math.floor(time / 60);
html += '<br><br>That\'s ' + time_min + ' minutes!';
}
html += '<br><img src="assets/img/music.svg" style="margin: auto; display: block; width: 15em;">';
} else {
html += '<br><img src="assets/img/watching-tv.svg" style="margin: auto; display: block; width: 15em;">';
@ -619,7 +642,7 @@ function top_list_names(array, title) {
function load_users() {
var text = "";
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color: #a2d1d0;'>";
text += "<div class='boks' data-color='blue' style='height: auto !important; width: 100%; padding-bottom: 25em; padding-top: 25em; height:10em; background-color: none;'>";
text += "<h1>Server-wide statistics!</h1>";
text += "<br><br><br><br><h2>It's okay to feel shame if you are on the list.</h2><p>(or missing from it...)</p>"
text += "<br><br>";
@ -710,20 +733,96 @@ function load_users() {
function load_outro() {
var text = "";
text += "<div class='boks' style='height: auto !important; width: 100%; padding-bottom: 15em; padding-top: 15em; height:10em; background-color:#39393A;'>";
text += "<div class='boks' data-color='black' style='height: auto !important; width: 100%; padding-bottom: 15em; padding-top: 15em; height:10em; background-color:none;'>";
text += "<div class='boks3'>";
text += "<div class='boks2'>";
text += '<img src="assets/img/new-years.svg" style="width:100%; ">';
text += "</div>";
text += "<div class='boks2' style='margin-top:5em;'>";
text += "<h1>Hope you are staying safe!</h1><br><br><h4>Goodbye.</h4>";
text += "</div>";
text += "</div>";
text += "<div class='boks3'>";
text += "<div class='boks2' style='margin-top:5em;'>";
if(!link_mode && functions.create_share_links) {
text += "<div class='form-group' id='share_wrapped_div' style=''>";
text += "<button class='form-control btn' name='share_wrapped_button' id='share_wrapped_button' onclick='create_wrapped_link()'>";
text += "<img src='assets/share.svg' class='btn_logo'>";
text += "<p2 id='share_wrapped_button_text'>Share wrapped page</p2>";
text += "</button>";
text += "<div class='form-group' id='share_wrapped_results_div' style='display: none; margin: 0.5em 0;'>";
text += "<div><p>This URL is valid for 7 days:</p></div>";
text += "<div id='share_wrapped_results_url' style='padding: 0.25em; background-color: white; border-radius: 0.25em; overflow: auto;'></div>";
text += "</div>";
text += "</div>";
}
var url_home = window.location.href.split('?')[0];
text += "<div class='form-group' id='return_home_div' style=''>";
text += `<button class='form-control btn' name='return_home_button' id='return_home_button' onclick='window.location.href = "` + url_home + `";'>`;
text += "<img src='assets/restart.svg' class='btn_logo'>";
text += "<p2 id='return_home_text'>Return</p2>";
text += "</button>";
text += "</div>";
text += "</div>";
text += "</div>";
text += "</div>";
document.getElementById("search_results").innerHTML += text;
}
function create_wrapped_link() {
document.getElementById("share_wrapped_button").disabled = true;
document.getElementById("share_wrapped_button").style.opacity = '0.5';
wrapped_form = {
"cookie" : get_cookie('plex-wrapped'),
"data" : results
};
var wrapped_data = JSON.stringify(wrapped_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) {
alert("API response can't be parsed.");
console.log('Error: ' + error);
console.log(this.responseText);
document.getElementById("share_wrapped_button").disabled = false;
document.getElementById("share_wrapped_button").style.opacity = '1';
}
if(result.error) {
alert(result.message);
} else {
document.getElementById('share_wrapped_results_url').innerHTML = '<span style="white-space: nowrap;">' + window.location.href.split('?')[0] + result.url + '</span>';
document.getElementById('share_wrapped_results_div').style.display = 'inline-block';
document.getElementById("share_wrapped_button").disabled = false;
document.getElementById("share_wrapped_button").style.opacity = '1';
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/create_link.php");
xhttp.send(wrapped_data);
}
function play_plays(plays) {
plays = parseInt(plays);
@ -883,3 +982,38 @@ function seconds_to_seconds(seconds) {
return second_string;
}
// Change background color for each category
$(window).scroll(function() {
if(loaded) {
// Select the window, body and elements containing stats
var $window = $(window),
$body = $('body'),
$panel = $('.boks');
// Change 33% earlier than scroll position so colour is there when you arrive
var scroll = $window.scrollTop() + ($window.height() / 3);
$panel.each(function () {
var $this = $(this);
// If position is within range of this panel.
// So position of (position of top of div <= scroll position) && (position of bottom of div > scroll position).
// Remember we set the scroll to 33% earlier in scroll var.
if ($this.position().top <= scroll && $this.position().top + $this.height() > scroll) {
// Remove all classes on body with color-
$body.removeClass(function (index, css) {
return (css.match (/(^|\s)color-\S+/g) || []).join(' ');
});
// Add class of currently active div
$body.addClass('color-' + $(this).data('color'));
}
});
}
}).scroll();

View file

@ -17,7 +17,7 @@
<script src="./get_functions.js"></script>
</head>
<body>
<body id='body'>
<div class="content_landing" id="login_content" style="">
@ -31,7 +31,7 @@
<div class="boks2" style="float: none !important; display: block; margin-top: 0em; padding-top: 0;">
<div class="stats_tekst" style="height: auto;">
<div class="stats_tekst" id='intro_text' style="height: auto;">
<p>
Did you get that thing from Spotify and wondered what your Plex statistics looked like?
<br><br>Well, have a look...
@ -46,31 +46,45 @@
<div class="search">
<form id='stats_form' class='form' onsubmit='return false' action="" method="post">
<div class='form-group'>
<label for="p_identity" title="The email or username for the Plex account you want to check">Plex email or username</label>
<input type="text" class='form-control' name="p_identity" id="p_identity" autocomplete="off" required/>
</div>
<div class='form-group'>
<input type="submit" class='form-control btn' value="Search" name="p_email" id="p_email" required/>
<form id='plex_login_form' class='form' onsubmit='return false' action="" method="post">
<div class="form-group" id='plex_login_div'>
<button class='form-control btn' name="plex_login_button" id="plex_login_button">
<img src='assets/external-link.svg' class='btn_logo'>
<p2 id='plex_login_button_text'>Sign in using Plex</p2>
</button>
</div>
</form>
<form id='search_wrapped_form' class='form' onsubmit='return false' action="" method="post" style="display: none;">
<div class="form-group" id='search_wrapped_div'>
<button class='form-control btn' name="search_wrapped_button" id="search_wrapped_button" style='opacity: 0.5;' disabled>
<img src='assets/done.svg' class='btn_logo'>
<p2 id='plex_login_button_text'>Plex Wrapped</p2>
</button>
</div>
<img id="loading_icon" src="assets/loading.gif" style="border-radius: 25px; background-color: white; padding: 1em;width: 4em; height: 4em; display:none;">
</form>
<div class="form-group" id='sign_out_div' style='display: none;'>
<button class='form-control btn' name="plex_signout_button" id="plex_signout_button" onclick='sign_out()'>
<img src='assets/close.svg' class='btn_logo'>
<p2 id='plex_signout_button_text'>Sign Out</p2>
</button>
</div>
</div>
<img id="loading_icon" src="assets/loading.gif" style="border-radius: 25px; background-color: white; padding: 1em;width: 4em; height: 4em; display:none; margin: 1em 0 0 0;">
<div id="results">
</div>
<div id="results_error" style="padding:0.5em; bottom:0; color: #ffbd55 !important;"></div>
</div>
@ -118,7 +132,7 @@
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="./">Wrapped</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="./admin">Admin</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="./caching">Caching</a> |
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub</a>
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" id="github_link" href="https://github.com/aunefyren/plex-wrapped" target="_blank">GitHub</a>
</div>
</div>
@ -128,9 +142,31 @@
<script type="text/javascript">
var root = "./";
var link_mode = false;
var results;
var functions;
$(document).ready(function() {
var url_string = window.location.href
var url = new URL(url_string);
var hash = url.searchParams.get("hash");
var close_me = url.searchParams.get("close_me");
get_plex_wrapped_version();
if(close_me !== null) {
window.close();
}
if(hash !== null) {
link_mode = true;
wrapped_link_actions(hash);
document.getElementById('intro_text').innerHTML = '';
document.getElementById('stats').innerHTML = '<img id="loading_icon" src="assets/loading.gif" style="border-radius: 25px; background-color: white; padding: 1em;width: 4em; height: 4em; display:block; margin: auto;">';;
} else {
cookie_login_actions();
}
});
</script>

235
index.js
View file

@ -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 = '<p>' + result.message + '</p><img id="bored_image" src="assets/img/bored.svg" style="width: 10em; height: 10em; display:block; margin: 1em auto;">';
} 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 <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' '){
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function validate_cookie(cookie) {
var json_cookie = JSON.stringify({"cookie": cookie});
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
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) {
set_cookie("plex-wrapped", "", 1);
document.getElementById('search_wrapped_form').style.display = 'none';
document.getElementById('sign_out_div').style.display = 'none';
document.getElementById('plex_login_form').style.display = 'block';
} else {
document.getElementById("plex_login_button").disabled = true;
document.getElementById("plex_login_button").style.opacity = '0.5';
document.getElementById("search_wrapped_button").disabled = false;
document.getElementById("search_wrapped_button").style.opacity = '1';
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/validate_login_cookie.php");
xhttp.send(json_cookie);
return;
}
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;
}

View file

@ -12,6 +12,7 @@ function set_config() {
"tautulli_port" : tautulli_port,
"tautulli_length" : tautulli_length,
"tautulli_root" : tautulli_root,
"tautulli_libraries" : tautulli_libraries,
"ssl" : ssl,
"password" : password,
"username" : username,
@ -19,6 +20,7 @@ function set_config() {
"wrapped_start" : Math.round(wrapped_start.getTime() / 1000),
"wrapped_end" : Math.round(wrapped_end.getTime() / 1000),
"stats_intro" : stats_intro,
"create_share_links" : create_share_links,
"get_user_movie_stats" : get_user_movie_stats,
"get_user_show_stats" : get_user_show_stats,
"get_user_show_buddy" : get_user_show_buddy,
@ -28,7 +30,9 @@ function set_config() {
"get_year_stats_music" : get_year_stats_music,
"get_year_stats_leaderboard" : get_year_stats_leaderboard,
"use_cache" : use_cache,
"use_logs" : use_logs
"use_logs" : use_logs,
"client_id" : client_id,
"plex_wrapped_root" : plex_wrapped_root
}
};