├── .gitignore ├── Procfile ├── README.md ├── app.py ├── helpers.py ├── playlist.py ├── requirements.txt ├── static ├── functions.js └── styles.css └── templates ├── about.html.jinja ├── addplaylist.html ├── connect.html.jinja ├── index.html.jinja ├── layout.html.jinja ├── playlists.html.jinja ├── server.html.jinja └── tryagain.html.jinja /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /__pycache__ 3 | /venv-plexweb 4 | venv -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Forks][forks-shield]][forks-url] 2 | [![Stargazers][stars-shield]][stars-url] 3 | [![Issues][issues-shield]][issues-url] 4 | 5 |
6 |

7 | 8 | Logo 9 | 10 |

PlexWeb

11 | 12 |

13 | Add IMDb playlists to your Plex server 14 |
15 | View Website 16 | · 17 | Report Bug or Request Feature 18 |

19 |

20 | 21 | ## Disclaimer 22 | 23 | This project was created when I first started with programming. It does not work anymore since the Plex API has changed. I mostly leave it here for nostalgic reasons. 24 | 25 | ## Table of contents 26 | - [Table of contents](#table-of-contents) 27 | - [About](#about) 28 | - [Built with](#built-with) 29 | - [Download and run](#download-and-run) 30 | - [Features](#features) 31 | - [Info](#info) 32 | - [Docker](#docker) 33 | - [Contributing](#contributing) 34 | 35 | ## :clapper: About 36 | ![PlexWeb web app](https://i.imgur.com/H08RhfC.png) 37 | 38 | PlexWeb is a web manager for Plex servers. Connect to your server via your URL and token to access features like viewing current activity and adding playlists based on IMDb lists. 39 | 40 | ### 📋 Built with 41 | * Python 42 | * Flask 43 | * Beautiful Soup 44 | * PlexAPI 45 | * HTML 46 | * CSS 47 | * Bootstrap 48 | * Javascript 49 | * jQuery 50 | 51 | ## ☁️ Download and run 52 | 53 | - Install [Python](https://www.python.org/) and [Git](https://git-scm.com/) 54 | 55 | ```bash 56 | # Clone repository 57 | $ git clone https://github.com/banjoanton/plex-web.git 58 | 59 | # Change directory to repository 60 | $ cd "plex-web" 61 | 62 | # Install requirements 63 | $ pip install -r requirements.txt 64 | # or pip3 in some cases 65 | $ pip3 install -r requirements.txt 66 | 67 | # Run flask server 68 | $ flask run 69 | ``` 70 | ## ✔️ Features 71 | 72 | * Sign in to your Plex server. 73 | * Use **url** and **token**. 74 | * See users connected to the server. 75 | * See what they are **currently watching**. 76 | * **Search** the Plex library for shows and movies. 77 | * See playlists on the server. 78 | * Click on a playlist to get the **full list of movies**, including: 79 | * Title 80 | * Year released 81 | * Rating (from TheMovieDB) 82 | * Link to IMDb 83 | * **Add playlist** to server. 84 | * Based on already created IMDb lists. 85 | * Choose name 86 | * **Choose users** that should receive the playlists as well 87 | 88 | ## :information_source: Info 89 | This is my **final project** in the **[CS50](https://www.edx.org/course/cs50s-introduction-to-computer-science)** course. It includes most of the programming languages that are covered in the course; HTML, Python, Javascript and Jinja. Python is used as back-end with Flask, and Javascript is used to make the page dynamic. The page design hasn't been the main focus, but Bootstrap has been used to make it look as good as possible. 90 | 91 | This project was based on **lack of playlists in Plex**. When you have a movie library, the built in selection are pretty much useless. Therefore, I created a simple web app that adds playlists based on scraping a choosen IMDb list (like [this](https://www.imdb.com/list/ls026173135/)). You can add any list you find, and the web is full of them. All movies that you have in your library that matches the movies in the list will be added to a playlist. 92 | 93 | The other features of the web app are just neat implementations that enhances the management of playlists. 94 | 95 | ## :whale: Docker 96 | [christronyxyocum](https://github.com/christronyxyocum) created a Docker image which can be accessed [here](https://hub.docker.com/r/tronyx/plex-web). 97 | 98 | Github link [here](https://github.com/christronyxyocum/docker-plex-web). 99 | 100 | 101 | ## 🔧 Contributing 102 | Pull requests are welcome. Feel free to add anything. 103 | 104 | 105 | [stars-shield]: https://img.shields.io/github/stars/banjo/plex-web 106 | [stars-url]: https://github.com/banjo/plex-web/stargazers 107 | [issues-shield]: https://img.shields.io/github/issues/banjo/plex-web 108 | [issues-url]: https://github.com/banjo/plex-web/issues 109 | [forks-shield]: https://img.shields.io/github/forks/banjo/plex-web 110 | [forks-url]: https://github.com/banjo/plex-web/network/members 111 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, redirect, render_template, request, session, url_for 2 | from tempfile import mkdtemp 3 | from helpers import get_users, check_server, check_activity, get_movies, get_playlists, get_playlist_movies, login_required, get_sections 4 | from playlist import add_playlist_to_plex 5 | from plexapi.server import PlexServer 6 | import json 7 | import traceback 8 | 9 | # init flask 10 | app = Flask(__name__) 11 | 12 | # configurations for session 13 | app.config["SECRET_KEY"] = "07id2CFJSpRY5sQ3KEoZTjb6hoNfSFcI&&/a..#¤%//aaborpp" 14 | app.config["SESSION_PERMANENT"] = False 15 | 16 | 17 | @app.route("/connect", methods=["GET", "POST"]) 18 | def connect(): 19 | 20 | if request.method == "GET": 21 | return render_template("connect.html.jinja") 22 | else: 23 | plex_url = request.form.get("plex-url") 24 | plex_token = request.form.get("plex-token") 25 | 26 | # check if plex server is available 27 | if check_server(plex_url, plex_token): 28 | session["plex_token"] = plex_token 29 | session["plex_url"] = plex_url 30 | return redirect("/") 31 | else: 32 | return redirect("/tryagain") 33 | 34 | 35 | @app.route("/about") 36 | def about(): 37 | return render_template("about.html.jinja") 38 | 39 | 40 | @app.route('/') 41 | @login_required 42 | def index(): 43 | return render_template("index.html.jinja") 44 | 45 | 46 | @app.route('/server') 47 | @login_required 48 | def server(): 49 | users = get_users(session["plex_url"], session["plex_token"]) 50 | return render_template("server.html.jinja", users=users) 51 | 52 | 53 | @app.route('/playlists') 54 | @login_required 55 | def playlists(): 56 | 57 | plex_playlists = get_playlists(session["plex_url"], session["plex_token"]) 58 | 59 | playlists = [] 60 | 61 | for i, p_list in enumerate(plex_playlists): 62 | 63 | playlists.append({"title": p_list.title, 64 | "id": f'playlist-{i}', 65 | }) 66 | 67 | return render_template("playlists.html.jinja", playlists=playlists) 68 | 69 | 70 | @app.route('/addplaylist') 71 | @login_required 72 | def addplaylist(): 73 | 74 | sections = get_sections(session["plex_url"], session["plex_token"]) 75 | users = get_users(session["plex_url"], session["plex_token"]) 76 | 77 | data = {"sections": sections, 78 | "users": users} 79 | 80 | return render_template('addplaylist.html', data=data) 81 | 82 | 83 | @app.route('/disconnect') 84 | def disconnect(): 85 | session.clear() 86 | return redirect("/") 87 | 88 | 89 | @app.route('/tryagain') 90 | def tryagain(): 91 | return render_template("tryagain.html.jinja") 92 | 93 | 94 | @app.route('/update_activity', methods=["POST"]) 95 | def update_activity(): 96 | # get the username from the form 97 | users = request.form.getlist("users[]") 98 | user_dict = {} 99 | 100 | # get all active users from plex 101 | activity = check_activity(session["plex_url"], session["plex_token"]) 102 | for user in users: 103 | # set standard to false 104 | user_dict[user] = {"active": False} 105 | 106 | for playing in activity: 107 | if str(playing.usernames[0]) == user: 108 | user_dict[user] = {"user": user, 109 | "show": playing.grandparentTitle if playing.type == "episode" else playing.title, 110 | "active": True} 111 | 112 | return jsonify(user_dict) 113 | 114 | 115 | @app.route('/search', methods=["GET"]) 116 | def search(): 117 | query = request.args.get("query") 118 | movies = get_movies(session["plex_url"], session["plex_token"], query) 119 | movies = [movie.title for movie in movies] 120 | return jsonify(movies=movies) 121 | 122 | 123 | @app.route('/playdata', methods=["GET"]) 124 | def playdata(): 125 | playlist = request.args.get("playlist") 126 | movies = get_playlist_movies( 127 | session["plex_url"], session["plex_token"], playlist) 128 | 129 | return jsonify(movies) 130 | 131 | 132 | @app.route('/addplaylisttoplex', methods=["POST"]) 133 | def addplaylisttoplex(): 134 | link = "https://www.imdb.com/list/" 135 | 136 | imdb = request.form.get('imdb') 137 | name = request.form.get('name') 138 | section = request.form.get('section') 139 | users = json.loads(request.form.get('users')) 140 | 141 | data = {"success": False, 142 | "error": "Something went wrong. Could not add the playlist to plex. Please try again.", 143 | "failed": []} 144 | 145 | try: 146 | failed_movies = add_playlist_to_plex( 147 | session["plex_url"], session["plex_token"], link + imdb, name, section, users) 148 | data["success"] = True 149 | data["error"] = None 150 | data["failed"] = failed_movies 151 | except IndexError: 152 | # gets raised when the list cannot be scraped. 153 | data["error"] = "Wrong list ID, could not retrieve data." 154 | except NameError as e: 155 | # gets raised when a user cannot access the library 156 | data["error"] = str(e) 157 | except Exception as e: 158 | # print error message to trace error 159 | print(traceback.print_exc()) 160 | 161 | return jsonify(data) -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, render_template, request, session 2 | from functools import wraps 3 | from plexapi.server import PlexServer 4 | 5 | 6 | def login_required(f): 7 | @wraps(f) 8 | def decorated_function(*args, **kwargs): 9 | if session.get("plex_token") is None: 10 | return redirect("/connect") 11 | return f(*args, **kwargs) 12 | return decorated_function 13 | 14 | 15 | def get_users(url, token): 16 | return [user.title for user in PlexServer(url, token).myPlexAccount().users() if user.friend] 17 | 18 | 19 | def check_server(url, token): 20 | try: 21 | plex = PlexServer(url, token) 22 | return True 23 | except: 24 | return False 25 | 26 | 27 | def check_activity(url, token): 28 | return [client for client in PlexServer(url, token).sessions()] 29 | 30 | 31 | def get_movies(url, token, query): 32 | return PlexServer(url, token).search(query) 33 | 34 | 35 | def get_playlists(url, token): 36 | return PlexServer(url, token).playlists() 37 | 38 | 39 | def get_playlist_movies(url, token, playlist): 40 | movies = PlexServer(url, token).playlist(playlist) 41 | 42 | movie_list = [] 43 | 44 | for movie in movies.items(): 45 | movie_list.append({"title": movie.title, 46 | "guid": movie.guid[26:], 47 | "year": movie.year, 48 | "rating": movie.audienceRating}) 49 | 50 | return movie_list 51 | 52 | 53 | def get_sections(url, token): 54 | return [section.title for section in PlexServer(url, token).library.sections() if section.type == "movie"] 55 | -------------------------------------------------------------------------------- /playlist.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import sys 4 | from plexapi.server import PlexServer 5 | 6 | URL_PAGE = "?&mode=detail&page=" 7 | HEADERS = {"Accept-Language": "en-US,en;q=0.5"} 8 | 9 | 10 | def add_playlist_to_plex(url, token, link, name, section, users): 11 | 12 | movies = scrape(link) 13 | playlist, failed_movies = add_playlist(movies, url, token, name) 14 | 15 | if len(users) > 0: 16 | copy_to_users(playlist, users) 17 | 18 | return failed_movies 19 | 20 | 21 | def scrape(website): 22 | 23 | # create lists that takes all data from every loop 24 | movie_final = [] 25 | year_final = [] 26 | 27 | k = 1 28 | while True: 29 | 30 | html = requests.get(website + URL_PAGE + str(k), headers=HEADERS) 31 | soup = BeautifulSoup(html.content, "lxml") 32 | 33 | # find popular movies and years, and convert to list 34 | movies = soup.select( 35 | '#main > div > div.lister.list.detail.sub-list > div.lister-list > div > div.lister-item-content > h3 > a') 36 | years = soup.select( 37 | "#main > div > div.lister.list.detail.sub-list > div.lister-list > div > div.lister-item-content > h3 > span.lister-item-year.text-muted.unbold" 38 | ) 39 | 40 | # break if there are not movies left 41 | if len(movies) == 0: 42 | break 43 | 44 | # get all movie titles and years 45 | movies_string = [movie.get_text() for movie in movies] 46 | years_int = [year.get_text().replace("(", "") for year in years] 47 | years_int = [year.replace(")", "") for year in years_int] 48 | years_int = [year.replace("I ", "") for year in years_int] 49 | 50 | # append all movies that are scraped 51 | movie_final = movie_final + movies_string.copy() 52 | year_final = year_final + years_int.copy() 53 | 54 | # loop 55 | k += 1 56 | 57 | # create dict for each movie 58 | movie_list = {} 59 | for i, movie in enumerate(movie_final): 60 | movie_list[movie] = {"title": movie, "year": year_final[i]} 61 | 62 | return movie_list 63 | 64 | 65 | def add_playlist(name_list, url, token, name): 66 | movie_list = [] 67 | failed_movies = [] 68 | 69 | for movie in name_list: 70 | 71 | # create the movie dict item 72 | movie = name_list[movie] 73 | 74 | # get movie if it exists 75 | temp = get_movie(movie, url, token) 76 | 77 | # loop if it can't find the movie 78 | if temp is False: 79 | failed_movies.append(movie["title"]) 80 | continue 81 | 82 | # add to list if it can find it 83 | movie_list.append(temp) 84 | 85 | # create playlist 86 | playlist = PlexServer(url, token).createPlaylist(name, movie_list) 87 | 88 | return playlist, failed_movies 89 | 90 | 91 | def get_movie(movie, url, token): 92 | results = PlexServer(url, token).search(movie["title"]) 93 | 94 | # return movie if it exists 95 | for plex_movie in results: 96 | 97 | if plex_movie.type == "movie" and str(plex_movie.year) in str(movie["year"]): 98 | return plex_movie 99 | 100 | return False 101 | 102 | 103 | def copy_to_users(playlist, users): 104 | for user in users: 105 | try: 106 | playlist.copyToUser(user) 107 | except: 108 | raise NameError(f"{user} does not have access to the library.") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjo/plex-web/21a7dbbdc2fdd65500c3afdcfae2b648e462ea73/requirements.txt -------------------------------------------------------------------------------- /static/functions.js: -------------------------------------------------------------------------------- 1 | // Update user activity function 2 | function update_activity() { 3 | let li = $("[name='active-users']"); 4 | let users = []; 5 | 6 | for (let i = 0; i < li.length; i++) { 7 | users.push(li[i].id); 8 | } 9 | 10 | $.post( 11 | "/update_activity", 12 | { 13 | users: users 14 | }, 15 | function(data) { 16 | // isee if user is active 17 | for (let user in data) { 18 | let element = document.getElementById(user); 19 | if (data[user]["active"]) { 20 | // update if active 21 | element.classList.add("active"); 22 | element.innerHTML = user + " - " + data[user]["show"]; 23 | } else { 24 | // update if not active 25 | element.classList.remove("active"); 26 | element.innerHTML = user; 27 | } 28 | } 29 | } 30 | ); 31 | } 32 | 33 | // Update activity on click 34 | $(document).ready(function() { 35 | $("#activity-button").click(function() { 36 | update_activity(); 37 | }); 38 | }); 39 | 40 | // Update activity on refresh 41 | $(document).ready(function() { 42 | if (top.location.pathname === "/server") { 43 | update_activity(); 44 | } 45 | }); 46 | 47 | // search movie 48 | function search() { 49 | let query = document.getElementById("query").value; 50 | 51 | $.get("/search?query=" + query, function(data) { 52 | let ul = document.getElementById("query-list"); 53 | ul.innerHTML = ""; 54 | 55 | data = data["movies"]; 56 | 57 | for (let i = 0; i < data.length; i++) { 58 | let movie = data[i]; 59 | 60 | let li = document.createElement("li"); 61 | li.classList.add("list-group-item"); 62 | li.appendChild(document.createTextNode(movie)); 63 | 64 | ul.appendChild(li); 65 | } 66 | }); 67 | } 68 | 69 | // get movies in playlist 70 | function content(button) { 71 | let buttonID = button.getAttribute("href").substring(1); 72 | let playlistName = button.innerHTML; 73 | let sendData = "playlist=" + playlistName; 74 | 75 | // create table 76 | let tbl = document.createElement("table"); 77 | tbl.setAttribute("id", "movie-table"); 78 | tbl.classList.add("table"); 79 | 80 | // create parts of table 81 | let thead = document.createElement("thead"); 82 | let tr = document.createElement("tr"); 83 | 84 | // create headers 85 | 86 | let th1 = document.createElement("th"); 87 | th1.setAttribute("scope", "col"); 88 | th1.innerHTML = "Movie"; 89 | 90 | let th2 = document.createElement("th"); 91 | th2.setAttribute("scope", "col"); 92 | th2.innerHTML = "Year"; 93 | 94 | let th3 = document.createElement("th"); 95 | th3.setAttribute("scope", "col"); 96 | th3.innerHTML = "Rating"; 97 | 98 | // create body 99 | let tbody = document.createElement("tbody"); 100 | 101 | // add body element 102 | tbl.appendChild(tbody); 103 | 104 | // add header elements 105 | tr.appendChild(th1); 106 | tr.appendChild(th2); 107 | tr.appendChild(th3); 108 | thead.appendChild(tr); 109 | tbl.appendChild(thead); 110 | 111 | $.get("/playdata?" + sendData, function(data) { 112 | let elem = document.getElementById(buttonID); 113 | elem.innerHTML = ""; 114 | 115 | for (let i = 0; i < data.length; i++) { 116 | movie = data[i]; 117 | let imdbLink = "https://www.imdb.com/title/" + movie["guid"]; 118 | 119 | let row = document.createElement("tr"); 120 | row.setAttribute("scope", "col"); 121 | 122 | // add title 123 | let title = document.createElement("td"); 124 | let aTitle = document.createElement("a"); 125 | aTitle.innerHTML = movie["title"]; 126 | aTitle.setAttribute("href", imdbLink); 127 | aTitle.setAttribute("target", "_blank"); 128 | 129 | title.appendChild(aTitle); 130 | row.appendChild(title); 131 | 132 | // add year 133 | let year = document.createElement("td"); 134 | year.innerHTML = movie["year"]; 135 | row.appendChild(year); 136 | 137 | // add rating 138 | let rating = document.createElement("td"); 139 | rating.innerHTML = movie["rating"]; 140 | row.appendChild(rating); 141 | 142 | tbody.appendChild(row); 143 | } 144 | 145 | elem.appendChild(tbl); 146 | }); 147 | } 148 | 149 | // select all button 150 | function selectAllUsers(self) { 151 | let checkboxes = document.getElementsByName("user-checkbox"); 152 | 153 | for (let i = 0; i < checkboxes.length; i++) { 154 | checkboxes[i].checked = true; 155 | } 156 | } 157 | 158 | // add playlist to plex 159 | $(document).ready(function() { 160 | $("#addPlaylistButton").click(function() { 161 | // Hide previous alerts 162 | $(".alert").hide(); 163 | 164 | let imdb = document.getElementById("imdb-ls").value; 165 | let name = document.getElementById("playlist-name").value; 166 | let section = document.getElementById("select-section").value; 167 | 168 | let users = document.getElementsByName("user-checkbox"); 169 | let checkedUsers = []; 170 | 171 | for (let i = 0; i < users.length; i++) { 172 | if (users[i].checked) { 173 | checkedUsers.push(users[i].value); 174 | } 175 | } 176 | 177 | $.post( 178 | "/addplaylisttoplex", 179 | { 180 | imdb: imdb, 181 | name: name, 182 | section: section, 183 | users: JSON.stringify(checkedUsers) 184 | }, 185 | function(data) { 186 | if (data["success"]) { 187 | // create list for missing movies 188 | let list = document.createElement("div"); 189 | 190 | data["failed"].forEach(function(movie) { 191 | let item = document.createElement("li"); 192 | 193 | item.appendChild(document.createTextNode(movie)); 194 | 195 | list.appendChild(item); 196 | }); 197 | 198 | document.getElementById("failed-movies").appendChild(list); 199 | 200 | $("#playlist-success").show(); 201 | $("#failed-movies-alert").show(); 202 | } else { 203 | // update error message 204 | $("#error-message").html(data["error"]); 205 | $("#playlist-failed").show(); 206 | } 207 | } 208 | ); 209 | }); 210 | }); 211 | 212 | // show spinner while adding playlist 213 | $(document).ready(function() { 214 | $(document) 215 | .ajaxStart(function() { 216 | $("#load-playlist").css("visibility", "visible"); 217 | }) 218 | .ajaxStop(function() { 219 | $("#load-playlist").css("visibility", "hidden"); 220 | }); 221 | }); -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | #search-button { 2 | margin-bottom: 43px; 3 | margin-left: 20px; 4 | } 5 | 6 | #activity-button { 7 | margin-bottom: 60px; 8 | } 9 | 10 | #load-playlist { 11 | margin-left: 20px; 12 | visibility: hidden; 13 | } 14 | 15 | #playlist-failed { 16 | display: none; 17 | margin-top: 20px; 18 | } 19 | 20 | #playlist-success { 21 | display: none; 22 | margin-top: 20px; 23 | } 24 | #failed-movies-alert { 25 | display: none; 26 | margin-top: 20px; 27 | } 28 | 29 | @import url(https://fonts.googleapis.com/css?family=Roboto:300); 30 | 31 | .login-page { 32 | width: 360px; 33 | padding: 8% 0 0; 34 | margin: auto; 35 | } 36 | #login-form { 37 | position: relative; 38 | z-index: 1; 39 | background: #ffffff; 40 | max-width: 360px; 41 | margin: 0 auto 100px; 42 | padding: 45px; 43 | text-align: center; 44 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 45 | } 46 | #login-form input { 47 | /* font-family: "Roboto", sans-serif; */ 48 | outline: 0; 49 | background: #f2f2f2; 50 | width: 100%; 51 | border: 0; 52 | margin: 0 0 15px; 53 | padding: 15px; 54 | box-sizing: border-box; 55 | font-size: 14px; 56 | } 57 | #login-form button { 58 | font-family: "Roboto", sans-serif; 59 | text-transform: uppercase; 60 | outline: 0; 61 | width: 100%; 62 | border: 0; 63 | padding: 15px; 64 | color: #ffffff; 65 | font-size: 14px; 66 | -webkit-transition: all 0.3 ease; 67 | transition: all 0.3 ease; 68 | cursor: pointer; 69 | } 70 | 71 | #about-button { 72 | margin-top: 10px; 73 | background: #3498db; 74 | } 75 | 76 | #submit-button { 77 | background: #4caf50; 78 | } 79 | 80 | #about-button:hover { 81 | background: #2980b9; 82 | } 83 | 84 | #submit-button:hover { 85 | background: #43a047; 86 | } 87 | 88 | #login-form .message { 89 | margin: 15px 0 0; 90 | color: #b3b3b3; 91 | font-size: 12px; 92 | } 93 | #login-form .message a { 94 | color: #4caf50; 95 | text-decoration: none; 96 | } 97 | #login-form .register-form { 98 | display: none; 99 | } 100 | 101 | body { 102 | background: #ffffff; /* fallback for old browsers */ 103 | background: -webkit-linear-gradient(right, #ffffff, #f1f2f6); 104 | background: -moz-linear-gradient(right, #ffffff, #f1f2f6); 105 | background: -o-linear-gradient(right, #ffffff, #f1f2f6); 106 | background: linear-gradient(to left, #ffffff, #f1f2f6); 107 | font-family: "Roboto", sans-serif; 108 | -webkit-font-smoothing: antialiased; 109 | -moz-osx-font-smoothing: grayscale; 110 | } 111 | 112 | .token-help { 113 | margin-bottom: 200px; 114 | } -------------------------------------------------------------------------------- /templates/about.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - About 5 | {% endblock %} 6 | 7 | {% block main %} 8 |

About

9 | 10 |
11 | 12 |

13 | PlexWeb is a webpage that - in a simple way - allows you to browse your Plex server and manage it. 14 |

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/addplaylist.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} {% block title %} - Add Playlist {% endblock 2 | %} {% block main %} 3 | 4 |

Add Playlist

5 |
6 |

7 | New playlists can be added by scraping an IMDb list. Go to the web link of a 8 | list (like 9 | this) 10 | and copy the list ID from the URL. Select the name and which users you want 11 | to copy the playlist to (The owner is included by default). 12 |

13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 27 | Get ID from the list URL. 30 |
31 | 32 |
33 | 34 | 41 | The name that the playlist will have on Plex. 44 |
45 | 46 |
47 | 50 | 55 | Which Plex library section the playlist will 57 | use. 59 |
60 | 61 |
62 | 69 |
74 | Loading... 75 |
76 |
77 |
78 |
79 | 80 |
81 | {% for user in data.users %} 82 |
83 | 90 | 93 |
94 | {% endfor %} 95 |
96 | 104 |
105 |
106 |
107 | 108 | 109 | 116 | 117 | 121 | 122 | 126 |
127 |
128 | {% endblock %} -------------------------------------------------------------------------------- /templates/connect.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - Connect 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 16 |
17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/index.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - Signed in 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 | Success! 10 | You have signed in. Access your server in the menu bar. 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/layout.html.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | PlexWeb{% block title %}{% endblock %} 30 | 31 | 32 | 33 | 34 | 58 | 59 |
60 | {% block main %}{% endblock %} 61 |
62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /templates/playlists.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - Playlists 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 |

Playlist

10 | 11 |
12 | 13 |

14 | All playlists connected to the owner of the plex server. Press a playlist to get a table with all movies on the playlist. 15 |

16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | {% for playlist in playlists %} 24 | {{playlist.title}} 26 | {% endfor %} 27 |
28 |
29 |
30 | 42 |
43 |
44 | 45 | 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /templates/server.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - Server 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 |

Users

12 | 13 |
14 | 15 |

16 | Press the button to update the user activity. Your own stream will not show. 17 |

18 | 19 | 20 | 21 |
22 | 23 |
    24 | {% for user in users %} 25 |
  • {{user}}
  • 26 | {% endfor %} 27 |
28 | 29 |
30 |
31 |

Movies

32 | 33 |
34 | 35 |

36 | Search for movies and shows in your plex library. No episodes will show. 37 |

38 | 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 |
    47 |
48 |
49 |
50 |
51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /templates/tryagain.html.jinja: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.jinja" %} 2 | 3 | {% block title %} 4 | - Try Again 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 | Wrong URL or Token! Could not connect to the server, please try again. 10 |
11 | {% endblock %} 12 | --------------------------------------------------------------------------------