Squashed commit of the following:

commit 23a91e9daad1c482435581c69c756257ac703149
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Mon Jul 4 17:47:18 2022 +0200

    Removed tmp text

commit 1c09e583b2fa5e30a36c809e85fc43b328e3ad7f
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun Jul 3 15:33:36 2022 +0200

    Read log in admin interface

commit 4ae253185a33bb13a94b1a200bf3aa92a62fb17d
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Jul 2 00:27:39 2022 +0200

    Added comments

commit 89cabc36e86ae541fa9b001928211ba48e9ba48b
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Jul 2 00:08:47 2022 +0200

    Update index.js

commit 12279e7ed47eeb45454ab525aa745eeaac0da180
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Jul 2 00:02:36 2022 +0200

    Update index.html

commit a1c76b3e4d06cd25f7795390155521f2c341465f
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 23:56:39 2022 +0200

    Errors and frontend

    Cookies are more stable
    Errors are more concise

commit ee170aeb3acf3f586c93e7b142a08bbb32a17134
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 22:33:27 2022 +0200

    Better error handling

commit 6317a1fa15ccfaf91fb2d8737e346621d9884008
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 22:18:45 2022 +0200

    Added returns on errors

commit 7126ddf2fbb824de0906fd6d7bfc2ca8acac0fdf
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 22:09:07 2022 +0200

    Added ASCII Art

commit 1fd45ede34abe6ae9555d9d6f23f87a2e67dba3b
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 21:39:24 2022 +0200

    Form CSS tweak

commit 9861c662cd5f2b66721b3d2f2030d92d84bdf8bc
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 21:24:03 2022 +0200

    Update route_admin_auth.go

commit 119de6b2b1124266aae6cd5fec90fff2d2ddd24f
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 21:19:36 2022 +0200

    Update route_admin_auth.go

commit 87a43dfc4945fe5ce45b8d9d06c7dcd6a5c9ee0c
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 21:17:21 2022 +0200

    Bugs and CSS

    Time zone validation
    Caching log is prettier
    Return bug on certain API endpoints fixed

commit 3e81df71516572944b8e46bad129da82235b1b85
Author: Aune <31650531+aunefyren@users.noreply.github.com>
Date:   Fri Jul 1 18:36:21 2022 +0200

    Create codeql-analysis.yml

commit 32d05f3fff0231139df0e590b98097d6c6e3cdfb
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 18:23:05 2022 +0200

    Update index.js

commit c3fcdee18406e1a9d96b26c85169e1db365bb36a
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 18:15:38 2022 +0200

    Capital W

commit a243d310597b9f8148621690b9662f93b8467e83
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 18:08:53 2022 +0200

    Update index.js

commit 068739fc76b96c4379bf93aa39d11db1c2dfcada
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 17:58:48 2022 +0200

    Update README.md

commit d61074bde69f5dac1a1a2dd6b2164564131f53d1
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 17:56:55 2022 +0200

    Docker-compose mistake

commit 3036905d92d23cb0664559471fa9e0a81565fa64
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 17:55:30 2022 +0200

    README touchups

commit 6f07775121cec5cddf17f25efe349b300d43b8e6
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jul 1 17:48:10 2022 +0200

    Rewrote README

commit 58a26b11c77e6d21be0f433cd22c4da46f5d0450
Author: Aune <oystein.sverre@gmail.com>
Date:   Fri Jul 1 10:00:58 2022 +0200

    Changed release workflow

commit ffeea7ca689f405b6e629411f5bc9fc8c19e6b34
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Jun 30 20:09:01 2022 +0200

    Updated workflow to 1.18

commit aa321e87729673b4ce047f87b68f7223ea02d159
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Jun 30 19:58:44 2022 +0200

    Update README.md

commit 5c4e59474882689f6dadf61f140a4ec18f744a1e
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Jun 30 18:16:30 2022 +0200

    CSS fixes

commit 97f3fc57a389e5dd637b560e48fee86c4163dcd5
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Jun 29 22:00:32 2022 +0200

    Option to disable winter theme

    And loading screen

commit cf804312c7b9b72b416ef2a53947efab79ab604b
Author: Aune <31650531+aunefyren@users.noreply.github.com>
Date:   Tue Jun 28 22:05:43 2022 +0200

    Update README.md

commit 362a15ee1daa2464ad4f27b7e3107a63d736e591
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Jun 28 18:41:19 2022 +0200

    Fixed buddy bug

commit 58f4d0060230c2349634689438088dcdf48d7715
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Mon Jun 27 19:26:37 2022 +0200

    Finalized show buddy

commit 0f89a1df974af9947d9e297f3c662d4082c0403d
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun Jun 26 12:47:37 2022 +0200

    Improved logging

commit b57cb24928c2e213c38ce566478af11937e6e4e3
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Jun 25 13:42:14 2022 +0200

    Finalized shareable links

    Links can be deleted
    Links are retrieved to the front page
    Links can be copied to clipboard

commit 000022b882877bc306e13203578a3d1e184afc9a
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun Jun 12 18:47:34 2022 +0200

    Get share links now functional

commit df697f68b2bcedc7c623d9d9ec291a9625b62160
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun Jun 12 00:29:52 2022 +0200

    Share link formatting bug

commit 3b357ca72767fcbb9742a935f29770ab488ac36f
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun Jun 12 00:26:15 2022 +0200

    Share link creation

    Started implementation
    Missing get and delete functionality
    Added delete cache option to Tautulli settings

commit 54e079951ff131d9177fd0f4a79238c03043d875
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Jun 10 19:13:01 2022 +0200

    Fixed bugs

    Shows with seasons in different years were split on top lists
    Amount of media plays was not unique to media-release, just amount of plays overall
    Fixed display for music minutes
    Logging can now be disabled
    Tautulli libraries were not included in API calls

commit 26508af56a182b9611affb4401e5db494ae27c8b
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Jun 7 20:38:08 2022 +0200

    Fixed bug where top-lists under 10 crashed the API

commit 0f28e39c8d4fe444c8d30caaaee9ca138d469270
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Jun 7 20:17:41 2022 +0200

    Fixed bug for year users duration-sum

commit 1e3cb11637cfe001936d4b950b28564d2eec2a4b
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun May 29 19:51:15 2022 +0200

    Statistics retrieval finished

    Revamped config get/set functions
    Redid some UI explanations
    Renamed template JSON

commit 93390d469bab45560574e678a88b38dcef60f100
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Mon May 23 22:23:31 2022 +0200

    Bug problem

commit 3eb49b8da1c77d33923913e817cb41201cab3963
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun May 22 23:48:11 2022 +0200

    Users

commit aa6b3ec5121bb73cbe5828d9f42e1fd14514ddbd
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun May 22 13:11:35 2022 +0200

    More stats

    Leaderboard and show buddy missing
    Working on bug that resets config (?!)

commit 29742a2b6305a3835c63fc2c7760018b3e2267f0
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sun May 22 00:41:19 2022 +0200

    Basis for stats retrieval

    personal movies, shows and music is functional

commit a0df0759a76689638d47657abde755cc34dd4a1b
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri May 20 17:06:24 2022 +0200

    Removed Docker folder

commit 3e192997c08a5406a3d98e850218a674559f9ff0
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri May 20 17:06:05 2022 +0200

    Renaming and Pop-up fix

    Pop ups should now not be blocked. Hopefully...

commit 57b2d03fb57762c6c2aec7433a4f2affd6faa090
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Apr 29 15:37:26 2022 +0200

    CSS changes

    New rounded corners
    Transparent background colors
    Yellow borders
    Blurred background image
    Better footer placement
    More depth to snow particles

commit b562e75bee6ff612019fa08d221fbf1ee69edd11
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Apr 29 12:37:18 2022 +0200

    API endpoints changes

    Get wrapperr-version now also retrieves config-state
    Login via Plex Auth disabled if Wrapperr is unconfigured
    Cookies changed to strict samesite value
    Logging changes

commit e01bfc41cf46b3d7bc3b6627c93b67c0f70aa718
Author: Aune <31650531+aunefyren@users.noreply.github.com>
Date:   Fri Apr 29 10:47:12 2022 +0200

    Update README.md

    More shields

commit 2d2893cb9b212016c3455fd4f8dd3a5b229550ba
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 27 22:09:24 2022 +0200

    Comments and time zone fixes

commit 271c16b00e0eb4d58a97ed3b82f0e96993a7d853
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 27 16:26:44 2022 +0200

    Caching and caching loop

commit 44376b4d57765258602ec585e59b0825938d3c77
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 27 10:33:18 2022 +0200

    Pulling Tautulli History

    Moved Tautulli functions to their own file

commit 5355dafc68c5c2ed5a7c7e413388981438314500
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 26 22:00:49 2022 +0200

    Start of Tautulli data collection

    Added time-zone setting in code
    Created cache structs

commit 8bcce997c3b1a0556752ff99b5925cce7e18c4b6
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 26 17:25:06 2022 +0200

    Comments and caching mode

commit 6e951efbcd6acb0b442a691ceb9919e95d3e9e98
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Sat Apr 23 19:03:00 2022 +0200

    User details fetch (From Plex & Tautulli)

commit cb5b9384c0b716ef33770a0f96f6e4c0601cff50
Author: Aune <31650531+aunefyren@users.noreply.github.com>
Date:   Sat Apr 23 12:07:57 2022 +0200

    Shields.io stats

commit 343e0ac727576652ccd81e565a7a385879b1bfb4
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Mon Apr 18 22:14:49 2022 +0200

    Begun stats function

    Moved Taut test to separate function from API endpoint

commit b7e34224d7524358a4b59bd667d75b3b6527361c
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Apr 15 02:26:54 2022 +0200

    Update util.go

commit 4a2598f77cce3c5e9d5fc66e9e870bf3cd4bfb51
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Apr 15 02:22:34 2022 +0200

    Error handling

commit 09b80cf16c054d2767cd1cf50ae17f7f72268a53
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 15:01:59 2022 +0200

    Updated readme to explain current standing

commit 273b08f43ca317b6cd4ede01efb5040e63cc9839
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 14:46:34 2022 +0200

    Removed test-log commands

commit f2e85beb9d4aa9157b32933c50495c5f029787e4
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 14:26:17 2022 +0200

    Removed legacy PHP files

commit aaafea6da38173224d1b15063d4f32468ec12879
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 14:24:41 2022 +0200

    URL builder & test Tautulli connection

commit a02a172a7fc85a5a595fdb4003b00c8aa7d5f475
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 02:37:48 2022 +0200

    Moved Plex Auth Get Pin to front end

    By moving this API call to the front end (JS), the IP address is identical when logging in. This removes the red warning.

commit 744fae84a088cc77ba86d3c3687aff8d9caa77d7
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 14 01:33:40 2022 +0200

    Plex Auth

    Plex Auth login, validation and JWT token creation

commit 820bbca58a5679ef6a1e98ec586442cd841ac958
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 13 20:45:00 2022 +0200

    Update config

    API now updates config file

commit cb581d69007b32e8e924ac22ba66c923dd9eef23
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 13 15:49:04 2022 +0200

    More API functions

    - Admin login
    - Config/Admin file creation
    - Config API retrieval

commit f832df727de5c5fe583f03e2d8906714ceaf8017
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 12 09:30:58 2022 +0200

    Functional API endpoints

    Some basic functionality
    - Retrieve wrapperr version
    - Retrieve admin state
    - Create admin

commit 76193b7230e90d578bc561c8f479a87885c0b50f
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Mon Apr 11 16:51:35 2022 +0200

    Config loaders

commit c197a1b91b6307432e560756da97887bbfb36de5
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Fri Apr 8 17:20:36 2022 +0200

    Config files

commit 2ef850426a14ecf3dc39ab4b122fb479bb1e8f11
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Thu Apr 7 13:16:01 2022 +0200

    Functional jwt validattion and creation

commit a52de743daeb3dce18db7e0848eea37d4c35b707
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 6 22:25:01 2022 +0200

    Auth token jwt

commit 2f446a484ad6e4330a3a40cc708a543ca47ce148
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 6 14:19:49 2022 +0200

    Middleware

commit 1a1bc6ef09b9bce995d9d4b3f4bf7b0d1ead988a
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 6 09:14:02 2022 +0200

    Added more comments

commit 8c36dc74a98aa66565851ab01aff91e641287f06
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Wed Apr 6 09:08:26 2022 +0200

    Port start argument

    Also included as ENV in Dockerfile

commit 6bfbbdf5c3f955ab680ee4b47863bd7f017ffd5a
Author: Aune <31650531+aunefyren@users.noreply.github.com>
Date:   Tue Apr 5 23:31:36 2022 +0200

    Whopsie

commit 7600f2812e01d266f3488ed32a49d1591740a3e6
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 5 20:05:13 2022 +0200

    Routing

commit 0dbb1c756fcd411e60b73752554d75c8bf04f0b5
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 5 18:20:12 2022 +0200

    Added warnings & Dockerfile

commit a787a0acd41ab182ca0607dc3c1796a28ce947bd
Author: aunefyren <oystein.sverre@gmail.com>
Date:   Tue Apr 5 17:51:48 2022 +0200

    Go branch init

    The assets are served, but no API
This commit is contained in:
aunefyren 2022-07-04 17:47:59 +02:00
parent 35615be606
commit 3b7e3c08d6
96 changed files with 6292 additions and 5300 deletions

72
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '36 23 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

25
.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Go
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

32
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Release GO binaries
on:
release:
types: [created]
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
# build and publish in parallel: linux/amd64
goos: [linux, windows]
goarch: [amd64, arm64, 386]
#exclude:
# - goarch: "386"
# goos: windows
steps:
- uses: actions/checkout@v2
- name: Run tests
run: go test -v -p=1 -timeout=0 ./...
- uses: wangyoucao577/go-release-action@v1.29
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
project_path: "./"
binary_name: "Wrapperr"
ldflags: "-s -w"
extra_files: README.md config web CODE_OF_CONDUCT.md config_default.json Dockerfile

10
.gitignore vendored
View file

@ -1,6 +1,4 @@
config/wrapped.log
config/config.json
config/cache.json
config/links
config/admin.json
config/*
!config/README.md
wrapperr.exe
wrapperr

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM golang:1.18-alpine
ENV port=8282
RUN apk update
RUN apk add git
ENV GO111MODULE=on
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
EXPOSE ${port}
ENTRYPOINT /app/Wrapperr -port=${port}

133
README.md
View file

@ -1,7 +1,13 @@
# Wrapperr
[![Github Stars](https://img.shields.io/github/stars/aunefyren/wrapperr?style=for-the-badge)](https://github.com/aunefyren/wrapperr)
[![Github Forks](https://img.shields.io/github/forks/aunefyren/wrapperr?style=for-the-badge)](https://github.com/aunefyren/wrapperr)
[![Docker Pulls](https://img.shields.io/docker/pulls/aunefyren/wrapperr?style=for-the-badge)](https://hub.docker.com/r/aunefyren/wrapperr)
[![Newest Release](https://img.shields.io/github/v/release/aunefyren/wrapperr?style=for-the-badge)](https://github.com/aunefyren/wrapperr/releases)
[![Go Version](https://img.shields.io/github/go-mod/go-version/aunefyren/wrapperr?style=for-the-badge)](https://go.dev/dl/)
## Introduction - What is this?
A website-based platform and API for collecting 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.
A website-based application and API for collecting 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>
@ -12,13 +18,14 @@ A website-based platform and API for collecting user stats within a set timefram
### Features
- Custom timeframes
- Plex Auth
- Customizable text fields
- Movies, shows & music
- Caching of data
- Friendly, dynamic display for statistics with nice illustrations
- Admin page with authentication for settings
- Customizable text
- Customizable appearance
- Movies, shows & music statistics
- Caching of results
- Admin interface
- Pre-caching functionality
- Shareable links
- Shareable links with expiration
<br>
@ -31,6 +38,7 @@ A website-based platform and API for collecting user stats within a set timefram
- 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)
- Superb background image from [Pexels](https://www.pexels.com/photo/snowy-forest-235621/)
<br>
@ -38,83 +46,86 @@ A website-based platform and API for collecting user stats within a set timefram
<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.
## Explanation - How does it work?
This is a web-based platform. It gathers and displays statistics using an API (application programming interface) that interacts with the Tautulli API. Install Wrapperr, configure the essential options, and Wrapperr will do the rest. Based on your exact configuration, Wrapperr will gather unique statistics for each user interacting with the application.
There are instructions for this further down.
<br>
<br>
### How does it work?
There are some things to know when you have the website running:
- 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``` on the server, 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 and encryption token is hashed and stored in the ```config/config.json```. This is a sensitive directory! There is an ```.htaccess``` file included that blocks traffic to the folder, but this is only effective with Apache. If you are using Nginx you must add a directory deny in your Nginx configuration!
- If finish essential setup on the admin page you can click 'Caching' and 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.
- Almost all statistics options are enabled by default. Go to the admin page, and then click on 'Wrapperr customization' and customize the statistics page for your liking.
In Wrapperr you configure a timeframe, from date-time A to date-time B. This is the timeframe from which the statistics are created. One could for instance have a wrap-up of multiple years of Tautulli data or just a week. If enabled, Wrapperr will verify the user with Plex to ensure personal data is kept private.
Most text is customizable through the Wrapperr admin interface which allows for regional translation. Certain statistics can be disabled and enabled based on relevance/interest. Users can if enabled, generate random URLs which can be shared between friends who want to see each other's statistics.
<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.
## Instructions - How do I install this?
There are two main ways. Docker information can be found further below.
### 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.
<br>
<br>
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.
### Download and start
There are multiple ways to install Wrapperr. The easiest is just to download the latest release from the [Release Page](https://github.com/aunefyren/wrapperr/releases) which matches your operating system, move all the content to a directory, and start the ```Wrapperr``` application located within the release. It should start right up, perhaps triggering some operating system or firewall warnings.
### Install Wrapperr
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.
<br>
<br>
For instance, I placed this repository in a folder within the document-root, so my document-root, with that folder, makes the full path: ```C:\xampp\htdocs\wrapperr```, which in turn makes the files accessable on ```http://localhost/wrapperr```. Notice how my folder inside the document-root altered the URL. If I placed the repository files directly into ```C:\xampp\htdocs``` the URL would be: ```http://localhost```.
### 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.
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!
On Windows I never had to change permissions for the ```config``` folder, PHP could access it by defult. On 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.
### Build with Go
If you want to build Wrapperr yourself, you can download whatever version/tag/branch you want, and place the files in a directory. With (Go)[https://go.dev/dl/] installed, from the Wrapperr directory, run the following commands to build and execute Wrapperr:
```
$ sudo chmod -R 0777 /var/www/html/config
$ go build
$ ./Wrapperr
```
Note, if building on another operating system, the executable could have a different name. Such as ```Wrapperr.exe``` on Windows.
### Test
Go to ```http://localhost```, or your variation as discussed earlier, and you should see the front page.
<br>
<br>
Everything should now be prepared, and the rest of the setup should be done on the admin page, followed up by a pre-caching. You might have to refer to PHP configuration section below if PHP is acting up. Go to the previous section named 'How does it work?' if you need information about the admin setup.
### Head to the website
If successful, Wrapperr should be accessible on ```http://localhost:8282```. From there you can click on ```admin``` in the footer at the bottom, or go to ```/admin``` in the URL. From there you can configure everything about Wrapperr in the different sections of the menu.
<br>
<br>
### Essential configuration options
A couple of configuration options are necessary for Wrapperr to function. First of all, Tautulli connection details. There is a test button available on the page to ensure you have entered the correct details. The second one is the time zone option on the ```Wrapperr Settings``` page.
It is recommended to keep ```Cache results for later use``` enabled on the ```Wrapperr Settings``` page, and head to the ```Caching``` page after configuration. This ensures a good, quick user experience.
<br>
<br>
### Wrap away!
Wrapperr should now be functional. Based on your settings, you can now either search with username/e-mail or log in with Plex on the front page. Continue tweaking on the admin menu to get the appearance/language you desire.
<br>
<br>
## Docker
Docker sets up the environment, but I recommend reading the start of the 'Instructions' section for an explanation of functionality/admin page! You might have to refer to the 'PHP Configuration' section below if PHP is acting up and giving API parsing errors.
Docker sets up the environment, but I recommend reading the start of the 'How do I install this?' section for an explanation of the functionality/admin page!
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/wrapperr) of Wrapperr, simply execute this docker command, pulling the image from Docker Hub and exposing it on port 80:
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 as preferred and then build it. If you just want to launch the [pre-built image](https://hub.docker.com/r/aunefyren/wrapperr) of Wrapperr, simply execute this docker command, pulling the image from Docker Hub and exposing it on port ```8282```:
```
$ docker run -p '80:80' --name 'wrapperr' aunefyren/wrapperr:latest
$ docker run -p '8282:8282' --name 'wrapperr' aunefyren/wrapperr:latest
```
It should now be accessable on: ```http://localhost```
It should now be accessible on: ```http://localhost:8282```
If you use Docker Compose you could do something like this in your docker-compose.yml:
If you use Docker Compose you could do something like this in your ```docker-compose.yml```:
```
version: '3.3'
services:
wrapperr:
ports:
- '80:80'
- '8282:8282'
container_name: wrapperr
image: 'aunefyren/wrapperr:latest'
restart: unless-stopped
```
And launch the file with:
```
$ docker-compose up
```
@ -126,14 +137,15 @@ version: '3.3'
services:
wrapperr:
ports:
- '80:80'
- '8282:8282'
container_name: wrapperr
image: 'aunefyren/wrapperr:latest'
restart: unless-stopped
volumes:
- './my-folder:/var/www/html/config'
- './my-folder:/app/config'
```
Afterwards, remember to chmod the mounted folder on the host so the container can write to it:
Afterward, remember to ```chmod``` the mounted folder on the host so the Wrapperr can write to it:
```
@ -143,35 +155,20 @@ $ 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>
## Frequently asked questions
### Q: Why are the plays different on Wrapperr compared to Tautulli
A: Data is retrieved from the Tautulli API, but not necasserly proccessed in the same manner. The difference could for example be that you have history entries for the same media (movie for example) split over different Tautulli items. For example, you could have two items for the movie 'Black Widow' from potentially updating the file on Plex, leading Tautulli to interperet it as a new item/media. The easiest way to check for this is by going to the 'History' tab and searching for the title. This might display more entries than clicking into the movie item, which displays all history items for that particular item.
A: Data is retrieved from the Tautulli API, but not necessarily processed in the same manner. The difference could for example be that you have history entries for the same media (a movie for example) split over different Tautulli items. For example, you could have two items for the movie 'Black Widow' from updating the file on Plex, leading Tautulli to interpret it as a new item/media. The easiest way to check for this is by going to the 'History' tab and searching for the title. This might display more entries than clicking into the movie item, which displays all history items for that particular item.
There is an option to merge different Tautulli items if this is your case.
What also could cause confusion is related to Tautulli grouping feature. When you have grouping enabled, different plays are grouped on an API call basis. Meaning that when you display all history items for a movie on Tautulli, six different plays spanning three days might be placed into one group. Wrapperr calls the Tautulli API on a day basis, meaning grouping never spans multiple days, potentially leading to an increase in plays because the groups are smaller.
What also could confuse is related to the Tautulli grouping feature. When you have grouping enabled, different plays are grouped on an API call basis. Meaning that when you display all history items for a movie on Tautulli, six different plays spanning three days might be placed into one group. Wrapperr calls the Tautulli API on 'day' based loop, meaning Tautulli's grouping never spans multiple days, potentially leading to an increase in plays because the groups are smaller in size.
<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.
If you have any issues feel free to open an issue here on GitHub. 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.
Have fun.
Have fun.

View file

@ -1,61 +0,0 @@
<?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/log.php');
require(dirname(__FILE__) . '/objects/admin.php');
// Create variables
$admin = new Admin();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data) || !isset($data->password) || !isset($data->username)) {
// Log use
$log->log_activity('create_admin.php', 'unknown', 'No input provided.');
echo json_encode(array("error" => true, "message" => "No input provided."));
exit(0);
}
// Remove potential harmfull input
$password = htmlspecialchars($data->password);
$username = htmlspecialchars($data->username);
// Check if confgiured
if($admin->is_configured()) {
// Log use
$log->log_activity('create_admin.php', 'unknown', 'Wrapperr admin is already configured.');
echo json_encode(array("error" => true, "message" => "Wrapperr admin is already configured."));
exit(0);
// VSave new admin
} else if($admin->create_admin($username, $password)) {
// Log use
$log->log_activity('create_admin.php', 'unknown', 'Created admin account: ' . $username . ".");
echo json_encode(array("error" => false, "message" => "Admin account created."));
exit(0);
// If creating failed
} else {
// Log use
$log->log_activity('create_admin.php', 'unknown', 'Failed to create Wrapperr admin.');
echo json_encode(array("error" => true, "message" => "Failed to create Wrapperr admin."));
exit(0);
}
?>

View file

@ -1,97 +0,0 @@
<?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', 'Wrapperr is not configured.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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', 'Wrapperr does not allow link creation in config.');
echo json_encode(array("error" => true, "message" => "Wrapperr option for link creation not enabled."));
exit(0);
}
// Remove potential harmfull input
$cookie = htmlspecialchars($data->cookie);
$wrapped_data = $data->data;
$wrapped_functions = $data->functions;
// 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. Could not create link.');
echo json_encode(array("error" => true, "message" => "Login not accepted. Try 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'), "wrapperr_version" => $config->wrapperr_version, "data" => $wrapped_data, "functions" => $wrapped_functions);
// Save the content to file
$link->save_link($link_content, $id);
// Log use
$log->log_activity('create_link.php', $id, 'Created Wrapperr link.');
// Return URL generated
echo json_encode(array("error" => false, "message" => "Link created.", "url" => "?hash=" . $url_hash));
exit(0);
?>

View file

@ -1,91 +0,0 @@
<?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) || empty($data->cookie)) {
// Log use
$log->log_activity('delete_link_user.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('delete_link_user.php', 'unknown', 'Wrapperr is not configured. Can\'t delete link.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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('delete_link_user.php', 'unknown', 'Wrapperr does not allow link creation in config. Won\'t delete link.');
echo json_encode(array("error" => true, "message" => "Wrapperr option for link creation not enabled."));
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('delete_link_user.php', 'unknown', 'Plex Token from cookie not valid. Could not create link.');
echo json_encode(array("error" => true, "message" => "Login not accepted. Try again."));
exit(0);
}
// Assign values from Plex Token
$id = $token_object->data->id;
// Delete content
if(!$link->delete_link($id)) {
// Log use
$log->log_activity('delete_link_user.php', $id, 'Failed to delete link. Not found.');
echo json_encode(array("error" => true, "message" => "This Wrapperr link has expired."));
exit(0);
}
// Log use
$log->log_activity('delete_link_user.php', $id, 'Wrapperr link deleted by user.');
echo json_encode(array("error" => false, "message" => "This Wrapperr link deleted."));
exit(0);
?>

View file

@ -1,29 +0,0 @@
<?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/admin.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$admin = new Admin();
$log = new Log();
// Log use
$log->log_activity('get_admin_state.php', 'unknown', 'Retrieved Wrapperr admin state.');
// Create JSON from functions
$functions_json = array("wrapperr_admin_configured" => $admin->is_configured(),
"error" => false,
"message" => "Admin configuration state retrieved."
);
// Encode JSON and print it
echo json_encode($functions_json);
exit(0);
?>

View file

@ -1,72 +0,0 @@
<?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');
require(dirname(__FILE__) . '/objects/admin.php');
// Create variables
$config = new Config();
$admin = new Admin();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data) || !isset($data->cookie)) {
// Log use
$log->log_activity('get_config.php', 'unknown', 'No admin login cookie provided.');
echo json_encode(array("error" => true, "message" => "No cookie provided."));
exit(0);
}
// Remove potential harmfull input
$cookie = htmlspecialchars($data->cookie);
// Check if confgiured
if(!$admin->is_configured()) {
// Log use
$log->log_activity('get_config.php', 'unknown', 'Wrapperr admin is not configured.');
echo json_encode(array("error" => true, "message" => "Wrapperr admin is not configured."));
exit(0);
} else if(!$config->is_configured()) {
// Log use
$log->log_activity('get_config.php', 'unknown', 'Wrapperr is not configured.');
echo json_encode(array("error" => false, "message" => "Wrapperr is not configured.", "wrapperr_configured" => false, "data" => $config, "admin" => $admin->username));
exit(0);
}
// Decrypt cookie
$cookie_object = json_decode($admin->decrypt_cookie($cookie));
// Validate admin cookie
if(!$admin->validate_cookie($cookie_object)) {
// Log use
$log->log_activity('get_config.php', 'unknown', 'Admin cookie not valid.');
echo json_encode(array("error" => true, "message" => "Admin cookie not accepted. Log in again."));
exit(0);
}
// Log use
$log->log_activity('get_config.php', 'admin', 'Retrieved Wrapperr configuration.');
echo json_encode(array("error" => false, "message" => "Config retrieved.", "wrapperr_configured" => true, "data" => $config, "admin" => $admin->username));
exit(0);
?>

View file

@ -1,78 +0,0 @@
<?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");
// 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);
}
// Remove potential harmfull input
$url = htmlspecialchars($data->url);
$apikey = htmlspecialchars($data->apikey);
// Create URL
$url = $url . '?apikey=' . $apikey . '&cmd=status';
// Attempt to call Tautulli API
try {
// Call Tautulli status API
// Initiate curl
$ch = curl_init();
// Set the options for curl
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute curl
$result = curl_exec($ch);
// Check if an error occurred
if(curl_errno($ch)) {
echo json_encode(array("error" => true, "message" => "Tautulli did not respond.", "data" => array()));
exit(0);
}
// Closing curl
curl_close($ch);
// Decode the JSON response
$decoded = json_decode($result, true);
// Check reponse for success
if($decoded["response"]["result"] == "success") {
echo json_encode(array("error" => false, "message" => "Tautulli reached and accepted.", "data" => $decoded));
exit(0);
}
// Check reponse for error
if($decoded["response"]["result"] == "error") {
$message = $decoded["response"]["message"];
echo json_encode(array("error" => true, "message" => "Tautulli error. Reply: $message", "data" => $decoded));
exit(0);
}
echo json_encode(array("error" => true, "message" => "Parsing Tautulli reponse failed. It could be working.", "data" => $decoded));
exit(0);
// Catch errors
} catch (Exception $e) {
echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array()));
exit(0);
}
?>

View file

@ -1,125 +0,0 @@
<?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"));
// Log use
$log->log_activity('get_functions.php', 'unknown', 'Retrieved Wrapperr functions.');
// Create JSON from functions
$functions_json = array("wrapperr_version" => $config->wrapperr_version,
"get_user_movie_stats" => $config->get_user_movie_stats,
"get_user_movie_stats_title" => $config->get_user_movie_stats_title,
"get_user_movie_stats_subtitle" => $config->get_user_movie_stats_subtitle,
"get_user_movie_stats_subsubtitle" => $config->get_user_movie_stats_subsubtitle,
"get_user_movie_stats_subtitle_one" => $config->get_user_movie_stats_subtitle_one,
"get_user_movie_stats_subsubtitle_one" => $config->get_user_movie_stats_subsubtitle_one,
"get_user_movie_stats_subtitle_none" => $config->get_user_movie_stats_subtitle_none,
"get_user_movie_stats_subsubtitle_none" => $config->get_user_movie_stats_subsubtitle_none,
"get_user_movie_stats_top_movie" => $config->get_user_movie_stats_top_movie,
"get_user_movie_stats_top_movie_plural" => $config->get_user_movie_stats_top_movie_plural,
"get_user_movie_stats_movie_completion_title" => $config->get_user_movie_stats_movie_completion_title,
"get_user_movie_stats_movie_completion_title_plural" => $config->get_user_movie_stats_movie_completion_title_plural,
"get_user_movie_stats_movie_completion_subtitle" => $config->get_user_movie_stats_movie_completion_subtitle,
"get_user_movie_stats_pause_title" => $config->get_user_movie_stats_pause_title,
"get_user_movie_stats_pause_subtitle" => $config->get_user_movie_stats_pause_subtitle,
"get_user_movie_stats_pause_title_one" => $config->get_user_movie_stats_pause_title_one,
"get_user_movie_stats_pause_subtitle_one" => $config->get_user_movie_stats_pause_subtitle_one,
"get_user_movie_stats_pause_title_none" => $config->get_user_movie_stats_pause_title_none,
"get_user_movie_stats_pause_subtitle_none" => $config->get_user_movie_stats_pause_subtitle_none,
"get_user_movie_stats_oldest_title" => $config->get_user_movie_stats_oldest_title,
"get_user_movie_stats_oldest_subtitle" => $config->get_user_movie_stats_oldest_subtitle,
"get_user_movie_stats_oldest_subtitle_pre_1950" => $config->get_user_movie_stats_oldest_subtitle_pre_1950,
"get_user_movie_stats_oldest_subtitle_pre_1975" => $config->get_user_movie_stats_oldest_subtitle_pre_1975,
"get_user_movie_stats_oldest_subtitle_pre_2000" => $config->get_user_movie_stats_oldest_subtitle_pre_2000,
"get_user_movie_stats_spent_title" => $config->get_user_movie_stats_spent_title,
"get_user_show_stats" => $config->get_user_show_stats,
"get_user_show_stats_buddy" => $config->get_user_show_stats_buddy,
"get_user_show_stats_title" => $config->get_user_show_stats_title,
"get_user_show_stats_subtitle" => $config->get_user_show_stats_subtitle,
"get_user_show_stats_subsubtitle" => $config->get_user_show_stats_subsubtitle,
"get_user_show_stats_subtitle_one" => $config->get_user_show_stats_subtitle_one,
"get_user_show_stats_subsubtitle_one" => $config->get_user_show_stats_subsubtitle_one,
"get_user_show_stats_subtitle_none" => $config->get_user_show_stats_subtitle_none,
"get_user_show_stats_subsubtitle_none" => $config->get_user_show_stats_subsubtitle_none,
"get_user_show_stats_top_show" => $config->get_user_show_stats_top_show,
"get_user_show_stats_top_show_plural" => $config->get_user_show_stats_top_show_plural,
"get_user_show_stats_spent_title" => $config->get_user_show_stats_spent_title,
"get_user_show_stats_most_played_title" => $config->get_user_show_stats_most_played_title,
"get_user_show_stats_most_played_subtitle" => $config->get_user_show_stats_most_played_subtitle,
"get_user_show_stats_buddy_title" => $config->get_user_show_stats_buddy_title,
"get_user_show_stats_buddy_subtitle" => $config->get_user_show_stats_buddy_subtitle,
"get_user_show_stats_buddy_title_none" => $config->get_user_show_stats_buddy_title_none,
"get_user_show_stats_buddy_subtitle_none" => $config->get_user_show_stats_buddy_subtitle_none,
"get_user_music_stats" => $config->get_user_music_stats,
"get_user_music_stats_title" => $config->get_user_music_stats_title,
"get_user_music_stats_subtitle" => $config->get_user_music_stats_subtitle,
"get_user_music_stats_subsubtitle" => $config->get_user_music_stats_subsubtitle,
"get_user_music_stats_subtitle_one" => $config->get_user_music_stats_subtitle_one,
"get_user_music_stats_subsubtitle_one" => $config->get_user_music_stats_subsubtitle_one,
"get_user_music_stats_subtitle_none" => $config->get_user_music_stats_subtitle_none,
"get_user_music_stats_subsubtitle_none" => $config->get_user_music_stats_subsubtitle_none,
"get_user_music_stats_top_track" => $config->get_user_music_stats_top_track,
"get_user_music_stats_top_track_plural" => $config->get_user_music_stats_top_track_plural,
"get_user_music_stats_top_album_plural" => $config->get_user_music_stats_top_album_plural,
"get_user_music_stats_top_artist_plural" => $config->get_user_music_stats_top_artist_plural,
"get_user_music_stats_spent_title" => $config->get_user_music_stats_spent_title,
"get_user_music_stats_spent_subtitle" => $config->get_user_music_stats_spent_subtitle,
"get_user_music_stats_oldest_album_title" => $config->get_user_music_stats_oldest_album_title,
"get_user_music_stats_oldest_album_subtitle" => $config->get_user_music_stats_oldest_album_subtitle,
"get_year_stats_title" => $config->get_year_stats_title,
"get_year_stats_subtitle" => $config->get_year_stats_subtitle,
"get_year_stats_subsubtitle" => $config->get_year_stats_subsubtitle,
"get_year_stats_movies" => $config->get_year_stats_movies,
"get_year_stats_movies_title" => $config->get_year_stats_movies_title,
"get_year_stats_movies_duration_title" => $config->get_year_stats_movies_duration_title,
"get_year_stats_shows" => $config->get_year_stats_shows,
"get_year_stats_shows_title" => $config->get_year_stats_shows_title,
"get_year_stats_shows_duration_title" => $config->get_year_stats_shows_duration_title,
"get_year_stats_music" => $config->get_year_stats_music,
"get_year_stats_music_title" => $config->get_year_stats_music_title,
"get_year_stats_music_duration_title" => $config->get_year_stats_music_duration_title,
"get_year_stats_leaderboard" => $config->get_year_stats_leaderboard,
"get_year_stats_leaderboard_title" => $config->get_year_stats_leaderboard_title,
"get_year_stats_duration_sum_title" => $config->get_year_stats_duration_sum_title,
"wrapperr_front_page_title" => $config->wrapperr_front_page_title,
"wrapperr_front_page_subtitle" => $config->wrapperr_front_page_subtitle,
"stats_intro_title" => $config->stats_intro_title,
"stats_intro_subtitle" => $config->stats_intro_subtitle,
"stats_outro_title" => $config->stats_outro_title,
"stats_outro_subtitle" => $config->stats_outro_subtitle,
"stats_order_by_plays" => $config->stats_order_by_plays,
"stats_order_by_duration" => $config->stats_order_by_duration,
"create_share_links" => $config->create_share_links,
"wrapperr_and" => $config->wrapperr_and,
"wrapperr_play" => $config->wrapperr_play,
"wrapperr_play_plural" => $config->wrapperr_play_plural,
"wrapperr_day" => $config->wrapperr_day,
"wrapperr_day_plural" => $config->wrapperr_day_plural,
"wrapperr_hour" => $config->wrapperr_hour,
"wrapperr_hour_plural" => $config->wrapperr_hour_plural,
"wrapperr_minute" => $config->wrapperr_minute,
"wrapperr_minute_plural" => $config->wrapperr_minute_plural,
"wrapperr_second" => $config->wrapperr_second,
"wrapperr_second_plural" => $config->wrapperr_second_plural,
"wrapperr_sort_plays" => $config->wrapperr_sort_plays,
"wrapperr_sort_duration" => $config->wrapperr_sort_duration,
"use_plex_auth" => $config->use_plex_auth
);
// Encode JSON and print it
echo json_encode($functions_json);
exit(0);
?>

View file

@ -1,145 +0,0 @@
<?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', 'Wrapperr is not configured.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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', 'Wrapperr does not allow link creation in config.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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 Wrapperr link. Can\'t parse input.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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 Wrapperr link. File not found.');
echo json_encode(array("error" => true, "message" => "There was an error fetching this Wrapperr 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 Wrapperr link. Hash did not match.');
echo json_encode(array("error" => true, "message" => "There was an error fetching this Wrapperr 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 Wrapperr 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 Wrapperr link has expired."));
exit(0);
}
if($link_data->wrapperr_version !== $config->wrapperr_version) {
// Log use
$log->log_activity('get_link.php', 'unknown', 'Wrapperr link for ID: ' . $id . ' is made for version: ' . $link_data->wrapperr_version . '. 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 Wrapperr link is made for another Wrapperr version. Create a new link on the current version."));
exit(0);
}
// Log use
$log->log_activity('get_link.php', 'unknown', 'Retrieved Wrapperr link for ID: ' . $id . '.');
// Return URL generated
echo json_encode(array("error" => false, "message" => "Link retrieved.", "data" => $link_data->data, "functions" => $link_data->functions));
exit(0);
?>

View file

@ -1,134 +0,0 @@
<?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->cookie)) {
// Log use
$log->log_activity('get_link_user.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_user.php', 'unknown', 'Wrapperr is not configured. Can\'t retrieve links.');
echo json_encode(array("error" => true, "message" => "Wrapperr 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_user.php', 'unknown', 'Wrapperr does not allow link creation in config. Will not retrieve link.');
echo json_encode(array("error" => true, "message" => "Wrapperr option for link creation not enabled."));
exit(0);
}
// Remove potential harmfull input
$cookie = htmlspecialchars($data->cookie);
// Get the current date
$now = new DateTime('NOW');
// Get Plex Token
$token_object = json_decode($auth->validate_token($cookie));
// Validate Plex ID
if(empty($token_object) || !isset($token_object->data->id)) {
// Log use
$log->log_activity('get_link_user.php', 'unknown', 'Plex Token from cookie not valid. Could not retrieve link.');
echo json_encode(array("error" => true, "message" => "Login not accepted. Try again."));
exit(0);
}
// Assign values from Plex Token
$id = $token_object->data->id;
// Save the content to file
$content = $link->open_link($id);
if(!$content) {
// Log use
$log->log_activity('get_link_user.php', $id, 'User does not have any Wrapperr link file.');
echo json_encode(array("error" => false, "message" => "Wrapperr links retrieved.", "links" => array()));
exit(0);
}
$link_data = json_decode($content);
$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_user.php', $id, 'Wrapperr link has expired. Deleting file.');
// Delete expired content
if(!$link->delete_link($id)) {
$log->log_activity('get_link_user.php', $id, 'Failed to delete link.');
}
echo json_encode(array("error" => false, "message" => "Wrapperr links retrieved.", "links" => array()));
exit(0);
}
if($link_data->wrapperr_version !== $config->wrapperr_version) {
// Log use
$log->log_activity('get_link_user.php', $id, 'Wrapperr link is made for version: ' . $link_data->wrapperr_version . '. Deleting file.');
// Delete expired content
if(!$link->delete_link($id)) {
$log->log_activity('get_link_user.php', $id, 'Failed to delete link.');
}
echo json_encode(array("error" => false, "message" => "Wrapperr links retrieved.", "links" => array()));
exit(0);
}
// Create link dataset
$links = array('url_hash' => $link_data->url_hash, 'date' => $link_data->date);
// Log use
$log->log_activity('get_link_user.php', $id, 'Retrieved Wrapperr link.');
// Return URL generated
echo json_encode(array("error" => false, "message" => "Wrapperr links retrieved.", "links" => array($links)));
exit(0);
?>

View file

@ -1,53 +0,0 @@
<?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/admin.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$admin = new Admin();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty or wrong
if(empty($data) || !isset($data->password) || !isset($data->username)) {
// Log use
$log->log_activity('get_login_admin_cookie.php', 'unknown', 'Input error from user.');
echo json_encode(array("error" => true, "message" => "Input error."));
exit(0);
}
// Remove potential harmfull input
$password = htmlspecialchars($data->password);
$username = htmlspecialchars($data->username);
// Get Plex token
$admin_cookie = $admin->get_login_admin_cookie($username, $password);
// Validate Plex ID
if(!$admin_cookie) {
// Log use
$log->log_activity('get_login_admin_cookie.php', 'unknown', 'Admin login not valid.');
echo json_encode(array("error" => true, "message" => "Login not accepted. Try again."));
exit(0);
}
// Log use
$log->log_activity('get_login_admin_cookie.php', 'admin', 'Admin login accepted. Returned cookie.');
// Print cookie and exit
echo json_encode(array("error" => false, "message" => "Login is valid. Admin cookie created.", "cookie" => $admin_cookie));
exit(0);
?>

View file

@ -1,42 +0,0 @@
<?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', 'Wrapperr login cookie created.');
// Print cookie and exit
echo json_encode(array("error" => false, "message" => "Cookie created.", "cookie" => $cookie));
exit(0);
?>

View file

@ -1,65 +0,0 @@
<?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', 'Wrapperr is not configured.');
echo json_encode(array("message" => "Wrapperr is not configured.", "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);
if(!isset($pin_object['id']) || !isset($pin_object['code'])) {
// Log URL creation
$log->log_activity('get_login_url.php', 'unknown', 'Failed to get ID or Code from Plex Auth. Exiting.');
// Return URL for login
echo json_encode(array("message" => 'Failed to get ID or Code from Plex Auth.', "error" => true));
exit(0);
}
// 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);
?>

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
<?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 Wrapperr version.');
// Create JSON from functions
$version_json = array( "wrapperr_version" => $config->wrapperr_version,
"application_name" => $config->application_name,
"use_plex_auth" => $config->use_plex_auth,
"wrapperr_front_page_title" => $config->wrapperr_front_page_title,
"wrapperr_front_page_subtitle" => $config->wrapperr_front_page_subtitle,
"message" => "Retrieved Wrapperr verison.",
"error" => false
);
// Encode JSON and print it
echo json_encode($version_json);
exit(0);
?>

View file

@ -1,167 +0,0 @@
<?php
class Admin {
// Object properties
// Admin path
private $path;
// Admin user
public $password;
public $username;
// Encryption variables
const METHOD = 'aes-256-ctr';
public $token_encrypter;
// Constructor
public function __construct(){
// Declare admin path
$this->path = dirname(__FILE__, 3) . '/config/admin.json';
// Check if config file exists, if not, create it
if(!file_exists($this->path)) {
$create_admin = fopen($this->path, "w");
if(!$create_admin) {
echo json_encode(array("message" => "Failed to create admin.json. Is the 'config' directory writable?", "error" => true));
exit();
}
fclose($create_admin);
}
// Parse JSON from config
$json = json_decode(file_get_contents($this->path));
if(!empty($json)) {
// Assign values from config file
$this->password = $json->password;
$this->username = $json->username;
$this->token_encrypter = $json->token_encrypter;
}
}
public function get_login_admin_cookie($username, $password) {
if(!password_verify($password, $this->password) || $username !== $this->username) {
return false;
}
$nonceSize = openssl_cipher_iv_length(self::METHOD);
$nonce = openssl_random_pseudo_bytes($nonceSize);
// Get the current date
$now = new DateTime('NOW');
$object = json_encode(array("username" => $this->username, "password" => $this->password, "date" => $now->format('Y-m-d')));
$token = openssl_encrypt(
$object,
self::METHOD,
$this->token_encrypter,
OPENSSL_RAW_DATA,
$nonce
);
return base64_encode($nonce.$token);
}
public function is_configured() {
if(!empty(file_get_contents($this->path)) && !empty($this->username) && !empty($this->password)) {
return true;
}
return false;
}
public function create_admin($username, $password) {
// Hash the new password if changed
$hash = password_hash($password, PASSWORD_DEFAULT);
// Save new username if it has changed
if($username === "") {
echo json_encode(array("message" => "Username not valid", "error" => true));
exit();
}
$this->username = $username;
$this->password = $hash;
if($this->save_admin()) {
return true;
} else {
return false;
}
}
public function save_admin() {
$this->token_encrypter = md5(rand(0,1000));
$save = json_encode(array("username" => $this->username, "password" => $this->password, "token_encrypter" => $this->token_encrypter));
if(file_put_contents($this->path, $save)) {
return true;
}
return false;
}
// Validate Plex Token
function decrypt_cookie($cookie) {
try {
// Debase cookie
$cookie_debased = base64_decode($cookie, true);
if ($cookie_debased === false) {
throw new Exception('Encryption failure. Cookie invalid.');
}
// Assign variables
$nonceSize = openssl_cipher_iv_length(self::METHOD);
$nonce = mb_substr($cookie_debased, 0, $nonceSize, '8bit');
$ciphertext = mb_substr($cookie_debased, $nonceSize, null, '8bit');
// Decrypt cookie
$cookie_data = openssl_decrypt(
$ciphertext,
self::METHOD,
$this->token_encrypter,
OPENSSL_RAW_DATA,
$nonce
);
// Decode the JSON response
$cookie_result = json_decode($cookie_data, true);
// Return Plex token
return json_encode(array("message" => 'Login accepted.', "error" => false, "data" => $cookie_result));
// Catch errors
} catch (Exception $e) {
echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array()));
exit(0);
}
}
function validate_cookie($cookie_object) {
// Validate admin variables
if(!$cookie_object || empty($cookie_object) || !isset($cookie_object->data->username) || !isset($cookie_object->data->password) || !isset($cookie_object->data->date)) {
return false;
}
$now = new DateTime('NOW');
$then = date_create_from_format('Y-m-d', $cookie_object->data->date);
$diff = (array) date_diff($now, $then);
if($diff['days'] > 2) {
return false;
}
return true;
}
}
?>

View file

@ -1,282 +0,0 @@
<?php
class Auth {
// Object properties
private $client_id;
private $token_encrypter;
private $strong = true;
private $header = 'application/json';
private $application_url;
private $x_plex_product = 'Wrapperr';
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;
$this->application_url = $config_file->application_url;
}
// 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);
// Declare config object
$config_file = new Config();
// Add payload
$payload = array( "strong"=> $this->strong,
"X-Plex-Product" => $this->x_plex_product,
"X-Plex-Client-Identifier" => $this->client_id,
"X-Plex-Version" => $config_file->wrapperr_version,
"X-Plex-Model" => "Plex OAuth",
"X-Plex-Language" => 'en'
);
$headers = [
"Accept: $this->header"
];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Add headers
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) {
// Log error
require_once(dirname(__FILE__) . '/log.php');
$log = new Log();
$log->log_activity('auth.php', 'unknown', 'Error: ' . $e->getMessage());
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);
// Declare config object
$config_file = new Config();
// Add payload
$payload = array(
"code"=> $code,
"X-Plex-Client-Identifier" => $this->client_id,
"X-Plex-Version" => $config_file->wrapperr_version,
"X-Plex-Model" => "Plex OAuth",
"X-Plex-Language" => 'en'
);
$headers = [
"Accept: $this->header"
];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
// Add headers
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) {
// Log error
require_once(dirname(__FILE__) . '/log.php');
$log = new Log();
$log->log_activity('auth.php', 'unknown', 'Error: ' . $e->getMessage());
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. Failed to decrypt login cookie.');
}
// 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);
// Declare config object
$config_file = new Config();
// Add payload
$payload = array(
"X-Plex-Token"=> $x_plex_token,
"X-Plex-Client-Identifier" => $this->client_id,
"X-Plex-Product" => $this->x_plex_product,
"X-Plex-Version" => $config_file->wrapperr_version,
"X-Plex-Model" => "Plex OAuth",
"X-Plex-Language" => 'en'
);
$headers = [
"Accept: " . $this->header
];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
// Add headers
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) {
// Log error
require_once(dirname(__FILE__) . '/log.php');
$log = new Log();
$log->log_activity('auth.php', 'unknown', 'Error: ' . $e->getMessage());
echo json_encode(array("message" => $e->getMessage(), "error" => true, "data" => array()));
exit(0);
}
}
}

View file

@ -1,61 +0,0 @@
<?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;
}
}

View file

@ -1,990 +0,0 @@
<?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 $tautulli_grouping;
public $https;
// Wrapperr config
public $wrapperr_version = 'v2.2.4';
public $timezone;
public $application_name;
public $application_url;
public $use_plex_auth;
public $use_cache;
public $use_logs;
public $client_id;
public $wrapperr_root;
public $token_encrypter;
public $create_share_links;
// Wrapperr custom
public $wrapped_start;
public $wrapped_end;
public $wrapperr_front_page_title;
public $wrapperr_front_page_subtitle;
public $stats_intro_title;
public $stats_intro_subtitle;
public $stats_outro_title;
public $stats_outro_subtitle;
public $stats_order_by_plays;
public $stats_order_by_duration;
// Wrapperr custom movies
public $get_user_movie_stats;
public $get_user_movie_stats_title;
public $get_user_movie_stats_subtitle;
public $get_user_movie_stats_subsubtitle;
public $get_user_movie_stats_subtitle_one;
public $get_user_movie_stats_subsubtitle_one;
public $get_user_movie_stats_subtitle_none;
public $get_user_movie_stats_subsubtitle_none;
public $get_user_movie_stats_top_movie;
public $get_user_movie_stats_top_movie_plural;
public $get_user_movie_stats_movie_completion_title;
public $get_user_movie_stats_movie_completion_title_plural;
public $get_user_movie_stats_movie_completion_subtitle;
public $get_user_movie_stats_pause_title;
public $get_user_movie_stats_pause_subtitle;
public $get_user_movie_stats_pause_title_one;
public $get_user_movie_stats_pause_subtitle_one;
public $get_user_movie_stats_pause_title_none;
public $get_user_movie_stats_pause_subtitle_none;
public $get_user_movie_stats_oldest_title;
public $get_user_movie_stats_oldest_subtitle;
public $get_user_movie_stats_oldest_subtitle_pre_1950;
public $get_user_movie_stats_oldest_subtitle_pre_1975;
public $get_user_movie_stats_oldest_subtitle_pre_2000;
public $get_user_movie_stats_spent_title;
// Wrapperr custom shows
public $get_user_show_stats;
public $get_user_show_buddy;
public $get_user_show_stats_title;
public $get_user_show_stats_subtitle;
public $get_user_show_stats_subsubtitle;
public $get_user_show_stats_subtitle_one;
public $get_user_show_stats_subsubtitle_one;
public $get_user_show_stats_subtitle_none;
public $get_user_show_stats_subsubtitle_none;
public $get_user_show_stats_top_show;
public $get_user_show_stats_top_show_plural;
public $get_user_show_stats_spent_title;
public $get_user_show_stats_most_played_title;
public $get_user_show_stats_most_played_subtitle;
public $get_user_show_stats_buddy_title;
public $get_user_show_stats_buddy_subtitle;
public $get_user_show_stats_buddy_title_none;
public $get_user_show_stats_buddy_subtitle_none;
// Wrapperr custom music
public $get_user_music_stats;
public $get_user_music_stats_title;
public $get_user_music_stats_subtitle;
public $get_user_music_stats_subsubtitle;
public $get_user_music_stats_subtitle_one;
public $get_user_music_stats_subsubtitle_one;
public $get_user_music_stats_subtitle_none;
public $get_user_music_stats_subsubtitle_none;
public $get_user_music_stats_top_track;
public $get_user_music_stats_top_track_plural;
public $get_user_music_stats_top_album_plural;
public $get_user_music_stats_top_artist_plural;
public $get_user_music_stats_spent_title;
public $get_user_music_stats_spent_subtitle;
public $get_user_music_stats_oldest_album_title;
public $get_user_music_stats_oldest_album_subtitle;
// Wrapperr custom server-wide
public $get_year_stats_title;
public $get_year_stats_subtitle;
public $get_year_stats_subsubtitle;
public $get_year_stats_movies;
public $get_year_stats_movies_title;
public $get_year_stats_shows;
public $get_year_stats_shows_title;
public $get_year_stats_music;
public $get_year_stats_music_title;
public $get_year_stats_leaderboard;
public $get_year_stats_leaderboard_title;
public $get_year_stats_movies_duration_title;
public $get_year_stats_shows_duration_title;
public $get_year_stats_music_duration_title;
public $get_year_stats_duration_sum_title;
// Wrapperr language variables
public $wrapperr_and;
public $wrapperr_play;
public $wrapperr_play_plural;
public $wrapperr_day;
public $wrapperr_day_plural;
public $wrapperr_hour;
public $wrapperr_hour_plural;
public $wrapperr_minute;
public $wrapperr_minute_plural;
public $wrapperr_second;
public $wrapperr_second_plural;
public $wrapperr_sort_plays;
public $wrapperr_sort_duration;
// 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));
// Assign values from config file
if(isset($json->tautulli_apikey)) {
$this->tautulli_apikey = $json->tautulli_apikey;
} else {
$this->tautulli_apikey = '';
}
if(isset($json->tautulli_ip)) {
$this->tautulli_ip = $json->tautulli_ip;
} else {
$this->tautulli_ip = '';
}
if(isset($json->tautulli_port)) {
$this->tautulli_port = $json->tautulli_port;
} else {
$this->tautulli_port = '';
}
if(isset($json->tautulli_length)) {
$this->tautulli_length = $json->tautulli_length;
} else {
$this->tautulli_length = 5000;
}
if(isset($json->tautulli_root)) {
$this->tautulli_root = $json->tautulli_root;
} else {
$this->tautulli_root = '';
}
if(isset($json->tautulli_libraries)) {
$this->tautulli_libraries = $json->tautulli_libraries;
} else {
$this->tautulli_libraries = '';
}
if(isset($json->tautulli_grouping)) {
$this->tautulli_grouping = $json->tautulli_grouping;
} else {
$this->tautulli_grouping = true;
}
if(isset($json->https)) {
$this->https = $json->https;
} else {
$this->https = false;
}
if(isset($json->timezone)) {
$this->timezone = $json->timezone;
} else {
$this->timezone = '';
}
if(isset($json->use_plex_auth)) {
$this->use_plex_auth = $json->use_plex_auth;
} else {
$this->use_plex_auth = true;
}
if(isset($json->use_cache)) {
$this->use_cache = $json->use_cache;
} else {
$this->use_cache = true;
}
if(isset($json->use_logs)) {
$this->use_logs = $json->use_logs;
} else {
$this->use_logs = true;
}
if(isset($json->client_id) && $json->client_id !== '') {
$this->client_id = $json->client_id;
} else {
$this->client_id = md5(rand(0,1000));
}
if(isset($json->token_encrypter) && $json->token_encrypter !== '') {
$this->token_encrypter = $json->token_encrypter;
} else {
$this->token_encrypter = md5(rand(0,1000));
}
if(isset($json->wrapperr_root)) {
$this->wrapperr_root = $json->wrapperr_root;
} else {
$this->wrapperr_root = '';
}
if(isset($json->application_name)) {
$this->application_name = $json->application_name;
} else {
$this->application_name = 'Wrapperr';
}
if(isset($json->application_url)) {
$this->application_url = $json->application_url;
} else {
$this->application_url = '';
}
if(isset($json->wrapped_start)) {
$this->wrapped_start = $json->wrapped_start;
} else {
$this->wrapped_start = 1609455600;
}
if(isset($json->wrapped_end)) {
$this->wrapped_end = $json->wrapped_end;
} else {
$this->wrapped_end = 1640991540;
}
if(isset($json->wrapperr_front_page_title)) {
$this->wrapperr_front_page_title = $json->wrapperr_front_page_title;
} else {
$this->wrapperr_front_page_title = 'Did you get that thing from Spotify and wondered what your Plex statistics looked like?';
}
if(isset($json->wrapperr_front_page_subtitle)) {
$this->wrapperr_front_page_subtitle = $json->wrapperr_front_page_subtitle;
} else {
$this->wrapperr_front_page_subtitle = 'Well, have a look...';
}
if(isset($json->stats_intro_title)) {
$this->stats_intro_title = $json->stats_intro_title;
} else {
$this->stats_intro_title = 'Hey there, {user}!';
}
if(isset($json->stats_intro_subtitle)) {
$this->stats_intro_subtitle = $json->stats_intro_subtitle;
} else {
$this->stats_intro_subtitle = 'New year, new page of statistics...';
}
if(isset($json->stats_outro_title)) {
$this->stats_outro_title = $json->stats_outro_title;
} else {
$this->stats_outro_title = 'Hope you are staying safe!';
}
if(isset($json->stats_outro_subtitle)) {
$this->stats_outro_subtitle = $json->stats_outro_subtitle;
} else {
$this->stats_outro_subtitle = 'Goodbye.';
}
if(isset($json->stats_order_by_plays)) {
$this->stats_order_by_plays = $json->stats_order_by_plays;
} else {
$this->stats_order_by_plays = true;
}
if(isset($json->stats_order_by_duration) && $this->stats_order_by_plays) {
$this->stats_order_by_duration = $json->stats_order_by_duration;
} else {
$this->stats_order_by_duration = true;
}
if(isset($json->create_share_links)) {
$this->create_share_links = $json->create_share_links;
} else {
$this->create_share_links = true;
}
// Movie values
if(isset($json->get_user_movie_stats)) {
$this->get_user_movie_stats = $json->get_user_movie_stats;
} else {
$this->get_user_movie_stats = true;
}
// Title
if(isset($json->get_user_movie_stats_title)) {
$this->get_user_movie_stats_title = $json->get_user_movie_stats_title;
} else {
$this->get_user_movie_stats_title = 'Movies!';
}
// Multiple movies subtitle
if(isset($json->get_user_movie_stats_subtitle)) {
$this->get_user_movie_stats_subtitle = $json->get_user_movie_stats_subtitle;
} else {
$this->get_user_movie_stats_subtitle = 'You watched {movie_count} movies. That\'s a lot of movies!';
}
// Multiple movies sub-subtitle
if(isset($json->get_user_movie_stats_subsubtitle)) {
$this->get_user_movie_stats_subsubtitle = $json->get_user_movie_stats_subsubtitle;
} else {
$this->get_user_movie_stats_subsubtitle = '(or not, I am pre-programmed to say that)';
}
// One movie subtitle
if(isset($json->get_user_movie_stats_subtitle_one)) {
$this->get_user_movie_stats_subtitle_one = $json->get_user_movie_stats_subtitle_one;
} else {
$this->get_user_movie_stats_subtitle_one = 'You watched one movie. You know what you like!';
}
// One movie sub-subtitle
if(isset($json->get_user_movie_stats_subsubtitle_one)) {
$this->get_user_movie_stats_subsubtitle_one = $json->get_user_movie_stats_subsubtitle_one;
} else {
$this->get_user_movie_stats_subsubtitle_one = '(at least you tried it out)';
}
// No movies subtitle
if(isset($json->get_user_movie_stats_subtitle_none)) {
$this->get_user_movie_stats_subtitle_none = $json->get_user_movie_stats_subtitle_none;
} else {
$this->get_user_movie_stats_subtitle_none = 'You watched no movies. That\'s impressive in itself!';
}
// No movies sub-subtitle
if(isset($json->get_user_movie_stats_subsubtitle_none)) {
$this->get_user_movie_stats_subsubtitle_none = $json->get_user_movie_stats_subsubtitle_none;
} else {
$this->get_user_movie_stats_subsubtitle_none = '(might wanna try it)';
}
// Top movie title
if(isset($json->get_user_movie_stats_top_movie)) {
$this->get_user_movie_stats_top_movie = $json->get_user_movie_stats_top_movie;
} else {
$this->get_user_movie_stats_top_movie = 'Your movie';
}
// Top movies title
if(isset($json->get_user_movie_stats_top_movie_plural)) {
$this->get_user_movie_stats_top_movie_plural = $json->get_user_movie_stats_top_movie_plural;
} else {
$this->get_user_movie_stats_top_movie_plural = 'Your top movies';
}
// Movie completion title
if(isset($json->get_user_movie_stats_movie_completion_title)) {
$this->get_user_movie_stats_movie_completion_title = $json->get_user_movie_stats_movie_completion_title;
} else {
$this->get_user_movie_stats_movie_completion_title = 'You saw {movie_finish_percent}% of the movie!';
}
// Movie completion title plural
if(isset($json->get_user_movie_stats_movie_completion_title_plural)) {
$this->get_user_movie_stats_movie_completion_title_plural = $json->get_user_movie_stats_movie_completion_title_plural;
} else {
$this->get_user_movie_stats_movie_completion_title_plural = 'Your average movie finishing percentage was {movie_finish_percent}%!';
}
// Movie completion subtitle
if(isset($json->get_user_movie_stats_movie_completion_subtitle)) {
$this->get_user_movie_stats_movie_completion_subtitle = $json->get_user_movie_stats_movie_completion_subtitle;
} else {
$this->get_user_movie_stats_movie_completion_subtitle = 'You\'re not watching the credits like a nerd, are you?';
}
// Movie pause title
if(isset($json->get_user_movie_stats_pause_title)) {
$this->get_user_movie_stats_pause_title = $json->get_user_movie_stats_pause_title;
} else {
$this->get_user_movie_stats_pause_title = 'Your longest movie pause was watching {movie_title}.';
}
// Movie pause subtitle
if(isset($json->get_user_movie_stats_pause_subtitle)) {
$this->get_user_movie_stats_pause_subtitle = $json->get_user_movie_stats_pause_subtitle;
} else {
$this->get_user_movie_stats_pause_subtitle = 'It was paused for {pause_duration}...';
}
// Movie pause title (one movie)
if(isset($json->get_user_movie_stats_pause_title_one)) {
$this->get_user_movie_stats_pause_title_one = $json->get_user_movie_stats_pause_title_one;
} else {
$this->get_user_movie_stats_pause_title_one = 'One movie, but you still had to pause it.';
}
// Movie pause subtitle (one movie)
if(isset($json->get_user_movie_stats_pause_subtitle_one)) {
$this->get_user_movie_stats_pause_subtitle_one = $json->get_user_movie_stats_pause_subtitle_one;
} else {
$this->get_user_movie_stats_pause_subtitle_one = 'It was paused for {pause_duration}...';
}
// Movie pause title (no pausing)
if(isset($json->get_user_movie_stats_pause_title_none)) {
$this->get_user_movie_stats_pause_title_none = $json->get_user_movie_stats_pause_title_none;
} else {
$this->get_user_movie_stats_pause_title_none = 'Bladder of steel!';
}
// Movie pause subtitle (no pausing)
if(isset($json->get_user_movie_stats_pause_subtitle_none)) {
$this->get_user_movie_stats_pause_subtitle_none = $json->get_user_movie_stats_pause_subtitle_none;
} else {
$this->get_user_movie_stats_pause_subtitle_none = 'You never paused a single movie.';
}
// Movie oldest title
if(isset($json->get_user_movie_stats_oldest_title)) {
$this->get_user_movie_stats_oldest_title = $json->get_user_movie_stats_oldest_title;
} else {
$this->get_user_movie_stats_oldest_title = 'The oldest movie you watched was {movie_title}.';
}
// Movie oldest subtitle
if(isset($json->get_user_movie_stats_oldest_subtitle)) {
$this->get_user_movie_stats_oldest_subtitle = $json->get_user_movie_stats_oldest_subtitle;
} else {
$this->get_user_movie_stats_oldest_subtitle = 'Enjoying the classics, huh?';
}
// Movie oldest (from before 1950) subtitle
if(isset($json->get_user_movie_stats_oldest_subtitle_pre_1950)) {
$this->get_user_movie_stats_oldest_subtitle_pre_1950 = $json->get_user_movie_stats_oldest_subtitle_pre_1950;
} else {
$this->get_user_movie_stats_oldest_subtitle_pre_1950 = 'I didn\'t even know they made movies back then...';
}
// Movie oldest (from before 1975) subtitle
if(isset($json->get_user_movie_stats_oldest_subtitle_pre_1975)) {
$this->get_user_movie_stats_oldest_subtitle_pre_1975 = $json->get_user_movie_stats_oldest_subtitle_pre_1975;
} else {
$this->get_user_movie_stats_oldest_subtitle_pre_1975 = 'Did it even have color?';
}
// Movie oldest (from before 2000) subtitle
if(isset($json->get_user_movie_stats_oldest_subtitle_pre_2000)) {
$this->get_user_movie_stats_oldest_subtitle_pre_2000 = $json->get_user_movie_stats_oldest_subtitle_pre_2000;
} else {
$this->get_user_movie_stats_oldest_subtitle_pre_2000 = 'Was it a 4K, UHD, 3D, Dolby Atmos remaster?';
}
// Movie spent title
if(isset($json->get_user_movie_stats_spent_title)) {
$this->get_user_movie_stats_spent_title = $json->get_user_movie_stats_spent_title;
} else {
$this->get_user_movie_stats_spent_title = 'You spent {movie_sum_duration} watching movies.';
}
// Show values
if(isset($json->get_user_show_stats)) {
$this->get_user_show_stats = $json->get_user_show_stats;
} else {
$this->get_user_show_stats = true;
}
// Get show-buddy
if(isset($json->get_user_show_stats_buddy)) {
$this->get_user_show_stats_buddy = $json->get_user_show_stats_buddy;
} else {
$this->get_user_show_stats_buddy = false;
}
// Show title
if(isset($json->get_user_show_stats_title)) {
$this->get_user_show_stats_title = $json->get_user_show_stats_title;
} else {
$this->get_user_show_stats_title = 'Shows!';
}
// Multiple shows subtitle
if(isset($json->get_user_show_stats_subtitle)) {
$this->get_user_show_stats_subtitle = $json->get_user_show_stats_subtitle;
} else {
$this->get_user_show_stats_subtitle = 'You watched {show_count} different shows.';
}
// Multiple shows sub-subtitle
if(isset($json->get_user_show_stats_subsubtitle)) {
$this->get_user_show_stats_subsubtitle = $json->get_user_show_stats_subsubtitle;
} else {
$this->get_user_show_stats_subsubtitle = '(no, watching The Office twice doesn\'t count as two shows)';
}
// One show subtitle
if(isset($json->get_user_show_stats_subtitle_one)) {
$this->get_user_show_stats_subtitle_one = $json->get_user_show_stats_subtitle_one;
} else {
$this->get_user_show_stats_subtitle_one = 'You watched one show.';
}
// One show sub-subtitle
if(isset($json->get_user_show_stats_subsubtitle_one)) {
$this->get_user_show_stats_subsubtitle_one = $json->get_user_show_stats_subsubtitle_one;
} else {
$this->get_user_show_stats_subsubtitle_one = '(better not be that same one again...)';
}
// No shows subtitle
if(isset($json->get_user_show_stats_subtitle_none)) {
$this->get_user_show_stats_subtitle_none = $json->get_user_show_stats_subtitle_none;
} else {
$this->get_user_show_stats_subtitle_none = 'You watched 0 shows. I get it, it\'s not for everyone!';
}
// No shows sub-subtitle
if(isset($json->get_user_show_stats_subsubtitle_none)) {
$this->get_user_show_stats_subsubtitle_none = $json->get_user_show_stats_subsubtitle_none;
} else {
$this->get_user_show_stats_subsubtitle_none = '(might wanna try it)';
}
// Top show title
if(isset($json->get_user_show_stats_top_show)) {
$this->get_user_show_stats_top_show = $json->get_user_show_stats_top_show;
} else {
$this->get_user_show_stats_top_show = 'Your show';
}
// Top shows title
if(isset($json->get_user_show_stats_top_show_plural)) {
$this->get_user_show_stats_top_show_plural = $json->get_user_show_stats_top_show_plural;
} else {
$this->get_user_show_stats_top_show_plural = 'Your top shows';
}
// Time spent on shows
if(isset($json->get_user_show_stats_spent_title)) {
$this->get_user_show_stats_spent_title = $json->get_user_show_stats_spent_title;
} else {
$this->get_user_show_stats_spent_title = 'You spent {show_sum_duration} watching shows.';
}
// Top episode title
if(isset($json->get_user_show_stats_most_played_title)) {
$this->get_user_show_stats_most_played_title = $json->get_user_show_stats_most_played_title;
} else {
$this->get_user_show_stats_most_played_title = 'You really liked the episode {show_episode} from {show_title}.';
}
// Top episode subtitle
if(isset($json->get_user_show_stats_most_played_subtitle)) {
$this->get_user_show_stats_most_played_subtitle = $json->get_user_show_stats_most_played_subtitle;
} else {
$this->get_user_show_stats_most_played_subtitle = 'It recieved {episode_play_sum} and was endured for {episode_duration_sum}.';
}
// Show buddy title
if(isset($json->get_user_show_stats_buddy_title)) {
$this->get_user_show_stats_buddy_title = $json->get_user_show_stats_buddy_title;
} else {
$this->get_user_show_stats_buddy_title = 'Your top show was {top_show_title}. And you\'re not alone! Your {top_show_title}-buddy is {buddy_username}!';
}
// Show buddy subtitle
if(isset($json->get_user_show_stats_buddy_subtitle)) {
$this->get_user_show_stats_buddy_subtitle = $json->get_user_show_stats_buddy_subtitle;
} else {
$this->get_user_show_stats_buddy_subtitle = 'Your combined efforts resulted in {buddy_duration_sum} of {top_show_title}!';
}
// No show buddy title
if(isset($json->get_user_show_stats_buddy_title_none)) {
$this->get_user_show_stats_buddy_title_none = $json->get_user_show_stats_buddy_title_none;
} else {
$this->get_user_show_stats_buddy_title_none = 'Your top show was {top_show_title}.';
}
// No show buddy subtitle
if(isset($json->get_user_show_stats_buddy_subtitle_none)) {
$this->get_user_show_stats_buddy_subtitle_none = $json->get_user_show_stats_buddy_subtitle_none;
} else {
$this->get_user_show_stats_buddy_subtitle_none = 'That means you dared to explore where no one else would, as you are the only viewer of that show. Spread the word!';
}
// Music values
if(isset($json->get_user_music_stats)) {
$this->get_user_music_stats = $json->get_user_music_stats;
} else {
$this->get_user_music_stats = true;
}
// Title
if(isset($json->get_user_music_stats_title)) {
$this->get_user_music_stats_title = $json->get_user_music_stats_title;
} else {
$this->get_user_music_stats_title = 'Music!';
}
// Multiple tracks subtitle
if(isset($json->get_user_music_stats_subtitle)) {
$this->get_user_music_stats_subtitle = $json->get_user_music_stats_subtitle;
} else {
$this->get_user_music_stats_subtitle = 'You listened to {track_count} different tracks.';
}
// Multiple tracks sub-subtitle
if(isset($json->get_user_music_stats_subsubtitle)) {
$this->get_user_music_stats_subsubtitle = $json->get_user_music_stats_subsubtitle;
} else {
$this->get_user_music_stats_subsubtitle = '(if you can call your taste "music"...)';
}
// One track subtitle
if(isset($json->get_user_music_stats_subtitle_one)) {
$this->get_user_music_stats_subtitle_one = $json->get_user_music_stats_subtitle_one;
} else {
$this->get_user_music_stats_subtitle_one = 'You listened to one track.';
}
// One track sub-subtitle
if(isset($json->get_user_music_stats_subsubtitle_one)) {
$this->get_user_music_stats_subsubtitle_one = $json->get_user_music_stats_subsubtitle_one;
} else {
$this->get_user_music_stats_subsubtitle_one = '(whatever floats your boat...)';
}
// No tracks subtitle
if(isset($json->get_user_music_stats_subtitle_none)) {
$this->get_user_music_stats_subtitle_none = $json->get_user_music_stats_subtitle_none;
} else {
$this->get_user_music_stats_subtitle_none = 'You listened to 0 tracks. No speakers, huh?';
}
// No tracks sub-subtitle
if(isset($json->get_user_music_stats_subsubtitle_none)) {
$this->get_user_music_stats_subsubtitle_none = $json->get_user_music_stats_subsubtitle_none;
} else {
$this->get_user_music_stats_subsubtitle_none = '(might wanna try it)';
}
// Top track title
if(isset($json->get_user_music_stats_top_track)) {
$this->get_user_music_stats_top_track = $json->get_user_music_stats_top_track;
} else {
$this->get_user_music_stats_top_track = 'Your track';
}
// Top tracks title
if(isset($json->get_user_music_stats_top_track_plural)) {
$this->get_user_music_stats_top_track_plural = $json->get_user_music_stats_top_track_plural;
} else {
$this->get_user_music_stats_top_track_plural = 'Your top tracks';
}
// Top albums title
if(isset($json->get_user_music_stats_top_album_plural)) {
$this->get_user_music_stats_top_album_plural = $json->get_user_music_stats_top_album_plural;
} else {
$this->get_user_music_stats_top_album_plural = 'Your top albums';
}
// Top artists title
if(isset($json->get_user_music_stats_top_artist_plural)) {
$this->get_user_music_stats_top_artist_plural = $json->get_user_music_stats_top_artist_plural;
} else {
$this->get_user_music_stats_top_artist_plural = 'Your top artists';
}
// Time spent on music title
if(isset($json->get_user_music_stats_spent_title)) {
$this->get_user_music_stats_spent_title = $json->get_user_music_stats_spent_title;
} else {
$this->get_user_music_stats_spent_title = 'You spent {music_sum_duration} listening to music.';
}
// Time spent on music subtitle
if(isset($json->get_user_music_stats_spent_subtitle)) {
$this->get_user_music_stats_spent_subtitle = $json->get_user_music_stats_spent_subtitle;
} else {
$this->get_user_music_stats_spent_subtitle = 'That is {music_sum_minutes}!';
}
// Oldest album title
if(isset($json->get_user_music_stats_oldest_album_title)) {
$this->get_user_music_stats_oldest_album_title = $json->get_user_music_stats_oldest_album_title;
} else {
$this->get_user_music_stats_oldest_album_title = 'The oldest album you listened to was {album_title} by {album_artist}.';
}
// Oldest album subtitle
if(isset($json->get_user_music_stats_oldest_album_subtitle)) {
$this->get_user_music_stats_oldest_album_subtitle = $json->get_user_music_stats_oldest_album_subtitle;
} else {
$this->get_user_music_stats_oldest_album_subtitle = 'How about a copy on vinyl?';
}
// Year stats title
if(isset($json->get_year_stats_title)) {
$this->get_year_stats_title = $json->get_year_stats_title;
} else {
$this->get_year_stats_title = 'Server-wide statistics!';
}
// Year stats subtitle
if(isset($json->get_year_stats_subtitle)) {
$this->get_year_stats_subtitle = $json->get_year_stats_subtitle;
} else {
$this->get_year_stats_subtitle = 'It\'s okay to feel shame if you see yourself on the list.';
}
// Server-wide stats subsubtitle
if(isset($json->get_year_stats_subsubtitle)) {
$this->get_year_stats_subsubtitle = $json->get_year_stats_subsubtitle;
} else {
$this->get_year_stats_subsubtitle = '(or if you don\'t...)';
}
// Get movie stats for server
if(isset($json->get_year_stats_movies)) {
$this->get_year_stats_movies = $json->get_year_stats_movies;
} else {
$this->get_year_stats_movies = true;
}
// Top movie stats for server title
if(isset($json->get_year_stats_movies_title)) {
$this->get_year_stats_movies_title = $json->get_year_stats_movies_title;
} else {
$this->get_year_stats_movies_title = 'Top movies';
}
// Movie stats duration sum for server title
if(isset($json->get_year_stats_movies_duration_title)) {
$this->get_year_stats_movies_duration_title = $json->get_year_stats_movies_duration_title;
} else {
$this->get_year_stats_movies_duration_title = 'All users combined spent {movie_duration_sum} watching movies.';
}
if(isset($json->get_year_stats_shows)) {
$this->get_year_stats_shows = $json->get_year_stats_shows;
} else {
$this->get_year_stats_shows = true;
}
// Top shows stats for server title
if(isset($json->get_year_stats_shows_title)) {
$this->get_year_stats_shows_title = $json->get_year_stats_shows_title;
} else {
$this->get_year_stats_shows_title = 'Top shows';
}
// Shows stats duration sum for server title
if(isset($json->get_year_stats_shows_duration_title)) {
$this->get_year_stats_shows_duration_title = $json->get_year_stats_shows_duration_title;
} else {
$this->get_year_stats_shows_duration_title = 'All users combined spent {show_duration_sum} watching shows.';
}
if(isset($json->get_year_stats_music)) {
$this->get_year_stats_music = $json->get_year_stats_music;
} else {
$this->get_year_stats_music = true;
}
// Top shows stats for server title
if(isset($json->get_year_stats_music_title)) {
$this->get_year_stats_music_title = $json->get_year_stats_music_title;
} else {
$this->get_year_stats_music_title = 'Top artists';
}
// Shows stats duration sum for server title
if(isset($json->get_year_stats_music_duration_title)) {
$this->get_year_stats_music_duration_title = $json->get_year_stats_music_duration_title;
} else {
$this->get_year_stats_music_duration_title = 'All users combined spent {music_duration_sum} listening to music.';
}
if(isset($json->get_year_stats_leaderboard)) {
$this->get_year_stats_leaderboard = $json->get_year_stats_leaderboard;
} else {
$this->get_year_stats_leaderboard = true;
}
// Top users for server title
if(isset($json->get_year_stats_leaderboard_title)) {
$this->get_year_stats_leaderboard_title = $json->get_year_stats_leaderboard_title;
} else {
$this->get_year_stats_leaderboard_title = 'Top users';
}
// Sum of duration for server title
if(isset($json->get_year_stats_duration_sum_title)) {
$this->get_year_stats_duration_sum_title = $json->get_year_stats_duration_sum_title;
} else {
$this->get_year_stats_duration_sum_title = 'That is {all_duration_sum} of content!';
}
// Language settings
if(isset($json->wrapperr_and)) {
$this->wrapperr_and = $json->wrapperr_and;
} else {
$this->wrapperr_and = 'and';
}
if(isset($json->wrapperr_play)) {
$this->wrapperr_play = $json->wrapperr_play;
} else {
$this->wrapperr_play = 'play';
}
if(isset($json->wrapperr_play_plural)) {
$this->wrapperr_play_plural = $json->wrapperr_play_plural;
} else {
$this->wrapperr_play_plural = 'plays';
}
if(isset($json->wrapperr_day)) {
$this->wrapperr_day = $json->wrapperr_day;
} else {
$this->wrapperr_day = 'day';
}
if(isset($json->wrapperr_day_plural)) {
$this->wrapperr_day_plural = $json->wrapperr_day_plural;
} else {
$this->wrapperr_day_plural = 'days';
}
if(isset($json->wrapperr_hour)) {
$this->wrapperr_hour = $json->wrapperr_hour;
} else {
$this->wrapperr_hour = 'hour';
}
if(isset($json->wrapperr_hour_plural)) {
$this->wrapperr_hour_plural = $json->wrapperr_hour_plural;
} else {
$this->wrapperr_hour_plural = 'hours';
}
if(isset($json->wrapperr_minute)) {
$this->wrapperr_minute = $json->wrapperr_minute;
} else {
$this->wrapperr_minute = 'minute';
}
if(isset($json->wrapperr_minute_plural)) {
$this->wrapperr_minute_plural = $json->wrapperr_minute_plural;
} else {
$this->wrapperr_minute_plural = 'minutes';
}
if(isset($json->wrapperr_second)) {
$this->wrapperr_second = $json->wrapperr_second;
} else {
$this->wrapperr_second = 'second';
}
if(isset($json->wrapperr_second_plural)) {
$this->wrapperr_second_plural = $json->wrapperr_second_plural;
} else {
$this->wrapperr_second_plural = 'seconds';
}
if(isset($json->wrapperr_sort_plays)) {
$this->wrapperr_sort_plays = $json->wrapperr_sort_plays;
} else {
$this->wrapperr_sort_plays = 'Sort by plays';
}
if(isset($json->wrapperr_sort_duration)) {
$this->wrapperr_sort_duration = $json->wrapperr_sort_duration;
} else {
$this->wrapperr_sort_duration = 'Sort by duration';
}
}
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 is_configured() {
// Make sure an admin is created
require_once(dirname(__FILE__) . '/admin.php');
$admin = new Admin();
if(!$admin->is_configured()) {
return false;
}
if($this->tautulli_ip !== '' && $this->tautulli_apikey !== '' && $this->tautulli_length !== '' && $this->timezone !== '' && $this->wrapped_start !== '' && $this->wrapped_end !== '')
return true;
else {
return false;
}
}
public function save_config($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();
}
}
// 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;
}
}
}
?>

View file

@ -1,63 +0,0 @@
<?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;
}
}

View file

@ -1,64 +0,0 @@
<?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, 'Wrapperr');
fclose($create_log);
}
$fail = false;
try {
// Assign use_logs configuration from config
include_once(dirname(__FILE__, 3) . '/api/objects/config.php');
$config = new Config();
$this->use_logs = $config->use_logs;
} catch (Exception $e) {
$fail = true;
}
if($fail) {
$this->use_logs = false;
}
}
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 . 'TIME: ' . $date . "\t" . 'API: ' . $function . "\t" . 'ID: ' . $id . "\t" . 'MESSAGE: ' . $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,79 +0,0 @@
<?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/log.php');
require(dirname(__FILE__) . '/objects/admin.php');
// Create variables
$admin = new Admin();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data) || !isset($data->cookie) || !isset($data->password) || !isset($data->username)) {
// Log use
$log->log_activity('set_admin.php', 'unknown', 'No input provided.');
echo json_encode(array("error" => true, "message" => "No input provided."));
exit(0);
}
// Remove potential harmfull input
$cookie = htmlspecialchars($data->cookie);
$password = $data->password;
$username = $data->username;
// Check if confgiured
if(!$admin->is_configured()) {
// Log use
$log->log_activity('set_admin.php', 'unknown', 'Wrapperr admin is not configured. Can\'t update admin configuration.');
echo json_encode(array("error" => true, "message" => "Wrapperr admin is not configured."));
exit(0);
}
// Decrypt cookie
$cookie_object = json_decode($admin->decrypt_cookie($cookie));
// Validate admin cookie
if(!$admin->validate_cookie($cookie_object)) {
// Log use
$log->log_activity('set_admin.php', 'unknown', 'Admin cookie not valid. Can\'t update admin configuration.');
echo json_encode(array("error" => true, "message" => "Admin cookie not accepted. Log in again."));
exit(0);
}
$admin->username = $username;
$admin->password = password_hash($password, PASSWORD_DEFAULT);
if($admin->save_admin()) {
// Log use
$log->log_activity('set_admin.php', 'admin', 'Updated and saved admin configuration.');
echo json_encode(array("error" => false, "message" => "Admin account updated."));
exit(0);
} else {
// Log use
$log->log_activity('set_admin.php', 'admin', 'Could not save admin configuration. Is the \'config\' directory writable?');
echo json_encode(array("error" => true, "message" => "Admin account was not updated. Is the \'config\' directory writable?"));
exit(0);
}
?>

View file

@ -1,279 +0,0 @@
<?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/admin.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$config = new Config();
$admin = new Admin();
$log = new Log();
$data = json_decode(file_get_contents("php://input"));
// If POST data is empty
if(empty($data) || !isset($data->cookie) || !isset($data->data_type)) {
// Log use
$log->log_activity('set_config.php', 'unknown', 'No admin login input provided.');
echo json_encode(array("error" => true, "message" => "Invalid input provided."));
exit(0);
}
// Remove potential harmfull input
$cookie = htmlspecialchars($data->cookie);
// Check if confgiured
if(!$admin->is_configured()) {
// Log use
$log->log_activity('set_config.php', 'unknown', 'Wrapperr admin is not configured.');
echo json_encode(array("error" => true, "message" => "Wrapperr admin is not configured."));
exit(0);
}
// Decrypt cookie
$cookie_object = json_decode($admin->decrypt_cookie($cookie));
// Validate admin cookie
if(!$admin->validate_cookie($cookie_object)) {
// Log use
$log->log_activity('set_config.php', 'unknown', 'Admin cookie not valid.');
echo json_encode(array("error" => true, "message" => "Admin cookie not accepted. Log in again."));
exit(0);
} else {
// Call save function
save_config($data->data, $data->data_type, $data->clear_cache);
}
// Retrieve data and save it
function save_config($data, $data_type, $clear_cache) {
global $config;
global $log;
if($data_type === 'tautulli_settings') {
$fail = false;
try {
$config->tautulli_apikey = $data->tautulli_apikey;
$config->tautulli_ip = $data->tautulli_ip;
$config->tautulli_port = $data->tautulli_port;
$config->tautulli_length = $data->tautulli_length;
$config->tautulli_root = $data->tautulli_root;
$config->tautulli_libraries = $data->tautulli_libraries;
$config->tautulli_grouping = $data->tautulli_grouping;
$config->https = $data->https;
} catch (Exception $e) {
$fail = true;
}
if($fail) {
// Log use
$log->log_activity('set_config.php', 'admin', 'Failed to assign variables from data. Data type: ' . $data_type . '.');
echo json_encode(array("error" => true, "message" => "Failed to assign variables from data."));
exit(0);
}
} else if($data_type === 'wrapperr_settings') {
// Confirm timezone in config is valid
$timezone_identifiers = DateTimeZone::listIdentifiers();
$valid_timezone = false;
for ($i = 0; $i < count($timezone_identifiers); $i++) {
if($data->timezone === $timezone_identifiers[$i]) {
$valid_timezone = true;
break;
}
}
// Set time-zone to the one configured or return error
if(!$valid_timezone) {
// Log activity
$log->log_activity('set_config.php', 'admin', 'Timezone not accepted. Not found in \'DateTimeZone::listIdentifiers()\' list.');
echo json_encode(array("message" => "Configured timezone invalid.", "error" => true));
exit(0);
}
$fail = false;
try {
$config->use_plex_auth = $data->use_plex_auth;
$config->use_cache = $data->use_cache;
$config->use_logs = $data->use_logs;
$config->wrapperr_root = $data->wrapperr_root;
$config->application_name = $data->application_name;
$config->application_url = $data->application_url;
$config->create_share_links = $data->create_share_links;
$config->timezone = $data->timezone;
} catch (Exception $e) {
$fail = true;
}
if($fail) {
// Log use
$log->log_activity('set_config.php', 'admin', 'Failed to assign variables from data. Data type: ' . $data_type . '.');
echo json_encode(array("error" => true, "message" => "Failed to assign variables from data."));
exit(0);
}
} else if($data_type === 'wrapperr_customization') {
$fail = false;
try {
$config->wrapped_start = $data->wrapped_start;
$config->wrapped_end = $data->wrapped_end;
$config->wrapperr_front_page_title = $data->wrapperr_front_page_title;
$config->wrapperr_front_page_subtitle = $data->wrapperr_front_page_subtitle;
$config->stats_intro_title = $data->stats_intro_title;
$config->stats_intro_subtitle = $data->stats_intro_subtitle;
$config->stats_outro_title = $data->stats_outro_title;
$config->stats_outro_subtitle = $data->stats_outro_subtitle;
$config->stats_order_by_plays = $data->stats_order_by_plays;
$config->stats_order_by_duration = $data->stats_order_by_duration;
$config->get_user_movie_stats = $data->get_user_movie_stats;
$config->get_user_movie_stats_title = $data->get_user_movie_stats_title;
$config->get_user_movie_stats_subtitle = $data->get_user_movie_stats_subtitle;
$config->get_user_movie_stats_subsubtitle = $data->get_user_movie_stats_subsubtitle;
$config->get_user_movie_stats_subtitle_one = $data->get_user_movie_stats_subtitle_one;
$config->get_user_movie_stats_subsubtitle_one = $data->get_user_movie_stats_subsubtitle_one;
$config->get_user_movie_stats_subtitle_none = $data->get_user_movie_stats_subtitle_none;
$config->get_user_movie_stats_subsubtitle_none = $data->get_user_movie_stats_subsubtitle_none;
$config->get_user_movie_stats_top_movie = $data->get_user_movie_stats_top_movie;
$config->get_user_movie_stats_top_movie_plural = $data->get_user_movie_stats_top_movie_plural;
$config->get_user_movie_stats_movie_completion_title = $data->get_user_movie_stats_movie_completion_title;
$config->get_user_movie_stats_movie_completion_title_plural = $data->get_user_movie_stats_movie_completion_title_plural;
$config->get_user_movie_stats_movie_completion_subtitle = $data->get_user_movie_stats_movie_completion_subtitle;
$config->get_user_movie_stats_pause_title = $data->get_user_movie_stats_pause_title;
$config->get_user_movie_stats_pause_subtitle = $data->get_user_movie_stats_pause_subtitle;
$config->get_user_movie_stats_pause_title_one = $data->get_user_movie_stats_pause_title_one;
$config->get_user_movie_stats_pause_subtitle_one = $data->get_user_movie_stats_pause_subtitle_one;
$config->get_user_movie_stats_pause_title_none = $data->get_user_movie_stats_pause_title_none;
$config->get_user_movie_stats_pause_subtitle_none = $data->get_user_movie_stats_pause_subtitle_none;
$config->get_user_movie_stats_oldest_title = $data->get_user_movie_stats_oldest_title;
$config->get_user_movie_stats_oldest_subtitle = $data->get_user_movie_stats_oldest_subtitle;
$config->get_user_movie_stats_oldest_subtitle_pre_1950 = $data->get_user_movie_stats_oldest_subtitle_pre_1950;
$config->get_user_movie_stats_oldest_subtitle_pre_1975 = $data->get_user_movie_stats_oldest_subtitle_pre_1975;
$config->get_user_movie_stats_oldest_subtitle_pre_2000 = $data->get_user_movie_stats_oldest_subtitle_pre_2000;
$config->get_user_movie_stats_spent_title = $data->get_user_movie_stats_spent_title;
$config->get_user_show_stats = $data->get_user_show_stats;
$config->get_user_show_stats_buddy = $data->get_user_show_stats_buddy;
$config->get_user_show_stats_title = $data->get_user_show_stats_title;
$config->get_user_show_stats_subtitle = $data->get_user_show_stats_subtitle;
$config->get_user_show_stats_subsubtitle = $data->get_user_show_stats_subsubtitle;
$config->get_user_show_stats_subtitle_one = $data->get_user_show_stats_subtitle_one;
$config->get_user_show_stats_subsubtitle_one = $data->get_user_show_stats_subsubtitle_one;
$config->get_user_show_stats_subtitle_none = $data->get_user_show_stats_subtitle_none;
$config->get_user_show_stats_subsubtitle_none = $data->get_user_show_stats_subsubtitle_none;
$config->get_user_show_stats_top_show = $data->get_user_show_stats_top_show;
$config->get_user_show_stats_top_show_plural = $data->get_user_show_stats_top_show_plural;
$config->get_user_show_stats_spent_title = $data->get_user_show_stats_spent_title;
$config->get_user_show_stats_most_played_title = $data->get_user_show_stats_most_played_title;
$config->get_user_show_stats_most_played_subtitle = $data->get_user_show_stats_most_played_subtitle;
$config->get_user_show_stats_buddy_title = $data->get_user_show_stats_buddy_title;
$config->get_user_show_stats_buddy_subtitle = $data->get_user_show_stats_buddy_subtitle;
$config->get_user_show_stats_buddy_title_none = $data->get_user_show_stats_buddy_title_none;
$config->get_user_show_stats_buddy_subtitle_none = $data->get_user_show_stats_buddy_subtitle_none;
$config->get_user_music_stats = $data->get_user_music_stats;
$config->get_user_music_stats_title = $data->get_user_music_stats_title;
$config->get_user_music_stats_subtitle = $data->get_user_music_stats_subtitle;
$config->get_user_music_stats_subsubtitle = $data->get_user_music_stats_subsubtitle;
$config->get_user_music_stats_subtitle_one = $data->get_user_music_stats_subtitle_one;
$config->get_user_music_stats_subsubtitle_one = $data->get_user_music_stats_subsubtitle_one;
$config->get_user_music_stats_subtitle_none = $data->get_user_music_stats_subtitle_none;
$config->get_user_music_stats_subsubtitle_none = $data->get_user_music_stats_subsubtitle_none;
$config->get_user_music_stats_top_track = $data->get_user_music_stats_top_track;
$config->get_user_music_stats_top_track_plural = $data->get_user_music_stats_top_track_plural;
$config->get_user_music_stats_top_album_plural = $data->get_user_music_stats_top_album_plural;
$config->get_user_music_stats_top_artist_plural = $data->get_user_music_stats_top_artist_plural;
$config->get_user_music_stats_spent_title = $data->get_user_music_stats_spent_title;
$config->get_user_music_stats_spent_subtitle = $data->get_user_music_stats_spent_subtitle;
$config->get_user_music_stats_oldest_album_title = $data->get_user_music_stats_oldest_album_title;
$config->get_user_music_stats_oldest_album_subtitle = $data->get_user_music_stats_oldest_album_subtitle;
$config->get_year_stats_title = $data->get_year_stats_title;
$config->get_year_stats_subtitle = $data->get_year_stats_subtitle;
$config->get_year_stats_subsubtitle = $data->get_year_stats_subsubtitle;
$config->get_year_stats_movies = $data->get_year_stats_movies;
$config->get_year_stats_movies_title = $data->get_year_stats_movies_title;
$config->get_year_stats_movies_duration_title = $data->get_year_stats_movies_duration_title;
$config->get_year_stats_shows = $data->get_year_stats_shows;
$config->get_year_stats_shows_title = $data->get_year_stats_shows_title;
$config->get_year_stats_shows_duration_title = $data->get_year_stats_shows_duration_title;
$config->get_year_stats_music = $data->get_year_stats_music;
$config->get_year_stats_music_title = $data->get_year_stats_music_title;
$config->get_year_stats_music_duration_title = $data->get_year_stats_music_duration_title;
$config->get_year_stats_leaderboard = $data->get_year_stats_leaderboard;
$config->get_year_stats_leaderboard_title = $data->get_year_stats_leaderboard_title;
$config->get_year_stats_duration_sum_title = $data->get_year_stats_duration_sum_title;
$config->wrapperr_and = $data->wrapperr_and;
$config->wrapperr_play = $data->wrapperr_play;
$config->wrapperr_play_plural = $data->wrapperr_play_plural;
$config->wrapperr_day = $data->wrapperr_day;
$config->wrapperr_day_plural = $data->wrapperr_day_plural;
$config->wrapperr_hour = $data->wrapperr_hour;
$config->wrapperr_hour_plural = $data->wrapperr_hour_plural;
$config->wrapperr_minute = $data->wrapperr_minute;
$config->wrapperr_minute_plural = $data->wrapperr_minute_plural;
$config->wrapperr_second = $data->wrapperr_second;
$config->wrapperr_second_plural = $data->wrapperr_second_plural;
$config->wrapperr_sort_plays = $data->wrapperr_sort_plays;
$config->wrapperr_sort_duration = $data->wrapperr_sort_duration;
} catch (Exception $e) {
$fail = true;
}
if($fail) {
// Log use
$log->log_activity('set_config.php', 'admin', 'Failed to assign variables from data. Data type: ' . $data_type . '.');
echo json_encode(array("error" => true, "message" => "Failed to assign variables from data."));
exit(0);
}
} else {
// Log use
$log->log_activity('set_config.php', 'admin', 'Invalid data type for saving config. Data type: ' . $data_type . '.');
echo json_encode(array("error" => true, "message" => "Invalid data type for saving config."));
exit(0);
}
// Call function to save data
if($config->save_config($clear_cache)) {
// Log use
$log->log_activity('set_config.php', 'admin', 'New config was saved. Data type: ' . $data_type . '.');
echo json_encode(array("error" => false, "message" => "Changes saved."));
exit(0);
} else {
// Log use
$log->log_activity('set_config.php', 'admin', 'Changes were not saved.');
echo json_encode(array("error" => true, "message" => "Changes were not saved. Is the directory 'config' writable?"));
exit(0);
}
}
?>

View file

@ -1,52 +0,0 @@
<?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/admin.php');
require(dirname(__FILE__) . '/objects/log.php');
// Create variables
$admin = new Admin();
$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_admin_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);
// Decrypt cookie
$cookie_object = json_decode($admin->decrypt_cookie($cookie));
// Validate admin cookie
if(!$admin->validate_cookie($cookie_object)) {
// Log use
$log->log_activity('validate_login_admin_cookie.php', 'unknown', 'Admin cookie not valid.');
echo json_encode(array("error" => true, "message" => "Admin cookie not accepted. Log in again."));
exit(0);
}
// Log use
$log->log_activity('validate_login_admin_cookie.php', 'admin', 'Admin login cookie accepted.');
// Print cookie and exit
echo json_encode(array("error" => false, "message" => "Cookie is valid."));
exit(0);
?>

View file

@ -1,52 +0,0 @@
<?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, 'Wrapperr login cookie accepted.');
// Print cookie and exit
echo json_encode(array("error" => false, "message" => "Cookie is valid."));
exit(0);
?>

View file

@ -1 +0,0 @@
Require all denied

1
config/README.md Normal file
View file

@ -0,0 +1 @@
This directory is utilized by Wrapperr and filled with configuration files, logs and data.

108
config_default.json Normal file
View file

@ -0,0 +1,108 @@
{
"wrapperr_customize": {
"wrapperr_front_page_title":"Did you get that thing from Spotify and wondered what your Plex statistics looked like?",
"wrapperr_front_page_subtitle":"Well, have a look...",
"stats_intro_title":"Hey there, {user}!",
"stats_intro_subtitle":"New year, new page of statistics. 2022 let's go...",
"stats_outro_title":"Hope you are staying safe!",
"stats_outro_subtitle":"Goodbye.",
"stats_order_by_plays":true,
"stats_order_by_duration":true,
"get_user_movie_stats":true,
"get_user_movie_stats_title":"Movies!",
"get_user_movie_stats_subtitle":"You watched {movie_count} movies. That's a lot of movies!",
"get_user_movie_stats_subsubtitle":"(or not, I am pre-programmed to say that)",
"get_user_movie_stats_subtitle_one":"You watched one movie. You know what you like!",
"get_user_movie_stats_subsubtitle_one":"(at least you tried it out)",
"get_user_movie_stats_subtitle_none":"You watched no movies. That's impressive in itself!",
"get_user_movie_stats_subsubtitle_none":"(might wanna try it)",
"get_user_movie_stats_top_movie":"Your movie",
"get_user_movie_stats_top_movie_plural":"Your top movies",
"get_user_movie_stats_movie_completion_title":"You saw {movie_finish_percent}% of the movie!",
"get_user_movie_stats_movie_completion_title_plural":"Your average movie finishing percentage was {movie_finish_percent}%!",
"get_user_movie_stats_movie_completion_subtitle":"You're not watching the credits like a nerd, are you?",
"get_user_movie_stats_pause_title":"Your longest movie pause was watching {movie_title}.",
"get_user_movie_stats_pause_subtitle":"It was paused for {pause_duration}...",
"get_user_movie_stats_pause_title_one":"One movie, but you still had to pause it.",
"get_user_movie_stats_pause_subtitle_one":"It was paused for {pause_duration}...",
"get_user_movie_stats_pause_title_none":"Bladder of steel!",
"get_user_movie_stats_pause_subtitle_none":"You never paused a single movie.",
"get_user_movie_stats_oldest_title":"The oldest movie you watched was {movie_title}.",
"get_user_movie_stats_oldest_subtitle":"Enjoying the classics, huh?",
"get_user_movie_stats_oldest_subtitle_pre_1950":"I didn't even know they made movies back then...",
"get_user_movie_stats_oldest_subtitle_pre_1975":"Did it even have color?",
"get_user_movie_stats_oldest_subtitle_pre_2000":"Was it a 4K, UHD, 3D, Dolby Atmos remaster?",
"get_user_movie_stats_spent_title":"You spent {movie_sum_duration} watching movies.",
"get_user_show_stats":true,
"get_user_show_buddy":true,
"get_user_show_stats_title":"Shows!",
"get_user_show_stats_subtitle":"You watched {show_count} different shows.",
"get_user_show_stats_subsubtitle":"(no, watching The Office twice doesn't count as two shows)",
"get_user_show_stats_subtitle_one":"You watched one show.",
"get_user_show_stats_subsubtitle_one":"(better not be that same one again...)",
"get_user_show_stats_subtitle_none":"You watched 0 shows. I get it, it's not for everyone!",
"get_user_show_stats_subsubtitle_none":"(might wanna try it)",
"get_user_show_stats_top_show":"Your show",
"get_user_show_stats_top_show_plural":"Your top shows",
"get_user_show_stats_spent_title":"You spent {show_sum_duration} watching shows.",
"get_user_show_stats_most_played_title":"You really liked the episode {show_episode} from {show_title}.",
"get_user_show_stats_most_played_subtitle":"It recieved {episode_play_sum} and was endured for {episode_duration_sum}.",
"get_user_show_stats_buddy_title":"Your top show was {top_show_title}. And you're not alone! Your {top_show_title}-buddy is {buddy_username}!",
"get_user_show_stats_buddy_subtitle":"Your combined efforts resulted in {buddy_duration_sum} of {top_show_title}!",
"get_user_show_stats_buddy_title_none":"Your top show was {top_show_title}.",
"get_user_show_stats_buddy_subtitle_none":"That means you dared to explore where no one else would, as you are the only viewer of that show. Spread the word!",
"get_user_music_stats":true,
"get_user_music_stats_title":"Music!",
"get_user_music_stats_subtitle":"You listened to {track_count} different tracks.",
"get_user_music_stats_subsubtitle":"(if you can call your taste \"music\"...)",
"get_user_music_stats_subtitle_one":"You listened to one track.",
"get_user_music_stats_subsubtitle_one":"(whatever floats your boat...)",
"get_user_music_stats_subtitle_none":"You listened to 0 tracks. No speakers, huh?",
"get_user_music_stats_subsubtitle_none":"(might wanna try it)",
"get_user_music_stats_top_track":"Your track",
"get_user_music_stats_top_track_plural":"Your top tracks",
"get_user_music_stats_top_album_plural":"Your top albums",
"get_user_music_stats_top_artist_plural":"Your top artists",
"get_user_music_stats_spent_title":"You spent {music_sum_duration} listening to music.",
"get_user_music_stats_spent_subtitle":"That is {music_sum_minutes} minutes!",
"get_user_music_stats_oldest_album_title":"The oldest album you listened to was {album_title} by {album_artist}.",
"get_user_music_stats_oldest_album_subtitle":"How about a copy on vinyl?",
"get_year_stats_title":"Server-wide statistics!",
"get_year_stats_subtitle":"It's okay to feel shame if you see yourself on the list.",
"get_year_stats_subsubtitle":"(or if you don't...)",
"get_year_stats_movies":true,
"get_year_stats_movies_title":"Top movies",
"get_year_stats_shows":true,
"get_year_stats_shows_title":"Top shows",
"get_year_stats_music":true,
"get_year_stats_music_title":"Top artists",
"get_year_stats_leaderboard":true,
"get_year_stats_leaderboard_title":"Top users",
"get_year_stats_movies_duration_title":"All users combined spent {movie_duration_sum} watching movies.",
"get_year_stats_shows_duration_title":"All users combined spent {show_duration_sum} watching shows.",
"get_year_stats_music_duration_title":"All users combined spent {music_duration_sum} listening to music.",
"get_year_stats_duration_sum_title":"That is {all_duration_sum} of content!",
"wrapperr_and":"and",
"wrapperr_play":"play",
"wrapperr_play_plural":"plays",
"wrapperr_day":"day",
"wrapperr_day_plural":"days",
"wrapperr_hour":"hour",
"wrapperr_hour_plural":"hours",
"wrapperr_minute":"minute",
"wrapperr_minute_plural":"minutes",
"wrapperr_second":"second",
"wrapperr_second_plural":"seconds",
"wrapperr_sort_plays":"Sort by plays",
"wrapperr_sort_duration":"Sort by duration",
"get_user_show_stats_buddy":true
},
"tautulli_config": {
"tautulli_port": 80,
"tautulli_length": 5000
},
"wrapped_start": 1640991600,
"wrapped_end": 1672527540,
"wrapperr_port" : 8282,
"application_name" : "Wrapperr"
}

View file

@ -1,7 +0,0 @@
FROM php:7.4-apache
LABEL maintainer="Wrapperr https://github.com/aunefyren/wrapperr"
RUN apt-get update && apt-get install -y \
git
RUN rm -d -r /var/www/html
RUN git clone https://github.com/aunefyren/wrapperr --branch v2.2.4 /var/www/html
RUN chmod -R 0777 /var/www/html/config

90
file_admin.go Normal file
View file

@ -0,0 +1,90 @@
package main
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"os"
"path/filepath"
)
var admin_config_path, _ = filepath.Abs("./config/admin.json")
// Check if the config file has been configured for usage
func GetAdminState() (bool, error) {
// Retrieve config object from function
admin_config, err := GetAdminConfig()
if err != nil {
log.Println("Admin config state retrival threw error.")
return false, err
}
// Check if certain parameters are set. These are essential paramteres the user must configure for basic functionality.
if admin_config.AdminUsername != "" && admin_config.AdminPassword != "" {
return true, nil
} else {
return false, nil
}
}
// Saves the given admin config struct as admin.json
func SaveAdminConfig(config AdminConfig) error {
file, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(admin_config_path, file, 0644)
if err != nil {
return err
}
return nil
}
// Read the config file and return the file as an object
func GetAdminConfig() (*AdminConfig, error) {
// Create admin.json if it doesn't exist
if _, err := os.Stat(admin_config_path); errors.Is(err, os.ErrNotExist) {
log.Println("Admin config file does not exist. Creating.")
err := CreateAdminConfigFile()
if err != nil {
return nil, err
}
}
// Load config file for alterations, information
file, err := os.Open(admin_config_path)
if err != nil {
log.Println("Admin config opening threw error.")
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
admin_config := AdminConfig{}
err = decoder.Decode(&admin_config)
if err != nil {
log.Println("Admin config parsing threw error.")
return nil, err
}
return &admin_config, nil
}
// Creates empty admin.json
func CreateAdminConfigFile() error {
var admin_config AdminConfig
err := SaveAdminConfig(admin_config)
if err != nil {
return err
}
return nil
}

86
file_cache.go Normal file
View file

@ -0,0 +1,86 @@
package main
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
)
var cache_path, _ = filepath.Abs("./config/cache.json")
// Saves the given config struct as cache.json
func SaveCache(cache *[]WrapperrDay) error {
file, err := json.MarshalIndent(cache, "", " ")
if err != nil {
return err
}
err = os.WriteFile(cache_path, file, 0644)
if err != nil {
return err
}
return nil
}
// Saves an empty cache, clearing any data present
func ClearCache() error {
cache := []WrapperrDay{}
err := SaveCache(&cache)
if err != nil {
return err
}
return nil
}
// Creates empty cache.json
func CreateCacheFile() error {
var cache []WrapperrDay
err := SaveCache(&cache)
if err != nil {
return err
}
return nil
}
// Read the cache file and return the file as an object
func GetCache() ([]WrapperrDay, error) {
// Create cache.json if it doesn't exist
if _, err := os.Stat(cache_path); errors.Is(err, os.ErrNotExist) {
log.Println("Failed to load cache file.")
err = CreateCacheFile()
if err != nil {
log.Println("Failed to create new cache file. Restarting Wrapperr.")
err = RestartSelf()
if err != nil {
return nil, err
}
}
}
// Load cache file for alterations, information
var cache []WrapperrDay
file, err := os.Open(cache_path)
if err != nil {
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
err = decoder.Decode(&cache)
if err != nil {
return nil, err
}
// Return cache object
return cache, nil
}

582
file_config.go Normal file
View file

@ -0,0 +1,582 @@
package main
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"github.com/google/uuid"
)
var wrapperr_version_parameter = "v3.0.0"
var config_path, _ = filepath.Abs("./config/config.json")
var default_config_path, _ = filepath.Abs("./config_default.json")
const minSecretKeySize = 32
// Check if the config file has been configured for usage
func GetConfigState() (bool, error) {
// Check if an admin is configured. Wrapperr must be claimed by an admin to function.
admin, err := GetAdminState()
if err != nil {
log.Println("Get config state threw error trying to validate admin state.")
return false, err
} else if !admin {
return false, nil
}
// Retrieve config object from function
config, err := GetConfig()
if err != nil {
log.Println("Get config state threw error trying to retrieve config.")
return false, err
}
// Check if certain parameters are set. These are essential paramteres the user must configure for basic functionality.
if config.TautulliConfig.TautulliApiKey != "" && config.TautulliConfig.TautulliIP != "" && config.TautulliConfig.TautulliLength != 0 && config.Timezone != "" && config.WrappedStart != 0 && config.WrappedEnd != 0 && config.WrapperrVersion != "" {
return true, nil
} else {
return false, nil
}
}
// Get private key from the config file
func GetPrivateKey() (string, error) {
// Retrieve config object from function
config, err := GetConfig()
if err != nil {
return "", err
}
// Get variable from env file
var private_key = config.PrivateKey
if len(config.PrivateKey) < minSecretKeySize {
return "", errors.New("Invalid private key size in configuration file.")
}
// If private key is not empty, return it.
// If empty create a new one, write to file, and return the new one
if private_key != "" {
return private_key, nil
} else {
NewPrivateKey, err := UpdatePrivateKey()
if err != nil {
return "", err
} else {
return NewPrivateKey, nil
}
}
}
// Update private key to random string
func UpdatePrivateKey() (string, error) {
// Retrieve config object from function
config, err := GetConfig()
if err != nil {
return "", err
}
// Get variable from env file
config.PrivateKey = uuid.New().String()
// Save new config
err = SaveConfig(config)
if err != nil {
return "", err
}
// Return empty error
return config.PrivateKey, nil
}
// Saves the given config struct as config.json
func SaveConfig(config *WrapperrConfig) error {
file, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
err = os.WriteFile(config_path, file, 0644)
if err != nil {
return err
}
return nil
}
// Creates empty config.json
func CreateConfigFile() error {
var config WrapperrConfig
// Define default boolean values since they are harder to seperate from deliberate boolean values
config.UseCache = true
config.PlexAuth = true
config.UseLogs = true
config.TautulliConfig.TautulliGrouping = true
config.CreateShareLinks = true
config.WinterTheme = true
config.WrapperrCustomize.StatsOrderByDuration = true
config.WrapperrCustomize.StatsOrderByPlays = true
config.WrapperrCustomize.GetUserMovieStats = true
config.WrapperrCustomize.GetUserShowStats = true
config.WrapperrCustomize.GetUserShowBuddy = true
config.WrapperrCustomize.GetUserMusicStats = true
config.WrapperrCustomize.GetYearStatsMovies = true
config.WrapperrCustomize.GetYearStatsShows = true
config.WrapperrCustomize.GetYearStatsMusic = true
config.WrapperrCustomize.GetYearStatsLeaderboard = true
err := SaveConfig(&config)
if err != nil {
return err
}
return nil
}
// Read the config file and return the file as an object
func GetConfig() (*WrapperrConfig, error) {
// Create config.json if it doesn't exist
if _, err := os.Stat(config_path); errors.Is(err, os.ErrNotExist) {
log.Println("Config file does not exist. Creating.")
err := CreateConfigFile()
if err != nil {
return nil, err
}
}
file, err := os.Open(config_path)
if err != nil {
log.Println("Get config file threw error trying to open the file.")
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
config := WrapperrConfig{}
err = decoder.Decode(&config)
if err != nil {
log.Println("Get config file threw error trying to parse the file.")
return nil, err
}
file, err = os.Open(default_config_path)
if err != nil {
log.Println("Get config file threw error trying to open the template file.")
return nil, err
}
defer file.Close()
decoder = json.NewDecoder(file)
config_default := WrapperrConfig{}
err = decoder.Decode(&config_default)
if err != nil {
log.Println("Get config file threw error trying to parse the template file.")
return nil, err
}
// Update the Wrapperr version the config file is created for
if config.WrapperrVersion != wrapperr_version_parameter {
config.WrapperrVersion = wrapperr_version_parameter
}
// Create a new Client Key if there is none
if config.ClientKey == "" {
config.ClientKey = uuid.New().String()
}
// Create a new Private Key if there is none
if config.PrivateKey == "" {
config.PrivateKey = uuid.New().String()
}
// If there is no application name, set it to Wrapperr (defined in the default template)
if config.ApplicationName == "" {
config.ApplicationName = config_default.ApplicationName
}
// If there is no application name, set it to Wrapperr (defined in the default template)
if config.WrapperrPort == 0 {
config.WrapperrPort = config_default.WrapperrPort
}
// Set Wrapperr start time to jan if there is no time
if config.WrappedStart == 0 {
config.WrappedStart = config_default.WrappedStart
}
// Set Wrapperr end time to dec if there is no time
if config.WrappedEnd == 0 {
config.WrappedEnd = config_default.WrappedEnd // If no start time, set to 31 Dec
}
// Set Tautulli length to 5000 if none is set
if config.TautulliConfig.TautulliLength == 0 {
config.TautulliConfig.TautulliLength = config_default.TautulliConfig.TautulliLength
}
// Set Tautulli port to 80 if none is set
if config.TautulliConfig.TautulliPort == 0 {
config.TautulliConfig.TautulliPort = config_default.TautulliConfig.TautulliPort
}
if config.WrapperrCustomize.WrapperrFrontPageTitle == "" {
config.WrapperrCustomize.WrapperrFrontPageTitle = config_default.WrapperrCustomize.WrapperrFrontPageTitle
}
if config.WrapperrCustomize.WrapperrFrontPageSubtitle == "" {
config.WrapperrCustomize.WrapperrFrontPageSubtitle = config_default.WrapperrCustomize.WrapperrFrontPageSubtitle
}
if config.WrapperrCustomize.StatsIntroTitle == "" {
config.WrapperrCustomize.StatsIntroTitle = config_default.WrapperrCustomize.StatsIntroTitle // If no intro title string, set to default intro title
}
if config.WrapperrCustomize.StatsIntroSubtitle == "" {
config.WrapperrCustomize.StatsIntroSubtitle = config_default.WrapperrCustomize.StatsIntroSubtitle // If no intro subtitle string, set to default intro subtitle
}
if config.WrapperrCustomize.StatsOutroTitle == "" {
config.WrapperrCustomize.StatsOutroTitle = config_default.WrapperrCustomize.StatsOutroTitle // If no outro title string, set to default outro title
}
if config.WrapperrCustomize.StatsOutroSubtitle == "" {
config.WrapperrCustomize.StatsOutroSubtitle = config_default.WrapperrCustomize.StatsOutroSubtitle // If no outro subtitle string, set to default outro subtitle
}
if !config.WrapperrCustomize.StatsOrderByDuration && !config.WrapperrCustomize.StatsOrderByPlays {
config.WrapperrCustomize.StatsOrderByDuration = true
}
if config.WrapperrCustomize.GetUserMovieStatsTitle == "" {
config.WrapperrCustomize.GetUserMovieStatsTitle = config_default.WrapperrCustomize.GetUserMovieStatsTitle
}
if config.WrapperrCustomize.GetUserMovieStatsSubtitle == "" {
config.WrapperrCustomize.GetUserMovieStatsSubtitle = config_default.WrapperrCustomize.GetUserMovieStatsSubtitle
}
if config.WrapperrCustomize.GetUserMovieStatsSubsubtitle == "" {
config.WrapperrCustomize.GetUserMovieStatsSubsubtitle = config_default.WrapperrCustomize.GetUserMovieStatsSubsubtitle
}
if config.WrapperrCustomize.GetUserMovieStatsSubtitleOne == "" {
config.WrapperrCustomize.GetUserMovieStatsSubtitleOne = config_default.WrapperrCustomize.GetUserMovieStatsSubtitleOne
}
if config.WrapperrCustomize.GetUserMovieStatsSubsubtitleOne == "" {
config.WrapperrCustomize.GetUserMovieStatsSubsubtitleOne = config_default.WrapperrCustomize.GetUserMovieStatsSubsubtitleOne
}
if config.WrapperrCustomize.GetUserMovieStatsSubtitleNone == "" {
config.WrapperrCustomize.GetUserMovieStatsSubtitleNone = config_default.WrapperrCustomize.GetUserMovieStatsSubtitleNone
}
if config.WrapperrCustomize.GetUserMovieStatsSubsubtitleNone == "" {
config.WrapperrCustomize.GetUserMovieStatsSubsubtitleNone = config_default.WrapperrCustomize.GetUserMovieStatsSubsubtitleNone
}
if config.WrapperrCustomize.GetUserMovieStatsTopMovie == "" {
config.WrapperrCustomize.GetUserMovieStatsTopMovie = config_default.WrapperrCustomize.GetUserMovieStatsTopMovie
}
if config.WrapperrCustomize.GetUserMovieStatsTopMoviePlural == "" {
config.WrapperrCustomize.GetUserMovieStatsTopMoviePlural = config_default.WrapperrCustomize.GetUserMovieStatsTopMoviePlural
}
if config.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitle == "" {
config.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitle = config_default.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitle
}
if config.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitlePlural == "" {
config.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitlePlural = config_default.WrapperrCustomize.GetUserMovieStatsMovieCompletionTitlePlural
}
if config.WrapperrCustomize.GetUserMovieStatsMovieCompletionSubtitle == "" {
config.WrapperrCustomize.GetUserMovieStatsMovieCompletionSubtitle = config_default.WrapperrCustomize.GetUserMovieStatsMovieCompletionSubtitle
}
if config.WrapperrCustomize.GetUserMovieStatsPauseTitle == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseTitle = config_default.WrapperrCustomize.GetUserMovieStatsPauseTitle
}
if config.WrapperrCustomize.GetUserMovieStatsPauseSubtitle == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseSubtitle = config_default.WrapperrCustomize.GetUserMovieStatsPauseSubtitle
}
if config.WrapperrCustomize.GetUserMovieStatsPauseTitleOne == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseTitleOne = config_default.WrapperrCustomize.GetUserMovieStatsPauseTitleOne
}
if config.WrapperrCustomize.GetUserMovieStatsPauseSubtitleOne == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseSubtitleOne = config_default.WrapperrCustomize.GetUserMovieStatsPauseSubtitleOne
}
if config.WrapperrCustomize.GetUserMovieStatsPauseTitleNone == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseTitleNone = config_default.WrapperrCustomize.GetUserMovieStatsPauseTitleNone
}
if config.WrapperrCustomize.GetUserMovieStatsPauseSubtitleNone == "" {
config.WrapperrCustomize.GetUserMovieStatsPauseSubtitleNone = config_default.WrapperrCustomize.GetUserMovieStatsPauseSubtitleNone
}
if config.WrapperrCustomize.GetUserMovieStatsOldestTitle == "" {
config.WrapperrCustomize.GetUserMovieStatsOldestTitle = config_default.WrapperrCustomize.GetUserMovieStatsOldestTitle
}
if config.WrapperrCustomize.GetUserMovieStatsOldestSubtitle == "" {
config.WrapperrCustomize.GetUserMovieStatsOldestSubtitle = config_default.WrapperrCustomize.GetUserMovieStatsOldestSubtitle
}
if config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1950 == "" {
config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1950 = config_default.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1950
}
if config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1975 == "" {
config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1975 = config_default.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre1975
}
if config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre2000 == "" {
config.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre2000 = config_default.WrapperrCustomize.GetUserMovieStatsOldestSubtitlePre2000
}
if config.WrapperrCustomize.GetUserMovieStatsSpentTitle == "" {
config.WrapperrCustomize.GetUserMovieStatsSpentTitle = config_default.WrapperrCustomize.GetUserMovieStatsSpentTitle
}
if config.WrapperrCustomize.GetUserShowStatsTitle == "" {
config.WrapperrCustomize.GetUserShowStatsTitle = config_default.WrapperrCustomize.GetUserShowStatsTitle
}
if config.WrapperrCustomize.GetUserShowStatsSubtitle == "" {
config.WrapperrCustomize.GetUserShowStatsSubtitle = config_default.WrapperrCustomize.GetUserShowStatsSubtitle
}
if config.WrapperrCustomize.GetUserShowStatsSubsubtitle == "" {
config.WrapperrCustomize.GetUserShowStatsSubsubtitle = config_default.WrapperrCustomize.GetUserShowStatsSubsubtitle
}
if config.WrapperrCustomize.GetUserShowStatsSubtitleOne == "" {
config.WrapperrCustomize.GetUserShowStatsSubtitleOne = config_default.WrapperrCustomize.GetUserShowStatsSubtitleOne
}
if config.WrapperrCustomize.GetUserShowStatsSubsubtitleOne == "" {
config.WrapperrCustomize.GetUserShowStatsSubsubtitleOne = config_default.WrapperrCustomize.GetUserShowStatsSubsubtitleOne
}
if config.WrapperrCustomize.GetUserShowStatsSubtitleNone == "" {
config.WrapperrCustomize.GetUserShowStatsSubtitleNone = config_default.WrapperrCustomize.GetUserShowStatsSubtitleNone
}
if config.WrapperrCustomize.GetUserShowStatsSubsubtitleNone == "" {
config.WrapperrCustomize.GetUserShowStatsSubsubtitleNone = config_default.WrapperrCustomize.GetUserShowStatsSubsubtitleNone
}
if config.WrapperrCustomize.GetUserShowStatsTopShow == "" {
config.WrapperrCustomize.GetUserShowStatsTopShow = config_default.WrapperrCustomize.GetUserShowStatsTopShow
}
if config.WrapperrCustomize.GetUserShowStatsTopShowPlural == "" {
config.WrapperrCustomize.GetUserShowStatsTopShowPlural = config_default.WrapperrCustomize.GetUserShowStatsTopShowPlural
}
if config.WrapperrCustomize.GetUserShowStatsSpentTitle == "" {
config.WrapperrCustomize.GetUserShowStatsSpentTitle = config_default.WrapperrCustomize.GetUserShowStatsSpentTitle
}
if config.WrapperrCustomize.GetUserShowStatsMostPlayedTitle == "" {
config.WrapperrCustomize.GetUserShowStatsMostPlayedTitle = config_default.WrapperrCustomize.GetUserShowStatsMostPlayedTitle
}
if config.WrapperrCustomize.GetUserShowStatsMostPlayedSubtitle == "" {
config.WrapperrCustomize.GetUserShowStatsMostPlayedSubtitle = config_default.WrapperrCustomize.GetUserShowStatsMostPlayedSubtitle
}
if config.WrapperrCustomize.GetUserShowStatsBuddyTitle == "" {
config.WrapperrCustomize.GetUserShowStatsBuddyTitle = config_default.WrapperrCustomize.GetUserShowStatsBuddyTitle
}
if config.WrapperrCustomize.GetUserShowStatsBuddySubtitle == "" {
config.WrapperrCustomize.GetUserShowStatsBuddySubtitle = config_default.WrapperrCustomize.GetUserShowStatsBuddySubtitle
}
if config.WrapperrCustomize.GetUserShowStatsBuddyTitleNone == "" {
config.WrapperrCustomize.GetUserShowStatsBuddyTitleNone = config_default.WrapperrCustomize.GetUserShowStatsBuddyTitleNone
}
if config.WrapperrCustomize.GetUserShowStatsBuddySubtitleNone == "" {
config.WrapperrCustomize.GetUserShowStatsBuddySubtitleNone = config_default.WrapperrCustomize.GetUserShowStatsBuddySubtitleNone
}
if config.WrapperrCustomize.GetUserMusicStatsTitle == "" {
config.WrapperrCustomize.GetUserMusicStatsTitle = config_default.WrapperrCustomize.GetUserMusicStatsTitle
}
if config.WrapperrCustomize.GetUserMusicStatsSubtitle == "" {
config.WrapperrCustomize.GetUserMusicStatsSubtitle = config_default.WrapperrCustomize.GetUserMusicStatsSubtitle
}
if config.WrapperrCustomize.GetUserMusicStatsSubsubtitle == "" {
config.WrapperrCustomize.GetUserMusicStatsSubsubtitle = config_default.WrapperrCustomize.GetUserMusicStatsSubsubtitle
}
if config.WrapperrCustomize.GetUserMusicStatsSubtitleOne == "" {
config.WrapperrCustomize.GetUserMusicStatsSubtitleOne = config_default.WrapperrCustomize.GetUserMusicStatsSubtitleOne
}
if config.WrapperrCustomize.GetUserMusicStatsSubsubtitleOne == "" {
config.WrapperrCustomize.GetUserMusicStatsSubsubtitleOne = config_default.WrapperrCustomize.GetUserMusicStatsSubsubtitleOne
}
if config.WrapperrCustomize.GetUserMusicStatsSubtitleNone == "" {
config.WrapperrCustomize.GetUserMusicStatsSubtitleNone = config_default.WrapperrCustomize.GetUserMusicStatsSubtitleNone
}
if config.WrapperrCustomize.GetUserMusicStatsSubsubtitleNone == "" {
config.WrapperrCustomize.GetUserMusicStatsSubsubtitleNone = config_default.WrapperrCustomize.GetUserMusicStatsSubsubtitleNone
}
if config.WrapperrCustomize.GetUserMusicStatsTopTrack == "" {
config.WrapperrCustomize.GetUserMusicStatsTopTrack = config_default.WrapperrCustomize.GetUserMusicStatsTopTrack
}
if config.WrapperrCustomize.GetUserMusicStatsTopTrackPlural == "" {
config.WrapperrCustomize.GetUserMusicStatsTopTrackPlural = config_default.WrapperrCustomize.GetUserMusicStatsTopTrackPlural
}
if config.WrapperrCustomize.GetUserMusicStatsTopAlbumPlural == "" {
config.WrapperrCustomize.GetUserMusicStatsTopAlbumPlural = config_default.WrapperrCustomize.GetUserMusicStatsTopAlbumPlural
}
if config.WrapperrCustomize.GetUserMusicStatsTopArtistPlural == "" {
config.WrapperrCustomize.GetUserMusicStatsTopArtistPlural = config_default.WrapperrCustomize.GetUserMusicStatsTopArtistPlural
}
if config.WrapperrCustomize.GetUserMusicStatsSpentTitle == "" {
config.WrapperrCustomize.GetUserMusicStatsSpentTitle = config_default.WrapperrCustomize.GetUserMusicStatsSpentTitle
}
if config.WrapperrCustomize.GetUserMusicStatsSpentSubtitle == "" {
config.WrapperrCustomize.GetUserMusicStatsSpentSubtitle = config_default.WrapperrCustomize.GetUserMusicStatsSpentSubtitle
}
if config.WrapperrCustomize.GetUserMusicStatsOldestAlbumTitle == "" {
config.WrapperrCustomize.GetUserMusicStatsOldestAlbumTitle = config_default.WrapperrCustomize.GetUserMusicStatsOldestAlbumTitle
}
if config.WrapperrCustomize.GetUserMusicStatsOldestAlbumSubtitle == "" {
config.WrapperrCustomize.GetUserMusicStatsOldestAlbumSubtitle = config_default.WrapperrCustomize.GetUserMusicStatsOldestAlbumSubtitle
}
if config.WrapperrCustomize.GetYearStatsTitle == "" {
config.WrapperrCustomize.GetYearStatsTitle = config_default.WrapperrCustomize.GetYearStatsTitle
}
if config.WrapperrCustomize.GetYearStatsSubtitle == "" {
config.WrapperrCustomize.GetYearStatsSubtitle = config_default.WrapperrCustomize.GetYearStatsSubtitle
}
if config.WrapperrCustomize.GetYearStatsSubsubtitle == "" {
config.WrapperrCustomize.GetYearStatsSubsubtitle = config_default.WrapperrCustomize.GetYearStatsSubsubtitle
}
if config.WrapperrCustomize.GetYearStatsMoviesTitle == "" {
config.WrapperrCustomize.GetYearStatsMoviesTitle = config_default.WrapperrCustomize.GetYearStatsMoviesTitle
}
if config.WrapperrCustomize.GetYearStatsShowsTitle == "" {
config.WrapperrCustomize.GetYearStatsShowsTitle = config_default.WrapperrCustomize.GetYearStatsShowsTitle
}
if config.WrapperrCustomize.GetYearStatsMusicTitle == "" {
config.WrapperrCustomize.GetYearStatsMusicTitle = config_default.WrapperrCustomize.GetYearStatsMusicTitle
}
if config.WrapperrCustomize.GetYearStatsLeaderboardTitle == "" {
config.WrapperrCustomize.GetYearStatsLeaderboardTitle = config_default.WrapperrCustomize.GetYearStatsLeaderboardTitle
}
if config.WrapperrCustomize.GetYearStatsMoviesDurationTitle == "" {
config.WrapperrCustomize.GetYearStatsMoviesDurationTitle = config_default.WrapperrCustomize.GetYearStatsMoviesDurationTitle
}
if config.WrapperrCustomize.GetYearStatsShowsDurationTitle == "" {
config.WrapperrCustomize.GetYearStatsShowsDurationTitle = config_default.WrapperrCustomize.GetYearStatsShowsDurationTitle
}
if config.WrapperrCustomize.GetYearStatsMusicDurationTitle == "" {
config.WrapperrCustomize.GetYearStatsMusicDurationTitle = config_default.WrapperrCustomize.GetYearStatsMusicDurationTitle
}
if config.WrapperrCustomize.GetYearStatsDurationSumTitle == "" {
config.WrapperrCustomize.GetYearStatsDurationSumTitle = config_default.WrapperrCustomize.GetYearStatsDurationSumTitle
}
if config.WrapperrCustomize.WrapperrAnd == "" {
config.WrapperrCustomize.WrapperrAnd = config_default.WrapperrCustomize.WrapperrAnd
}
if config.WrapperrCustomize.WrapperrPlay == "" {
config.WrapperrCustomize.WrapperrPlay = config_default.WrapperrCustomize.WrapperrPlay
}
if config.WrapperrCustomize.WrapperrPlayPlural == "" {
config.WrapperrCustomize.WrapperrPlayPlural = config_default.WrapperrCustomize.WrapperrPlayPlural
}
if config.WrapperrCustomize.WrapperrDay == "" {
config.WrapperrCustomize.WrapperrDay = config_default.WrapperrCustomize.WrapperrDay
}
if config.WrapperrCustomize.WrapperrDayPlural == "" {
config.WrapperrCustomize.WrapperrDayPlural = config_default.WrapperrCustomize.WrapperrDayPlural
}
if config.WrapperrCustomize.WrapperrHour == "" {
config.WrapperrCustomize.WrapperrHour = config_default.WrapperrCustomize.WrapperrHour
}
if config.WrapperrCustomize.WrapperrHourPlural == "" {
config.WrapperrCustomize.WrapperrHourPlural = config_default.WrapperrCustomize.WrapperrHourPlural
}
if config.WrapperrCustomize.WrapperrMinute == "" {
config.WrapperrCustomize.WrapperrMinute = config_default.WrapperrCustomize.WrapperrMinute
}
if config.WrapperrCustomize.WrapperrMinutePlural == "" {
config.WrapperrCustomize.WrapperrMinutePlural = config_default.WrapperrCustomize.WrapperrMinutePlural
}
if config.WrapperrCustomize.WrapperrSecond == "" {
config.WrapperrCustomize.WrapperrSecond = config_default.WrapperrCustomize.WrapperrSecond
}
if config.WrapperrCustomize.WrapperrSecondPlural == "" {
config.WrapperrCustomize.WrapperrSecondPlural = config_default.WrapperrCustomize.WrapperrSecondPlural
}
if config.WrapperrCustomize.WrapperrSortPlays == "" {
config.WrapperrCustomize.WrapperrSortPlays = config_default.WrapperrCustomize.WrapperrSortPlays
}
if config.WrapperrCustomize.WrapperrSortDuration == "" {
config.WrapperrCustomize.WrapperrSortDuration = config_default.WrapperrCustomize.WrapperrSortDuration
}
// Save new version of config json
err = SaveConfig(&config)
if err != nil {
return nil, err
}
// Return config object
return &config, nil
}

103
file_link.go Normal file
View file

@ -0,0 +1,103 @@
package main
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"strconv"
)
var link_path, _ = filepath.Abs("./config/links")
// Save new link object to the correct path
func SaveLink(link_object *WrapperrShareLink) error {
// Check if the link folder exists
err := CheckLinkDir()
if err != nil {
return err
}
file, err := json.MarshalIndent(link_object, "", " ")
if err != nil {
return err
}
var link_object_path, _ = filepath.Abs(link_path + "/" + strconv.Itoa(link_object.UserID) + ".json")
err = os.WriteFile(link_object_path, file, 0644)
if err != nil {
return err
}
return nil
}
// Create the directory in which links are stored
func CreateLinkDir() error {
err := os.Mkdir(link_path, 0755)
if err != nil {
return err
}
return nil
}
// Check if the link directory exists. If not, attempt to create it
func CheckLinkDir() error {
_, err := os.Stat(link_path)
if os.IsNotExist(err) {
log.Println("Link directory does not exist. Creating...")
err := CreateLinkDir()
if err != nil {
return err
}
log.Println("Created link directory.")
}
return nil
}
// Read
func GetLink(UserID string) (*WrapperrShareLink, error) {
// Check if the link folder exists
err := CheckLinkDir()
if err != nil {
return nil, err
}
link_object := WrapperrShareLink{}
share_link_path, err := filepath.Abs(link_path + "/" + UserID + ".json")
if err != nil {
return nil, err
}
_, err = os.Stat(share_link_path)
if err != nil {
return nil, errors.New("Invalid share link.")
}
file, err := os.Open(share_link_path)
if err != nil {
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
err = decoder.Decode(&link_object)
if err != nil {
return nil, err
}
if link_object.Expired {
return nil, errors.New("This Wrapped link has expired.")
}
// Return config object
return &link_object, nil
}

67
file_log.go Normal file
View file

@ -0,0 +1,67 @@
package main
import (
"bufio"
"log"
"os"
"path/filepath"
"regexp"
)
var log_path, _ = filepath.Abs("./config/wrapperr.log")
var max_lines_returned = 200
func GetLogLines() ([]WrapperrLogLine, error) {
readFile, err := os.Open(log_path)
if err != nil {
log.Println(err)
return nil, err
}
fileScanner := bufio.NewScanner(readFile)
fileScanner.Split(bufio.ScanLines)
var fileLines []string
for fileScanner.Scan() {
fileLines = append(fileLines, fileScanner.Text())
}
readFile.Close()
var logline_array []WrapperrLogLine
var re = regexp.MustCompile(`([0-9]{4,4}\/{1,1}[0-9]{2,2}\/[0-9]{2,2})\s([0-9]{2,2}:[0-9]{2,2}:[0-9]{2,2})\s([^\n]{1,})`)
for _, line := range fileLines {
match := re.FindStringSubmatch(line)
var logline WrapperrLogLine
if len(match) != 4 {
logline.Date = "Error"
logline.Time = "Error"
logline.Message = "This line was unparseable for front-end reading."
} else {
logline.Date = match[1]
logline.Time = match[2]
logline.Message = match[3]
}
logline_array = append(logline_array, logline)
}
if len(logline_array) > max_lines_returned {
logline_array = logline_array[len(logline_array)-max_lines_returned:]
}
return logline_array, nil
}

19
go.mod Normal file
View file

@ -0,0 +1,19 @@
module Wrapperr
go 1.18
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gbrlsnchs/jwt/v3 v3.0.1
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
)
require (
github.com/google/go-cmp v0.5.8 // indirect
github.com/magefile/mage v1.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
)

35
go.sum Normal file
View file

@ -0,0 +1,35 @@
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gbrlsnchs/jwt/v3 v3.0.1 h1:lbUmgAKpxnClrKloyIwpxm4OuWeDl5wLk52G91ODPw4=
github.com/gbrlsnchs/jwt/v3 v3.0.1/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 h1:+gFSK6FP5Ky3BPFrxQjHz92uRsj0DsrBL+xoIbiWRco=
github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583/go.mod h1:DyFPU22sg+Or/eRPmpwVVp0fUw+aQSYddY0DzzNjSN4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=

120
main.go Normal file
View file

@ -0,0 +1,120 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/gorilla/mux"
)
func main() {
PrintASCII()
// Create and define file for logging
file, err := os.OpenFile("config/wrapperr.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Println("Failed to load configuration file. Error: ")
log.Println(err)
fmt.Println("Failed to load configuration file. Error: ")
fmt.Println(err)
os.Exit(1)
}
config, err := GetConfig()
if err != nil {
log.Println("Failed to load configuration file. Error: ")
log.Println(err)
fmt.Println("Failed to load configuration file. Error: ")
fmt.Println(err)
os.Exit(1)
}
// Set time zone from config if it is not empty
if config.Timezone != "" {
loc, err := time.LoadLocation(config.Timezone)
if err != nil {
fmt.Println("Failed to set time zone from config. Error: ")
fmt.Println(err)
fmt.Println("Removing value...")
log.Println("Failed to set time zone from config. Error: ")
log.Println(err)
log.Println("Removing value...")
config.Timezone = ""
err = SaveConfig(config)
if err != nil {
log.Println("Failed to set new time zone in the config. Error: ")
log.Println(err)
log.Println("Exiting...")
os.Exit(1)
}
} else {
time.Local = loc
}
}
// Set log file is logging is enabled
if config.UseLogs {
log.SetOutput(file)
}
// Define port variable with the port from the config file as default
var port int
flag.IntVar(&port, "port", config.WrapperrPort, "The port Wrapperr is listening on.")
// Parse the flags from input
flag.Parse()
// Alert what port is in use
log.Println("Starting Wrapperr on port: " + strconv.Itoa(port) + ".")
fmt.Println("Starting Wrapperr on port: " + strconv.Itoa(port) + ".")
// Assign routes
router := mux.NewRouter().StrictSlash(true)
// Admin auth routes
router.HandleFunc("/api/validate/admin", ApiValidateAdmin)
router.HandleFunc("/api/get/config", ApiGetConfig)
router.HandleFunc("/api/get/log", ApiGetLog)
router.HandleFunc("/api/set/config", ApiSetConfig)
router.HandleFunc("/api/update/admin", ApiUpdateAdmin)
// No-auth routes
router.HandleFunc("/api/get/config-state", ApiWrapperrConfigured)
router.HandleFunc("/api/login/admin", ApiLogInAdmin)
router.HandleFunc("/api/get/wrapperr-version", ApiGetWrapperrVersion)
router.HandleFunc("/api/get/admin-state", ApiGetAdminState)
router.HandleFunc("/api/get/functions", ApiGetFunctions)
router.HandleFunc("/api/create/admin", ApiCreateAdmin)
router.HandleFunc("/api/get/tautulli-connection", ApiGetTautulliConncection)
router.HandleFunc("/api/get/share-link", ApiGetShareLink)
// User auth routes
router.HandleFunc("/api/get/login-url", ApiGetLoginURL)
router.HandleFunc("/api/login/plex-auth", ApiLoginPlexAuth)
router.HandleFunc("/api/validate/plex-auth", ApiValidatePlexAuth)
router.HandleFunc("/api/create/share-link", ApiCreateShareLink)
router.HandleFunc("/api/get/user-share-link", ApiGetUserShareLink)
router.HandleFunc("/api/delete/user-share-link", ApiDeleteUserShareLink)
// Get stats route
router.HandleFunc("/api/get/statistics", ApiWrapperGetStatistics)
// Static routes
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/")))
// Start web-server
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), router))
}

640
models.go Normal file
View file

@ -0,0 +1,640 @@
package main
import (
"time"
"github.com/gbrlsnchs/jwt/v3"
"github.com/google/uuid"
)
type Default_Reply struct {
Message string `json:"message"`
Error bool `json:"error"`
}
type CustomPayload struct {
jwt.Payload
Username string `json:"username,omitempty"`
PlexID int `json:"plexid,omitempty"`
Admin bool `json:"admin,omitempty"`
}
type Payload struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
Admin bool `json:"admin"`
AuthToken string `json:"authtoken"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
type JWTMaker struct {
secretKey string
}
type AdminConfig struct {
AdminUsername string `json:"admin_username"`
AdminPassword string `json:"admin_password"`
}
type WrapperrConfig struct {
TautulliConfig TautulliConfig `json:"tautulli_config"`
WrapperrCustomize WrapperrCustomize `json:"wrapperr_customize"`
WrapperrVersion string `json:"wrapperr_version"`
Timezone string `json:"timezone"`
ApplicationName string `json:"application_name"`
ApplicationURL string `json:"application_url"`
UseCache bool `json:"use_cache"`
UseLogs bool `json:"use_logs"`
ClientKey string `json:"client_key"`
WrapperrRoot string `json:"wrapperr_root"`
PrivateKey string `json:"private_key"`
CreateShareLinks bool `json:"create_share_links"`
WrappedStart int `json:"wrapped_start"`
WrappedEnd int `json:"wrapped_end"`
WrapperrPort int `json:"wrapperr_port"`
PlexAuth bool `json:"plex_auth"`
WinterTheme bool `json:"winter_theme"`
}
type TautulliConfig struct {
TautulliApiKey string `json:"tautulli_apikey"`
TautulliIP string `json:"tautulli_ip"`
TautulliPort int `json:"tautulli_port"`
TautulliLength int `json:"tautulli_length"`
TautulliRoot string `json:"tautulli_root"`
TautulliLibraries string `json:"tautulli_libraries"`
TautulliGrouping bool `json:"tautulli_grouping"`
TautulliHttps bool `json:"tautulli_https"`
}
type WrapperrCustomize struct {
WrapperrFrontPageTitle string `json:"wrapperr_front_page_title"`
WrapperrFrontPageSubtitle string `json:"wrapperr_front_page_subtitle"`
StatsIntroTitle string `json:"stats_intro_title"`
StatsIntroSubtitle string `json:"stats_intro_subtitle"`
StatsOutroTitle string `json:"stats_outro_title"`
StatsOutroSubtitle string `json:"stats_outro_subtitle"`
StatsOrderByPlays bool `json:"stats_order_by_plays"`
StatsOrderByDuration bool `json:"stats_order_by_duration"`
GetUserMovieStats bool `json:"get_user_movie_stats"`
GetUserMovieStatsTitle string `json:"get_user_movie_stats_title"`
GetUserMovieStatsSubtitle string `json:"get_user_movie_stats_subtitle"`
GetUserMovieStatsSubsubtitle string `json:"get_user_movie_stats_subsubtitle"`
GetUserMovieStatsSubtitleOne string `json:"get_user_movie_stats_subtitle_one"`
GetUserMovieStatsSubsubtitleOne string `json:"get_user_movie_stats_subsubtitle_one"`
GetUserMovieStatsSubtitleNone string `json:"get_user_movie_stats_subtitle_none"`
GetUserMovieStatsSubsubtitleNone string `json:"get_user_movie_stats_subsubtitle_none"`
GetUserMovieStatsTopMovie string `json:"get_user_movie_stats_top_movie"`
GetUserMovieStatsTopMoviePlural string `json:"get_user_movie_stats_top_movie_plural"`
GetUserMovieStatsMovieCompletionTitle string `json:"get_user_movie_stats_movie_completion_title"`
GetUserMovieStatsMovieCompletionTitlePlural string `json:"get_user_movie_stats_movie_completion_title_plural"`
GetUserMovieStatsMovieCompletionSubtitle string `json:"get_user_movie_stats_movie_completion_subtitle"`
GetUserMovieStatsPauseTitle string `json:"get_user_movie_stats_pause_title"`
GetUserMovieStatsPauseSubtitle string `json:"get_user_movie_stats_pause_subtitle"`
GetUserMovieStatsPauseTitleOne string `json:"get_user_movie_stats_pause_title_one"`
GetUserMovieStatsPauseSubtitleOne string `json:"get_user_movie_stats_pause_subtitle_one"`
GetUserMovieStatsPauseTitleNone string `json:"get_user_movie_stats_pause_title_none"`
GetUserMovieStatsPauseSubtitleNone string `json:"get_user_movie_stats_pause_subtitle_none"`
GetUserMovieStatsOldestTitle string `json:"get_user_movie_stats_oldest_title"`
GetUserMovieStatsOldestSubtitle string `json:"get_user_movie_stats_oldest_subtitle"`
GetUserMovieStatsOldestSubtitlePre1950 string `json:"get_user_movie_stats_oldest_subtitle_pre_1950"`
GetUserMovieStatsOldestSubtitlePre1975 string `json:"get_user_movie_stats_oldest_subtitle_pre_1975"`
GetUserMovieStatsOldestSubtitlePre2000 string `json:"get_user_movie_stats_oldest_subtitle_pre_2000"`
GetUserMovieStatsSpentTitle string `json:"get_user_movie_stats_spent_title"`
GetUserShowStats bool `json:"get_user_show_stats"`
GetUserShowBuddy bool `json:"get_user_show_stats_buddy"`
GetUserShowStatsTitle string `json:"get_user_show_stats_title"`
GetUserShowStatsSubtitle string `json:"get_user_show_stats_subtitle"`
GetUserShowStatsSubsubtitle string `json:"get_user_show_stats_subsubtitle"`
GetUserShowStatsSubtitleOne string `json:"get_user_show_stats_subtitle_one"`
GetUserShowStatsSubsubtitleOne string `json:"get_user_show_stats_subsubtitle_one"`
GetUserShowStatsSubtitleNone string `json:"get_user_show_stats_subtitle_none"`
GetUserShowStatsSubsubtitleNone string `json:"get_user_show_stats_subsubtitle_none"`
GetUserShowStatsTopShow string `json:"get_user_show_stats_top_show"`
GetUserShowStatsTopShowPlural string `json:"get_user_show_stats_top_show_plural"`
GetUserShowStatsSpentTitle string `json:"get_user_show_stats_spent_title"`
GetUserShowStatsMostPlayedTitle string `json:"get_user_show_stats_most_played_title"`
GetUserShowStatsMostPlayedSubtitle string `json:"get_user_show_stats_most_played_subtitle"`
GetUserShowStatsBuddyTitle string `json:"get_user_show_stats_buddy_title"`
GetUserShowStatsBuddySubtitle string `json:"get_user_show_stats_buddy_subtitle"`
GetUserShowStatsBuddyTitleNone string `json:"get_user_show_stats_buddy_title_none"`
GetUserShowStatsBuddySubtitleNone string `json:"get_user_show_stats_buddy_subtitle_none"`
GetUserMusicStats bool `json:"get_user_music_stats"`
GetUserMusicStatsTitle string `json:"get_user_music_stats_title"`
GetUserMusicStatsSubtitle string `json:"get_user_music_stats_subtitle"`
GetUserMusicStatsSubsubtitle string `json:"get_user_music_stats_subsubtitle"`
GetUserMusicStatsSubtitleOne string `json:"get_user_music_stats_subtitle_one"`
GetUserMusicStatsSubsubtitleOne string `json:"get_user_music_stats_subsubtitle_one"`
GetUserMusicStatsSubtitleNone string `json:"get_user_music_stats_subtitle_none"`
GetUserMusicStatsSubsubtitleNone string `json:"get_user_music_stats_subsubtitle_none"`
GetUserMusicStatsTopTrack string `json:"get_user_music_stats_top_track"`
GetUserMusicStatsTopTrackPlural string `json:"get_user_music_stats_top_track_plural"`
GetUserMusicStatsTopAlbumPlural string `json:"get_user_music_stats_top_album_plural"`
GetUserMusicStatsTopArtistPlural string `json:"get_user_music_stats_top_artist_plural"`
GetUserMusicStatsSpentTitle string `json:"get_user_music_stats_spent_title"`
GetUserMusicStatsSpentSubtitle string `json:"get_user_music_stats_spent_subtitle"`
GetUserMusicStatsOldestAlbumTitle string `json:"get_user_music_stats_oldest_album_title"`
GetUserMusicStatsOldestAlbumSubtitle string `json:"get_user_music_stats_oldest_album_subtitle"`
GetYearStatsTitle string `json:"get_year_stats_title"`
GetYearStatsSubtitle string `json:"get_year_stats_subtitle"`
GetYearStatsSubsubtitle string `json:"get_year_stats_subsubtitle"`
GetYearStatsMovies bool `json:"get_year_stats_movies"`
GetYearStatsMoviesTitle string `json:"get_year_stats_movies_title"`
GetYearStatsShows bool `json:"get_year_stats_shows"`
GetYearStatsShowsTitle string `json:"get_year_stats_shows_title"`
GetYearStatsMusic bool `json:"get_year_stats_music"`
GetYearStatsMusicTitle string `json:"get_year_stats_music_title"`
GetYearStatsLeaderboard bool `json:"get_year_stats_leaderboard"`
GetYearStatsLeaderboardTitle string `json:"get_year_stats_leaderboard_title"`
GetYearStatsMoviesDurationTitle string `json:"get_year_stats_movies_duration_title"`
GetYearStatsShowsDurationTitle string `json:"get_year_stats_shows_duration_title"`
GetYearStatsMusicDurationTitle string `json:"get_year_stats_music_duration_title"`
GetYearStatsDurationSumTitle string `json:"get_year_stats_duration_sum_title"`
WrapperrAnd string `json:"wrapperr_and"`
WrapperrPlay string `json:"wrapperr_play"`
WrapperrPlayPlural string `json:"wrapperr_play_plural"`
WrapperrDay string `json:"wrapperr_day"`
WrapperrDayPlural string `json:"wrapperr_day_plural"`
WrapperrHour string `json:"wrapperr_hour"`
WrapperrHourPlural string `json:"wrapperr_hour_plural"`
WrapperrMinute string `json:"wrapperr_minute"`
WrapperrMinutePlural string `json:"wrapperr_minute_plural"`
WrapperrSecond string `json:"wrapperr_second"`
WrapperrSecondPlural string `json:"wrapperr_second_plural"`
WrapperrSortPlays string `json:"wrapperr_sort_plays"`
WrapperrSortDuration string `json:"wrapperr_sort_duration"`
}
type WrapperrVersion struct {
WrapperrVersion string `json:"wrapperr_version"`
ApplicationName string `json:"application_name"`
PlexAuth bool `json:"plex_auth"`
WrapperrFrontPageTitle string `json:"wrapperr_front_page_title"`
WrapperrFrontPageSubtitle string `json:"wrapperr_front_page_subtitle"`
ClientKey string `json:"client_key"`
WrapperrConfigured bool `json:"wrapperr_configured"`
WinterTheme bool `json:"winter_theme"`
Message string `json:"message"`
Error bool `json:"error"`
}
type BooleanReply struct {
Message string `json:"message"`
Error bool `json:"error"`
Data bool `json:"data"`
}
type StringReply struct {
Message string `json:"message"`
Error bool `json:"error"`
Data string `json:"data"`
}
type ConfigReply struct {
Message string `json:"message"`
Error bool `json:"error"`
Data WrapperrConfig `json:"data"`
Username string `json:"username"`
}
type WrapperrFunctions struct {
WrapperrCustomize WrapperrCustomize `json:"wrapperr_customize"`
WrapperrVersion string `json:"wrapperr_version"`
PlexAuth bool `json:"plex_auth"`
CreateShareLinks bool `json:"create_share_links"`
}
type SetWrapperrConfig struct {
ClearCache bool `json:"clear_cache"`
DataType string `json:"data_type"`
TautulliConfig TautulliConfig `json:"tautulli_config"`
WrapperrCustomize WrapperrCustomize `json:"wrapperr_customize"`
WrapperrData struct {
UseCache bool `json:"use_cache"`
UseLogs bool `json:"use_logs"`
PlexAuth bool `json:"plex_auth"`
WrapperrRoot string `json:"wrapperr_root"`
CreateShareLinks bool `json:"create_share_links"`
Timezone string `json:"timezone"`
ApplicationName string `json:"application_name"`
ApplicationURL string `json:"application_url"`
WrappedStart int `json:"wrapped_start"`
WrappedEnd int `json:"wrapped_end"`
WinterTheme bool `json:"winter_theme"`
} `json:"wrapperr_data"`
}
type GetLoginURL struct {
HomeURL string `json:"home_url"`
}
type GetLoginURLReply struct {
ID int `json:"id"`
Code string `json:"code"`
URL string `json:"url"`
Message string `json:"message"`
Error bool `json:"error"`
}
type PlexGetPinReply struct {
ID int `json:"id"`
Code string `json:"code"`
Product string `json:"product"`
Trusted bool `json:"trusted"`
ClientIdentifier string `json:"clientIdentifier"`
Location struct {
Code string `json:"code"`
EuropeanUnionMember bool `json:"european_union_member"`
ContinentCode string `json:"continent_code"`
Country string `json:"country"`
City string `json:"city"`
TimeZone string `json:"time_zone"`
PostalCode string `json:"postal_code"`
InPrivacyRestrictedCountry bool `json:"in_privacy_restricted_country"`
Subdivisions string `json:"subdivisions"`
Coordinates string `json:"coordinates"`
} `json:"location"`
ExpiresIn int `json:"expiresIn"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
AuthToken string `json:"authToken"`
NewRegistration interface{} `json:"newRegistration"`
}
type LoginPlexAuth struct {
ID int `json:"id"`
Code string `json:"code"`
}
type PlexGetUserReply struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Username string `json:"username"`
Title string `json:"title"`
Email string `json:"email"`
FriendlyName string `json:"friendlyName"`
Locale interface{} `json:"locale"`
Confirmed bool `json:"confirmed"`
EmailOnlyAuth bool `json:"emailOnlyAuth"`
HasPassword bool `json:"hasPassword"`
Protected bool `json:"protected"`
Thumb string `json:"thumb"`
AuthToken string `json:"authToken"`
MailingListStatus string `json:"mailingListStatus"`
MailingListActive bool `json:"mailingListActive"`
ScrobbleTypes string `json:"scrobbleTypes"`
Country string `json:"country"`
Pin string `json:"pin"`
Subscription struct {
Active bool `json:"active"`
SubscribedAt time.Time `json:"subscribedAt"`
Status string `json:"status"`
PaymentService string `json:"paymentService"`
Plan string `json:"plan"`
Features []string `json:"features"`
} `json:"subscription"`
SubscriptionDescription string `json:"subscriptionDescription"`
Restricted bool `json:"restricted"`
Anonymous interface{} `json:"anonymous"`
Home bool `json:"home"`
Guest bool `json:"guest"`
HomeSize int `json:"homeSize"`
HomeAdmin bool `json:"homeAdmin"`
MaxHomeSize int `json:"maxHomeSize"`
CertificateVersion int `json:"certificateVersion"`
RememberExpiresAt int `json:"rememberExpiresAt"`
Profile struct {
AutoSelectAudio bool `json:"autoSelectAudio"`
DefaultAudioLanguage string `json:"defaultAudioLanguage"`
DefaultSubtitleLanguage string `json:"defaultSubtitleLanguage"`
AutoSelectSubtitle int `json:"autoSelectSubtitle"`
DefaultSubtitleAccessibility int `json:"defaultSubtitleAccessibility"`
DefaultSubtitleForced int `json:"defaultSubtitleForced"`
PlexPassVisibility string `json:"plexPassVisibility"`
AccountAgeVisibility string `json:"accountAgeVisibility"`
} `json:"profile"`
Entitlements []string `json:"entitlements"`
Roles []string `json:"roles"`
Services []struct {
Identifier string `json:"identifier"`
Endpoint string `json:"endpoint"`
Token string `json:"token"`
Secret interface{} `json:"secret"`
Status string `json:"status"`
} `json:"services"`
AdsConsent interface{} `json:"adsConsent"`
AdsConsentSetAt interface{} `json:"adsConsentSetAt"`
AdsConsentReminderAt interface{} `json:"adsConsentReminderAt"`
ExperimentalFeatures bool `json:"experimentalFeatures"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
BackupCodesCreated bool `json:"backupCodesCreated"`
}
type TautulliStatusReply struct {
Response struct {
Result string `json:"result"`
Message string `json:"message"`
Data struct {
} `json:"data"`
} `json:"response"`
}
type SearchWrapperrRequest struct {
CachingMode bool `json:"caching"`
CachingLimit int `json:"cache_limit"`
PlexIdentity string `json:"plex_identity"`
}
type TautulliGetUsersReply struct {
Response struct {
Result string `json:"result"`
Message interface{} `json:"message"`
Data []struct {
RowID int `json:"row_id"`
UserID int `json:"user_id"`
Username string `json:"username"`
FriendlyName string `json:"friendly_name"`
Thumb interface{} `json:"thumb"`
Email string `json:"email"`
IsActive int `json:"is_active"`
IsAdmin int `json:"is_admin"`
IsHomeUser interface{} `json:"is_home_user"`
IsAllowSync interface{} `json:"is_allow_sync"`
IsRestricted interface{} `json:"is_restricted"`
DoNotify int `json:"do_notify"`
KeepHistory int `json:"keep_history"`
AllowGuest int `json:"allow_guest"`
ServerToken interface{} `json:"server_token"`
SharedLibraries interface{} `json:"shared_libraries"`
FilterAll interface{} `json:"filter_all"`
FilterMovies interface{} `json:"filter_movies"`
FilterTv interface{} `json:"filter_tv"`
FilterMusic interface{} `json:"filter_music"`
FilterPhotos interface{} `json:"filter_photos"`
} `json:"data"`
} `json:"response"`
}
type WrapperrDay struct {
Date string `json:"date"`
Data []TautulliEntry `json:"data"`
DataComplete bool `json:"data_complete"`
}
type TautulliEntry struct {
Date int `json:"date"`
Duration int `json:"duration"`
RowID int `json:"row_id"`
FriendlyName string `json:"friendly_name"`
FullTitle string `json:"full_title"`
GrandparentRatingKey int `json:"grandparent_rating_key"`
GrandparentTitle string `json:"grandparent_title"`
OriginalTitle string `json:"original_title"`
MediaType string `json:"media_type"`
ParentRatingKey int `json:"parent_rating_key"`
ParentTitle string `json:"parent_title"`
PausedCounter int `json:"paused_counter"`
PercentComplete int `json:"percent_complete"`
RatingKey int `json:"rating_key"`
Title string `json:"title"`
User string `json:"user"`
UserID int `json:"user_id"`
Year int `json:"year"`
Plays int `json:"plays"`
}
type WrapperrYearUserEntry struct {
FriendlyName string `json:"friendly_name"`
GrandparentTitle string `json:"grandparent_title"`
OriginalTitle string `json:"original_title"`
ParentTitle string `json:"parent_title"`
PausedCounter int `json:"paused_counter"`
Title string `json:"title"`
User string `json:"user"`
UserID int `json:"user_id"`
Year int `json:"year"`
Plays int `json:"plays"`
DurationMovies int `json:"duration_movies"`
DurationShows int `json:"duration_shows"`
DurationArtists int `json:"duration_artists"`
Duration int `json:"duration"`
}
type TautulliGetHistoryReply struct {
Response struct {
Result string `json:"result"`
Message interface{} `json:"message"`
Data struct {
RecordsFiltered int `json:"recordsFiltered"`
RecordsTotal int `json:"recordsTotal"`
Data []TautulliHistoryItem `json:"data"`
Draw int `json:"draw"`
FilterDuration string `json:"filter_duration"`
TotalDuration string `json:"total_duration"`
} `json:"data"`
} `json:"response"`
}
type TautulliHistoryItem struct {
ReferenceID int `json:"reference_id"`
RowID int `json:"row_id"`
ID int `json:"id"`
Date int `json:"date"`
Started int `json:"started"`
Stopped int `json:"stopped"`
Duration int `json:"duration"`
PausedCounter int `json:"paused_counter"`
UserID int `json:"user_id"`
User string `json:"user"`
FriendlyName string `json:"friendly_name"`
Platform string `json:"platform"`
Product string `json:"product"`
Player string `json:"player"`
IPAddress string `json:"ip_address"`
Live int `json:"live"`
MachineID string `json:"machine_id"`
Location string `json:"location"`
Secure interface{} `json:"secure"`
Relayed interface{} `json:"relayed"`
MediaType string `json:"media_type"`
RatingKey int `json:"rating_key"`
ParentRatingKey int `json:"parent_rating_key"`
GrandparentRatingKey int `json:"grandparent_rating_key"`
FullTitle string `json:"full_title"`
Title string `json:"title"`
ParentTitle string `json:"parent_title"`
GrandparentTitle string `json:"grandparent_title"`
OriginalTitle string `json:"original_title"`
Year int `json:"year"`
MediaIndex string `json:"media_index"`
ParentMediaIndex string `json:"parent_media_index"`
Thumb string `json:"thumb"`
OriginallyAvailableAt string `json:"originally_available_at"`
GUID string `json:"guid"`
TranscodeDecision string `json:"transcode_decision"`
PercentComplete int `json:"percent_complete"`
WatchedStatus int `json:"watched_status"`
GroupCount int `json:"group_count"`
GroupIds string `json:"group_ids"`
State interface{} `json:"state"`
SessionKey interface{} `json:"session_key"`
}
type WrapperrStatisticsReply struct {
Error bool `json:"error"`
Date string `json:"date"`
Message string `json:"message"`
User struct {
Name string `json:"name"`
ID int `json:"id"`
UserMovies struct {
Data struct {
MoviesDuration []TautulliEntry `json:"movies_duration"`
MoviesPlays []TautulliEntry `json:"movies_plays"`
UserMovieMostPaused struct {
Title string `json:"title"`
Year int `json:"year"`
Plays int `json:"plays"`
Duration int `json:"duration"`
PausedCounter int `json:"paused_counter"`
} `json:"user_movie_most_paused"`
UserMovieFinishingPercent float64 `json:"user_movie_finishing_percent"`
UserMovieOldest struct {
Title string `json:"title"`
Year int `json:"year"`
Plays int `json:"plays"`
Duration int `json:"duration"`
PausedCounter int `json:"paused_counter"`
Error bool `json:"error"`
} `json:"user_movie_oldest"`
MovieDuration int `json:"movie_duration"`
MoviePlays int `json:"movie_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"user_movies"`
UserShows struct {
Data struct {
ShowsDuration []TautulliEntry `json:"shows_duration"`
ShowsPlays []TautulliEntry `json:"shows_plays"`
EpisodeDurationLongest struct {
Title string `json:"title"`
ParentTitle string `json:"parent_title"`
GrandparentTitle string `json:"grandparent_title"`
Duration int `json:"duration"`
Plays int `json:"plays"`
Error bool `json:"error"`
} `json:"episode_duration_longest"`
ShowDuration int `json:"show_duration"`
ShowPlays int `json:"show_plays"`
ShowBuddy WrapperrShowBuddy `json:"show_buddy"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"user_shows"`
UserMusic struct {
Data struct {
TracksDuration []TautulliEntry `json:"tracks_duration"`
TracksPlays []TautulliEntry `json:"tracks_plays"`
AlbumsDuration []TautulliEntry `json:"albums_duration"`
AlbumsPlays []TautulliEntry `json:"albums_plays"`
UserAlbumOldest struct {
ParentTitle string `json:"parent_title"`
GrandparentTitle string `json:"grandparent_title"`
Year int `json:"year"`
Plays int `json:"plays"`
Duration int `json:"duration"`
Error bool `json:"error"`
} `json:"user_album_oldest"`
ArtistsDuration []TautulliEntry `json:"artists_duration"`
ArtistsPlays []TautulliEntry `json:"artists_plays"`
TrackDuration int `json:"track_duration"`
TrackPlays int `json:"track_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"user_music"`
} `json:"user"`
YearStats struct {
YearMovies struct {
Data struct {
MoviesDuration []TautulliEntry `json:"movies_duration"`
MoviesPlays []TautulliEntry `json:"movies_plays"`
MovieDuration int `json:"movie_duration"`
MoviePlays int `json:"movie_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"year_movies"`
YearShows struct {
Data struct {
ShowsDuration []TautulliEntry `json:"shows_duration"`
ShowsPlays []TautulliEntry `json:"shows_plays"`
ShowDuration int `json:"show_duration"`
ShowPlays int `json:"show_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"year_shows"`
YearMusic struct {
Data struct {
ArtistsDuration []TautulliEntry `json:"artists_duration"`
ArtistsPlays []TautulliEntry `json:"artists_plays"`
MusicDuration int `json:"music_duration"`
MusicPlays int `json:"music_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"year_music"`
YearUsers struct {
Data struct {
UsersDuration []WrapperrYearUserEntry `json:"users_duration"`
UsersPlays []WrapperrYearUserEntry `json:"users_plays"`
} `json:"data"`
Message string `json:"message"`
Error bool `json:"error"`
} `json:"year_users"`
} `json:"year_stats"`
}
type WrapperrShareLinkCreateRequest struct {
Data WrapperrStatisticsReply `json:"data"`
Functions WrapperrCustomize `json:"functions"`
}
type WrapperrShareLinkGetRequest struct {
Hash string `json:"hash"`
}
type WrapperrShareLink struct {
Date string `json:"date"`
UserID int `json:"user_id"`
WrapperrVersion string `json:"wrapperr_version"`
Hash string `json:"hash"`
Content WrapperrShareLinkCreateRequest `json:"content"`
Message string `json:"message"`
Error bool `json:"error"`
Expired bool `json:"expired"`
}
type WrapperrShowBuddy struct {
Message string `json:"message"`
Error bool `json:"error"`
BuddyName string `json:"buddy_name"`
BuddyDuration int `json:"buddy_duration"`
BuddyFound bool `json:"buddy_found"`
}
type WrapperrLogLine struct {
Date string `json:"date"`
Time string `json:"time"`
Message string `json:"message"`
}
type WrapperrLogLineReply struct {
Message string `json:"message"`
Error bool `json:"error"`
Data []WrapperrLogLine `json:"data"`
Limit int `json:"limit"`
}

109
module_authorize.go Normal file
View file

@ -0,0 +1,109 @@
package main
import (
"errors"
"log"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
)
// AuthorizeToken validates JWT tokens using the private key.
func AuthorizeToken(writer http.ResponseWriter, request *http.Request) (*Payload, error) {
PrivateKey, err := GetPrivateKey()
if err != nil {
log.Println("Failed to load JWT Token settings. Error: ")
log.Println(err)
return &Payload{}, errors.New("Failed to load JWT Token settings.")
}
// Check if Authorization header is available
header := request.Header.Get("Authorization")
if header == "" || !strings.Contains(header, " ") || !strings.Contains(strings.ToLower(header), "bearer") {
log.Println("No valid Authorization token found in header during API request.")
return &Payload{}, errors.New("No valid Authorization token found in header.")
}
headerParts := strings.Split(header, " ")
if len(headerParts) < 2 {
log.Println("Failed to parse header. Error: ")
log.Println(err)
return &Payload{}, errors.New("Failed to parse header.")
}
jwtToken := headerParts[1]
payload, err := VerifyToken(PrivateKey, jwtToken)
if err != nil {
log.Println("Session token not accepted. Error: ")
log.Println(err)
return &Payload{}, errors.New("Session token not accepted. Please relog.")
}
return payload, nil
}
// VerifyToken checks if the token is valid or not
func VerifyToken(PrivateKey string, token string) (*Payload, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(PrivateKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
if err != nil {
verr, ok := err.(*jwt.ValidationError)
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, ok := jwtToken.Claims.(*Payload)
if !ok {
return nil, ErrInvalidToken
}
return payload, nil
}
// CreateToken creates a new JWT token used to validate a users session. Valid for three days by default.
func CreateToken(username string, admin bool, authtoken string) (string, error) {
PrivateKey, err := GetPrivateKey()
if err != nil {
log.Println("Failed to load JWT Token settings. Error: ")
log.Println(err)
return "", errors.New("Failed to load JWT Token settings.")
}
duration := time.Minute * 60 * 24 * 3
token, _, err := CreateTokenTwo(PrivateKey, username, admin, authtoken, duration)
if err != nil {
log.Println("Failed to create session token. Error: ")
log.Println(err)
return "", errors.New("Failed to create session token.")
}
return token, nil
}
// CreateToken creates a new token for a specific username and duration
func CreateTokenTwo(PrivateKey string, username string, admin bool, authtoken string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(username, admin, authtoken, duration)
if err != nil {
return "", payload, err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
token, err := jwtToken.SignedString([]byte(PrivateKey))
return token, payload, err
}

140
module_plex.go Normal file
View file

@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
)
var content_type string = "application/json"
var x_plex_product string = "Wrapperr"
var strong bool = true
var x_plex_model string = "Plex OAuth"
var x_plex_language string = "en"
func GetPin(ClientKey string, WrapperrVersion string) (*PlexGetPinReply, error) {
url_string := "https://plex.tv/api/v2/pins"
params := url.Values{}
params.Add("strong", strconv.FormatBool(strong))
params.Add("X-Plex-Product", x_plex_product)
params.Add("X-Plex-Client-Identifier", ClientKey)
params.Add("X-Plex-Version", WrapperrVersion)
params.Add("X-Plex-Model", x_plex_model)
params.Add("X-Plex-Language", x_plex_language)
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("POST", url_string, payload)
if err != nil {
return nil, err
}
req.Header.Add("Accept", content_type)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply PlexGetPinReply
json.Unmarshal(body, &body_reply)
if err != nil {
return nil, err
}
return &body_reply, nil
}
func GetLoginURLString(client_id string, code string, home_url string) string {
base := "https://app.plex.tv/auth#?"
forwardUrl := home_url + "?close_me=true"
return base + "clientID=" + url.QueryEscape(client_id) + "&code=" + url.QueryEscape(code) + "&context%5Bdevice%5D%5Bproduct%5D=" + url.QueryEscape(x_plex_product) + "&forwardUrl=" + url.QueryEscape(forwardUrl)
}
func GetPlexAuthLogin(ID int, Code string, WrapperrVersion string, ClientKey string) (*PlexGetPinReply, error) {
url_string := "https://plex.tv/api/v2/pins/" + strconv.Itoa(ID)
params := url.Values{}
params.Add("X-Plex-Client-Identifier", ClientKey)
params.Add("X-Plex-Version", WrapperrVersion)
params.Add("X-Plex-Model", x_plex_model)
params.Add("X-Plex-Language", x_plex_language)
params.Add("code", Code)
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
return nil, err
}
req.Header.Add("Accept", content_type)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply PlexGetPinReply
json.Unmarshal(body, &body_reply)
if err != nil {
return nil, err
}
return &body_reply, nil
}
func PlexAuthValidateToken(PlexAuth string, ClientKey string, WrapperrVersion string) (*PlexGetUserReply, error) {
url_string := "https://plex.tv/api/v2/user"
params := url.Values{}
params.Add("X-Plex-Client-Identifier", ClientKey)
params.Add("X-Plex-Version", WrapperrVersion)
params.Add("X-Plex-Product", x_plex_product)
params.Add("X-Plex-Model", x_plex_model)
params.Add("X-Plex-Language", x_plex_language)
params.Add("X-Plex-Token", PlexAuth)
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
return nil, err
}
req.Header.Add("Accept", content_type)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply PlexGetUserReply
json.Unmarshal(body, &body_reply)
if err != nil {
return nil, err
}
return &body_reply, nil
}

159
module_tautulli.go Normal file
View file

@ -0,0 +1,159 @@
package main
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
func TautulliTestConnection(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string) (bool, error) {
url_string, err := BuildURL(TautulliPort, TautulliIP, TautulliHttps, TautulliRoot)
if err != nil {
log.Println(err)
return false, errors.New("Failed to build Tautulli connection URL.")
}
url_string = url_string + "api/v2/" + "?apikey=" + TautulliApiKey + "&cmd=status"
params := url.Values{}
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
log.Println(err)
return false, errors.New("Failed to reach Tautulli server.")
}
req.Header.Add("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Println(err)
return false, errors.New("Failed to reach Tautulli server.")
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply TautulliStatusReply
json.Unmarshal(body, &body_reply)
if err != nil {
log.Println(err)
return false, errors.New("Failed to parse Tautulli response.")
}
var tautulli_status bool = false
if body_reply.Response.Result == "success" {
tautulli_status = true
}
return tautulli_status, nil
}
func TautulliGetUserId(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string, PlexUser string) (int, string, error) {
url_string, err := BuildURL(TautulliPort, TautulliIP, TautulliHttps, TautulliRoot)
if err != nil {
log.Println(err)
return 0, "", errors.New("Failed to build Tautulli connection URL.")
}
url_string = url_string + "api/v2/" + "?apikey=" + TautulliApiKey + "&cmd=get_users"
params := url.Values{}
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
log.Println(err)
return 0, "", errors.New("Failed to reach Tautulli server.")
}
req.Header.Add("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Println(err)
return 0, "", errors.New("Failed to reach Tautulli server.")
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply TautulliGetUsersReply
json.Unmarshal(body, &body_reply)
if err != nil {
log.Println(err)
return 0, "", errors.New("Failed to parse Tautulli response.")
}
for i := 0; i < len(body_reply.Response.Data); i++ {
if body_reply.Response.Data[i].UserID != 0 && (strings.ToLower(body_reply.Response.Data[i].Username) == strings.ToLower(PlexUser) || strings.ToLower(body_reply.Response.Data[i].Email) == strings.ToLower(PlexUser)) {
var username string
if body_reply.Response.Data[i].FriendlyName != "" {
username = body_reply.Response.Data[i].FriendlyName
} else if body_reply.Response.Data[i].Username != "" {
username = body_reply.Response.Data[i].Username
} else {
return 0, "", errors.New("Failed retrieve Plex username.")
}
return body_reply.Response.Data[i].UserID, username, nil
}
}
log.Println("Could not find any user that matched the given Plex Identity: '" + PlexUser + "'.")
return 0, "", errors.New("Failed to find user.")
}
func TautulliDownloadStatistics(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string, TautulliLength int, Libraries string, Grouping string, StartDate string) ([]TautulliHistoryItem, error) {
url_string, err := BuildURL(TautulliPort, TautulliIP, TautulliHttps, TautulliRoot)
if err != nil {
log.Println(err)
return []TautulliHistoryItem{}, errors.New("Failed to build Tautulli connection URL.")
}
url_string = url_string + "api/v2/" + "?apikey=" + TautulliApiKey + "&cmd=get_history&order_column=date&order_dir=desc&include_activity=0" + Libraries + "&grouping=" + Grouping + "&length=" + strconv.Itoa(TautulliLength) + "&start_date=" + StartDate
params := url.Values{}
payload := strings.NewReader(params.Encode())
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
log.Println(err)
return []TautulliHistoryItem{}, errors.New("Failed to reach Tautulli server.")
}
req.Header.Add("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Println(err)
return []TautulliHistoryItem{}, errors.New("Failed to reach Tautulli server.")
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var body_reply TautulliGetHistoryReply
json.Unmarshal(body, &body_reply)
if err != nil {
log.Println(err)
return []TautulliHistoryItem{}, errors.New("Failed to parse Tautulli response.")
}
return body_reply.Response.Data.Data, nil
}

40
payload.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"errors"
"time"
"github.com/google/uuid"
)
// Different types of error returned by the VerifyToken function
var (
ErrInvalidToken = errors.New("token is invalid")
ErrExpiredToken = errors.New("token has expired")
)
// NewPayload creates a new token payload with a specific username and duration
func NewPayload(username string, admin bool, authtoken string, duration time.Duration) (*Payload, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
payload := &Payload{
ID: tokenID,
Username: username,
Admin: admin,
AuthToken: authtoken,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(duration),
}
return payload, nil
}
// Valid checks if the token payload is valid or not
func (payload *Payload) Valid() error {
if time.Now().After(payload.ExpiredAt) {
return ErrExpiredToken
}
return nil
}

328
route_admin_auth.go Normal file
View file

@ -0,0 +1,328 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
// API route used to retrieve the Wrapperr configuration file.
func ApiGetConfig(w http.ResponseWriter, r *http.Request) {
payload, err := AuthorizeToken(w, r)
if err == nil && payload.Admin {
config, err := GetConfig()
if err != nil {
respond_default_error(w, r, errors.New("Failed to retrieve Wrapperr configuration."), 500)
} else {
config_reply := ConfigReply{
Data: *config,
Message: "Retrieved Wrapperr config.",
Error: false,
Username: payload.Username,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr configuration." + ip_string)
respondWithJSON(w, http.StatusOK, config_reply)
return
}
} else if !payload.Admin {
log.Println(errors.New("Only the admin can retrieve the config."))
respond_default_error(w, r, errors.New("Only the admin can retrieve the config."), 401)
return
} else {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to authorize JWT token."), 500)
return
}
}
// API route used to update the Wrapperr configuration file.
func ApiSetConfig(w http.ResponseWriter, r *http.Request) {
payload, err := AuthorizeToken(w, r)
if err == nil && payload.Admin {
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve Wrapperr configuration."), 500)
} else {
// Read payload from Post input
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("Failed to parse config request. Error:")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to parse config request."), 401)
return
}
var config_payload SetWrapperrConfig
json.Unmarshal(reqBody, &config_payload)
// Confirm username length
if config_payload.DataType == "" {
log.Println("Cannot set new config. Invalid data type recieved.")
respond_default_error(w, r, errors.New("Data type specified is invalid."), 400)
return
}
if config_payload.DataType == "tautulli_config" {
config.TautulliConfig = config_payload.TautulliConfig
err = SaveConfig(config)
if err != nil {
respond_default_error(w, r, errors.New("Failed to save new Wrapperr configuration."), 500)
}
} else if config_payload.DataType == "wrapperr_customize" {
config.WrapperrCustomize = config_payload.WrapperrCustomize
err = SaveConfig(config)
if err != nil {
respond_default_error(w, r, errors.New("Failed to save new Wrapperr configuration."), 500)
return
}
} else if config_payload.DataType == "wrapperr_data" {
_, err := time.LoadLocation(config_payload.WrapperrData.Timezone)
if err != nil {
log.Println("Failed to set the new time zone. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Given time zone is invalid."), 401)
return
}
config.UseCache = config_payload.WrapperrData.UseCache
config.UseLogs = config_payload.WrapperrData.UseLogs
config.PlexAuth = config_payload.WrapperrData.PlexAuth
config.WrapperrRoot = config_payload.WrapperrData.WrapperrRoot
config.CreateShareLinks = config_payload.WrapperrData.CreateShareLinks
config.Timezone = config_payload.WrapperrData.Timezone
config.ApplicationName = config_payload.WrapperrData.ApplicationName
config.ApplicationURL = config_payload.WrapperrData.ApplicationURL
config.WrappedEnd = config_payload.WrapperrData.WrappedEnd
config.WrappedStart = config_payload.WrapperrData.WrappedStart
config.WinterTheme = config_payload.WrapperrData.WinterTheme
err = SaveConfig(config)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to save new Wrapperr configuration."), 500)
return
}
} else {
log.Println("Cannot set new config. Invalid data type recieved. Type: " + config_payload.DataType)
respond_default_error(w, r, errors.New("Failed to save new Wrapperr confguration."), 400)
return
}
if config_payload.ClearCache {
log.Println("Clear cache setting set to true. Clearing cache.")
err = ClearCache()
if err != nil {
log.Println("Failed to clear cache:")
log.Println(err)
}
}
log.Println("New Wrapperr configuration saved for type: " + config_payload.DataType + ".")
respond_default_okay(w, r, "Saved new Wrapperr config.")
return
}
} else if !payload.Admin {
log.Println("User not authenticated as admin.")
respond_default_error(w, r, errors.New("User not authenticated as admin."), 401)
return
} else {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to save config."), 500)
return
}
}
// API route used to update admin accounts details (username, password).
func ApiUpdateAdmin(w http.ResponseWriter, r *http.Request) {
admin, err := GetAdminState()
if err != nil {
log.Println("Failed to load admin state. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load admin state."), 500)
return
}
if !admin {
log.Print("Admin update failed. No admin is configured.")
respond_default_error(w, r, errors.New("No admin is configured."), 400)
return
} else {
payload, err := AuthorizeToken(w, r)
if err == nil && payload.Admin {
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var admin_payload AdminConfig
json.Unmarshal(reqBody, &admin_payload)
// Confirm username length
if len(admin_payload.AdminUsername) < 4 {
log.Println("Admin update failed. Admin username requires four or more characters.")
respond_default_error(w, r, errors.New("Admin username is too short. Four characters or more required."), 500)
return
}
// Confirm password length
if len(admin_payload.AdminPassword) < 8 {
log.Println("Admin update failed. Admin password requires eight or more characters.")
respond_default_error(w, r, errors.New("Admin password is too short. Eight characters or more required."), 500)
return
}
// Hash new password
hash, err := hashAndSalt(admin_payload.AdminPassword)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to hash your password."), 500)
return
}
admin_payload.AdminPassword = hash
// Save new admin config
err = SaveAdminConfig(admin_payload)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to update admin."), 500)
return
}
// Update the private key to delete old logins
_, err = UpdatePrivateKey()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Admin account updated, but failed to rotate private key. Old logins still function."), 500)
return
}
log.Println("New admin account created. Server is now claimed.")
fmt.Println("New admin account created. Server is now claimed.")
respond_default_okay(w, r, "Admin created.")
return
} else if !payload.Admin {
log.Println("User not authenticated as admin.")
respond_default_error(w, r, errors.New("User not authenticated as admin."), 401)
return
} else {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to update admin."), 500)
return
}
}
}
// API route which validates an admin JWT token
func ApiValidateAdmin(w http.ResponseWriter, r *http.Request) {
payload, err := AuthorizeToken(w, r)
if err == nil && payload.Admin {
log.Println("Admin login session JWT validated.")
respond_default_okay(w, r, "The admin login session is valid.")
return
} else if !payload.Admin {
log.Println("User not authenticated as admin.")
respond_default_error(w, r, errors.New("User not authenticated as admin."), 401)
return
} else {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to validate admin."), 500)
return
}
}
// API route which retrieves lines from the log file
func ApiGetLog(w http.ResponseWriter, r *http.Request) {
payload, err := AuthorizeToken(w, r)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to validate admin."), 500)
return
} else if !payload.Admin {
log.Println("User not authenticated as admin.")
respond_default_error(w, r, errors.New("User not authenticated as admin."), 401)
return
}
log_lines, err := GetLogLines()
if err != nil {
log.Println("Error trying to retrieve log lines. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve log file."), 500)
return
}
log_lines_return := WrapperrLogLineReply{
Message: "Log lines retrieved",
Error: false,
Data: log_lines,
Limit: max_lines_returned,
}
log.Println("Log lines retrieved for admin.")
respondWithJSON(w, http.StatusOK, log_lines_return)
return
}

447
route_no_auth.go Normal file
View file

@ -0,0 +1,447 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
)
// API route which retrieves the Wrapperr version and some minor details (application name, Plex-Auth...).
func ApiGetWrapperrVersion(w http.ResponseWriter, r *http.Request) {
configured_bool, err := GetConfigState()
if err != nil {
log.Println("Failed to retrieve configuration state. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
}
config, err := GetConfig()
if err != nil {
log.Println("Failed to load configuration file. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
}
version_reply := WrapperrVersion{
WrapperrVersion: config.WrapperrVersion,
ApplicationName: config.ApplicationName,
PlexAuth: config.PlexAuth,
WrapperrFrontPageTitle: config.WrapperrCustomize.WrapperrFrontPageTitle,
WrapperrFrontPageSubtitle: config.WrapperrCustomize.WrapperrFrontPageSubtitle,
ClientKey: config.ClientKey,
WrapperrConfigured: configured_bool,
WinterTheme: config.WinterTheme,
Message: "Retrieved Wrapperr version.",
Error: false,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr version." + ip_string)
respondWithJSON(w, http.StatusOK, version_reply)
return
}
// API route which returns if whether or not a Wrapperr admin is configured.
func ApiGetAdminState(w http.ResponseWriter, r *http.Request) {
admin, err := GetAdminState()
if err != nil {
log.Println(err)
log.Println("Failed to load admin state.")
return
}
boolean_reply := BooleanReply{
Message: "Retrieved Wrapperr version.",
Error: false,
Data: admin,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr admin state." + ip_string)
respondWithJSON(w, http.StatusOK, boolean_reply)
return
}
// API route which retrieves the Wrapperr settings needed for the front-end.
func ApiGetFunctions(w http.ResponseWriter, r *http.Request) {
config, err := GetConfig()
if err != nil {
log.Println(err)
log.Println("Failed to load configuration file.")
fmt.Println("Failed to load configuration file.")
return
}
function_reply := WrapperrFunctions{
WrapperrVersion: config.WrapperrVersion,
PlexAuth: config.PlexAuth,
WrapperrCustomize: config.WrapperrCustomize,
CreateShareLinks: config.CreateShareLinks,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr functions." + ip_string)
respondWithJSON(w, http.StatusOK, function_reply)
return
}
// API route used to create the admin account and claim the Wrapperr server
func ApiCreateAdmin(w http.ResponseWriter, r *http.Request) {
admin, err := GetAdminState()
if err != nil {
log.Println(err)
log.Println("Failed to load admin state.")
fmt.Println("Failed to load admin state.")
return
}
if admin {
log.Println("Admin creation failed. Admin already configured.")
respond_default_error(w, r, errors.New("Admin already configured."), 401)
return
} else {
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var admin_payload AdminConfig
json.Unmarshal(reqBody, &admin_payload)
// Confirm username length
if len(admin_payload.AdminUsername) < 4 {
log.Println("Admin creation failed. Admin username requires four or more characters.")
respond_default_error(w, r, errors.New("Admin username is too short. Four characters or more required."), 500)
return
}
// Confirm password length
if len(admin_payload.AdminPassword) < 8 {
log.Println("Admin creation failed. Admin password requires eight or more characters.")
respond_default_error(w, r, errors.New("Admin password is too short. Eight characters or more required."), 500)
return
}
// Hash new password
hash, err := hashAndSalt(admin_payload.AdminPassword)
if err != nil {
errors.New("Admin creation failed. Could not hash new password. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to hash your password."), 500)
return
}
admin_payload.AdminPassword = hash
// Save new admin config
err = SaveAdminConfig(admin_payload)
if err != nil {
errors.New("Admin creation failed. Could not save configuration. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to save new admin."), 500)
return
}
log.Println("New admin account created. Server is now claimed.")
fmt.Println("New admin account created. Server is now claimed.")
respond_default_okay(w, r, "Admin created.")
return
}
}
// API route which returns if whether or not Wrapperr is configured.
func ApiWrapperrConfigured(w http.ResponseWriter, r *http.Request) {
bool, err := GetConfigState()
if err != nil {
log.Panicln(err)
respond_default_error(w, r, errors.New("Failed to retrieve confguration state."), 500)
return
} else {
boolean_reply := BooleanReply{
Message: "Retrieved Wrapperr configuration state.",
Error: false,
Data: bool,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr configuration state." + ip_string)
respondWithJSON(w, http.StatusOK, boolean_reply)
return
}
}
// API route which trades admin login credentials for an admin JWT session token. Valid for three days.
func ApiLogInAdmin(w http.ResponseWriter, r *http.Request) {
admin, err := GetAdminState()
if err != nil {
log.Println(err)
log.Println("Failed to load admin state.")
fmt.Println("Failed to load admin state.")
return
}
if !admin {
log.Println("Admin login failed. Admin is not configured.")
respond_default_error(w, r, errors.New("No admin configured."), 400)
return
} else {
admin_config, err := GetAdminConfig()
if err != nil {
log.Println(err)
log.Println("Failed to load admin config.")
fmt.Println("Failed to load admin config.")
return
}
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var admin_payload AdminConfig
json.Unmarshal(reqBody, &admin_payload)
// Confirm username length
if len(admin_payload.AdminUsername) < 4 {
log.Println("Admin creation failed. Admin username requires four or more characters.")
respond_default_error(w, r, errors.New("Admin username is too short. Four characters or more required."), 500)
return
}
// Confirm password length
if len(admin_payload.AdminPassword) < 8 {
log.Println("Admin creation failed. Admin password requires eight or more characters.")
respond_default_error(w, r, errors.New("Admin password is too short. Eight characters or more required."), 500)
return
}
// Hash new password
password_validity := comparePasswords(admin_config.AdminPassword, admin_payload.AdminPassword)
// Validate admin username and password
if !password_validity || admin_config.AdminUsername != admin_payload.AdminUsername {
ip_string := GetOriginIPString(w, r)
log.Println("Admin login failed. Incorrect admin username or password." + ip_string)
fmt.Println("Admin login failed. Incorrect admin username or password." + ip_string)
respond_default_error(w, r, errors.New("Login failed. Username or password is incorrect."), 401)
return
}
token, err := CreateToken(admin_config.AdminUsername, true, "")
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to create JWT token."), 500)
return
}
string_reply := StringReply{
Message: "Login cookie created",
Error: false,
Data: token,
}
ip_string := GetOriginIPString(w, r)
log.Println("Created and retrieved admin login JWT Token." + ip_string)
fmt.Println("Created and retrieved admin login JWT Token." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
}
}
// APi route which trades admin login credentials for an admin JWT session token. Valid for three days.
func ApiGetTautulliConncection(w http.ResponseWriter, r *http.Request) {
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var tautulli_connection TautulliConfig
json.Unmarshal(reqBody, &tautulli_connection)
if tautulli_connection.TautulliApiKey == "" || tautulli_connection.TautulliIP == "" || tautulli_connection.TautulliPort == 0 {
log.Println("Cannot test Tautulli connection. Invalid Tautulli connection details recieved.")
respond_default_error(w, r, errors.New("Tautulli connection details specified are invalid."), 400)
return
}
tautulli_state, err := TautulliTestConnection(tautulli_connection.TautulliPort, tautulli_connection.TautulliIP, tautulli_connection.TautulliHttps, tautulli_connection.TautulliRoot, tautulli_connection.TautulliApiKey)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to reach Tautulli server."), 500)
return
}
boolean_reply := BooleanReply{
Message: "Tested Tautulli connection.",
Error: false,
Data: tautulli_state,
}
ip_string := GetOriginIPString(w, r)
log.Println("Tested Tautulli connection." + ip_string)
respondWithJSON(w, http.StatusOK, boolean_reply)
return
}
// Get shareable link
func ApiGetShareLink(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println("Failed to retrieve configuration state. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state"), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
if !config.CreateShareLinks {
log.Println("Shareable links are not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Shareable links are not enabled in the Wrapperr configuration."), 400)
return
}
// Read payload from Post input
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to parse payload for request."), 500)
return
}
var link_payload WrapperrShareLinkGetRequest
json.Unmarshal(reqBody, &link_payload)
hash_array := strings.Split(link_payload.Hash, "-")
if len(hash_array) < 2 {
log.Println("Failed to split hash string while looking for user ID.")
respond_default_error(w, r, errors.New("Failed to parse payload hash for Wrapperr link."), 500)
return
}
user_id := hash_array[0]
hash := ""
for j := 1; j < len(hash_array); j++ {
if j != 1 {
hash = hash + "-"
}
hash = hash + hash_array[j]
}
share_link_object, err := GetLink(user_id)
if err != nil {
log.Println(err)
respond_default_error(w, r, err, 500)
return
}
currentTime := time.Now()
linkTime, err := time.Parse("2006-01-02", share_link_object.Date)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve saved Wrapperr link."), 500)
return
}
linkTime = linkTime.Add(7 * 24 * time.Hour)
if !linkTime.Before(currentTime) && share_link_object.Hash == hash {
share_link_object.Message = "Shared link retrieved."
share_link_object.Error = false
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr share link made by User ID: " + user_id + "." + ip_string)
respondWithJSON(w, http.StatusOK, share_link_object)
return
} else {
return_error := errors.New("Invalid share link.")
if linkTime.Before(currentTime) {
share_link_object.Expired = true
err = SaveLink(share_link_object)
if err != nil {
log.Println(err)
}
return_error = errors.New("This Wrapped link has expired.")
}
log.Println("Failed to retrieve Wrapperr share link with hash: " + link_payload.Hash + ".")
respond_default_error(w, r, return_error, 401)
return
}
}

1222
route_statistics.go Normal file

File diff suppressed because it is too large Load diff

536
route_user_auth.go Normal file
View file

@ -0,0 +1,536 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
"github.com/google/uuid"
)
func ApiGetLoginURL(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve confguration state."), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr confguration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var homeurl_payload GetLoginURL
json.Unmarshal(reqBody, &homeurl_payload)
// Confirm username length
if homeurl_payload.HomeURL == "" {
log.Println("Cannot retrieve Plex Auth login URL. Invalid HomeURL recieved.")
respond_default_error(w, r, errors.New("HomeURL specified is invalid."), 400)
return
}
plex_pin, err := GetPin(config.ClientKey, config.WrapperrVersion)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve Plex Auth pin."), 500)
return
}
if plex_pin.ID == 0 || plex_pin.Code == "" {
log.Println("Plex Auth response invalid. No ID and/or Code.")
respond_default_error(w, r, errors.New("Plex Auth response invalid."), 500)
return
}
login_url := GetLoginURLString(config.ClientKey, plex_pin.Code, homeurl_payload.HomeURL)
url_reply := GetLoginURLReply{
Message: "Plex Auth login URL retrieved.",
Error: false,
URL: login_url,
Code: plex_pin.Code,
ID: plex_pin.ID,
}
ip_string := GetOriginIPString(w, r)
log.Println("Created and retrieved Plex Auth login URL." + ip_string)
fmt.Println("Created and retrieved Plex Auth login URL." + ip_string)
respondWithJSON(w, http.StatusOK, url_reply)
return
}
func ApiLoginPlexAuth(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
// Read payload from Post input
reqBody, _ := ioutil.ReadAll(r.Body)
var payload LoginPlexAuth
json.Unmarshal(reqBody, &payload)
// Confirm username length
if payload.ID == 0 || payload.Code == "" {
log.Println("Cannot retrieve Plex Auth login state. Invalid ID or Code recieved.")
respond_default_error(w, r, errors.New("Login ID and/or Code is invalid."), 400)
return
}
plex_auth, err := GetPlexAuthLogin(payload.ID, payload.Code, config.WrapperrVersion, config.ClientKey)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve Plex Auth pin."), 500)
return
}
if plex_auth.AuthToken == "" {
log.Println("Plex Auth response invalid. No Authtoken recieved.")
respond_default_error(w, r, errors.New("Plex Auth response invalid."), 400)
return
}
token, err := CreateToken("Plex Auth", false, plex_auth.AuthToken)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Faield to create JWT token."), 500)
return
}
string_reply := StringReply{
Message: "Login cookie created",
Error: false,
Data: token,
}
ip_string := GetOriginIPString(w, r)
log.Println("Created and retrieved Plex Auth login JWT Token." + ip_string)
fmt.Println("Created and retrieved Plex Auth login JWT Token." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
}
// API route which validates an admin JWT token
func ApiValidatePlexAuth(w http.ResponseWriter, r *http.Request) {
payload, err := AuthorizeToken(w, r)
if err != nil {
log.Println("Failed to parse login token. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to parse login token."), 500)
return
} else if payload.Admin {
log.Println("Recieved JWT token is for admin use.")
respond_default_error(w, r, errors.New("Recieved JWT token is for admin use."), 401)
return
}
config, err := GetConfig()
if err != nil {
log.Println("Failed to load Wrapperr configuration. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
_, err = PlexAuthValidateToken(payload.AuthToken, config.ClientKey, config.WrapperrVersion)
if err != nil {
log.Println("Could not validate Plex Auth login. Error: ")
log.Println(err)
respond_default_error(w, r, errors.New("Could not validate Plex Auth login."), 500)
return
}
log.Println("Plex Auth JWT Token validated using Plex API.")
respond_default_okay(w, r, "Plex Auth validated.")
return
}
// Create shareable link using Plex Auth
func ApiCreateShareLink(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
if !config.CreateShareLinks {
log.Panicln("Shareable links are not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Shareable links are not enabled in the Wrapperr configuration."), 400)
return
}
// Try to authorize bearer token from header
payload, err := AuthorizeToken(w, r)
var user_name string
var user_id int
if err != nil || payload.Admin {
log.Println(err)
log.Println(payload.Admin)
respond_default_error(w, r, errors.New("Failed to authorize request."), 401)
return
} else {
plex_object, err := PlexAuthValidateToken(payload.AuthToken, config.ClientKey, config.WrapperrVersion)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Could not validate Plex Auth login."), 500)
return
}
user_name = plex_object.Username
user_id = plex_object.ID
}
// Read payload from Post input
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to parse link payload."), 500)
return
}
var link_payload WrapperrShareLinkCreateRequest
json.Unmarshal(reqBody, &link_payload)
currentTime := time.Now()
hash_value := uuid.New().String()
link_object := WrapperrShareLink{
Content: link_payload,
UserID: user_id,
Hash: hash_value,
Date: currentTime.Format("2006-01-02"),
WrapperrVersion: config.WrapperrVersion,
Expired: false,
}
err = SaveLink(&link_object)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to save new link."), 500)
return
}
string_reply := StringReply{
Message: "Saved Wrapperr link.",
Error: false,
Data: strconv.Itoa(user_id) + "-" + hash_value,
}
ip_string := GetOriginIPString(w, r)
log.Println("Saved new Wrapperr share link for " + user_name + " (" + strconv.Itoa(user_id) + ")." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
}
// Get users shareable link
func ApiGetUserShareLink(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
if !config.CreateShareLinks {
log.Println("Shareable links are not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Shareable links are not enabled in the Wrapperr configuration."), 400)
return
}
// Try to authorize bearer token from header
payload, err := AuthorizeToken(w, r)
var user_name string
var user_id int
if err != nil {
log.Println(err)
log.Println(payload.Admin)
respond_default_error(w, r, errors.New("Failed to authorize request."), 500)
return
} else if payload.Admin {
log.Println("Admin tried to retrieve share links.")
respond_default_error(w, r, errors.New("Admin cannot retrieve share links."), 401)
return
} else {
plex_object, err := PlexAuthValidateToken(payload.AuthToken, config.ClientKey, config.WrapperrVersion)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Could not validate Plex Auth login."), 500)
return
}
user_name = plex_object.Username
user_id = plex_object.ID
}
share_link_object, err := GetLink(strconv.Itoa(user_id))
if err != nil {
string_reply := StringReply{
Message: "No Wrapperr links found for user.",
Error: true,
Data: "",
}
ip_string := GetOriginIPString(w, r)
log.Println("No Wrapperr links found for " + user_name + " (" + strconv.Itoa(user_id) + ")." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
}
currentTime := time.Now()
linkTime, err := time.Parse("2006-01-02", share_link_object.Date)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve saved Wrapperr link."), 500)
return
}
linkTime = linkTime.Add(7 * 24 * time.Hour)
if !linkTime.Before(currentTime) {
string_reply := StringReply{
Message: "Retrieved Wrapperr link created by user.",
Error: false,
Data: strconv.Itoa(user_id) + "-" + share_link_object.Hash,
}
ip_string := GetOriginIPString(w, r)
log.Println("Retrieved Wrapperr link created by " + user_name + " (" + strconv.Itoa(user_id) + ")." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
} else {
string_reply := StringReply{
Message: "No Wrapperr links found for user.",
Error: true,
Data: "",
}
ip_string := GetOriginIPString(w, r)
log.Println("No Wrapperr links found for " + user_name + " (" + strconv.Itoa(user_id) + ")." + ip_string)
respondWithJSON(w, http.StatusOK, string_reply)
return
}
}
// Delete users shareable link
func ApiDeleteUserShareLink(w http.ResponseWriter, r *http.Request) {
config_bool, err := GetConfigState()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve configuration state."), 500)
return
} else if !config_bool {
log.Println("Wrapperr is not configured.")
respond_default_error(w, r, errors.New("Wrapperr is not configured."), 400)
return
}
config, err := GetConfig()
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to load Wrapperr configuration."), 500)
return
}
if !config.PlexAuth {
log.Println("Plex Auth is not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Plex Auth is not enabled in the Wrapperr configuration."), 400)
return
}
if !config.CreateShareLinks {
log.Println("Shareable links are not enabled in the Wrapperr configuration.")
respond_default_error(w, r, errors.New("Shareable links are not enabled in the Wrapperr configuration."), 400)
return
}
// Try to authorize bearer token from header
payload, err := AuthorizeToken(w, r)
var user_name string
var user_id int
if err != nil || payload.Admin {
log.Println(err)
log.Println(payload.Admin)
respond_default_error(w, r, errors.New("Failed to authorize request."), 401)
return
} else {
plex_object, err := PlexAuthValidateToken(payload.AuthToken, config.ClientKey, config.WrapperrVersion)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Could not validate Plex Auth login."), 500)
return
}
user_name = plex_object.Username
user_id = plex_object.ID
}
share_link_object, err := GetLink(strconv.Itoa(user_id))
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to retrieve any saved Wrapperr link."), 500)
return
}
share_link_object.Date = "1970-01-01"
err = SaveLink(share_link_object)
if err != nil {
log.Println(err)
respond_default_error(w, r, errors.New("Failed to overwrite saved Wrapperr link."), 500)
return
}
log.Println("Deleted Wrapperr link for user " + user_name + " (" + strconv.Itoa(user_id) + ").")
respond_default_okay(w, r, "Deleted Wrapperr link.")
return
}

205
util.go Normal file
View file

@ -0,0 +1,205 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"math/big"
"net/http"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/kardianos/osext"
"golang.org/x/crypto/bcrypt"
)
func decodeBase64BigInt(s string) *big.Int {
buffer, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(s)
if err != nil {
log.Fatalf("failed to decode base64: %v", err)
}
return big.NewInt(0).SetBytes(buffer)
}
func checkScopes(requiredScopes []string, providedScopes string) bool {
for _, value := range requiredScopes {
if !strings.Contains(providedScopes, value) {
return false
}
}
return true
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
response, err := json.Marshal(payload)
if err != nil {
log.Println(err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
return
}
func respond_default_error(writer http.ResponseWriter, request *http.Request, error_reply error, http_code int) {
ip_string := GetOriginIPString(writer, request)
log.Println("Returned error: '" + error_reply.Error() + "'" + ip_string)
reply := Default_Reply{
Message: error_reply.Error(),
Error: true,
}
respondWithJSON(writer, http_code, reply)
return
}
func respond_default_okay(writer http.ResponseWriter, request *http.Request, reply_string string) {
ip_string := GetOriginIPString(writer, request)
log.Println("Returned reply: '" + reply_string + "'" + ip_string)
reply := Default_Reply{
Message: reply_string,
Error: false,
}
respondWithJSON(writer, http.StatusOK, reply)
return
}
func hashAndSalt(pwd_string string) (string, error) {
pwd := []byte(pwd_string)
// Use GenerateFromPassword to hash & salt pwd.
// MinCost is just an integer constant provided by the bcrypt
// package along with DefaultCost & MaxCost.
// The cost can be any value you want provided it isn't lower
// than the MinCost (4)
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
return "", err
}
// GenerateFromPassword returns a byte slice so we need to
// convert the bytes to a string and return it
return string(hash), nil
}
func comparePasswords(hashedPwd string, pwd string) bool {
// Since we'll be getting the hashed password from the DB it
// will be a string so we'll need to convert it to a byte slice
plainPwd := []byte(pwd)
byteHash := []byte(hashedPwd)
err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)
if err != nil {
return false
}
return true
}
func RestartSelf() error {
self, err := osext.Executable()
if err != nil {
return err
}
args := os.Args
env := os.Environ()
// Windows does not support exec syscall.
if runtime.GOOS == "windows" {
cmd := exec.Command(self, args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = env
err := cmd.Run()
if err == nil {
os.Exit(0)
}
return err
}
return syscall.Exec(self, args, env)
}
func GetOriginIPString(writer http.ResponseWriter, request *http.Request) string {
ip := request.RemoteAddr
xforward := request.Header.Get("X-Forwarded-For")
real_ip := request.Header.Get("X-Real-Ip")
string_reply := " - Origin: " + string(ip) + " "
if xforward != "" {
string_reply = string_reply + "(Forwarded for: " + string(xforward) + ") "
}
if real_ip != "" {
string_reply = string_reply + "(Real IP: " + string(real_ip) + ") "
}
return string_reply
}
func BuildURL(port int, domain_ip string, https bool, url_base string) (string, error) {
var url string = ""
if https {
url = url + "https://"
} else {
url = url + "http://"
}
domain_ip = strings.TrimPrefix(domain_ip, "http://")
domain_ip = strings.TrimPrefix(domain_ip, "https://")
url = url + domain_ip
if !(https && port == 443) && !(!https && port == 80) {
url = url + ":" + strconv.Itoa(port)
}
if url_base != "" {
url_base = strings.TrimPrefix(url_base, "/")
url_base = strings.TrimSuffix(url_base, "/")
url = url + "/" + url_base + "/"
} else {
url = strings.TrimPrefix(url, "/")
url = url + "/"
}
return url, nil
}
func PrintASCII() {
fmt.Println(``)
fmt.Println(` ___ __ ________ ________ ________ ________ _______ ________ ________ `)
fmt.Println(`|\ \ |\ \ |\ __ \ |\ __ \ |\ __ \ |\ __ \ |\ ___ \ |\ __ \ |\ __ \ `)
fmt.Println(`\ \ \ \ \ \\ \ \|\ \\ \ \|\ \\ \ \|\ \\ \ \|\ \\ \ __/| \ \ \|\ \\ \ \|\ \ `)
fmt.Println(` \ \ \ __\ \ \\ \ _ _\\ \ __ \\ \ ____\\ \ ____\\ \ \_|/__\ \ _ _\\ \ _ _\ `)
fmt.Println(` \ \ \|\__\_\ \\ \ \\ \|\ \ \ \ \\ \ \___| \ \ \___| \ \ \_|\ \\ \ \\ \|\ \ \\ \| `)
fmt.Println(` \ \____________\\ \__\\ _\ \ \__\ \__\\ \__\ \ \__\ \ \_______\\ \__\\ _\ \ \__\\ _\ `)
fmt.Println(` \|____________| \|__|\|__| \|__|\|__| \|__| \|__| \|_______| \|__|\|__| \|__|\|__|`)
fmt.Println(` ____________ ____________ ____________ ____________ ____________ ____________ ____________ `)
fmt.Println(`|\____________\|\____________\|\____________\|\____________\|\____________\|\____________\|\____________\`)
fmt.Println(`\|____________|\|____________|\|____________|\|____________|\|____________|\|____________|\|____________|`)
fmt.Println(``)
}

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@
<body>
<div class="content">
<div class="content" style="">
<div class="container">
@ -36,10 +36,12 @@
</div>
</div>
<div class="content" id="footer">
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../">Wrapperr</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;" id="github_link" href="https://github.com/aunefyren/wrapperr" target="_blank">GitHub</a>
<div class="footer">
<div class="footer-elements">
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="../">Wrapperr</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;" id="github_link" href="https://github.com/aunefyren/wrapperr" target="_blank">GitHub</a>
</div>
</div>
<script type="text/javascript">
@ -56,13 +58,13 @@ var tautulli_length = '';
var tautulli_root = '';
var tautulli_libraries = '';
var tautulli_grouping = '';
var https = '';
var tautulli_https = '';
var password = '';
var username = '';
var timezone = '';
var use_plex_auth = '';
var plex_auth = '';
var use_cache = '';
var use_logs = '';
var wrapperr_front_page_title = '';
@ -75,6 +77,7 @@ var stats_order_by_plays = '';
var stats_order_by_duration = '';
var client_id = '';
var wrapperr_root = '';
var winter_theme = false;
var application_name_str = '';
var application_url_str = '';

View file

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

View file

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -8,10 +8,14 @@
--green: #43AA8B;
--cadetblue: #4D908E;
--queenblue: #577590;
--lightblue: #71d2fc;
--blue: #277DA1;
--white: #f1f1f1;
--black: #181818;;
--charcoal: #414141;
--charcoal-accent-red: 65;
--charcoal-accent-green: 65;
--charcoal-accent-blue: 65;
}
html {
@ -20,7 +24,7 @@ html {
body {
margin: 0;
box-sizing: content-box;
box-sizing: border-box;
padding: 0;
top: 0;
font-family: 'Roboto', serif;
@ -31,18 +35,20 @@ body {
align-content: center;
justify-content: space-between;
align-items: center;
background-color: var(--mango);
background-color: var(--pistach);
height: 100%;
}
.content {
margin: 1em 0.5em;
width: 90%;
background-color: var(--charcoal);
padding: 0.5em;
border-radius: 6px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.19);
color: var(--white);
border: var(--yellow) 0.25em solid;
background-color: rgba(var(--charcoal-accent-red), var(--charcoal-accent-green), var(--charcoal-accent-blue), 0.75);
box-sizing: inherit;
}
.container {
@ -55,6 +61,8 @@ body {
justify-content: center;
align-items: flex-start;
font-size: 1em;
overflow-x:auto;
box-sizing: inherit;
}
.sub_container {
@ -226,6 +234,7 @@ p2 {
hr {
display: block;
width: 100%;
margin: 2em 0;
}
a {
@ -235,27 +244,69 @@ a {
.warning {
padding: 1em;
margin: auto;
background-color: var(--yellow);
border-radius: 25px;
background-color: var(--lightblue);
border: 2px solid var(--blue);
border-radius: 0.5em;
color: var(--black);
font-size: 1em;
max-width: 25em;
text-align: center;
}
#footer {
.footer {
text-align: center;
background: none;
box-shadow: none;
}
#cache {
width: 100%;
margin: 0 1em;
}
#cache_results {
border: 2px solid grey;
height: fit-content;
margin: 2em 0.5em;
width: 90%;
padding: 0.5em;
border-radius: 6px;
padding: 0.5em 0.75em;
color: var(--white);
}
.footer-elements {
width: fit-content;
height: fit-content;
margin: auto;
color: var(--white);
text-align: center;
background-color: rgba(var(--charcoal-accent-red), var(--charcoal-accent-green), var(--charcoal-accent-blue), 0.75);
padding: 0.5em;
color: var(--white);
border: var(--yellow) 0.12em solid;
border-radius: 0.5em;
}
#cache, #log {
width: 100%;
margin: 0;
}
#cache_results, #log_results {
padding: 0;
margin: 1em;
border: 1px solid var(--yellow);
width: auto;
}
.cacher-table, .log-table {
width: 100%;
overflow: scroll;
border-spacing: 0;
}
.cacher-th, .cacher-td, .log-th, .log-td {
padding: 0.25em;
border: 1px solid var(--yellow);
background-color: var(--charcoal);
color: var(--white);
}
.cacher-th, .log-th {
text-align: center;
}
.cacher-td, .log-td {
text-align: right;
}

View file

@ -10,8 +10,11 @@
--queenblue: #577590;
--blue: #277DA1;
--white: #f1f1f1;
--black: #181818;;
--black: #181818;
--charcoal: #414141;
--charcoal-accent-red: 65;
--charcoal-accent-green: 65;
--charcoal-accent-blue: 65;
}
@ -29,10 +32,25 @@ body {
width: 100%;
font-family: 'Roboto', serif;
font-weight: normal;
background-color: var(--mango);
background-color: var(--black);
box-sizing: border-box;
}
#background_image {
box-sizing: border-box;
background-image: none;
background-color: var(--mango);
background-repeat: no-repeat;
background-size: cover;
background-position: bottom;
filter: blur(8px);
-webkit-filter: blur(8px);
position: fixed;
height: inherit;
width: inherit;
transition: 2s;
}
h1{
font-size: 2.5em;
color: var(--white);
@ -190,7 +208,7 @@ img {
padding: 0.5em;
margin: 0.5em;
background-color: var(--yellow);
border-radius: 25px;
border-radius: 0.5em;
color: var(--black);
font-size: 0.75em;
}
@ -204,15 +222,15 @@ img {
.server:hover {
background-color: var(--white);
border-radius: 25px;
border-radius: 0.5em;
}
.status {
display: inline-block;
margin: auto;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.19);
background-color: var(--charcoal);
border-radius: 25px;
background-color: rgba(var(--charcoal-accent-red), var(--charcoal-accent-green), var(--charcoal-accent-blue), 0.75);
border-radius: 0.5em;
padding: 0.5em;
width: 100%;
top: 0;
@ -220,6 +238,7 @@ img {
font-size: 1.25em;
box-sizing: border-box;
max-width: 100%;
border: var(--yellow) 0.12em solid;
}
.stats-list {
@ -270,14 +289,14 @@ img {
width: 90%;
margin: 0.5em;
text-align: center;
border-radius: 25px;
border-radius: 0.5em;
border: none;
height: 2em;
}
.item:hover {
background-color: var(--black) !important;
border-radius: 25px;
border-radius: 0.5em;
}
.item {
@ -287,17 +306,17 @@ img {
.gold {
background-color: #d4af37;
border-radius: 25px;
border-radius: 0.5em;
}
.silver {
background-color: #C0C0C0 ;
border-radius: 25px;
border-radius: 0.5em;
}
.bronze {
background-color: #cd7f32;
border-radius: 25px;
border-radius: 0.5em;
}
.list {
@ -305,7 +324,7 @@ img {
margin: 1em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.19);
background-color: var(--white);
border-radius: 25px;
border-radius: 0.5em;
padding: 0.5em;
text-align: left;
width: 10em;
@ -382,7 +401,7 @@ img {
display: inline-block;
box-sizing: border-box;
text-align: center;
border-radius: 0.25em;
border-radius: 0.5em;
border: none;
}
@ -405,6 +424,7 @@ img {
align-content: center;
justify-content: center;
align-items: center;
color: var(--black);
}
label {
@ -427,6 +447,20 @@ input[type="checkbox" i] {
height: 2em;
color: var(--white);
text-align: center;
margin: 0 0 2em 0;
}
.footer-elements {
width: fit-content;
height: fit-content;
margin: auto;
color: var(--white);
text-align: center;
background-color: rgba(var(--charcoal-accent-red), var(--charcoal-accent-green), var(--charcoal-accent-blue), 0.75);
padding: 0.5em;
color: var(--white);
border: var(--yellow) 0.12em solid;
border-radius: 0.5em;
}
.sign_out {
@ -439,7 +473,7 @@ input[type="checkbox" i] {
background-color: var(--yellow);
border: none;
cursor: pointer;
border-radius: 0.25em;
border-radius: 0.5em;
font-family: 'Roboto', serif;
padding: 0.25em 0.5em;
margin: auto;
@ -453,6 +487,10 @@ input[type="checkbox" i] {
}
/*Snowflakes*/
#snowflakes, #snowflakes2 {
display: none;
}
.snowflake {
color: var(--white);
font-size: 1em;
@ -460,8 +498,18 @@ input[type="checkbox" i] {
text-shadow: 0 0 1px #000;
}
.snowflake-2 {
color: var(--white);
font-size: 1em;
font-family: Arial;
text-shadow: 0 0 1px #000;
color: transparent;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
z-index: auto !important;
}
@-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}
@-webkit-keyframes snowflakes-fall{0%{top:-40%}100%{top:100%}}@-webkit-keyframes snowflakes-shake{0%{-webkit-transform:translateX(0px);transform:translateX(0px)}50%{-webkit-transform:translateX(30px);transform:translateX(50px)}100%{-webkit-transform:translateX(0px);transform:translateX(0px)}}@keyframes snowflakes-fall{0%{top:-15%}100%{top:100%}}@keyframes snowflakes-shake{0%{transform:translateX(0px)}50%{transform:translateX(90px)}100%{transform:translateX(0px)}}.snowflake-2{position:fixed;top:-45%;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:12s,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:12s,3s;animation-timing-function:linear,ease-in-out;animation-iteration-count:infinite,infinite;animation-play-state:running,running}.snowflake-2:nth-of-type(0){left:5%;-webkit-animation-delay:0s,0s;animation-delay:0s,0s}.snowflake-2:nth-of-type(1){left:55%;-webkit-animation-delay:1s,1s;animation-delay:1s,1s}.snowflake-2:nth-of-type(2){left:55%;-webkit-animation-delay:8s,.5s;animation-delay:8s,.5s}.snowflake-2:nth-of-type(3){left:60%;-webkit-animation-delay:6s,2s;animation-delay:4s,2s}.snowflake-2:nth-of-type(4){left:80%;-webkit-animation-delay:2s,2s;animation-delay:2s,2s}.snowflake-2:nth-of-type(5){left:20%;-webkit-animation-delay:6s,3s;animation-delay:8s,3s}.snowflake-2:nth-of-type(6){left:40%;-webkit-animation-delay:6s,2s;animation-delay:6s,2s}.snowflake-2:nth-of-type(7){left:30%;-webkit-animation-delay:2.5s,1s;animation-delay:2.5s,1s}.snowflake-2:nth-of-type(8){left:30%;-webkit-animation-delay:1s,0s;animation-delay:1s,0s}.snowflake-2:nth-of-type(9){left:60%;-webkit-animation-delay:3s,1.5s;animation-delay:4s,1.5s}
.color-red {
background-color: var(--red);
@ -497,11 +545,11 @@ hr {
margin: 0.5em 0;
}
#share_wrapped_url {
.share_wrapped_url {
padding: 0.25em;
background: none;
color: var(--black);
border-radius: 0.25em;
border-radius: 0.5em;
overflow: auto;
font-size: 0.8em;
width: 80%;
@ -517,7 +565,7 @@ hr {
#share_wrapped_delete {
padding: 0 0 0 0.25em;
background: none;
border-radius: 0.25em;
border-radius: 0.5em;
overflow: auto;
font-size: 0.8em;
width: 10%;
@ -529,11 +577,36 @@ hr {
#share_wrapped_copy {
padding: 0 0 0 0.5em;
background: none;
border-radius: 0.25em;
border-radius: 0.5em;
overflow: auto;
font-size: 0.8em;
width: 10%;
box-sizing: border-box;
height: 1.5em;
cursor: pointer;
}
/* Loading section */
#loading {
position: fixed;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100%;
top: 0;
left: 0;
text-align: center;
opacity: 1;
background-color: var(--charcoal);
z-index: 99;
transition: 2s;
overflow: hidden;
}
#loading-image {
margin: auto;
}

1
web/assets/document.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="192px" height="192px"> <path d="M13.172,2H6C4.9,2,4,2.9,4,4v16c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V8.828c0-0.53-0.211-1.039-0.586-1.414l-4.828-4.828 C14.211,2.211,13.702,2,13.172,2z M15,18H9c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h6c0.552,0,1,0.448,1,1v0 C16,17.552,15.552,18,15,18z M15,14H9c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h6c0.552,0,1,0.448,1,1v0 C16,13.552,15.552,14,15,14z M13,9V3.5L18.5,9H13z"/></svg>

After

Width:  |  Height:  |  Size: 525 B

View file

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View file

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

View file

Before

Width:  |  Height:  |  Size: 612 B

After

Width:  |  Height:  |  Size: 612 B

View file

Before

Width:  |  Height:  |  Size: 875 B

After

Width:  |  Height:  |  Size: 875 B

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View file

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View file

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 555 B

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View file

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 815 B

View file

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 742 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 562 B

After

Width:  |  Height:  |  Size: 562 B

View file

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 655 B

BIN
web/assets/winter.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
web/assets/winter.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -2,7 +2,7 @@ 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 + ";";
document.cookie = cname + "=" + cvalue + ";" + expires + ";" + "samesite=strict" + ";";
}
function get_cookie(cname) {
@ -197,4 +197,10 @@ function play_plays(plays) {
}
return play_string;
}
function pad_number(num, size) {
num = num.toString();
while (num.length < size) num = "0" + num;
return num;
}

View file

@ -15,13 +15,16 @@ function get_functions() {
document.getElementById("plex_signout_button").style.opacity = '1';
document.getElementById('results_error').innerHTML = result.message;
} else {
functions = result;
functions = result.wrapperr_customize;
functions.plex_auth = result.plex_auth
functions.create_share_links = result.create_share_links
functions.wrapperr_version = result.wrapperr_version
get_stats();
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", root + "api/get_functions.php");
xhttp.open("post", root + "api/get/functions");
xhttp.send(config_data);
}

View file

@ -5,7 +5,6 @@ function get_stats() {
var loading_icon = document.getElementById("loading_icon");
stats_form = {
"cookie" : cookie,
"caching" : false,
"plex_identity" : plex_identity
};
@ -52,7 +51,9 @@ function get_stats() {
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_stats.php");
xhttp.open("post", "api/get/statistics");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(stats_data);
loading_icon.style.display = "inline";
@ -60,8 +61,10 @@ function get_stats() {
function load_page(data){
// Remove snow
// Remove snow and background
document.getElementById('snowflakes').style.display = 'none';
document.getElementById('snowflakes2').style.display = 'none';
document.getElementById('background_image').style.display = 'none';
// Set body background color to introduction
document.getElementById("body").classList.add('color-red');
@ -132,8 +135,8 @@ function load_introduction() {
}
// Toggle list to plays or duration
function top_list_sort_by(array, title, music, year, div_id, button_id, functions_data) {
document.getElementById(div_id).outerHTML = top_list(array, title, music, year, div_id);
function top_list_sort_by(array, title, music, show, year, div_id, button_id, functions_data) {
document.getElementById(div_id).outerHTML = top_list(array, title, music, show, year, div_id);
var button_text = document.getElementById(button_id + '_text');
if(button_text.innerHTML.includes(functions_data.wrapperr_sort_plays)) {
@ -178,13 +181,13 @@ function load_movies() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_movies_data_movies_button" id="user_movies_data_movies_button" onclick="top_list_sort_by(results.user.user_movies.data.movies_plays, functions.get_user_movie_stats_top_movie_plural, false, true, \'user_movies_data_movies\', \'user_movies_data_movies_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_movies_data_movies_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_movies_data_movies_button" id="user_movies_data_movies_button" onclick="top_list_sort_by(results.user.user_movies.data.movies_plays, functions.get_user_movie_stats_top_movie_plural, false, false, true, \'user_movies_data_movies\', \'user_movies_data_movies_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_movies_data_movies_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.user.user_movies.data.movies_duration, functions.get_user_movie_stats_top_movie_plural, false, true, 'user_movies_data_movies');
text += top_list(results.user.user_movies.data.movies_duration, functions.get_user_movie_stats_top_movie_plural, false, false, true, 'user_movies_data_movies');
} else {
text += top_list(results.user.user_movies.data.movies_plays, functions.get_user_movie_stats_top_movie_plural, false, true, 'user_movies_data_movies');
text += top_list(results.user.user_movies.data.movies_plays, functions.get_user_movie_stats_top_movie_plural, false, false, true, 'user_movies_data_movies');
}
text += "</div>";
@ -233,7 +236,7 @@ function load_movies() {
text += "<div class='boks3'>";
text += "<div class='boks2'>";
text += top_list(results.user.user_movies.data.movies_duration, functions.get_user_movie_stats_top_movie, false, true);
text += top_list(results.user.user_movies.data.movies_duration, functions.get_user_movie_stats_top_movie, false, false, true);
text += "</div>";
text += "<div class='boks2' style='padding: 0;'>";
@ -295,13 +298,13 @@ function load_shows() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_shows_data_shows_button" id="user_shows_data_shows_button" onclick="top_list_sort_by(results.user.user_shows.data.shows_plays, functions.get_user_show_stats_top_show_plural, false, true, \'user_shows_data_shows\', \'user_shows_data_shows_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_shows_data_shows_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_shows_data_shows_button" id="user_shows_data_shows_button" onclick="top_list_sort_by(results.user.user_shows.data.shows_plays, functions.get_user_show_stats_top_show_plural, false, true, false, \'user_shows_data_shows\', \'user_shows_data_shows_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_shows_data_shows_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.user.user_shows.data.shows_duration, functions.get_user_show_stats_top_show_plural, false, true, 'user_shows_data_shows');
text += top_list(results.user.user_shows.data.shows_duration, functions.get_user_show_stats_top_show_plural, false, true, false, 'user_shows_data_shows');
} else {
text += top_list(results.user.user_shows.data.shows_plays, functions.get_user_show_stats_top_show_plural, false, true, 'user_shows_data_shows');
text += top_list(results.user.user_shows.data.shows_plays, functions.get_user_show_stats_top_show_plural, false, true, false, 'user_shows_data_shows');
}
text += "</div>";
@ -348,7 +351,7 @@ function load_shows() {
text += "<div class='boks3'>";
text += "<div class='boks2'>";
text += top_list(results.user.user_shows.data.shows_duration, functions.get_user_show_stats_top_show, false, true);
text += top_list(results.user.user_shows.data.shows_duration, functions.get_user_show_stats_top_show, false, true, false);
text += "</div>";
if(results.user.user_shows.data.shows_duration.length > 0 && !results.user.user_shows.data.show_buddy.error && functions.get_user_show_stats_buddy) {
@ -411,13 +414,13 @@ function load_music() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_tracks_button" id="user_music_data_tracks_button" onclick="top_list_sort_by(results.user.user_music.data.tracks_plays, functions.get_user_music_stats_top_track_plural, \'track\', false, \'user_music_data_tracks\', \'user_music_data_tracks_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_tracks_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_tracks_button" id="user_music_data_tracks_button" onclick="top_list_sort_by(results.user.user_music.data.tracks_plays, functions.get_user_music_stats_top_track_plural, \'track\', false, false, \'user_music_data_tracks\', \'user_music_data_tracks_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_tracks_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.user.user_music.data.tracks_duration, functions.get_user_music_stats_top_track_plural, "track", false, 'user_music_data_tracks');
text += top_list(results.user.user_music.data.tracks_duration, functions.get_user_music_stats_top_track_plural, "track", false, false, 'user_music_data_tracks');
} else {
text += top_list(results.user.user_music.data.tracks_plays, functions.get_user_music_stats_top_track_plural, "track", false, 'user_music_data_tracks');
text += top_list(results.user.user_music.data.tracks_plays, functions.get_user_music_stats_top_track_plural, "track", false, false, 'user_music_data_tracks');
}
text += "</div>";
@ -425,13 +428,13 @@ function load_music() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_albums_button" id="user_music_data_albums_button" onclick="top_list_sort_by(results.user.user_music.data.albums_plays, functions.get_user_music_stats_top_album_plural, \'album\', false, \'user_music_data_albums\', \'user_music_data_albums_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_albums_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_albums_button" id="user_music_data_albums_button" onclick="top_list_sort_by(results.user.user_music.data.albums_plays, functions.get_user_music_stats_top_album_plural, \'album\', false, false, \'user_music_data_albums\', \'user_music_data_albums_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_albums_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.user.user_music.data.albums_duration, functions.get_user_music_stats_top_album_plural, "album", false, 'user_music_data_albums');
text += top_list(results.user.user_music.data.albums_duration, functions.get_user_music_stats_top_album_plural, "album", false, false, 'user_music_data_albums');
} else {
text += top_list(results.user.user_music.data.albums_plays, functions.get_user_music_stats_top_album_plural, "album", false, 'user_music_data_albums');
text += top_list(results.user.user_music.data.albums_plays, functions.get_user_music_stats_top_album_plural, "album", false, false, 'user_music_data_albums');
}
text += "</div>";
@ -439,13 +442,13 @@ function load_music() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_artists_button" id="user_music_data_artists_button" onclick="top_list_sort_by(results.user.user_music.data.artists_plays, functions.get_user_music_stats_top_artist_plural, \'artist\', false, \'user_music_data_artists\', \'user_music_data_artists_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_artists_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="user_music_data_artists_button" id="user_music_data_artists_button" onclick="top_list_sort_by(results.user.user_music.data.artists_plays, functions.get_user_music_stats_top_artist_plural, \'artist\', false, false, \'user_music_data_artists\', \'user_music_data_artists_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="user_music_data_artists_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.user.user_music.data.artists_duration, functions.get_user_music_stats_top_artist_plural, "artist", false, 'user_music_data_artists');
text += top_list(results.user.user_music.data.artists_duration, functions.get_user_music_stats_top_artist_plural, "artist", false, false, 'user_music_data_artists');
} else {
text += top_list(results.user.user_music.data.artists_plays, functions.get_user_music_stats_top_artist_plural, "artist", false, 'user_music_data_artists');
text += top_list(results.user.user_music.data.artists_plays, functions.get_user_music_stats_top_artist_plural, "artist", false, false, 'user_music_data_artists');
}
text += "</div>";
@ -489,7 +492,7 @@ function load_music() {
text += "<div class='boks3'>";
text += "<div class='boks2'>";
text += top_list(results.user.user_music.data.tracks_duration, functions.get_user_music_stats_top_track, true, false);
text += top_list(results.user.user_music.data.tracks_duration, functions.get_user_music_stats_top_track, true, false, false);
text += "</div>";
text += "</div>";
@ -631,16 +634,16 @@ function load_showbuddy(buddy_object, top_show, functions_data) {
html += "<div class='status' id='list3' style='padding:1em;min-width:15em;'>";
html += "<div class='stats'>";
if(!buddy_object.error) {
if(!buddy_object.found) {
html += functions_data.get_user_show_stats_buddy_title_none.replaceAll('{top_show_title}', '<b>' + top_show.title + '</b>');
if(!buddy_object.buddy_found) {
html += functions_data.get_user_show_stats_buddy_title_none.replaceAll('{top_show_title}', '<b>' + top_show.grandparent_title + '</b>');
html += '<br><img src="assets/img/quest.svg" style="margin: auto; display: block; width: 15em;"><br>';
html += functions_data.get_user_show_stats_buddy_subtitle_none;
} else {
html += functions_data.get_user_show_stats_buddy_title.replaceAll('{top_show_title}', '<b>' + top_show.title + '</b>').replaceAll('{buddy_username}', buddy_object.friendly_name);
var combined = results.user.user_shows.data.show_buddy.duration + parseInt(results.user.user_shows.data.shows_duration[0].duration);
html += functions_data.get_user_show_stats_buddy_title.replaceAll('{top_show_title}', '<b>' + top_show.grandparent_title + '</b>').replaceAll('{buddy_username}', buddy_object.buddy_name);
var combined = parseInt(top_show.duration) + parseInt(buddy_object.buddy_duration);
var combined_2 = seconds_to_time(combined);
html += '<img src="assets/img/social-event.svg" style="margin: auto; display: block; width: 15em;">';
html += functions_data.get_user_show_stats_buddy_subtitle.replaceAll('{buddy_duration_sum}', combined_2).replaceAll('{top_show_title}', top_show.title);
html += functions_data.get_user_show_stats_buddy_subtitle.replaceAll('{buddy_duration_sum}', combined_2).replaceAll('{top_show_title}', top_show.grandparent_title);
}
}
html += "</div>";
@ -692,9 +695,8 @@ function you_spent(time, category, functions_data) {
html += functions_data.get_user_music_stats_spent_title.replaceAll('{music_sum_duration}', time_str);
if(time > 3600) {
var time_min = Math.floor(time / 60);
var time_str = time_min + ' ' + functions.wrapperr_minute_plural;
html += '<br><br>';
html += functions_data.get_user_music_stats_spent_subtitle.replaceAll('{music_sum_minutes}', number_with_spaces(time_str));
html += functions_data.get_user_music_stats_spent_subtitle.replaceAll('{music_sum_minutes}', number_with_spaces(time_min));
}
html += '<br><img src="assets/img/music.svg" style="margin: auto; display: block; width: 15em;">';
html += "</div>";
@ -708,7 +710,7 @@ function you_spent(time, category, functions_data) {
return html;
}
function top_list(array, title, music, year, div_id) {
function top_list(array, title, music, show, year, div_id) {
var html = "";
html += "<div class='status' id='" + div_id + "'>";
@ -724,24 +726,30 @@ function top_list(array, title, music, year, div_id) {
html += "<div class='movie_name'>";
if(music === "track" || music === "album") {
html+= array[i].grandparent_title + "<br>";
}
html += "<b>";
if(music === "album") {
html += array[i].parent_title;
} else if(music === "artist") {
html += array[i].grandparent_title;
} else {
html += array[i].title;
}
html += "</b>";
var movie_hour = seconds_to_time(array[i].duration, true);
if(typeof(array[i].year) !== "undefined" && year) {
html += " (" + array[i].year + ")";
} else if(show) {
html+= "<b>" + array[i].grandparent_title + "</b>";
}
if(!show) {
html += "<b>";
if(music === "album") {
html += array[i].parent_title;
} else if(music === "artist") {
html += array[i].grandparent_title;
} else {
html += array[i].title;
}
}
html += "</b>";
var movie_hour = seconds_to_time(array[i].duration, true);
if(typeof(array[i].year) !== "undefined" && year) {
html += " (" + array[i].year + ")";
}
html += "<br>" + movie_hour + "<br>" + play_plays(array[i].plays);
html += "</div>";
@ -890,13 +898,13 @@ function load_users() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_movies_button" id="year_stats_year_movies_button" onclick="top_list_sort_by(results.year_stats.year_movies.data.movies_plays, \'Top movies\', false, true, \'year_stats_year_movies\', \'year_stats_year_movies_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_movies_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_movies_button" id="year_stats_year_movies_button" onclick="top_list_sort_by(results.year_stats.year_movies.data.movies_plays, \'Top movies\', false, false, true, \'year_stats_year_movies\', \'year_stats_year_movies_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_movies_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.year_stats.year_movies.data.movies_duration, functions.get_year_stats_movies_title, false, true, 'year_stats_year_movies');
text += top_list(results.year_stats.year_movies.data.movies_duration, functions.get_year_stats_movies_title, false, false, true, 'year_stats_year_movies');
} else {
text += top_list(results.year_stats.year_movies.data.movies_plays, functions.get_year_stats_movies_title, false, true, 'year_stats_year_movies');
text += top_list(results.year_stats.year_movies.data.movies_plays, functions.get_year_stats_movies_title, false, false, true, 'year_stats_year_movies');
}
text += "</div>";
@ -906,13 +914,13 @@ function load_users() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_shows_button" id="year_stats_year_shows_button" onclick="top_list_sort_by(results.year_stats.year_shows.data.shows_plays, \'Top shows\', false, false, \'year_stats_year_shows\', \'year_stats_year_shows_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_shows_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_shows_button" id="year_stats_year_shows_button" onclick="top_list_sort_by(results.year_stats.year_shows.data.shows_plays, \'Top shows\', false, true, false, \'year_stats_year_shows\', \'year_stats_year_shows_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_shows_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.year_stats.year_shows.data.shows_duration, functions.get_year_stats_shows_title, false, false, 'year_stats_year_shows');
text += top_list(results.year_stats.year_shows.data.shows_duration, functions.get_year_stats_shows_title, false, true, false, 'year_stats_year_shows');
} else {
text += top_list(results.year_stats.year_shows.data.shows_plays, functions.get_year_stats_shows_title, false, false, 'year_stats_year_shows');
text += top_list(results.year_stats.year_shows.data.shows_plays, functions.get_year_stats_shows_title, false, true, false, 'year_stats_year_shows');
}
text += "</div>";
@ -922,13 +930,13 @@ function load_users() {
text += "<div class='boks2'>";
if(functions.stats_order_by_plays !== false && functions.stats_order_by_duration !== false) {
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_music_button" id="year_stats_year_music_button" onclick="top_list_sort_by(results.year_stats.year_music.data.artists_plays, \'Top artists\', \'artist\', false, \'year_stats_year_music\', \'year_stats_year_music_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_music_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
text += '<button class="form-control btn" style="margin: 0.5em auto; width: fit-content;" name="year_stats_year_music_button" id="year_stats_year_music_button" onclick="top_list_sort_by(results.year_stats.year_music.data.artists_plays, \'Top artists\', \'artist\', false, false, \'year_stats_year_music\', \'year_stats_year_music_button\', functions);"><img src="assets/tweak.svg" class="btn_logo"><p2 id="year_stats_year_music_button_text">' + functions.wrapperr_sort_plays + '</p2></button>';
}
if(functions.stats_order_by_duration !== false) {
text += top_list(results.year_stats.year_music.data.artists_duration, functions.get_year_stats_music_title, "artist", false, 'year_stats_year_music');
text += top_list(results.year_stats.year_music.data.artists_duration, functions.get_year_stats_music_title, "artist", false, false, 'year_stats_year_music');
} else {
text += top_list(results.year_stats.year_music.data.artists_plays, functions.get_year_stats_music_title, "artist", false, 'year_stats_year_music');
text += top_list(results.year_stats.year_music.data.artists_plays, functions.get_year_stats_music_title, "artist", false, false, 'year_stats_year_music');
}
text += "</div>";
@ -961,22 +969,28 @@ function load_outro() {
text += "<div class='boks2' style='margin-top:5em;'>";
if(!link_mode && functions.create_share_links && functions.use_plex_auth) {
if(!link_mode && functions.create_share_links && functions.plex_auth) {
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_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>";
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: var(--white); border-radius: 0.25em; overflow: auto;'></div>";
text += "<div class='form-group' id='share_wrapped_results_title_div' style='margin: 0; display: none;'>";
text += "<p style='margin: 0'>Shareable link (valid for 7 days):</p>";
text += "</div>";
text += "<div class='form-group' id='share_wrapped_results_div' style='display: none; flex-direction: row; flex-wrap: wrap; align-content: flex-start; justify-content: center; align-items: flex-start; border-radius: 6px; margin: 0 0 0.5em 0; padding: 0.5em;'>";
text += "<div class='form-control btn' id='share_wrapped_delete_button' style='background-color: var(--white); cursor: default;'>";
text += "<span id='share_wrapped_results_url' class='share_wrapped_url' style=''></span>";
text += "<img id='share_wrapped_copy' src='assets/share.svg' style='' title='Click to copy the URL' onclick='copy_link_user();'>";
text += "<img id='share_wrapped_delete' src='assets/trash.svg' style='' title='Click to delete this URL' onclick='delete_new_link_user();'>";
text += "</div>";
text += "</div>";
text += "</div>";
}
}
var url_home = window.location.href.split('?')[0];
@ -1001,7 +1015,6 @@ function create_wrapped_link() {
document.getElementById("share_wrapped_button").style.opacity = '0.5';
wrapped_form = {
"cookie" : get_cookie('wrapperr-user'),
"data" : results,
"functions" : functions
};
@ -1020,21 +1033,27 @@ function create_wrapped_link() {
document.getElementById("share_wrapped_button").disabled = false;
document.getElementById("share_wrapped_button").style.opacity = '1';
}
if(result.error) {
alert(result.message);
document.getElementById("share_wrapped_button").disabled = false;
document.getElementById("share_wrapped_button").style.opacity = '1';
} 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';
document.getElementById('share_wrapped_results_url').innerHTML = window.location.href.split('?')[0] + '?hash=' + result.data;
document.getElementById('share_wrapped_results_title_div').style.display = 'block';
document.getElementById('share_wrapped_results_div').style.display = 'flex';
document.getElementById("share_wrapped_button").disabled = true;
document.getElementById("share_wrapped_button").style.opacity = '0.5';
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/create_link.php");
xhttp.open("post", "api/create/share-link");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(wrapped_data);
}
@ -1058,6 +1077,58 @@ function seconds_to_time(seconds, comma) {
return time;
}
function copy_link_user() {
/* Get the text field */
var copyText = document.getElementById("share_wrapped_results_url").innerHTML
/* Copy the text inside the text field */
navigator.clipboard.writeText(copyText)
alert("URL copied to clipboard.")
}
function delete_new_link_user() {
if(!confirm('Are you sure you want to delete this link?')) {
return;
}
cookie_form = {};
var cookie_data = JSON.stringify(cookie_form);
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 Wrapperr response. Response: ' + this.responseText)
}
if(!result.error) {
document.getElementById('share_wrapped_results_url').innerHTML = "";
document.getElementById('share_wrapped_results_title_div').style.display = 'none';
document.getElementById('share_wrapped_results_div').style.display = 'none';
document.getElementById("share_wrapped_button").disabled = false;
document.getElementById("share_wrapped_button").style.opacity = '1';
} else {
console.log(result.message);
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/delete/user-share-link");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(cookie_data);
return;
}
// Change background color for each category
$(window).scroll(function() {

View file

@ -20,6 +20,51 @@
</head>
<body id='body'>
<div id="loading">
<img id="loading-image" style="border-radius: 25px; background-color: var(--white); padding: 1em;width: 4em; height: 4em;" src="assets/loading.gif" alt="Loading..." />
<div class="form-group" id=''>
<div id="results_error_loading_screen" style="color: var(--white) !important;">Loading...</div>
</div>
</div>
<div id="background_image"></div>
<div class="snowflakes" aria-hidden="true" id="snowflakes2">
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
<div class="snowflake-2">
</div>
</div>
<div class="content_landing" id="login_content" style="">
<div class="boks">
@ -27,7 +72,7 @@
<div class="boks3" id="" style="padding: 0 !important;">
<div class="boks2" style="float: none !important; display: block; padding: 0; margin: 1em 0 0 0;">
<img src="assets/img/gift.svg" onclick="window.location.href = './';" style="width: 15em; cursor: pointer;">
<img src="assets/img/gift.svg" onclick="window.location.href = './';" style="width: 12.5em; cursor: pointer;">
</div>
</div>
@ -36,7 +81,7 @@
<div class="boks2" style="float: none !important; display: block; margin-top: 0em; padding-top: 0;">
<div class="stats_tekst" id='intro_text' style="height: auto;">
<div class="stats_tekst" id='intro_text' style="height: auto; width: -webkit-fill-available;">
<p id='wrapperr_front_page_title'>
Did you get that thing from Spotify and wondered what your Plex statistics looked like?
</p>
@ -57,7 +102,7 @@
<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" onclick='plex_login()'>
<button class='form-control btn' name="plex_login_button" id="plex_login_button" onclick='pop_up_login(); return false;' style='opacity: 0.5;' disabled>
<img src='assets/external-link.svg' class='btn_logo'>
<p2 id='plex_login_button_text'>Sign in using Plex</p2>
</button>
@ -110,8 +155,8 @@
padding: 0.5em;
'>
<div class='form-control btn' id="share_wrapped_delete_button" style='background-color: var(--white); cursor: default;'>
<span id='share_wrapped_url' style=''></span>
<img id='share_wrapped_copy' src='assets/external-link.svg' style='' onclick='copy_link_user();'>
<span id='share_wrapped_url' class='share_wrapped_url' style=''></span>
<img id='share_wrapped_copy' src='assets/external-link.svg' style='' onclick='open_link_user();'>
<img id='share_wrapped_delete' src='assets/trash.svg' style='' onclick='delete_link_user();'>
</div>
</div>
@ -152,16 +197,18 @@
<div class="snowflake">
</div>
</div>
</div>
</div>
<div class="footer" id="footer">
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="./">Wrapperr</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;" id="github_link" href="https://github.com/aunefyren/wrapperr" target="_blank">GitHub</a>
<div class="footer-elements">
<a style="color: white; font-weight: normal; font-size: 0.75em; text-decoration: none;" href="./">Wrapperr</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;" id="github_link" href="https://github.com/aunefyren/wrapperr" target="_blank">GitHub</a>
</div>
</div>
</div>
@ -170,35 +217,56 @@
<script type="text/javascript">
// Initialize variables for global reference
var root = "./";
var link_mode = false;
var results;
var functions;
var plex_identity = false;
var cookie = false;
var client_key = "";
var wrapperr_configured = false;
$(document).ready(function() {
var url = "";
var code = 0;
var id = 0;
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");
document.onreadystatechange = function() {
switch (document.readyState) {
case "loading":
break;
case "interactive":
break;
case "complete":
// Get parameters from URL string
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");
// Attempt to close the tab if instructed to
if(close_me !== null) {
window.close();
return;
}
// If a hash is recieved, attempt to load shared link
if(hash !== null) {
document.getElementById('loading').style.display = "none";
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;">';
}
// Contact the Wrapperr API to get configuration details
get_wrapper_version();
get_wrapper_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>
</body>

View file

@ -1,6 +1,9 @@
function cookie_login_actions() {
// Get the cookie from the browser
cookie = get_cookie('wrapperr-user');
// If the cookie exists and 'plex_identity' is false,
if(cookie && !plex_identity) {
document.getElementById('search_wrapped_form').style.display = 'block';
document.getElementById('plex_login_form').style.display = 'none';
@ -8,6 +11,7 @@ function cookie_login_actions() {
validate_cookie_user(cookie);
}
}
function wrapped_link_actions(hash) {
@ -20,26 +24,29 @@ function wrapped_link_actions(hash) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
} catch(error) {
document.getElementById('stats').innerHTML = "API response can't be parsed.";
console.log("API response can't be parsed. Response: " + this.responseText);
reset_button();
return;
}
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;
functions = result.functions;
results = result.content.data;
functions = result.content.functions;
load_page();
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_link.php");
xhttp.open("post", "api/get/share-link");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(hash_data);
}
@ -71,43 +78,58 @@ function search_wrapperr(){
}
function plex_login(){
if(!wrapperr_configured) {
document.getElementById('results_error').innerHTML = "Wrapperr is not configured.";
return;
}
window_url = window.location.href.split('?')[0];
auth_form = {
"home_url" : window_url
};
var auth_data = JSON.stringify(auth_form);
var params = 'strong=true' + '&X-Plex-Product=Wrapperr' + '&X-Plex-Client-Identifier=' + client_key
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) {
if (this.readyState == 4) {
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.";
console.log("API response can't be parsed. Response: " + this.responseText);
return;
}
//console.log(result);
if(result.error) {
document.getElementById('results_error').innerHTML = result.message;
if(result.id == "" || result.code == "") {
document.getElementById('results_error').innerHTML = "Failed to retrieve ID and/or Code from Plex Auth.";
} else {
pop_up_login(result.url, result.code, result.id);
var base = "https://app.plex.tv/auth#?"
var forwardUrl = window_url + "?close_me=true"
var url_build = base + "clientID=" + client_key + "&code=" + result.code + "&context%5Bdevice%5D%5Bproduct%5D=" + "Wrapperr" + "&forwardUrl=" + forwardUrl
url = url_build;
code = result.code;
id = result.id;
document.getElementById("plex_login_button").disabled = false;
document.getElementById("plex_login_button").style.opacity = '1';
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_login_url.php");
xhttp.send(auth_data);
xhttp.withCredentials = false;
xhttp.open("post", "https://plex.tv/api/v2/pins");
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.setRequestHeader('Accept', 'application/json');
xhttp.send(params);
}
function pop_up_login(url, code, id) {
function pop_up_login() {
document.getElementById('plex_login_button_text').innerHTML = 'Loading...';
document.getElementById("plex_login_button").disabled = true;
@ -116,24 +138,37 @@ function pop_up_login(url, code, id) {
const openedWindow = window.open(
url,
"Plex Login",
"width=500,height=750,resizable,scrollbars,popup=yes"
"width=500,height=750,resizable,scrollbars,popup=yes,toolbar=0,menubar=0,location=0,status=0",
);
if (openedWindow == null || typeof(openedWindow)=='undefined') {
alert("Failed to open login window. Your browser might be blocking pop-up windows.");
reset_button();
return;
}
var timer = setInterval(function() {
if(openedWindow.closed) {
check_token(code, id);
if(openedWindow != null && typeof(openedWindow) !== 'undefined') {
wait_for_close();
clearInterval(timer);
}
}, 1000);
function wait_for_close() {
var timer = setInterval(function() {
if(openedWindow.closed) {
check_token(code, id);
clearInterval(timer);
}
}, 1000);
}
}
function check_token(code, id) {
@ -147,13 +182,14 @@ function check_token(code, id) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
} catch(error) {
document.getElementById('results_error').innerHTML = "API response can't be parsed.";
console.log('API response can\'t be parsed. Response: ' + this.responseText);
reset_button();
return;
}
//console.log(result);
@ -161,13 +197,14 @@ function check_token(code, id) {
if(result.error) {
reset_button();
} else {
set_cookie("wrapperr-user", result.cookie, 7);
set_cookie("wrapperr-user", result.data, 7);
location.reload();
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_login_cookie.php");
xhttp.open("post", "api/login/plex-auth");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.send(auth_data);
}
@ -179,10 +216,9 @@ function reset_button() {
}
function validate_cookie_user(cookie) {
var json_cookie = JSON.stringify({"cookie": cookie});
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
@ -190,6 +226,7 @@ function validate_cookie_user(cookie) {
document.getElementById('results_error').innerHTML = "API response can't be parsed.";
console.log("API response can't be parsed. Response: " + this.responseText);
reset_button();
return;
}
//console.log(result);
@ -210,85 +247,133 @@ function validate_cookie_user(cookie) {
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/validate_login_cookie.php");
xhttp.send(json_cookie);
xhttp.open("post", "/api/validate/plex-auth");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send();
return;
}
function get_user_links(cookie) {
cookie_form = {
"cookie" : cookie
};
cookie_form = {};
var cookie_data = JSON.stringify(cookie_form);
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
} catch(error) {
console.log('Failed to parse Wrapperr links. Response: ' + this.responseText)
return;
}
if(!result.error) {
for(var i = 0; i < result.links.length; i++) {
document.getElementById('share_wrapped_url').innerHTML = window.location.href.split('?')[0] + '?hash=' + result.links[i].url_hash;
document.getElementById('share_wrapped_title_div').style.display = 'block';
document.getElementById('share_wrapped_div').style.display = 'flex';
}
document.getElementById('share_wrapped_url').innerHTML = window.location.href.split('?')[0] + '?hash=' + result.data;
document.getElementById('share_wrapped_title_div').style.display = 'block';
document.getElementById('share_wrapped_div').style.display = 'flex';
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_link_user.php");
xhttp.open("post", "api/get/user-share-link");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(cookie_data);
return;
}
// Contact the Wrapperr API and get configuration details. Start processes based on the result
function get_wrapper_version() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (this.readyState == 4) {
// Attempt to parse the JSON response
try {
var result= JSON.parse(this.responseText);
} catch(error) {
console.log('Failed to parse Wrapperr version. Response: ' + this.responseText)
// Place error on the loading screen
document.getElementById("results_error_loading_screen").innerHTML = 'The API did not respond correctly.';
return;
}
if(!result.error) {
// If there was no error in the API request and link mode is not enabled
if(!result.error && !link_mode) {
// Set the current version in the footer
document.getElementById('github_link').innerHTML = 'GitHub (' + result.wrapperr_version + ')';
if(result.application_name && result.application_name !== '' && !link_mode) {
// Change the application name based on Wrapperr configuration
if(result.application_name && result.application_name !== '') {
document.getElementById('application_name').innerHTML = result.application_name;
document.title = result.application_name;
}
if(!result.use_plex_auth) {
wrapperr_search_function();
// Set the client key in the JS variable
client_key = result.client_key;
// Set the theme option in the JS variable
winter_theme = result.winter_theme;
// Enable snow and background image based on variable
if(winter_theme) {
document.getElementById('snowflakes').style.display = "block";
document.getElementById('snowflakes2').style.display = "block";
document.getElementById('background_image').style.backgroundImage = "url('assets/winter.webp')";
}
// Change the title based based on Wrapperr configuration
if(result.wrapperr_front_page_title !== '') {
document.getElementById('wrapperr_front_page_title').innerHTML = result.wrapperr_front_page_title;
}
// Change the subtitle based based on Wrapperr configuration
if(result.wrapperr_front_page_subtitle !== '') {
document.getElementById('wrapperr_front_page_subtitle').innerHTML = result.wrapperr_front_page_subtitle;
}
// Set the 'configured' option in the JS variable
wrapperr_configured = result.wrapperr_configured;
// Change search function to use Plex search instead
if(!result.plex_auth) {
wrapperr_search_function();
}
// If configured and using Plex Auth, call function
// If not configured throw error
if(result.plex_auth && wrapperr_configured) {
cookie_login_actions();
plex_login();
} else if(!wrapperr_configured) {
document.getElementById('results_error').innerHTML = "Wrapperr is not configured.";
}
// Remove the loading screen after one second
setTimeout(function(){
document.getElementById('loading').style.display = "none";
},1000);
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/get_wrapperr_version.php");
xhttp.open("post", "api/get/wrapperr-version");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.send();
return;
}
function copy_link_user() {
function open_link_user() {
window.open(document.getElementById('share_wrapped_url').innerHTML, '_blank').focus();
}
@ -298,11 +383,9 @@ function delete_link_user() {
return;
}
cookie_form = {
"cookie" : cookie
};
cookie_form = {};
var cookie_data = JSON.stringify(cookie_form);
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
@ -311,6 +394,7 @@ function delete_link_user() {
var result= JSON.parse(this.responseText);
} catch(error) {
console.log('Failed to parse Wrapperr response. Response: ' + this.responseText)
return;
}
if(!result.error) {
@ -323,15 +407,15 @@ function delete_link_user() {
}
};
xhttp.withCredentials = true;
xhttp.open("post", "api/delete_link_user.php");
xhttp.open("post", "api/delete/user-share-link");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", "Bearer " + cookie);
xhttp.send(cookie_data);
return;
}
function wrapperr_search_function() {
set_cookie("wrapperr-user", "", 1);
var html = '';
html += '<div class="form-group">';