From f8aa5ff2d716f3ce517823c7fbb68beee4398d6f Mon Sep 17 00:00:00 2001 From: Matthew Welch Date: Wed, 5 Aug 2020 18:47:29 -0700 Subject: [PATCH] Changed file formatting and fixed comic viewer. --- admin/admin.py | 12 +- comics/comics.py | 298 ++-- comics/templates/comics/comicGallery.html | 17 +- games/games.py | 218 ++- games/templates/games/index.html | 2 +- rpiWebApp.py | 455 +++--- scripts/database.py | 1367 +++++++++-------- scripts/fix_thumbnail_size.py | 52 +- scripts/func.py | 935 ++++++----- scripts/imdb_import.py | 30 +- scripts/tmdb.py | 134 +- static/images/Seven Seas Entertainment.png | Bin 0 -> 21470 bytes static/images/default.png | Bin 17826 -> 7026 bytes templates/base.html | 7 +- .../templates/tv_movies/episodeViewer.html | 20 +- tv_movies/templates/tv_movies/index.html | 36 +- .../templates/tv_movies/movieViewer.html | 22 +- tv_movies/templates/tv_movies/search.html | 36 +- tv_movies/tv_movies.py | 284 ++-- 19 files changed, 2180 insertions(+), 1745 deletions(-) create mode 100644 static/images/Seven Seas Entertainment.png diff --git a/admin/admin.py b/admin/admin.py index f298a39..87afdfe 100644 --- a/admin/admin.py +++ b/admin/admin.py @@ -1,5 +1,5 @@ -from flask import Blueprint, flash, redirect, url_for, render_template -from flask_login import login_required, current_user +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import current_user, login_required Admin = Blueprint("admin", __name__, template_folder="templates") @@ -7,7 +7,7 @@ Admin = Blueprint("admin", __name__, template_folder="templates") @Admin.route("/admin") @login_required def index(): - if not current_user.is_admin: - flash("you must be an admin to access this page, login with an admin account.") - return redirect(url_for("login")) - return render_template("admin/index.html", title="Admin") + if not current_user.is_admin: + flash("you must be an admin to access this page, login with an admin account.") + return redirect(url_for("login")) + return render_template("admin/index.html", title="Admin") diff --git a/comics/comics.py b/comics/comics.py index 35ab492..507e1ba 100644 --- a/comics/comics.py +++ b/comics/comics.py @@ -1,10 +1,12 @@ -from flask import Blueprint, render_template, request, make_response, current_app -from flask_login import login_required - -from urllib import parse -import filetype -import os, pytz, datetime +import datetime import inspect +import os +from urllib import parse + +import filetype +import pytz +from flask import Blueprint, current_app, make_response, render_template, request +from flask_login import login_required from scripts import database, func @@ -16,145 +18,195 @@ MOBILE_DEVICES = ["android", "blackberry", "ipad", "iphone"] @Comics.route("/comics") @login_required def index(): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - publishers = database.get_publishers() - start = (max_items*(page-1)) - end = len(publishers) if len(publishers) < max_items*page else max_items*page - return render_template("comics/index.html", title="Comics", publishers=publishers, page=page, max_items=max_items, start=start, end=end, item_count=len(publishers)) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + publishers = database.get_publishers() + start = max_items * (page - 1) + end = len(publishers) if len(publishers) < max_items * page else max_items * page + return render_template( + "comics/index.html", + title="Comics", + publishers=publishers, + page=page, + max_items=max_items, + start=start, + end=end, + item_count=len(publishers), + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @Comics.route("/comics/search") @login_required def search(): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - publisher_end = 0 - series_end = 0 - comics_end = 0 - publisher_start = 0 - series_start = 0 - comics_start = 0 - item_count = 0 - query = request.args.get("q") - results = { - "publisher": [], - "series": [], - "comics": [] - } - if query: - results = database.db_search_comics(query) - item_count = len(results["publisher"]) + len(results["series"]) + len(results["comics"]) - for temp_page in range(1, page+1): - publisher_start = publisher_end - series_start = series_end - comics_start = comics_end - items = 0 - publisher_end = len(results["publisher"]) if len(results["publisher"]) < max_items*temp_page else max_items*temp_page - items += publisher_end - publisher_start - if items < max_items: - series_end = len(results["series"]) if len(results["series"]) < (max_items*temp_page)-items else (max_items*temp_page)-items - items += series_end-series_start - if items < max_items: - comics_end = len(results["comics"]) if len(results["comics"]) < (max_items*temp_page)-series_end-publisher_end else (max_items*temp_page)-series_end-publisher_end - return render_template("comics/search.html", title="Comics: Search", publishers=results["publisher"], publisher_series=results["series"], - comics=results["comics"], page=page, max_items=max_items, item_count=item_count, - publisher_start=publisher_start, series_start=series_start, comics_start=comics_start, - publisher_end=publisher_end, series_end=series_end, comics_end=comics_end) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e))+" "+str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + publisher_end = 0 + series_end = 0 + comics_end = 0 + publisher_start = 0 + series_start = 0 + comics_start = 0 + item_count = 0 + query = request.args.get("q") + results = {"publisher": [], "series": [], "comics": []} + if query: + results = database.db_search_comics(query) + item_count = len(results["publisher"]) + len(results["series"]) + len(results["comics"]) + for temp_page in range(1, page + 1): + publisher_start = publisher_end + series_start = series_end + comics_start = comics_end + items = 0 + publisher_end = len(results["publisher"]) if len(results["publisher"]) < max_items * temp_page else max_items * temp_page + items += publisher_end - publisher_start + if items < max_items: + series_end = ( + len(results["series"]) if len(results["series"]) < (max_items * temp_page) - items else (max_items * temp_page) - items + ) + items += series_end - series_start + if items < max_items: + comics_end = ( + len(results["comics"]) + if len(results["comics"]) < (max_items * temp_page) - series_end - publisher_end + else (max_items * temp_page) - series_end - publisher_end + ) + return render_template( + "comics/search.html", + title="Comics: Search", + publishers=results["publisher"], + publisher_series=results["series"], + comics=results["comics"], + page=page, + max_items=max_items, + item_count=item_count, + publisher_start=publisher_start, + series_start=series_start, + comics_start=comics_start, + publisher_end=publisher_end, + series_end=series_end, + comics_end=comics_end, + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @Comics.route("/comics/") @login_required def comics_publisher(publisher): - try: - publisher = parse.unquote(publisher) - series = request.args.get("series") - series_year = request.args.get("seriesYear") - issue = request.args.get("issue") - page_number = request.args.get("pageNumber") - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - publisher_series = database.db_get_series_by_publisher(publisher) - start = (max_items*(page-1)) - end = len(publisher_series) if len(publisher_series) < max_items*page else max_items*page - if series: - comics = database.db_get_comics_in_series(series, publisher, series_year) - start = (max_items * (page - 1)) - end = len(comics) if len(comics) < max_items * page else max_items * page - if issue: - return comic_gallery(publisher, series, series_year, issue) - comics_dict = [] - for i in comics: - item = i.__dict__ - item.pop('_sa_instance_state', None) - item.pop('path', None) - comics_dict.append(item) - return render_template("comics/seriesView.html", title="Comics", comics=comics_dict, - start=start, end=end, page=page, max_items=max_items, item_count=len(comics)) - pub_series_dict = [] - for i in publisher_series: - item = i.__dict__ - item.pop('_sa_instance_state', None) - item.pop('path', None) - pub_series_dict.append(item) - return render_template("comics/publisherSeriesView.html", title="Comics", publisher_series=pub_series_dict, - start=start, end=end, page=page, max_items=max_items, item_count=len(publisher_series)) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + publisher = parse.unquote(publisher) + series = request.args.get("series") + series_year = request.args.get("seriesYear") + issue = request.args.get("issue") + page_number = request.args.get("pageNumber") + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + publisher_series = database.db_get_series_by_publisher(publisher) + start = max_items * (page - 1) + end = len(publisher_series) if len(publisher_series) < max_items * page else max_items * page + if series: + comics = database.db_get_comics_in_series(series, publisher, series_year) + start = max_items * (page - 1) + end = len(comics) if len(comics) < max_items * page else max_items * page + if issue: + return comic_gallery(publisher, series, series_year, issue) + comics_dict = [] + for i in comics: + item = i.__dict__ + item.pop("_sa_instance_state", None) + item.pop("path", None) + comics_dict.append(item) + return render_template( + "comics/seriesView.html", + title="Comics", + comics=comics_dict, + start=start, + end=end, + page=page, + max_items=max_items, + item_count=len(comics), + ) + pub_series_dict = [] + for i in publisher_series: + item = i.__dict__ + item.pop("_sa_instance_state", None) + item.pop("path", None) + pub_series_dict.append(item) + return render_template( + "comics/publisherSeriesView.html", + title="Comics", + publisher_series=pub_series_dict, + start=start, + end=end, + page=page, + max_items=max_items, + item_count=len(publisher_series), + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @Comics.route("/comics/") @login_required def comic_gallery(comic_id): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - meta = database.db_get_comic(comic_id) - start = (max_items*(page-1)) - end = meta.pagecount if meta.pagecount < max_items*page else max_items*page - comic_dict = meta.__dict__ - comic_dict.pop('_sa_instance_state', None) - comic_dict.pop('path', None) - return render_template("comics/comicGallery.html", title="Comics", comic=comic_dict, start=start, end=end, page=page, max_items=max_items, item_count=meta.pagecount) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + meta = database.db_get_comic(comic_id) + start = max_items * (page - 1) + end = meta.pagecount if meta.pagecount < max_items * page else max_items * page + comic_dict = meta.__dict__ + comic_dict.pop("_sa_instance_state", None) + comic_dict.pop("path", None) + return render_template( + "comics/comicGallery.html", + title="Comics", + comic=comic_dict, + start=start, + end=end, + page=page, + max_items=max_items, + item_count=meta.pagecount, + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @Comics.route("/comics/get_comic//") @login_required def get_comic_page(comic_id, page_number): - meta = database.db_get_comic_by_id(comic_id) - comic = func.open_comic(meta.path) - byte_image = comic.getPage(page_number) - type = filetype.guess(byte_image).mime - response = make_response(byte_image) - response.headers["content-type"] = type - response.headers["cache-control"] = "public" - date = pytz.utc.localize(datetime.datetime.utcfromtimestamp(os.path.getmtime(meta.path))) - response.headers["last-modified"] = date.strftime('%a, %d %b %Y %H:%M:%S %Z') - response.headers["Content-Disposition"] = "attachment; filename=\"{} {}_{}{}\"".format(str(meta.series), meta.issuetext, str(page_number), filetype.guess(byte_image).extension) - return response + meta = database.db_get_comic_by_id(comic_id) + comic = func.open_comic(meta.path) + byte_image = comic.getPage(page_number) + type = filetype.guess(byte_image).mime + response = make_response(byte_image) + response.headers["content-type"] = type + response.headers["cache-control"] = "public" + date = pytz.utc.localize(datetime.datetime.utcfromtimestamp(os.path.getmtime(meta.path))) + response.headers["last-modified"] = date.strftime("%a, %d %b %Y %H:%M:%S %Z") + response.headers["Content-Disposition"] = 'attachment; filename="{} {}_{}{}"'.format( + str(meta.series), meta.issuetext, str(page_number), filetype.guess(byte_image).extension + ) + return response @Comics.route("/comics/get_comic///thumbnail") @login_required def get_comic_thumbnail(comic_id, page_number): - meta = database.db_get_comic_by_id(comic_id) - thumb = database.db_get_thumbnail_by_id_page(comic_id, page_number) - response = make_response(thumb.image) - response.headers["cache-control"] = "public" - date = pytz.utc.localize(datetime.datetime.utcfromtimestamp(os.path.getmtime(meta.path))) - response.headers["last-modified"] = date.strftime('%a, %d %b %Y %H:%M:%S %Z') - response.headers["content-type"] = thumb.type - response.headers["Content-Disposition"] = "attachment; filename=\"{} {}_{}_thumbnail\"".format(str(meta.series), meta.issuetext, str(page_number)) - return response + meta = database.db_get_comic_by_id(comic_id) + thumb = database.db_get_thumbnail_by_id_page(comic_id, page_number) + response = make_response(thumb.image) + response.headers["cache-control"] = "public" + date = pytz.utc.localize(datetime.datetime.utcfromtimestamp(os.path.getmtime(meta.path))) + response.headers["last-modified"] = date.strftime("%a, %d %b %Y %H:%M:%S %Z") + response.headers["content-type"] = thumb.type + response.headers["Content-Disposition"] = 'attachment; filename="{} {}_{}_thumbnail"'.format(str(meta.series), meta.issuetext, str(page_number)) + return response diff --git a/comics/templates/comics/comicGallery.html b/comics/templates/comics/comicGallery.html index c7916b2..24c382f 100644 --- a/comics/templates/comics/comicGallery.html +++ b/comics/templates/comics/comicGallery.html @@ -117,7 +117,7 @@ page_container.innerHTML = ""; for (i = start;i < end; i++) { var list_element = `
- +

${1+i}/${comic.pagecount}

`; page_container.innerHTML += list_element; @@ -134,15 +134,24 @@ let next_image = document.getElementById("next"); let prev_image = document.getElementById("prev"); + for (let i=0; i<{{ item_count }};i++) { + light_box_content.innerHTML += `` + } + function load_next_image(page_number) { if (document.getElementById(page_number.toString())) {return;} if (page_number >= page_count) {return;} console.log("start loading: page "+(page_number+1).toString()); - let image = '
'; + let image = ''; - light_box_content.innerHTML += image; + {#light_box_content.innerHTML += image;#} + var slides = document.getElementsByClassName("images"); + slides[page_number].outerHTML = image + if (imageIndex == page_number+1) { + currentImage(page_number+1) + } } // Open the Modal @@ -202,4 +211,6 @@ }); load_next_image(0); + {% endblock %} diff --git a/games/games.py b/games/games.py index 183f17c..c5cf8c0 100644 --- a/games/games.py +++ b/games/games.py @@ -1,10 +1,9 @@ -from flask import Blueprint, render_template, request, send_file, current_app, jsonify, abort -from flask_login import login_required - -from pathvalidate import sanitize_filename -import os import inspect -import re +import json +import os + +from flask import Blueprint, abort, current_app, jsonify, render_template, request, send_file +from flask_login import login_required from scripts import database, func @@ -14,78 +13,157 @@ Games = Blueprint("games", __name__, template_folder="templates") @Games.route("/games") @login_required def index(): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - games = database.get_all_games() - start = (max_items*(page-1)) - end = len(games) if len(games) < max_items*page else max_items*page - return render_template("games/index.html", title="Games", games=games) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + games = database.get_all_games() + start = max_items * (page - 1) + end = len(games) if len(games) < max_items * page else max_items * page + return render_template("games/index.html", title="Games", games=games) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) -@Games.route('/games/get_games') +@Games.route("/games/get_games") @login_required def get_games(): - try: - games = database.get_all_games() - games_json = {} - for game in games: - games_json[game.game_id] = { - "id": game.game_id, - "title": game.title, - "windows": game.windows, - "mac": game.mac, - "linux": game.linux, - "description": game.description, - "poster_path": game.poster_path - } - return jsonify(games_json) - # return jsonify({game["id"]: game for game in games}) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + games = database.get_all_games() + games_json = {} + for game in games: + games_json[game.game_id] = { + "id": game.game_id, + "title": game.title, + "description": game.description, + "poster_path": game.poster_path, + "windows": game.windows, + "mac": game.mac, + "linux": game.linux, + "title_sanitized": game.title_sanitized, + } + return jsonify(games_json) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) -@Games.route('/games/get_game/') +@Games.route("/games/get_games/windows") +@login_required +def get_games_windows(): + try: + games = database.get_windows_games() + games_json = {} + for game in games: + with open(os.path.join(game.path, "info.json")) as f: + info = json.load(f) + games_json[game.game_id] = { + "id": game.game_id, + "title": game.title, + "description": game.description, + "poster_path": game.poster_path, + "windows": {"executable": info["windows"]["executable"]}, + "title_sanitized": game.title_sanitized, + } + return jsonify(games_json) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) + + +@Games.route("/games/get_games/mac") +@login_required +def get_games_mac(): + try: + games = database.get_mac_games() + games_json = {} + for game in games: + with open(os.path.join(game.path, "info.json")) as f: + info = json.load(f) + games_json[game.game_id] = { + "id": game.game_id, + "title": game.title, + "description": game.description, + "poster_path": game.poster_path, + "title_sanitized": game.title_sanitized, + "mac": {}, + } + if "executable" in info["mac"].keys(): + games_json[game.game_id]["mac"] = {"executable": info["mac"]["executable"]} + return jsonify(games_json) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) + + +@Games.route("/games/get_games/linux") +@login_required +def get_games_linux(): + try: + games = database.get_linux_games() + games_json = {} + for game in games: + with open(os.path.join(game.path, "info.json")) as f: + info = json.load(f) + games_json[game.game_id] = { + "id": game.game_id, + "title": game.title, + "description": game.description, + "poster_path": game.poster_path, + "title_sanitized": game.title_sanitized, + "linux": {"executable": info["linux"]["executable"]}, + } + return jsonify(games_json) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) + + +@Games.route("/games/get_game/") @login_required def get_game(game_id): - try: - game = database.get_game(game_id) - if game: - game_json = { - "title": game.title, - "game_id": game.game_id, - "description": game.description, - "poster_path": game.poster_path, - "windows": game.windows, - "mac": game.mac, - "linux": game.linux - } - return jsonify(game_json) - abort(404) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) + try: + game = database.get_game(game_id) + if game: + with open(os.path.join(game.path, "info.json")) as f: + info = json.load(f) + windows = None + mac = None + linux = None + if "windows" in info.keys(): + windows = info["windows"] + if "mac" in info.keys(): + mac = info["mac"] + if "linux" in info.keys(): + linux = info["linux"] + game_json = { + "title": game.title, + "game_id": game.game_id, + "description": game.description, + "poster_path": game.poster_path, + "windows": windows, + "mac": mac, + "linux": linux, + "title_sanitized": game.title_sanitized, + } + return jsonify(game_json) + abort(404) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) -@Games.route("/games/download/") +@Games.route("/games/download_hash//") @login_required -def download_game(game_id): - try: - game = database.get_game(game_id) - if game: - files = game.windows["files"] - filename = sanitize_filename(files[0]) - folder = re.match(r"(.+)_setup_win.(exe|msi)", filename).group(1) - if len(files) > 1: - filename = sanitize_filename(game.title+".zip") - path = os.path.join(func.GAMES_DIRECTORY, folder, filename) - return send_file(path, as_attachment=True, attachment_filename=filename) - else: - abort(404) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) +def download_game_hash(game_id, file_hash): + try: + game = database.get_game(game_id) + if game: + folder = game.path + path = os.path.join(folder, file_hash[:2], file_hash) + return send_file(path, as_attachment=True, attachment_filename=file_hash) + else: + abort(404) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) diff --git a/games/templates/games/index.html b/games/templates/games/index.html index 5706467..5bad66d 100644 --- a/games/templates/games/index.html +++ b/games/templates/games/index.html @@ -2,7 +2,7 @@ {% block content %} {% for game in games %} - Download + Download

{{ game.title }} - {{ game.game_id }}

{% endfor %} {% endblock %} diff --git a/rpiWebApp.py b/rpiWebApp.py index 2d82692..1e71cb3 100644 --- a/rpiWebApp.py +++ b/rpiWebApp.py @@ -1,47 +1,48 @@ -from flask import Flask -from flask import render_template, request, g, redirect, url_for, flash, current_app, Response -from flask_login import LoginManager, current_user, login_user, logout_user, login_required +import base64 +import datetime +import inspect +import json +import logging +import os +import pathlib +import threading +from urllib.parse import urljoin, urlsplit, urlunsplit +import inotify.adapters +import inotify.constants +import requests +from flask import Flask, Response, current_app, flash, g, redirect, render_template, request, url_for +from flask_login import LoginManager, current_user, login_required, login_user, logout_user from oauthlib.oauth2 import WebApplicationClient -import threading -import logging -import inotify.adapters, inotify.constants -import inspect -import datetime -import requests -import json -import os -import base64 - import scripts.func as func -from scripts import database from admin import admin from comics import comics -from tv_movies import tv_movies from games import games +from scripts import database +from tv_movies import tv_movies class NullHandler(logging.Handler): - def emit(self, record=None): - pass + def emit(self, record=None): + pass + + def debug(self, *arg): + pass + - def debug(self, *arg): - pass nullLog = NullHandler() inotify.adapters._LOGGER = nullLog GOOGLE_CLIENT_ID = "***REMOVED***" GOOGLE_CLIENT_SECRET = "***REMOVED***" -GOOGLE_DISCOVERY_URL = ( - "https://accounts.google.com/.well-known/openid-configuration" -) +GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration" client = WebApplicationClient(GOOGLE_CLIENT_ID) def get_google_provider_cfg(): - return requests.get(GOOGLE_DISCOVERY_URL).json() + return requests.get(GOOGLE_DISCOVERY_URL).json() app = Flask(__name__) @@ -54,7 +55,7 @@ app.logger.setLevel("DEBUG") # app.use_x_sendfile = True login_manager = LoginManager(app) -login_manager.login_view = "login" +# login_manager.login_view = "login" app.config["REMEMBER_COOKIE_DOMAIN"] = "narnian.us" @@ -62,101 +63,135 @@ MOBILE_DEVICES = ["android", "blackberry", "ipad", "iphone"] def get_comics(): - with app.app_context(): - i = inotify.adapters.InotifyTree(func.COMICS_DIRECTORY) - func.get_comics() - for event in i.event_gen(yield_nones=False): - (header, type_names, path, filename) = event - file_path = os.path.join(path, filename) - if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: - func.get_comic(file_path) + with app.app_context(): + i = inotify.adapters.InotifyTree(func.COMICS_DIRECTORY) + new_dirs = [] + func.get_comics() + while True: + for event in i.event_gen(timeout_s=5*60, yield_nones=False): + (header, type_names, path, filename) = event + file_path = pathlib.Path(path, filename) + if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: + func.get_comic(file_path) + elif "IN_CREATE" in type_names: + if file_path.is_dir(): + new_dirs.append(file_path) + for new_dir in new_dirs: + for file in new_dir.glob("*"): + func.get_comic(file) + new_dirs.clear() + def get_movies(): - with app.app_context(): - i = inotify.adapters.InotifyTree(func.MOVIES_DIRECTORY) - func.get_movies() - for event in i.event_gen(yield_nones=False): - (header, type_names, path, filename) = event - file_path = os.path.join(path, filename) - if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: - func.get_movie(file_path) + with app.app_context(): + i = inotify.adapters.InotifyTree(func.MOVIES_DIRECTORY) + + func.get_movies() + + for event in i.event_gen(yield_nones=False): + (header, type_names, path, filename) = event + file_path = pathlib.Path(path, filename) + if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: + func.get_movie(file_path) def get_tv_shows(): - with app.app_context(): - i = inotify.adapters.InotifyTree(func.TV_SHOWS_DIRECTORY) - func.get_tv_shows() - func.get_tv_episodes() - for event in i.event_gen(yield_nones=False): - (header, type_names, path, filename) = event - file_path = os.path.join(path, filename) - if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: - func.get_tv_shows() - func.get_tv_episode(file_path) + with app.app_context(): + i = inotify.adapters.InotifyTree(func.TV_SHOWS_DIRECTORY) + func.get_tv_shows() + func.get_tv_episodes() + for event in i.event_gen(yield_nones=False): + (header, type_names, path, filename) = event + file_path = pathlib.Path(path, filename) + if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: + if file_path.is_dir(): + func.get_tv_shows() + else: + func.get_tv_episode(file_path) def get_games(): - with app.app_context(): - func.get_games() + with app.app_context(): + i = inotify.adapters.Inotify() + i.add_watch(func.GAMES_DIRECTORY) + for directory in os.listdir(func.GAMES_DIRECTORY): + path = pathlib.Path(func.GAMES_DIRECTORY, directory) + if path.is_dir(): + i.add_watch(str(path)) + + func.get_games() + func.update_games() + + for event in i.event_gen(yield_nones=False): + (header, type_names, path, filename) = event + file_path = pathlib.Path(path, filename) + if "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names: + func.get_game(file_path) + elif "IN_CREATE" in type_names: + if file_path.is_dir() and len(file_path.name) > 2: + i.add_watch(str(file_path)) + elif "IN_DELETE_SELF" in type_names: + if file_path.is_dir(): + i.remove_watch(file_path) with app.app_context(): - current_app.logger.info("server start") - thread = threading.Thread(target=get_comics, args=()) - thread.daemon = True - thread.start() - thread2 = threading.Thread(target=get_movies, args=()) - thread2.daemon = True - thread2.start() - thread3 = threading.Thread(target=get_tv_shows, args=()) - thread3.daemon = True - thread3.start() - thread4 = threading.Thread(target=get_games, args=()) - thread4.daemon = True - thread4.start() + current_app.logger.info("server start") + thread = threading.Thread(target=get_comics, args=()) + thread.daemon = True + thread.start() + thread2 = threading.Thread(target=get_movies, args=()) + thread2.daemon = True + thread2.start() + thread3 = threading.Thread(target=get_tv_shows, args=()) + thread3.daemon = True + thread3.start() + thread4 = threading.Thread(target=get_games, args=()) + thread4.daemon = True + thread4.start() @app.teardown_appcontext def close_connection(exception): - db = getattr(g, '_database', None) - if db is not None: - db.close() + db = getattr(g, "_database", None) + if db is not None: + db.close() def update_comic_db(sender, **kw): - try: - database.add_comics(kw["meta"], kw["thumbnails"]) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + database.add_comics(kw["meta"], kw["thumbnails"]) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def update_movie_db(sender, **kw): - try: - database.add_movies(kw["movies"]) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + database.add_movies(kw["movies"]) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def update_tv_show_db(sender, **kw): - try: - database.add_tv_shows(kw["tv_show"]) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + database.add_tv_shows(kw["tv_show"]) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def update_tv_episodes_db(sender, **kw): - try: - database.add_tv_episodes(kw["tv_episodes"]) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + database.add_tv_episodes(kw["tv_episodes"]) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def update_games_db(sender, **kw): - try: - database.add_games(kw["games"]) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + database.add_games(kw["games"]) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) func.comic_loaded.connect(update_comic_db) @@ -168,177 +203,183 @@ func.games_loaded.connect(update_games_db) @login_manager.user_loader def load_user(email): - try: - return database.get_user(email) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + return database.get_user(email) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) @login_manager.request_loader def load_user_from_request(request): - try: - api_key = request.headers.get('Authorization') - if api_key: - api_key = api_key.replace('Basic ', '', 1) - try: - api_key = base64.b64decode(api_key).decode("utf-8") - except TypeError: - pass - email = api_key.split(":")[0] - password = api_key.split(":")[1] - user = load_user(email) - if user and user.check_password(password): - return user - return None - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + api_key = request.headers.get("Authorization") + if api_key: + api_key = api_key.replace("Basic ", "", 1) + try: + api_key = base64.b64decode(api_key).decode("utf-8") + except TypeError: + pass + email = api_key.split(":")[0] + password = api_key.split(":")[1] + user = load_user(email) + if user and user.check_password(password): + return user + return None + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) @app.route("/login", methods=["GET", "POST"]) def login(): - try: - google_provider_cfg = get_google_provider_cfg() - authorization_endpoint = google_provider_cfg["authorization_endpoint"] + try: + url = urlsplit(request.url) + google_redirect = urlunsplit(("", "", url.path, url.query, "")) + next_page = google_redirect + if "login" in url.path: + next_page = url_for("home") + if request.args.get("url", default=None): + next_page = request.args.get("url", default=None) - request_uri = client.prepare_request_uri( - authorization_endpoint, - redirect_uri=request.base_url + "/callback", - scope=["openid", "email", "profile"], - ) + google_provider_cfg = get_google_provider_cfg() + authorization_endpoint = google_provider_cfg["authorization_endpoint"] - if request.method == "POST": - email = request.form.get("email") - password = request.form.get("password") - user = database.get_user(email) - if user is None or not user.check_password(password): - flash("invalid email or password") - return redirect(url_for("login")) - login_user(user, remember=True, duration=datetime.timedelta(days=7)) - next_page = request.args.get("next") - if not next_page: - next_page = url_for("home") - return redirect(next_page) - return render_template("login.html", title="login", auth_url=request_uri) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + request_uri = client.prepare_request_uri( + authorization_endpoint, + redirect_uri=urljoin(request.host_url, url_for("callback")), + scope=["openid", "email", "profile"], + state=next_page, + hd="narnian.us", + ) + + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + user = database.get_user(email) + if user is None or not user.check_password(password): + flash("invalid email or password") + return redirect(url_for("login")) + login_user(user, remember=True, duration=datetime.timedelta(days=7)) + return redirect(next_page) + return render_template("login.html", title="login", auth_url=request_uri) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) @app.route("/login/callback") def callback(): - try: - # Get authorization code Google sent back to you - code = request.args.get("code") + try: + # Get authorization code Google sent back to you + code = request.args.get("code") - google_provider_cfg = get_google_provider_cfg() - token_endpoint = google_provider_cfg["token_endpoint"] + google_provider_cfg = get_google_provider_cfg() + token_endpoint = google_provider_cfg["token_endpoint"] - token_url, headers, body = client.prepare_token_request( - token_endpoint, - authorization_response=request.url, - redirect_url=request.base_url, - code=code - ) - token_response = requests.post( - token_url, - headers=headers, - data=body, - auth=(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET) - ) + token_url, headers, body = client.prepare_token_request( + token_endpoint, authorization_response=request.url, redirect_url=request.base_url, code=code + ) + token_response = requests.post(token_url, headers=headers, data=body, auth=(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)) - client.parse_request_body_response(json.dumps(token_response.json())) + client.parse_request_body_response(json.dumps(token_response.json())) - userinfo_endpoint = google_provider_cfg["userinfo_endpoint"] - uri, headers, body = client.add_token(userinfo_endpoint) - userinfo_response = requests.get(uri, headers=headers, data=body) + userinfo_endpoint = google_provider_cfg["userinfo_endpoint"] + uri, headers, body = client.add_token(userinfo_endpoint) + userinfo_response = requests.get(uri, headers=headers, data=body) - if userinfo_response.json().get("email_verified"): - unique_id = userinfo_response.json()["sub"] - users_email = userinfo_response.json()["email"] - users_name = userinfo_response.json()["given_name"] - else: - return "User email not available or not verified by Google.", 400 + if userinfo_response.json().get("email_verified"): + unique_id = userinfo_response.json()["sub"] + users_email = userinfo_response.json()["email"] + users_name = userinfo_response.json()["given_name"] + else: + return "User email not available or not verified by Google.", 400 - data = (unique_id, users_name, users_email, None, False) + data = (unique_id, users_name, users_email, None, False) - current_app.logger.info("user data from google: " + str(data)) - user = database.get_user(users_email) + current_app.logger.info("user data from google: " + str(data)) + user = database.get_user(users_email) - if not user: - user = database.add_user(data) - current_app.logger.info("new user: {} created".format(users_email)) + if not user: + user = database.add_user(data) + current_app.logger.info("new user: {} created".format(users_email)) - current_app.logger.info("email: "+str(user.email)) - current_app.logger.info("username: "+str(user.username)) - current_app.logger.info("authenticated: "+str(user.is_authenticated)) - current_app.logger.info("active: "+str(user.is_active)) - current_app.logger.info("id: "+str(user.get_id())) - login_user(user, remember=True, duration=datetime.timedelta(days=7), force=True) + current_app.logger.info("email: " + str(user.email)) + current_app.logger.info("username: " + str(user.username)) + current_app.logger.info("authenticated: " + str(user.is_authenticated)) + current_app.logger.info("active: " + str(user.is_active)) + current_app.logger.info("id: " + str(user.get_id())) + login_user(user, remember=True, duration=datetime.timedelta(days=7), force=True) - return redirect(url_for("home")) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + return redirect(request.args.get("state")) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) @app.route("/logout") def logout(): - try: - logout_user() - return redirect(url_for("login")) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + try: + logout_user() + return redirect(url_for("login")) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) @app.route("/") def root(): - return redirect(url_for("home")) + return redirect(url_for("home")) @app.route("/home") @login_required def home(): - try: - return render_template("home.html", title="Home", current_user=current_user) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + try: + return render_template("home.html", title="Home", current_user=current_user) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) @app.after_request def apply_headers(response: Response): - try: - user_name = "anonymous" - if current_user: - user_name = getattr(current_user, "email", "anonymous") - response.set_cookie("rpi_name", user_name) - return response - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + try: + user_name = "anonymous" + if current_user: + user_name = getattr(current_user, "email", "anonymous") + response.set_cookie("rpi_name", user_name) + response.headers.set("email", user_name) + return response + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) @app.route("/music") @login_required def music(): - return "No music" + return "No music" @app.errorhandler(404) def resource_not_found(e): - try: - return render_template("404.html"), 404 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(e) + try: + return render_template("404.html"), 404 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(e) + + +@login_manager.unauthorized_handler +def handle_unauthorized(): + temp = login() + return temp, 401 @app.errorhandler(Exception) def handle_exception(e): - return render_template("error.html", e=e), 500 + return render_template("error.html", e=e), 500 games.Games.register_error_handler(404, resource_not_found) @@ -351,5 +392,5 @@ comics.Comics.register_error_handler(Exception, handle_exception) admin.Admin.register_error_handler(Exception, handle_exception) -if __name__ == '__main__': - app.run() +if __name__ == "__main__": + app.run() diff --git a/scripts/database.py b/scripts/database.py index 280b613..1a48752 100644 --- a/scripts/database.py +++ b/scripts/database.py @@ -1,24 +1,19 @@ -from flask import g, current_app -from flask_login import UserMixin, current_user - -from werkzeug.security import check_password_hash -from io import BytesIO -from wand.image import Image -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import create_engine -from sqlalchemy.exc import IntegrityError -from sqlalchemy import Column, Integer, String, BLOB, Boolean, DateTime, Numeric, func, over, ARRAY, TIMESTAMP, JSON -from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy.pool import NullPool -from sqlalchemy.sql.expression import cast -import sqlalchemy -import sqlite3 -import os +import datetime import inspect import logging -import datetime +import os +import sqlalchemy from comicapi.issuestring import IssueString +from flask import current_app +from flask_login import UserMixin, current_user +from sqlalchemy import ARRAY, BLOB, JSON, TIMESTAMP, Boolean, Column, DateTime, Integer, Numeric, String, create_engine, func, over +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.pool import NullPool +from sqlalchemy.sql.expression import cast +from werkzeug.security import check_password_hash from scripts import tmdb @@ -33,7 +28,7 @@ USER_DATABASE = RPI_USER_DATABASE if os.path.exists(RPI_USER_DATABASE) else MC_U engine = create_engine("***REMOVED***", poolclass=NullPool) -logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) +logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) session_factory = sessionmaker(bind=engine) Session = scoped_session(session_factory) @@ -41,300 +36,298 @@ Base = declarative_base() class Comic(Base): - __tablename__ = "comics" + __tablename__ = "comics" - path = Column(String, unique=True) - tagorigin = Column(String) - series = Column(String) - issue = Column(Integer) - issuetext = Column(String) - title = Column(String) - publisher = Column(String) - month = Column(Integer) - year = Column(Integer) - day = Column(Integer) - seriesyear = Column(Integer) - issuecount = Column(Integer) - volume = Column(String) - genre = Column(String) - language = Column(String) - comments = Column(String) - volumecount = Column(Integer) - criticalrating = Column(String) - country = Column(String) - alternateseries = Column(String) - alternatenumber = Column(String) - alternatecount = Column(Integer) - imprint = Column(String) - notes = Column(String) - weblink = Column(String) - format = Column(String) - manga = Column(String) - blackandwhite = Column(String) - pagecount = Column(Integer) - maturityrating = Column(String) - storyarc = Column(String) - seriesgroup = Column(String) - scaninfo = Column(String) - characters = Column(String) - teams = Column(String) - locations = Column(String) - id = Column(Integer, primary_key=True, autoincrement=True) + path = Column(String, unique=True) + tagorigin = Column(String) + series = Column(String) + issue = Column(Integer) + issuetext = Column(String) + title = Column(String) + publisher = Column(String) + month = Column(Integer) + year = Column(Integer) + day = Column(Integer) + seriesyear = Column(Integer) + issuecount = Column(Integer) + volume = Column(String) + genre = Column(String) + language = Column(String) + comments = Column(String) + volumecount = Column(Integer) + criticalrating = Column(String) + country = Column(String) + alternateseries = Column(String) + alternatenumber = Column(String) + alternatecount = Column(Integer) + imprint = Column(String) + notes = Column(String) + weblink = Column(String) + format = Column(String) + manga = Column(String) + blackandwhite = Column(String) + pagecount = Column(Integer) + maturityrating = Column(String) + storyarc = Column(String) + seriesgroup = Column(String) + scaninfo = Column(String) + characters = Column(String) + teams = Column(String) + locations = Column(String) + id = Column(Integer, primary_key=True, autoincrement=True) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - if column.name == "id": - continue - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) +" "+ str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + if column.name == "id": + continue + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "Comic" + " " + str(type(e)) + " " + str(e)) - def __repr__(self): - return "".format(series=self.series, issue=self.issuetext) + def __repr__(self): + return "".format(series=self.series, issue=self.issuetext) class ComicThumbnail(Base): - __tablename__ = "comic_thumbnails" + __tablename__ = "comic_thumbnails" - comic_id = Column(Integer) - pagenumber = Column(Integer) - image = Column(BLOB) - type = Column(String) - id = Column(Integer, primary_key=True, autoincrement=True) + comic_id = Column(Integer) + pagenumber = Column(Integer) + image = Column(BLOB) + type = Column(String) + id = Column(Integer, primary_key=True, autoincrement=True) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - if column.name == "id": - continue - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) +" "+ str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + if column.name == "id": + continue + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "ComicThumbnail" + " " + str(type(e)) + " " + str(e)) class Movie(Base): - __tablename__ = "movies" + __tablename__ = "movies" - path = Column(String, primary_key=True) - imdb_id = Column(String) - tmdb_id = Column(Integer) - title = Column(String) - year = Column(Integer) - length = Column(Integer) - description = Column(String) - extended = Column(Boolean) - directors_cut = Column(Boolean) - poster_path = Column(String) - backdrop_path = Column(String) + path = Column(String, primary_key=True) + tmdb_id = Column(Integer) + title = Column(String) + year = Column(Integer) + description = Column(String) + extended = Column(Boolean) + directors_cut = Column(Boolean) + poster_path = Column(String) + backdrop_path = Column(String) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) +" "+ str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "Movie" + " " + str(type(e)) + " " + str(e)) class TvShow(Base): - __tablename__ = "tv_shows" + __tablename__ = "tv_shows" - imdb_id = Column(String, primary_key=True) - tmdb_id = Column(Integer) - title = Column(String) - year = Column(Integer) - description = Column(String) - poster_path = Column(String) - path = Column(String) + tmdb_id = Column(Integer, primary_key=True) + title = Column(String) + year = Column(Integer) + description = Column(String) + poster_path = Column(String) + path = Column(String) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "TvShow" + " " + str(type(e)) + " " + str(e)) class TvEpisode(Base): - __tablename__ = "tv_episodes" + __tablename__ = "tv_episodes" - imdb_id = Column(String, primary_key=True) - parent_imdb_id = Column(String) - tmdb_id = Column(Integer) - title = Column(String) - season = Column(Integer) - episode = Column(Integer) - description = Column(String) - still_path = Column(String) - path = Column(String) + tmdb_id = Column(Integer, primary_key=True) + parent_tmdb_id = Column(String) + title = Column(String) + season = Column(Integer) + episode = Column(Integer) + description = Column(String) + still_path = Column(String) + path = Column(String) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "TvEpisode" + " " + str(type(e)) + " " + str(e)) class DupComic(Base): - __tablename__ = "duplicate_comics" + __tablename__ = "duplicate_comics" - path = Column(String, unique=True) - tagorigin = Column(String) - series = Column(String) - issue = Column(Integer) - issuetext = Column(String) - title = Column(String) - publisher = Column(String) - month = Column(Integer) - year = Column(Integer) - day = Column(Integer) - seriesyear = Column(Integer) - issuecount = Column(Integer) - volume = Column(String) - genre = Column(String) - language = Column(String) - comments = Column(String) - volumecount = Column(Integer) - criticalrating = Column(String) - country = Column(String) - alternateseries = Column(String) - alternatenumber = Column(String) - alternatecount = Column(Integer) - imprint = Column(String) - notes = Column(String) - weblink = Column(String) - format = Column(String) - manga = Column(String) - blackandwhite = Column(String) - pagecount = Column(Integer) - maturityrating = Column(String) - storyarc = Column(String) - seriesgroup = Column(String) - scaninfo = Column(String) - characters = Column(String) - teams = Column(String) - locations = Column(String) - id = Column(Integer, primary_key=True, autoincrement=True) + path = Column(String, unique=True) + tagorigin = Column(String) + series = Column(String) + issue = Column(Integer) + issuetext = Column(String) + title = Column(String) + publisher = Column(String) + month = Column(Integer) + year = Column(Integer) + day = Column(Integer) + seriesyear = Column(Integer) + issuecount = Column(Integer) + volume = Column(String) + genre = Column(String) + language = Column(String) + comments = Column(String) + volumecount = Column(Integer) + criticalrating = Column(String) + country = Column(String) + alternateseries = Column(String) + alternatenumber = Column(String) + alternatecount = Column(Integer) + imprint = Column(String) + notes = Column(String) + weblink = Column(String) + format = Column(String) + manga = Column(String) + blackandwhite = Column(String) + pagecount = Column(Integer) + maturityrating = Column(String) + storyarc = Column(String) + seriesgroup = Column(String) + scaninfo = Column(String) + characters = Column(String) + teams = Column(String) + locations = Column(String) + id = Column(Integer, primary_key=True, autoincrement=True) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - if column.name == "id": - continue - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + if column.name == "id": + continue + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "DupComic" + " " + str(type(e)) + " " + str(e)) class Game(Base): - __tablename__ = "games" + __tablename__ = "games" - title = Column(String) - game_id = Column(Integer, primary_key=True) - description = Column(String) - poster_path = Column(String) - windows = Column(JSON) - mac = Column(JSON) - linux = Column(JSON) + title = Column(String) + game_id = Column(Integer, primary_key=True) + description = Column(String) + poster_path = Column(String) + path = Column(String) + windows = Column(Boolean) + mac = Column(Boolean) + linux = Column(Boolean) + title_sanitized = Column(String) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "Game" + " " + str(type(e)) + " " + str(e)) class User(Base, UserMixin): - __tablename__ = "users" + __tablename__ = "users" - id = Column(Numeric, primary_key=True) - username = Column(String) - email = Column(String, unique=True) - passwordHash = Column(String(128)) - isAdmin = Column(Boolean, default=False) + id = Column(Numeric, primary_key=True) + username = Column(String) + email = Column(String, unique=True) + passwordHash = Column(String(128)) + isAdmin = Column(Boolean, default=False) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - setattr(self, column.name, data[i]) - i += 1 - pass - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + setattr(self, column.name, data[i]) + i += 1 + pass + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "User" + " " + str(type(e)) + " " + str(e)) - def get_id(self): - return self.email + def get_id(self): + return self.email - def check_password(self, password): - if not self.passwordHash: - return - result = check_password_hash(self.passwordHash, password) - return result + def check_password(self, password): + if not self.passwordHash: + return + result = check_password_hash(self.passwordHash, password) + return result class UserTvMovieData(Base): - __tablename__ = "user_tv_movie_data" + __tablename__ = "user_tv_movie_data" - id = Column(Integer, primary_key=True, autoincrement=True) - user = Column(String) - imdb_id = Column(String) - parent_imdb = Column(String) - time = Column(Integer) - length = Column(Integer) - finished = Column(Boolean, default=False) - time_stamp = Column(DateTime) - extended = Column(Boolean, default=False) - directors_cut = Column(Boolean, default=False) + id = Column(Integer, primary_key=True, autoincrement=True) + user = Column(String) + tmdb_id = Column(String) + parent_tmdb = Column(String) + time = Column(Integer) + length = Column(Integer) + finished = Column(Boolean, default=False) + time_stamp = Column(DateTime) + extended = Column(Boolean, default=False) + directors_cut = Column(Boolean, default=False) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - if column.name == "id": - continue - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + if column.name == "id": + continue + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "UserTvMovieData" + " " + str(type(e)) + " " + str(e)) class TvMovieKeywords(Base): - __tablename__ = "tv_movie_keywords" + __tablename__ = "tv_movie_keywords" - imdb_id = Column(String) - extended = Column(Boolean, default=False) - directors_cut = Column(Boolean, default=False) - key_words = Column(ARRAY(String)) - id = Column(Integer, primary_key=True) - date = Column(TIMESTAMP) + tmdb_id = Column(String) + extended = Column(Boolean, default=False) + directors_cut = Column(Boolean, default=False) + key_words = Column(ARRAY(String)) + id = Column(Integer, primary_key=True) + date = Column(TIMESTAMP) - def __init__(self, data): - i = 0 - try: - for column in self.__table__.columns: - if column.name == "id" or column.name == "date": - continue - setattr(self, column.name, data[i]) - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + def __init__(self, data): + i = 0 + try: + for column in self.__table__.columns: + if column.name == "id" or column.name == "date": + continue + setattr(self, column.name, data[i]) + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + "TvMovieKeywords" + " " + str(type(e)) + " " + str(e)) """class UserComicData(Base): @@ -364,502 +357,580 @@ class TvMovieKeywords(Base): # return db -def get_imdb(): - db = getattr(g, '_imdb_database', None) - if db is None: - db = g._imdb_database = sqlite3.connect(IMDB_DATABASE) - - db.row_factory = sqlite3.Row - return db +# def get_imdb(): +# db = getattr(g, '_imdb_database', None) +# if db is None: +# db = g._imdb_database = sqlite3.connect(IMDB_DATABASE) +# +# db.row_factory = sqlite3.Row +# return db -def update_user_tv_movie_data(imdb_id, parent_id, time, length, finished=False, extended=False, directors_cut=False): - session = Session() - email = current_user.email - user_data = session.query(UserTvMovieData).filter(UserTvMovieData.imdb_id == imdb_id, UserTvMovieData.user == email, UserTvMovieData.extended == extended, UserTvMovieData.directors_cut == directors_cut).one_or_none() - if user_data: - user_data.time = time - user_data.finished = finished - user_data.time_stamp = datetime.datetime.now() - if not user_data.length > 0 and length > 0: - user_data.length = length - session.commit() - return user_data - else: - data = UserTvMovieData((email, imdb_id, parent_id, time, length, finished, datetime.datetime.now(), extended, directors_cut)) - session.add(data) - session.commit() - return data +def update_user_tv_movie_data(tmdb_id, parent_id, time, length, finished=False, extended=False, directors_cut=False): + session = Session() + email = current_user.email + user_data = ( + session.query(UserTvMovieData) + .filter( + UserTvMovieData.tmdb_id == tmdb_id, + UserTvMovieData.user == email, + UserTvMovieData.extended == extended, + UserTvMovieData.directors_cut == directors_cut, + ) + .one_or_none() + ) + if user_data: + user_data.time = time + user_data.finished = finished + user_data.time_stamp = datetime.datetime.now() + if not user_data.length > 0 and length > 0: + user_data.length = length + session.commit() + return user_data + else: + data = UserTvMovieData((email, tmdb_id, parent_id, time, length, finished, datetime.datetime.now(), extended, directors_cut)) + session.add(data) + session.commit() + return data def add_user(data): - session = Session() - user = User(data) - session.add(user) - session.commit() - return user + session = Session() + user = User(data) + session.add(user) + session.commit() + return user def add_movies(movies): - session = Session() - for movie_data in movies: - movie = Movie(movie_data) - session.add(movie) - session.commit() + session = Session() + for movie_data in movies: + movie = Movie(movie_data) + session.add(movie) + session.commit() def add_tv_shows(tv_show_data): - try: - session = Session() - tv_show = TvShow(tv_show_data) - session.add(tv_show) - session.commit() - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + session = Session() + tv_show = TvShow(tv_show_data) + session.add(tv_show) + session.commit() + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def add_tv_episodes(episodes): - session = Session() - for episode_data in episodes: - try: - episode = TvEpisode(episode_data) - if not session.query(TvEpisode).filter(TvEpisode.tmdb_id == episode.tmdb_id).one_or_none(): - session.add(episode) - session.commit() - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + session = Session() + for episode_data in episodes: + try: + episode = TvEpisode(episode_data) + if not session.query(TvEpisode).filter(TvEpisode.tmdb_id == episode.tmdb_id).one_or_none(): + session.add(episode) + session.commit() + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def add_comics(meta, thumbnails): - session = Session() - data = [] - for info in meta: - issue = IssueString(info[1].issue).asFloat() - data.append([info[0], info[1].tagOrigin, info[1].series, (issue or -1), info[1].issue, info[1].title, info[1].publisher, - info[1].month, info[1].year, info[1].day, info[1].seriesYear, info[1].issueCount, info[1].volume, info[1].genre, - info[1].language, info[1].comments, info[1].volumeCount, info[1].criticalRating, info[1].country, - info[1].alternateSeries, info[1].alternateNumber, info[1].alternateCount, info[1].imprint, - info[1].notes, info[1].webLink, info[1].format, info[1].manga, info[1].blackAndWhite, - info[1].pageCount, info[1].maturityRating, info[1].storyArc, info[1].seriesGroup, info[1].scanInfo, - info[1].characters, info[1].teams, info[1].locations]) - for comic_data in data: - for i in range(len(comic_data)): - if comic_data[i] == "": - comic_data[i] = None - dup_comics = [] - for comic_data, images in zip(data, thumbnails): - try: - comic = Comic(comic_data) - if comic.publisher is None: - dup_comics.append(comic_data) - continue - session.add(comic) - session.commit() - comic_id = session.query(Comic.id).order_by(Comic.id.desc()).first()[0] - for index in range(len(images)): - thumbnail = ComicThumbnail([comic_id, index, images[index][0], images[index][1]]) - session.add(thumbnail) - session.commit() - except IntegrityError as e: - session.rollback() - dup_comics.append(comic_data) - for dup in dup_comics: - try: - dup_comic = DupComic(dup) - session.add(dup_comic) - session.commit() - except IntegrityError as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - session.rollback() - current_app.logger.info("{} comic{} added".format(len(meta), "s" if len(meta)>1 or len(meta)<1 else "")) + session = Session() + data = [] + for info in meta: + issue = IssueString(info[1].issue).asFloat() + data.append( + [ + info[0], + info[1].tagOrigin, + info[1].series, + (issue or -1), + info[1].issue, + info[1].title, + info[1].publisher, + info[1].month, + info[1].year, + info[1].day, + info[1].seriesYear, + info[1].issueCount, + info[1].volume, + info[1].genre, + info[1].language, + info[1].comments, + info[1].volumeCount, + info[1].criticalRating, + info[1].country, + info[1].alternateSeries, + info[1].alternateNumber, + info[1].alternateCount, + info[1].imprint, + info[1].notes, + info[1].webLink, + info[1].format, + info[1].manga, + info[1].blackAndWhite, + info[1].pageCount, + info[1].maturityRating, + info[1].storyArc, + info[1].seriesGroup, + info[1].scanInfo, + info[1].characters, + info[1].teams, + info[1].locations, + ] + ) + for comic_data in data: + for i in range(len(comic_data)): + if comic_data[i] == "": + comic_data[i] = None + dup_comics = [] + for comic_data, images in zip(data, thumbnails): + try: + comic = Comic(comic_data) + if comic.publisher is None: + dup_comics.append(comic_data) + continue + session.add(comic) + session.commit() + comic_id = session.query(Comic.id).order_by(Comic.id.desc()).first()[0] + for index in range(len(images)): + thumbnail = ComicThumbnail([comic_id, index, images[index][0], images[index][1]]) + session.add(thumbnail) + session.commit() + except IntegrityError as e: + session.rollback() + dup_comics.append(comic_data) + for dup in dup_comics: + try: + dup_comic = DupComic(dup) + session.add(dup_comic) + session.commit() + except IntegrityError as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + session.rollback() + current_app.logger.info("{} comic{} added".format(len(meta), "s" if len(meta) > 1 or len(meta) < 1 else "")) def add_games(games): - session = Session() - for game_data in games: - game = Game(game_data) - session.add(game) - session.commit() + session = Session() + for game_data in games: + game = Game(game_data) + session.add(game) + session.commit() + + +def update_game(game_data): + session = Session() + game = session.query(Game).filter(Game.game_id == game_data[0]).one_or_none() + game.windows = game_data[1] + game.mac = game_data[2] + game.linux = game_data[3] + session.commit() def db_get_all_comics(): - session = Session() - result = session.query(Comic).all() - return result + session = Session() + result = session.query(Comic).all() + return result def db_get_series_by_publisher(publisher): - session = Session() - result = session.query(Comic).filter(Comic.publisher == publisher).order_by(Comic.series, Comic.seriesyear, Comic.issue).distinct(Comic.series).all() - series = result - return series + session = Session() + result = ( + session.query(Comic).filter(Comic.publisher == publisher).order_by(Comic.series, Comic.seriesyear, Comic.issue).distinct(Comic.series).all() + ) + series = result + return series def db_get_comics_in_series(series, publisher, series_year): - session = Session() - result = session.query(Comic).filter(Comic.publisher == publisher, Comic.series == series, Comic.seriesyear == series_year).order_by(Comic.issue).all() - comics = result - return comics + session = Session() + result = ( + session.query(Comic).filter(Comic.publisher == publisher, Comic.series == series, Comic.seriesyear == series_year).order_by(Comic.issue).all() + ) + comics = result + return comics def get_publishers(): - session = Session() - result = session.query(Comic.publisher).distinct(Comic.publisher).order_by(Comic.publisher).all() - publishers = [r for (r,) in result] - return publishers + session = Session() + result = session.query(Comic.publisher).distinct(Comic.publisher).order_by(Comic.publisher).all() + publishers = [r for (r,) in result] + return publishers def db_get_comic_by_id(comic_id): - session = Session() - comic = session.query(Comic).filter(Comic.id == comic_id).one_or_none() - return comic + session = Session() + comic = session.query(Comic).filter(Comic.id == comic_id).one_or_none() + return comic def db_get_thumbnail_by_id_page(comic_id, pageNumber): - session = Session() - thumbnail = session.query(ComicThumbnail).filter(ComicThumbnail.comic_id == comic_id, ComicThumbnail.pagenumber == pageNumber).one_or_none() - return thumbnail + session = Session() + thumbnail = session.query(ComicThumbnail).filter(ComicThumbnail.comic_id == comic_id, ComicThumbnail.pagenumber == pageNumber).one_or_none() + return thumbnail def db_get_comic(comic_id): - session = Session() - comic = session.query(Comic).filter(Comic.id == comic_id).one_or_none() - #comic = session.query(Comic).filter(Comic.publisher == publisher, Comic.series == series, Comic.seriesyear == series_year, Comic.issue == issue).one_or_none() - return comic + session = Session() + comic = session.query(Comic).filter(Comic.id == comic_id).one_or_none() + # comic = session.query(Comic).filter(Comic.publisher == publisher, Comic.series == series, Comic.seriesyear == series_year, Comic.issue == issue).one_or_none() + return comic def comic_path_in_db(path): - try: - session = Session() - result = session.query(Comic).filter(Comic.path == path).one_or_none() - result2 = session.query(DupComic).filter(DupComic.path == path).one_or_none() - if result or result2: - return True - except Exception as e: - current_app.logger.info(path) - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return False + try: + session = Session() + result = session.query(Comic).filter(Comic.path == path).one_or_none() + result2 = session.query(DupComic).filter(DupComic.path == path).one_or_none() + if result or result2: + return True + except Exception as e: + current_app.logger.info(path) + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return False def movie_path_in_db(path): - try: - session = Session() - result = session.query(Movie).filter(Movie.path == path).one_or_none() - if result: - return True - except Exception as e: - current_app.logger.info(path) - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return False + try: + session = Session() + result = session.query(Movie).filter(Movie.path == path).one_or_none() + if result: + return True + except Exception as e: + current_app.logger.info(path) + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return False def tv_show_path_in_db(path): - try: - session = Session() - result = session.query(TvShow).filter(TvShow.path == path).one_or_none() - if result: - return True - except Exception as e: - current_app.logger.info(path) - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return False + try: + session = Session() + result = session.query(TvShow).filter(TvShow.path == path).one_or_none() + if result: + return True + except Exception as e: + current_app.logger.info(path) + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return False def tv_episode_path_in_db(path): - try: - session = Session() - result = session.query(TvEpisode).filter(TvEpisode.path == path).one_or_none() - if result: - return True - except Exception as e: - current_app.logger.info(path) - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return False + try: + session = Session() + result = session.query(TvEpisode).filter(TvEpisode.path == path).one_or_none() + if result: + return True + except Exception as e: + current_app.logger.info(path) + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return False def game_in_db(game_id): - try: - session = Session() - result = session.query(Game).filter(Game.game_id == game_id).one_or_none() - if result: - return True - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return False + try: + session = Session() + result = session.query(Game).filter(Game.game_id == game_id).one_or_none() + if result: + return True + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return False -def imdb_get_movie(title, year): - row = get_imdb().execute("SELECT tconst, runtimeMinutes FROM title_basics WHERE (originalTitle LIKE ? OR primaryTitle LIKE ?) AND (titleType LIKE '%movie' OR titleType='video') AND startYear=?", (title, title, year)).fetchone() - return row +# def imdb_get_movie(title, year): +# row = get_imdb().execute("SELECT tconst, runtimeMinutes FROM title_basics WHERE (originalTitle LIKE ? OR primaryTitle LIKE ?) AND (titleType LIKE '%movie' OR titleType='video') AND startYear=?", (title, title, year)).fetchone() +# return row +# +# +# def imdb_get_tv_show(title, year, info={}): +# if not info: +# row = get_imdb().execute( +# "SELECT tconst FROM title_basics WHERE (originalTitle LIKE ? OR primaryTitle LIKE ?) AND (titleType LIKE 'tvSeries' OR titleType LIKE 'tvMiniSeries') AND startYear=?", +# (title, title, year)).fetchone() +# else: +# row = get_imdb().execute( +# "SELECT tconst FROM title_basics WHERE originalTitle=? AND primaryTitle=? AND titleType=? AND startYear=? AND endYear=? AND runtimeMinutes=? AND genres=?", +# (info["originalTitle"], info["primaryTitle"], info["titleType"], info["startYear"], info["endYear"], info["runtimeMinutes"], info["genres"])).fetchone() +# return row +# +# +# def imdb_get_tv_episode(imdb_id, season, episode): +# row = get_imdb().execute( +# "SELECT tconst FROM title_episode WHERE parentTconst=? AND seasonNumber=? AND episodeNumber=?", +# [imdb_id, season, episode]).fetchone() +# return row -def imdb_get_tv_show(title, year, info={}): - if not info: - row = get_imdb().execute( - "SELECT tconst FROM title_basics WHERE (originalTitle LIKE ? OR primaryTitle LIKE ?) AND (titleType LIKE 'tvSeries' OR titleType LIKE 'tvMiniSeries') AND startYear=?", - (title, title, year)).fetchone() - else: - row = get_imdb().execute( - "SELECT tconst FROM title_basics WHERE originalTitle=? AND primaryTitle=? AND titleType=? AND startYear=? AND endYear=? AND runtimeMinutes=? AND genres=?", - (info["originalTitle"], info["primaryTitle"], info["titleType"], info["startYear"], info["endYear"], info["runtimeMinutes"], info["genres"])).fetchone() - return row - - -def imdb_get_tv_episode(imdb_id, season, episode): - row = get_imdb().execute( - "SELECT tconst FROM title_episode WHERE parentTconst=? AND seasonNumber=? AND episodeNumber=?", - [imdb_id, season, episode]).fetchone() - return row - - -def tmdb_get_movie_by_imdb_id(imdb_id): - data = tmdb.get_movie_data(imdb_id) - return data - - -def tmdb_get_tv_show_by_imdb_id(imdb_id): - data = tmdb.get_tv_show_data(imdb_id) - return data - - -def tmdb_get_tv_episode_by_imdb_id(imdb_id): - data = tmdb.get_tv_episode_data(imdb_id) - return data +# def tmdb_get_movie_by_imdb_id(imdb_id): +# data = tmdb.get_movie_data(imdb_id) +# return data +# +# +# def tmdb_get_tv_show_by_imdb_id(imdb_id): +# data = tmdb.get_tv_show_data(imdb_id) +# return data +# +# +# def tmdb_get_tv_episode_by_imdb_id(imdb_id): +# data = tmdb.get_tv_episode_data(imdb_id) +# return data def db_get_all_movies(): - session = Session() - movies = session.query(Movie).order_by(Movie.title, Movie.year).all() - if current_user: - email = current_user.email - movies = [(i, session.execute("SELECT rpiwebapp.is_movie_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) for i in movies] - return movies + session = Session() + movies = session.query(Movie).order_by(Movie.title, Movie.year).all() + if current_user: + email = current_user.email + movies = [(i, session.execute("SELECT rpiwebapp.is_movie_finished({}, '{}')".format(i.tmdb_id, email)).first()[0]) for i in movies] + return movies -def db_get_movie_by_imdb_id(imdb_id, extended=False, directors_cut=False): - session = Session() - result = session.query(Movie).filter(Movie.imdb_id == imdb_id, Movie.extended == extended, - Movie.directors_cut == directors_cut).one_or_none() - return result +def db_get_movie_by_tmdb_id(tmdb_id, extended=False, directors_cut=False): + session = Session() + result = session.query(Movie).filter(Movie.tmdb_id == tmdb_id, Movie.extended == extended, Movie.directors_cut == directors_cut).one_or_none() + return result def tv_movie_sort(a): - if type(a) is tuple: - return a[0].title - return a.title + if type(a) is tuple: + return a[0].title + return a.title def get_all_tv_movies(): - session = Session() - movies = session.query(Movie).order_by(Movie.title, Movie.year).all() - shows = session.query(TvShow).order_by(TvShow.title, TvShow.year).all() - tv_movies = movies + shows - if current_user: - email = current_user.email - shows = [ - (i, session.execute("SELECT rpiwebapp.is_tv_show_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) - for i in shows] - movies = [ - (i, session.execute("SELECT rpiwebapp.is_movie_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) - for i in movies] - tv_movies = movies + shows - tv_movies = sorted(tv_movies, key=tv_movie_sort) - return tv_movies + session = Session() + movies = session.query(Movie).order_by(Movie.title, Movie.year).all() + shows = session.query(TvShow).order_by(TvShow.title, TvShow.year).all() + tv_movies = movies + shows + if current_user: + email = current_user.email + shows = [(i, session.execute("SELECT rpiwebapp.is_tv_show_finished({}, '{}')".format(i.tmdb_id, email)).first()[0]) for i in shows] + movies = [(i, session.execute("SELECT rpiwebapp.is_movie_finished({}, '{}')".format(i.tmdb_id, email)).first()[0]) for i in movies] + tv_movies = movies + shows + tv_movies = sorted(tv_movies, key=tv_movie_sort) + return tv_movies -def get_tv_movie_by_imdb_id(imdb_id, extended=False, directors_cut=False): - session = Session() - result = session.query(Movie).filter(Movie.imdb_id == imdb_id, Movie.extended == extended, - Movie.directors_cut == directors_cut).one_or_none() - if not result: - result = session.query(TvShow).filter(TvShow.imdb_id == imdb_id).one_or_none() - return result +def get_tv_movie_by_tmdb_id(tmdb_id, extended=False, directors_cut=False): + session = Session() + result = session.query(Movie).filter(Movie.tmdb_id == tmdb_id, Movie.extended == extended, Movie.directors_cut == directors_cut).one_or_none() + if not result: + result = session.query(TvShow).filter(TvShow.tmdb_id == tmdb_id).one_or_none() + return result def get_currently_watching(): - pass + pass def get_all_tv_shows(): - session = Session() - result = session.query(TvShow).order_by(TvShow.title, TvShow.year).all() - if current_user: - email = current_user.email - shows = [(i, session.execute("SELECT rpiwebapp.is_tv_show_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) for i in result] - else: - shows = result - return shows + session = Session() + result = session.query(TvShow).order_by(TvShow.title, TvShow.year).all() + if current_user: + email = current_user.email + shows = [(i, session.execute("SELECT rpiwebapp.is_tv_show_finished({}, {})".format(i.tmdb_id, email)).first()[0]) for i in result] + else: + shows = result + return shows -def get_tv_show(imdb_id): - session = Session() - result = session.query(TvShow).filter(TvShow.imdb_id == imdb_id).one_or_none() - return result +def get_tv_show(tmdb_id): + session = Session() + result = session.query(TvShow).filter(TvShow.tmdb_id == tmdb_id).one_or_none() + return result -def get_tv_show_episodes_by_imdb_id(imdb_id): - session = Session() - result = session.query(TvEpisode).filter(TvEpisode.parent_imdb_id == imdb_id).order_by(TvEpisode.season, TvEpisode.episode).all() - return result +def get_tv_show_episodes_by_tmdb_id(tmdb_id): + session = Session() + result = session.query(TvEpisode).filter(TvEpisode.parent_tmdb_id == tmdb_id).order_by(TvEpisode.season, TvEpisode.episode).all() + return result -def db_get_episode_by_imdb_id(imdb_id): - session = Session() - result = session.query(TvEpisode).filter(TvEpisode.imdb_id == imdb_id).one_or_none() - return result +def db_get_episode_by_tmdb_id(tmdb_id): + session = Session() + result = session.query(TvEpisode).filter(TvEpisode.tmdb_id == tmdb_id).one_or_none() + return result -def db_get_user_tv_movie_data(imdb_id, extended=False, directors_cut=False): - session = Session() - email = current_user.email - result = session.query(UserTvMovieData).filter(UserTvMovieData.user == email, UserTvMovieData.imdb_id == imdb_id, UserTvMovieData.extended == extended, UserTvMovieData.directors_cut == directors_cut).one_or_none() - return result +def db_get_user_tv_movie_data(tmdb_id, extended=False, directors_cut=False): + session = Session() + email = current_user.email + result = ( + session.query(UserTvMovieData) + .filter( + UserTvMovieData.user == email, + UserTvMovieData.tmdb_id == tmdb_id, + UserTvMovieData.extended == extended, + UserTvMovieData.directors_cut == directors_cut, + ) + .one_or_none() + ) + return result -def db_get_user_tv_show_episodes_data(parent_imdb_id) -> list: - session = Session() - email = current_user.email - result = session.query(UserTvMovieData).filter(UserTvMovieData.user == email, - UserTvMovieData.parent_imdb == parent_imdb_id).all() - return result +def db_get_user_tv_show_episodes_data(parent_tmdb_id) -> list: + session = Session() + email = current_user.email + result = session.query(UserTvMovieData).filter(UserTvMovieData.user == email, UserTvMovieData.parent_tmdb == parent_tmdb_id).all() + return result -def db_get_current_tv_show_episode_and_data(parent_imdb_id, episodes): - session = Session() - email = current_user.email - result = session.query(UserTvMovieData).filter(UserTvMovieData.user == email, - UserTvMovieData.parent_imdb == parent_imdb_id).all() - if not result: - return episodes[0], None - most_recent_data = result[0] - most_recent_episode = episodes[0] - for episode_data in result: - if episode_data.time_stamp and most_recent_data.time_stamp: - if episode_data.time_stamp > most_recent_data.time_stamp: - most_recent_data = episode_data - for episode in episodes: - if episode.imdb_id == most_recent_data.imdb_id: - if most_recent_data.finished: - if episode == episodes[-1]: - most_recent_episode = episodes[0] - most_recent_data = None - break - most_recent_episode = episodes[episodes.index(episode)+1] - for episode_data in result: - if most_recent_episode.imdb_id == episode_data.imdb_id: - most_recent_data = episode_data - break - else: - most_recent_data = None - break - most_recent_episode = episode - break - return most_recent_episode, most_recent_data +def db_get_current_tv_show_episode_and_data(parent_tmdb_id, episodes): + session = Session() + email = current_user.email + result = session.query(UserTvMovieData).filter(UserTvMovieData.user == email, UserTvMovieData.parent_tmdb == parent_tmdb_id).all() + if not result: + return episodes[0], None + most_recent_data = result[0] + most_recent_episode = episodes[0] + for episode_data in result: + if episode_data.time_stamp and most_recent_data.time_stamp: + if episode_data.time_stamp > most_recent_data.time_stamp: + most_recent_data = episode_data + for episode in episodes: + if episode.tmdb_id == most_recent_data.tmdb_id: + if most_recent_data.finished: + if episode == episodes[-1]: + most_recent_episode = episodes[0] + most_recent_data = None + break + most_recent_episode = episodes[episodes.index(episode) + 1] + for episode_data in result: + if most_recent_episode.tmdb_id == episode_data.tmdb_id: + most_recent_data = episode_data + break + else: + most_recent_data = None + break + most_recent_episode = episode + break + return most_recent_episode, most_recent_data def get_all_games(): - session = Session() - result = session.query(Game).order_by(Game.title).all() - return result + session = Session() + result = session.query(Game).order_by(Game.title).all() + return result + + +def get_windows_games(): + session = Session() + result = session.query(Game).filter(Game.windows == True).order_by(Game.title).all() + return result + + +def get_mac_games(): + session = Session() + result = session.query(Game).filter(Game.mac == True).order_by(Game.title).all() + return result + + +def get_linux_games(): + session = Session() + result = session.query(Game).filter(Game.linux == True).order_by(Game.title).all() + return result def get_game(game_id): - session = Session() - result = session.query(Game).filter(Game.game_id == game_id).one_or_none() - return result + session = Session() + result = session.query(Game).filter(Game.game_id == game_id).one_or_none() + return result def db_search_table_columns_by_query(query, table, columns, group=[], order=[]): - session = Session() - results = {} - final_query = "%" + query.replace(" ", "%") + "%" - for column in columns: - results[column.name] = [i[0] for i in session.query(table, over(func.rank(), partition_by=group, order_by=order)).filter(cast(column, sqlalchemy.String).ilike(final_query)).all()] - return results + session = Session() + results = {} + final_query = "%" + query.replace(" ", "%") + "%" + for column in columns: + results[column.name] = [ + i[0] + for i in session.query(table, over(func.rank(), partition_by=group, order_by=order)) + .filter(cast(column, sqlalchemy.String).ilike(final_query)) + .all() + ] + return results def db_search_comics(query): - publishers = [] - series = [] - comics = [] + publishers = [] + series = [] + comics = [] - results = db_search_table_columns_by_query(query, Comic, [Comic.publisher, Comic.title, Comic.series, Comic.year]) - series_results = db_search_table_columns_by_query(query, Comic, [Comic.publisher, Comic.title, Comic.series, Comic.year], - group=[Comic.series, Comic.seriesyear], order=[Comic.issue]) - for row in results["publisher"]: - if row["publisher"] not in publishers: - publishers.append(row.publisher) - for row in series_results["series"]: - if row not in series: - series.append(row) - for row in results["title"]: - if row not in comics: - comics.append(row) - for row in results["year"]: - if row not in comics: - comics.append(row) + results = db_search_table_columns_by_query(query, Comic, [Comic.publisher, Comic.title, Comic.series, Comic.year]) + series_results = db_search_table_columns_by_query( + query, Comic, [Comic.publisher, Comic.title, Comic.series, Comic.year], group=[Comic.series, Comic.seriesyear], order=[Comic.issue] + ) + for row in results["publisher"]: + if row["publisher"] not in publishers: + publishers.append(row.publisher) + for row in series_results["series"]: + if row not in series: + series.append(row) + for row in results["title"]: + if row not in comics: + comics.append(row) + for row in results["year"]: + if row not in comics: + comics.append(row) - return {"publisher": publishers, "series": series, "comics": comics} + return {"publisher": publishers, "series": series, "comics": comics} def db_search_movies(query): - results = db_search_table_columns_by_query(query, Movie, [Movie.title, Movie.year, Movie.description], order=[Movie.title]) - movies = [] - for movie in results["title"]: - if movie not in movies: - movies.append(movie) - for movie in results["description"]: - if movie not in movies: - movies.append(movie) - for movie in results["year"]: - if movie not in movies: - movies.append(movie) - session = Session() - email = current_user.email - movies = [(i, session.execute("SELECT rpiwebapp.is_movie_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) for i in movies] + results = db_search_table_columns_by_query(query, Movie, [Movie.title, Movie.year, Movie.description], order=[Movie.title]) + movies = [] + for movie in results["title"]: + if movie not in movies: + movies.append(movie) + for movie in results["description"]: + if movie not in movies: + movies.append(movie) + for movie in results["year"]: + if movie not in movies: + movies.append(movie) + session = Session() + email = current_user.email + movies = [(i, session.execute("SELECT rpiwebapp.is_movie_finished({}, '{}')".format(i.tmdb_id, email)).first()[0]) for i in movies] - return movies + return movies def db_search_tv_shows(query): - results = db_search_table_columns_by_query(query, TvShow, [TvShow.title, TvShow.year, TvShow.description], order=[TvShow.title]) - tv_shows = [] - for show in results["title"]: - if show not in tv_shows: - tv_shows.append(show) - for show in results["description"]: - if show not in tv_shows: - tv_shows.append(show) - for show in results["year"]: - if show not in tv_shows: - tv_shows.append(show) - session = Session() - email = current_user.email - shows = [ - (i, session.execute("SELECT rpiwebapp.is_tv_show_finished('{}', '{}')".format(i.imdb_id, email)).first()[0]) for - i in tv_shows] - return shows + results = db_search_table_columns_by_query(query, TvShow, [TvShow.title, TvShow.year, TvShow.description], order=[TvShow.title]) + tv_shows = [] + for show in results["title"]: + if show not in tv_shows: + tv_shows.append(show) + for show in results["description"]: + if show not in tv_shows: + tv_shows.append(show) + for show in results["year"]: + if show not in tv_shows: + tv_shows.append(show) + session = Session() + email = current_user.email + shows = [(i, session.execute("SELECT rpiwebapp.is_tv_show_finished({}, '{}')".format(i.tmdb_id, email)).first()[0]) for i in tv_shows] + return shows def db_search_tv_movie(query): - movies = db_search_movies(query) - shows = db_search_tv_shows(query) - tv_movies = sorted(movies + shows, key=tv_movie_sort) - return tv_movies + movies = db_search_movies(query) + shows = db_search_tv_shows(query) + tv_movies = sorted(movies + shows, key=tv_movie_sort) + return tv_movies def resize_image(image, new_width=256, new_height=256): - new_image = image - orig_height = new_image.height - orig_width = new_image.width - if orig_height >= orig_width: - width = int((orig_width / orig_height) * new_height) - height = new_height - else: - height = int((orig_height / orig_width) * new_width) - width = new_width - new_image.thumbnail(width, height) - return new_image + new_image = image + orig_height = new_image.height + orig_width = new_image.width + if orig_height >= orig_width: + width = int((orig_width / orig_height) * new_height) + height = new_height + else: + height = int((orig_height / orig_width) * new_width) + width = new_width + new_image.thumbnail(width, height) + return new_image # def fix_thumbnails(): @@ -880,11 +951,11 @@ def resize_image(image, new_width=256, new_height=256): def get_user(email): - try: - session = Session() - result = session.query(User).filter(User.email == email).one_or_none() - if result: - return result - return None - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + try: + session = Session() + result = session.query(User).filter(User.email == email).one_or_none() + if result: + return result + return None + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) diff --git a/scripts/fix_thumbnail_size.py b/scripts/fix_thumbnail_size.py index 8947674..e70cc81 100644 --- a/scripts/fix_thumbnail_size.py +++ b/scripts/fix_thumbnail_size.py @@ -1,7 +1,8 @@ +import os +import sqlite3 from io import BytesIO -from wand.image import Image -import sqlite3, os +from wand.image import Image RPI_DATABASE = "/var/lib/rpiWebApp/database.db" @@ -15,32 +16,35 @@ db.row_factory = sqlite3.Row def resize_image(image, new_width=256, new_height=256): - new_image = image - orig_height = new_image.height - orig_width = new_image.width - if orig_height >= orig_width: - width = int((orig_width/orig_height) * new_height) - height = new_height - else: - height = int((orig_height/orig_width) * new_width) - width = new_width - new_image.thumbnail(width, height) - return new_image + new_image = image + orig_height = new_image.height + orig_width = new_image.width + if orig_height >= orig_width: + width = int((orig_width / orig_height) * new_height) + height = new_height + else: + height = int((orig_height / orig_width) * new_width) + width = new_width + new_image.thumbnail(width, height) + return new_image def fix_thumbnails(): - new_height = 256 - new_width = 256 - print("Start fix thumbnail size") - rows = db.execute("SELECT * FROM comic_thumbnails") - print("got list of all thumbnails\n") + new_height = 256 + new_width = 256 + print("Start fix thumbnail size") + rows = db.execute("SELECT * FROM comic_thumbnails") + print("got list of all thumbnails\n") - for row in rows: - image = Image(file=BytesIO(row["image"])) - if image.width > new_width or image.height > new_height: - print("id:", row["id"], "pageNumber:", row["pageNumber"]) - db.execute("UPDATE comic_thumbnails SET image=? WHERE id=? AND pageNumber=?", [resize_image(image, new_width, new_height).make_blob(), row["id"], row["pageNumber"]]) - db.commit() + for row in rows: + image = Image(file=BytesIO(row["image"])) + if image.width > new_width or image.height > new_height: + print("id:", row["id"], "pageNumber:", row["pageNumber"]) + db.execute( + "UPDATE comic_thumbnails SET image=? WHERE id=? AND pageNumber=?", + [resize_image(image, new_width, new_height).make_blob(), row["id"], row["pageNumber"]], + ) + db.commit() fix_thumbnails() diff --git a/scripts/func.py b/scripts/func.py index 7fa6687..3d45983 100644 --- a/scripts/func.py +++ b/scripts/func.py @@ -1,15 +1,17 @@ -from flask import current_app -from comicapi import comicarchive -from blinker import Namespace -from datetime import timedelta - -from io import BytesIO -from wand.image import Image -import os, re import inspect import json +import os +import pathlib +import re +from datetime import timedelta +from io import BytesIO + import enzyme import requests +from blinker import Namespace +from comicapi import comicarchive +from flask import current_app +from wand.image import Image from scripts import database @@ -22,6 +24,8 @@ games_loaded = rpi_signals.signal("games_loaded") publishers_to_ignore = ["***REMOVED***"] +API_KEY = "***REMOVED***" + # Directories RPI_COMICS_DIRECTORY = "/usb/storage/media/Comics/" @@ -45,434 +49,555 @@ GAMES_DIRECTORY = RPI_GAMES_DIRECTORY if os.path.exists(RPI_GAMES_DIRECTORY) els def get_comics(): - total_comics = 0 - comics_in_db = 0 - comics_added = 0 - meta = [] - thumbnails = [] - i = 0 - for root, dirs, files in os.walk(COMICS_DIRECTORY): - for f in files: - if "temp" in root: - continue - if f.endswith(".cbr"): - total_comics += 1 - path = os.path.join(root, f) - if not database.comic_path_in_db(path): - try: - test_path = path.encode("utf8") - except Exception as e: - current_app.logger.info("encoding failed on: "+path) - continue - archive = open_comic(path) - md = archive.readCIX() - if md.publisher in publishers_to_ignore: - continue - current_app.logger.info(path) - try: - meta.append((path, md)) - thumbnails.append(get_comic_thumbnails(archive)) - comics_added += 1 - i += 1 - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - continue - if i >= 2: - comic_loaded.send("anonymous", meta=meta.copy(), thumbnails=thumbnails.copy()) - meta.clear() - thumbnails.clear() - i = 0 - comics_in_db += 1 - current_app.logger.info("total number of comics: "+str(total_comics)) - current_app.logger.info("comics in database: "+str(comics_in_db)) - current_app.logger.info("number of comics added: "+str(comics_added)) - comic_loaded.send("anonymous", meta=meta, thumbnails=thumbnails) + total_comics = 0 + comics_in_db = 0 + comics_added = 0 + meta = [] + thumbnails = [] + i = 0 + for root, dirs, files in os.walk(COMICS_DIRECTORY): + for f in files: + if "temp" in root: + continue + if f.endswith(".cbr"): + total_comics += 1 + path = os.path.join(root, f) + if not database.comic_path_in_db(path): + try: + test_path = path.encode("utf8") + except Exception as e: + current_app.logger.info("encoding failed on: " + path) + continue + archive = open_comic(path) + md = archive.readCIX() + if md.publisher in publishers_to_ignore: + continue + current_app.logger.info(path) + try: + meta.append((path, md)) + thumbnails.append(get_comic_thumbnails(archive)) + comics_added += 1 + i += 1 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + continue + if i >= 2: + comic_loaded.send("anonymous", meta=meta.copy(), thumbnails=thumbnails.copy()) + meta.clear() + thumbnails.clear() + i = 0 + comics_in_db += 1 + current_app.logger.info("total number of comics: " + str(total_comics)) + current_app.logger.info("comics in database: " + str(comics_in_db)) + current_app.logger.info("number of comics added: " + str(comics_added)) + comic_loaded.send("anonymous", meta=meta, thumbnails=thumbnails) -def get_comic(path): - meta = [] - thumbnails = [] - if path.endswith(".cbr"): - if not database.comic_path_in_db(path): - try: - test_path = path.encode("utf8") - except Exception as e: - current_app.logger.info("encoding failed on: "+path) - return - archive = open_comic(path) - md = archive.readCIX() - if md.publisher in publishers_to_ignore: - return - current_app.logger.info(path) - meta.append((path, md)) - try: - thumbnails.append(get_comic_thumbnails(archive)) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return - comic_loaded.send("anonymous", meta=meta, thumbnails=thumbnails) +def get_comic(path: pathlib.Path): + meta = [] + thumbnails = [] + if path.suffix == ".cbr": + if not database.comic_path_in_db(str(path)): + try: + test_path = str(path).encode("utf8") + except Exception as e: + current_app.logger.info(f"encoding failed on: {path}") + return + archive = open_comic(str(path)) + md = archive.readCIX() + if md.publisher in publishers_to_ignore: + return + current_app.logger.info(path) + meta.append((str(path), md)) + try: + thumbnails.append(get_comic_thumbnails(archive)) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return + comic_loaded.send("anonymous", meta=meta, thumbnails=thumbnails) def get_comic_thumbnails(comic): - thumbnails = [] - size = "256x256" - new_height = 256 - new_width = 256 - for page in range(comic.getNumberOfPages()): - image_bytes = BytesIO(comic.getPage(page)) - image = Image(file=image_bytes) - orig_height = image.height - orig_width = image.width - if orig_height >= orig_width: - width = int((orig_width/orig_height) * new_height) - height = new_height - else: - height = int((orig_height/orig_width) * new_width) - width = new_width - image.thumbnail(width, height) - thumbnails.append((image.make_blob(), "image/"+image.format)) - return thumbnails + thumbnails = [] + size = "256x256" + new_height = 256 + new_width = 256 + for page in range(comic.getNumberOfPages()): + image_bytes = BytesIO(comic.getPage(page)) + image = Image(file=image_bytes) + orig_height = image.height + orig_width = image.width + if orig_height >= orig_width: + width = int((orig_width / orig_height) * new_height) + height = new_height + else: + height = int((orig_height / orig_width) * new_width) + width = new_width + image.thumbnail(width, height) + thumbnails.append((image.make_blob(), "image/" + image.format)) + return thumbnails def open_comic(path): - archive = comicarchive.ComicArchive(path, default_image_path="static/images/icon.png") - return archive + archive = comicarchive.ComicArchive(path, default_image_path="static/images/icon.png") + return archive def get_movies(): - current_app.logger.info("start load movies") - pattern = r"(?P.+) \((?P<year>\d+)\)(?P<extended>\(extended\))?(?P<directors_cut> Director's Cut)?(?P<extension>\.mkv)" - movies = [] - total_movies = 0 - movies_in_db = 0 - movies_added = 0 - for root, dirs, files in os.walk(MOVIES_DIRECTORY): - for f in files: - if f.endswith(".mkv"): - total_movies += 1 - path = os.path.join(root, f) - if not database.movie_path_in_db(path): - try: - match = re.match(pattern, f) - if not match: - current_app.logger.info(f+" did not match regex.") - continue - current_app.logger.info("movie path: "+path) - title = match.group("title") - current_app.logger.info("movie title: "+title) - year = int(match.group("year")) - extended = False - directors_cut = False - if match.group("extended"): - extended = True - imdb_data = database.imdb_get_movie(title.replace(match.group("extended"), ""), year) - elif match.group("directors_cut"): - imdb_data = database.imdb_get_movie(title.replace(match.group("directors_cut"), ""), year) - directors_cut = True - else: - imdb_data = database.imdb_get_movie(title, year) - if not imdb_data: - current_app.logger.info("could not get imdb data for: "+title+" "+str(year)) - continue - imdb_id = imdb_data["tconst"] - length = imdb_data["runtimeMinutes"] + current_app.logger.info("start loading movies") + pattern = r"(?P<title>.+) \((?P<year>\d+)\)(?P<extended>\(extended\))?(?P<directors_cut> Director's Cut)?(?P<extension>\.mkv)" + url = "https://api.themoviedb.org/3/search/movie" + movies = [] + total_movies = 0 + movies_in_db = 0 + movies_added = 0 + for root, dirs, files in os.walk(MOVIES_DIRECTORY): + for f in files: + if f.endswith(".mkv"): + total_movies += 1 + path = os.path.join(root, f) + if not database.movie_path_in_db(path): + try: + match = re.match(pattern, f) + if not match: + current_app.logger.info(f + " did not match regex.") + continue + current_app.logger.info("movie path: " + path) + title = match.group("title") + current_app.logger.info("movie title: " + title) + year = int(match.group("year")) + extended = True if match.group("extended") else False + directors_cut = True if match.group("directors_cut") else False - tmdb_data = database.tmdb_get_movie_by_imdb_id(imdb_id) - if not tmdb_data: - current_app.logger.info("could not get tmdb data") - continue - tmdb_id = tmdb_data[0] - description = tmdb_data[1] - poster_path = tmdb_data[2] - backdrop_path = tmdb_data[3] - movies_added += 1 + data = { + "api_key": API_KEY, + "query": title, + "primary_release_year": year, + "language": "en-US", + } + r = requests.get(url, params=data) + if len(r.json()["results"]) == 0: + data = { + "api_key": API_KEY, + "query": title, + "year": year, + "language": "en-US", + } + r = requests.get(url, params=data) + if len(r.json()["results"]) == 0: + current_app.logger.info(f"no movie results for {title} - ({year})") + continue + info = r.json()["results"][0] - movies.append((path, imdb_id, tmdb_id, title, year, length, description, extended, directors_cut, poster_path, backdrop_path)) - if len(movies) >= 20: - movie_loaded.send("anonymous", movies=movies.copy()) - movies.clear() - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) +" "+ str(e)) - # print(e) - movies_in_db += 1 - movie_loaded.send("anonymous", movies=movies) - current_app.logger.info("finish load movies") - current_app.logger.info("total movies: "+str(total_movies)) - current_app.logger.info("movies in database: "+str(movies_in_db)) - current_app.logger.info("movies added: "+str(movies_added)) + tmdb_id = info["id"] + description = info["overview"] + poster_path = info["poster_path"] + backdrop_path = info["backdrop_path"] + movies_added += 1 + + movies.append((path, tmdb_id, title, year, description, extended, directors_cut, poster_path, backdrop_path,)) + if len(movies) >= 20: + movie_loaded.send("anonymous", movies=movies.copy()) + movies.clear() + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + # print(e) + movies_in_db += 1 + movie_loaded.send("anonymous", movies=movies) + current_app.logger.info("finish loading movies") + current_app.logger.info("total movies: " + str(total_movies)) + current_app.logger.info("movies in database: " + str(movies_in_db)) + current_app.logger.info("movies added: " + str(movies_added)) -def get_movie(path): - pattern = r"(?P<title>.+) \((?P<year>\d+)\)(?P<extended>\(extended\))?(?P<directors_cut> Director's Cut)?(?P<extension>\.mkv)" - movies = [] - if not database.movie_path_in_db(path): - try: - match = re.match(pattern, path) - if not match: - current_app.logger.info(path + " did not match regex.") - return - current_app.logger.info("movie path: " + path) - title = match.group("title") - current_app.logger.info("movie title: " + title) - year = int(match.group("year")) - extended = False - directors_cut = False - if match.group("extended"): - extended = True - imdb_data = database.imdb_get_movie(title.replace(match.group("extended"), ""), year) - elif match.group("directors_cut"): - imdb_data = database.imdb_get_movie(title.replace(match.group("directors_cut"), ""), year) - directors_cut = True - else: - imdb_data = database.imdb_get_movie(title, year) - if not imdb_data: - current_app.logger.info("could not get imdb data for: " + title + " " + str(year)) - return - imdb_id = imdb_data["tconst"] - length = imdb_data["runtimeMinutes"] +def get_movie(path: pathlib.Path): + pattern = r"(?P<title>.+) \((?P<year>\d+)\)(?P<extended>\(extended\))?(?P<directors_cut> Director's Cut)?(?P<extension>\.mkv)" + url = "https://api.themoviedb.org/3/search/movie" + movies = [] + if not database.movie_path_in_db(str(path)): + try: + match = re.match(pattern, path.name) + if not match: + current_app.logger.info(f"{path.name} did not match regex.") + return + current_app.logger.info(f"movie path: {path}") + title = match.group("title") + current_app.logger.info("movie title: " + title) + year = int(match.group("year")) + extended = match.group("extended") is True + directors_cut = match.group("directors_cut") is True - tmdb_data = database.tmdb_get_movie_by_imdb_id(imdb_id) - if not tmdb_data: - current_app.logger.info("could not get tmdb data") - return - tmdb_id = tmdb_data[0] - description = tmdb_data[1] - poster_path = tmdb_data[2] - backdrop_path = tmdb_data[3] + data = { + "api_key": API_KEY, + "query": title, + "primary_release_year": year, + "language": "en-US", + } + r = requests.get(url, params=data) + if len(r.json()["results"]) == 0: + data = { + "api_key": API_KEY, + "query": title, + "year": year, + "language": "en-US", + } + r = requests.get(url, params=data) + info = r.json()["results"][0] + if len(r.json()["results"]) == 0: + current_app.logger.info(f"no movie results for {title} - ({year})") + return - movies.append((path, imdb_id, tmdb_id, title, year, length, description, extended, directors_cut, - poster_path, backdrop_path)) - movie_loaded.send("anonymous", movies=movies.copy()) - movies.clear() - current_app.logger.info("finish load movie") - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + tmdb_id = info["id"] + description = info["overview"] + poster_path = info["poster_path"] + backdrop_path = info["backdrop_path"] + + movies.append((str(path), tmdb_id, title, year, description, extended, directors_cut, poster_path, backdrop_path,)) + movie_loaded.send("anonymous", movies=movies.copy()) + movies.clear() + current_app.logger.info("finish loading movie") + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def get_tv_shows(): - dir_pattern = r"(?P<title>.+) \((?P<year>\d+)\)" - for dir in sorted(os.listdir(TV_SHOWS_DIRECTORY)): - dir_match = re.match(dir_pattern, dir) - if dir_match: - path = TV_SHOWS_DIRECTORY+dir - if not database.tv_show_path_in_db(path): - info = {} - if os.path.exists(path+"/info.json"): - with open(path+"/info.json") as f: - info = json.load(f) - series_name = dir_match.group("title") - series_year = int(dir_match.group("year")) - imdb_data = database.imdb_get_tv_show(series_name, series_year, info) - if not imdb_data: - current_app.logger.info("could not get imdb data for:"+series_name+" "+str(series_year)) - # print("could not get imdb data for:", series_name, series_year) - continue - imdb_id = imdb_data["tconst"] - tmdb_data = database.tmdb_get_tv_show_by_imdb_id(imdb_id) - if not tmdb_data: - current_app.logger.info("could not get tmdb data for:" + series_name + " " + str(series_year)) - # print("could not get tmdb data for:", series_name, series_year) - with open("/var/lib/rpiWebApp/log.txt", "a") as f: - f.write("could not get tmdb data for: " + imdb_id + " " + series_name + " " + str(series_year)+"\n") - continue - tmdb_id = tmdb_data[0] - description = tmdb_data[1] - poster_path = tmdb_data[2] - tv_show_data = (imdb_id, tmdb_id, series_name, series_year, description, poster_path, path) - tv_show_loaded.send("anonymous", tv_show=tv_show_data) - current_app.logger.info("finished load tv shows.") + dir_pattern = r"(?P<title>.+) \((?P<year>\d+)\)" + search_url = "https://api.themoviedb.org/3/search/tv" + tv_url = "https://api.themoviedb.org/3/tv/" + current_app.logger.info("start loading tv shows") + for dir in sorted(os.listdir(TV_SHOWS_DIRECTORY)): + dir_match = re.match(dir_pattern, dir) + if dir_match: + path = TV_SHOWS_DIRECTORY + dir + if not database.tv_show_path_in_db(path): + json_info = {} + if os.path.exists(path + "/info.json"): + with open(path + "/info.json") as f: + json_info = json.load(f) + series_name = dir_match.group("title") + series_year = int(dir_match.group("year")) + + if not json_info: + data = { + "api_key": API_KEY, + "query": series_name, + "first_air_date_year": series_year, + "language": "en-US", + } + r = requests.get(search_url, params=data) + if len(r.json()["results"]) == 0: + current_app.logger.info(f"no tv show results for {series_name} - ({series_year})") + continue + info = r.json()["results"][0] + else: + data = {"api_key": API_KEY, "language": "en-US"} + r = requests.get(tv_url + str(json_info["tmdb_id"]), params=data) + if "status_code" in r.json().keys(): + current_app.logger.info(f"no tv show results for {series_name} - ({series_year})") + continue + info = r.json() + + tmdb_id = info["id"] + description = info["overview"] + poster_path = info["poster_path"] + tv_show_data = ( + tmdb_id, + series_name, + series_year, + description, + poster_path, + path, + ) + tv_show_loaded.send("anonymous", tv_show=tv_show_data) + current_app.logger.info("finished loading tv shows.") def get_tv_episodes(): - try: - video_pattern = r"S(?P<season>\d+)E(?P<episode>\d+) - (?P<title>.+)(?P<extension>.mp4|.mkv)" - rows = database.get_all_tv_shows() - for tv_show in rows: - episodes = [] - for video in sorted(os.listdir(tv_show.path)): - video_match = re.match(video_pattern, video) - if video_match: - path = os.path.join(tv_show.path, video) - if not database.tv_episode_path_in_db(path): - season = int(video_match.group("season")) - episode = int(video_match.group("episode")) - episode_name = video_match.group("title") - episode_imdb_data = database.imdb_get_tv_episode(tv_show.imdb_id, season, episode) - if not episode_imdb_data: - current_app.logger.info("could not get imdb data for: "+tv_show.title+" "+str(tv_show.year)+" "+str(season)+" "+str(episode)) - print("could not get imdb data for:", tv_show.title, tv_show.year, season, episode) - continue - episode_imdb_id = episode_imdb_data["tconst"] - episode_tmdb_data = database.tmdb_get_tv_episode_by_imdb_id(episode_imdb_id) - if not episode_tmdb_data: - current_app.logger.info("could not get tmdb data for: "+tv_show.title+" "+str(tv_show.year)+" "+str(season)+" "+str(episode)) - with open("/var/lib/rpiWebApp/log.txt", "w") as f: - f.write("could not get tmdb data for: " + episode_imdb_id + " " + tv_show.title + " " + str( - tv_show.year) + " " + str(season) + " " + str(episode) + "\n") - continue - episode_tmdb_id = episode_tmdb_data[0] - episode_description = episode_tmdb_data[1] - episode_still_path = episode_tmdb_data[2] - episodes.append((episode_imdb_id, tv_show.imdb_id, episode_tmdb_id, episode_name, season, episode, - episode_description, episode_still_path, path)) - tv_episodes_loaded.send("anonymous", tv_episodes=episodes) - current_app.logger.info("finished load tv episodes") - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + video_pattern = r"S(?P<season>\d+)E(?P<episode>\d+) - (?P<title>.+)(?P<extension>.mkv)" + rows = database.get_all_tv_shows() + current_app.logger.info("start loading tv episodes") + for tv_show in rows: + try: + episodes = [] + for video in sorted(os.listdir(tv_show.path)): + video_match = re.match(video_pattern, video) + if video_match: + path = os.path.join(tv_show.path, video) + if not database.tv_episode_path_in_db(path): + season = int(video_match.group("season")) + episode = int(video_match.group("episode")) + episode_name = video_match.group("title") + current_app.logger.info(f"S{season} E{episode} - {tv_show.title}: {episode_name}") + url = f"https://api.themoviedb.org/3/tv/{tv_show.tmdb_id}/season/{season}/episode/{episode}" + + data = {"api_key": API_KEY, "language": "en-US"} + r = requests.get(url, params=data) + if "status_code" in r.json().keys(): + current_app.logger.info(f"no tv episode results for S{season} E{episode} - {tv_show.title}: {episode_name}") + continue + info = r.json() + + episode_tmdb_id = info["id"] + episode_description = info["overview"] + episode_still_path = info["still_path"] + episodes.append( + (episode_tmdb_id, tv_show.tmdb_id, episode_name, season, episode, episode_description, episode_still_path, path,) + ) + if len(episodes) >= 10: + tv_episodes_loaded.send("anonymous", tv_episodes=episodes.copy()) + episodes.clear() + tv_episodes_loaded.send("anonymous", tv_episodes=episodes) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + current_app.logger.info("finished loading tv episodes") -def get_tv_episode(path): - folder, name = os.path.split(path) - video_pattern = r"S(?P<season>\d+)E(?P<episode>\d+) - (?P<title>.+)(?P<extension>.mp4|.mkv)" - video_match = re.match(video_pattern, name) - if video_match: - rows = database.get_all_tv_shows() - for tv_show in rows: - if folder == tv_show.path: - if not database.tv_episode_path_in_db(path): - episodes = [] - season = int(video_match.group("season")) - episode = int(video_match.group("episode")) - episode_name = video_match.group("title") - episode_imdb_data = database.imdb_get_tv_episode(tv_show.imdb_id, season, episode) - if not episode_imdb_data: - current_app.logger.info( - "could not get imdb data for: " + tv_show.title + " " + str(tv_show.year) + " " + str( - season) + " " + str(episode)) - print("could not get imdb data for:", tv_show.title, tv_show.year, season, episode) - return - episode_imdb_id = episode_imdb_data["tconst"] - episode_tmdb_data = database.tmdb_get_tv_episode_by_imdb_id(episode_imdb_id) - if not episode_tmdb_data: - current_app.logger.info( - "could not get tmdb data for: " + tv_show.title + " " + str(tv_show.year) + " " + str( - season) + " " + str(episode)) - with open("/var/lib/rpiWebApp/log.txt", "w") as f: - f.write("could not get tmdb data for: " + episode_imdb_id + " " + tv_show.title + " " + str( - tv_show.year) + " " + str(season) + " " + str(episode) + "\n") - return - episode_tmdb_id = episode_tmdb_data[0] - episode_description = episode_tmdb_data[1] - episode_still_path = episode_tmdb_data[2] - episodes.append((episode_imdb_id, tv_show.imdb_id, episode_tmdb_id, episode_name, season, episode, - episode_description, episode_still_path, path)) - tv_episodes_loaded.send("anonymous", tv_episodes=episodes) - current_app.logger.info("finished load tv episode") +def get_tv_episode(path: pathlib.Path): + video_pattern = r"S(?P<season>\d+)E(?P<episode>\d+) - (?P<title>.+)(?P<extension>.mkv)" + video_match = re.match(video_pattern, path.name) + if video_match: + rows = database.get_all_tv_shows() + for tv_show in rows: + if path.parent == tv_show.path: + if not database.tv_episode_path_in_db(str(path)): + episodes = [] + season = int(video_match.group("season")) + episode = int(video_match.group("episode")) + episode_name = video_match.group("title") + url = f"https://api.themoviedb.org/3/tv/{tv_show.tmdb_id}/season/{season}/episode/{episode}" + + data = {"api_key": API_KEY, "language": "en-US"} + r = requests.get(url, params=data) + if "status_code" in r.json().keys(): + current_app.logger.info(f"no tv episode results for S{season} E{episode} - {tv_show.title}: {episode_name}") + continue + info = r.json() + + episode_tmdb_id = info["id"] + episode_description = info["overview"] + episode_still_path = info["still_path"] + episodes.append( + (episode_tmdb_id, tv_show.tmdb_id, episode_name, season, episode, episode_description, episode_still_path, str(path),) + ) + tv_episodes_loaded.send("anonymous", tv_episodes=episodes) + current_app.logger.info("finished loading tv episode") def get_chapters(path): - try: - with open(path, 'rb') as f: - mkv = enzyme.MKV(f) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - mkv_info = {} - for chapter in mkv.chapters: - if chapter.string == "Intro": - mkv_info["intro"] = { - "start": chapter.start.seconds, - "end": timedelta(microseconds=chapter.end//1000).seconds - } - if chapter.string == "Credits": - mkv_info["credits"] = {"start": chapter.start.seconds} - if chapter.string == "end-credit scene": - if "end-credit scene" not in mkv_info.keys(): - mkv_info["end-credit scene"] = [] - end_credit = {"start": chapter.start.seconds} - if chapter.end: - end_credit["end"] = timedelta(microseconds=chapter.end//1000).seconds - mkv_info["end-credit scene"].append(end_credit) - return mkv_info + try: + with open(path, "rb") as f: + mkv = enzyme.MKV(f) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return {} + mkv_info = {} + for chapter in mkv.chapters: + if chapter.string == "Intro": + mkv_info["intro"] = { + "start": chapter.start.seconds, + "end": timedelta(microseconds=chapter.end // 1000).seconds, + } + if chapter.string == "Credits": + mkv_info["credits"] = {"start": chapter.start.seconds} + if chapter.string == "end-credit scene": + if "end-credit scene" not in mkv_info.keys(): + mkv_info["end-credit scene"] = [] + end_credit = {"start": chapter.start.seconds} + if chapter.end: + end_credit["end"] = timedelta(microseconds=chapter.end // 1000).seconds + mkv_info["end-credit scene"].append(end_credit) + return mkv_info def get_tags(path): - try: - with open(path, 'rb') as f: - mkv = enzyme.MKV(f) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - mkv_info = {} - for tag in mkv.tags: - if tag.targets.data[0].data == 70: - mkv_info["collection"] = {} - for simple in tag.simpletags: - if simple.name == "TITLE": - mkv_info["collection"]["title"] = simple.string - if simple.name == "TOTAL_PARTS": - mkv_info["collection"]["episodes"] = int(simple.string) - if simple.name == "KEYWORDS": - mkv_info["collection"]["key_words"] = simple.string.split(",") - if simple.name == "DATE_RELEASED": - mkv_info["collection"]["year"] = int(simple.string) - if simple.name == "SUMMARY": - mkv_info["collection"]["summary"] = simple.string - if tag.targets.data[0].data == 60: - mkv_info["season"] = {} - for simple in tag.simpletags: - if simple.name == "TITLE": - mkv_info["season"]["title"] = simple.string - if simple.name == "TOTAL_PARTS": - mkv_info["season"]["episodes"] = int(simple.string) - if tag.targets.data[0].data == 50: - mkv_info["movie"] = {} - for simple in tag.simpletags: - if simple.name == "TITLE": - mkv_info["movie"]["title"] = simple.string - if simple.name == "DATE_RELEASED": - mkv_info["movie"]["year"] = int(simple.string) - if simple.name == "PART_NUMBER": - mkv_info["movie"]["episode"] = int(simple.string) - if simple.name == "KEYWORDS": - mkv_info["movie"]["key_words"] = simple.string.split(",") - if simple.name == "SUMMARY": - mkv_info["movie"]["summary"] = simple.string - return mkv_info + try: + with open(path, "rb") as f: + mkv = enzyme.MKV(f) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + mkv_info = {} + for tag in mkv.tags: + if tag.targets.data[0].data == 70: + mkv_info["collection"] = {} + for simple in tag.simpletags: + if simple.name == "TITLE": + mkv_info["collection"]["title"] = simple.string + if simple.name == "TOTAL_PARTS": + mkv_info["collection"]["episodes"] = int(simple.string) + if simple.name == "KEYWORDS": + mkv_info["collection"]["key_words"] = simple.string.split(",") + if simple.name == "DATE_RELEASED": + mkv_info["collection"]["year"] = int(simple.string) + if simple.name == "SUMMARY": + mkv_info["collection"]["summary"] = simple.string + if tag.targets.data[0].data == 60: + mkv_info["season"] = {} + for simple in tag.simpletags: + if simple.name == "TITLE": + mkv_info["season"]["title"] = simple.string + if simple.name == "TOTAL_PARTS": + mkv_info["season"]["episodes"] = int(simple.string) + if tag.targets.data[0].data == 50: + mkv_info["movie"] = {} + for simple in tag.simpletags: + if simple.name == "TITLE": + mkv_info["movie"]["title"] = simple.string + if simple.name == "DATE_RELEASED": + mkv_info["movie"]["year"] = int(simple.string) + if simple.name == "PART_NUMBER": + mkv_info["movie"]["episode"] = int(simple.string) + if simple.name == "KEYWORDS": + mkv_info["movie"]["key_words"] = simple.string.split(",") + if simple.name == "SUMMARY": + mkv_info["movie"]["summary"] = simple.string + return mkv_info def get_games(): - games = [] - cover_url = "https://api-v3.igdb.com/covers" - games_url = "https://api-v3.igdb.com/games" - headers = { - "accept": "application/json", - "user-key": "641f7f0e3af5273dcc1105ce851ea804" - } - i = 0 - for folder in sorted(os.listdir(GAMES_DIRECTORY), key=str.casefold): - root = os.path.join(GAMES_DIRECTORY, folder) - if os.path.isdir(os.path.join(root)): - path = os.path.join(root, "info.json") - with open(path, "r") as f: - info = json.load(f) - game_id = info["id"] - if not database.game_in_db(game_id): - current_app.logger.info(f"start loading game: {info['name']}:{info['id']}") - data = f"fields summary;limit 1;where id={game_id};" - r = requests.get(games_url, headers=headers, data=data).json()[0] - description = "" - if "summary" in r.keys(): - description = r["summary"] - data = f"fields image_id;limit 1;where game={game_id};" - r = requests.get(cover_url, headers=headers, data=data).json() - poster_path = None - if r: - if "image_id" in r[0].keys(): - poster_path = "https://images.igdb.com/igdb/image/upload/t_cover_big/" + r[0]["image_id"] + ".jpg" - windows = None - mac = None - linux = None - if "windows" in info.keys(): - windows = info["windows"] - if "mac" in info.keys(): - mac = info["mac"] - if "linux" in info.keys(): - linux = info["linux"] - game = (info["name"], game_id, description, poster_path, windows, mac, linux) - games.append(game) - i += 1 - if i >= 5: - games_loaded.send("anonymous", games=games.copy()) - games.clear() - i = 0 - games_loaded.send("anonymous", games=games) - current_app.logger.info("finished loading games") + games = [] + cover_url = "https://api-v3.igdb.com/covers" + games_url = "https://api-v3.igdb.com/games" + headers = { + "accept": "application/json", + "user-key": "641f7f0e3af5273dcc1105ce851ea804", + } + i = 0 + current_app.logger.info("start loading games") + for folder in sorted(os.listdir(GAMES_DIRECTORY), key=str.casefold): + root = os.path.join(GAMES_DIRECTORY, folder) + if os.path.isdir(os.path.join(root)): + try: + path = os.path.join(root, "info.json") + with open(path, "r") as f: + info = json.load(f) + game_id = info["id"] + if not database.game_in_db(game_id): + current_app.logger.info(f"start loading game: {info['name']}:{info['id']}") + data = f"fields summary;limit 1;where id={game_id};" + r = requests.get(games_url, headers=headers, data=data).json()[0] + description = "" + if "summary" in r.keys(): + description = r["summary"] + data = f"fields image_id;limit 1;where game={game_id};" + r = requests.get(cover_url, headers=headers, data=data).json() + poster_path = None + if r: + if "image_id" in r[0].keys(): + poster_path = "https://images.igdb.com/igdb/image/upload/t_cover_big/" + r[0]["image_id"] + ".jpg" + windows = False + mac = False + linux = False + if "windows" in info.keys(): + windows = True + if "mac" in info.keys(): + mac = True + if "linux" in info.keys(): + linux = True + game = ( + info["name"], + game_id, + description, + poster_path, + root, + windows, + mac, + linux, + folder, + ) + games.append(game) + i += 1 + if i >= 5: + games_loaded.send("anonymous", games=games.copy()) + games.clear() + i = 0 + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + games_loaded.send("anonymous", games=games) + current_app.logger.info("finished loading games") + + +def get_game(path: pathlib.Path): + try: + games = [] + cover_url = "https://api-v3.igdb.com/covers" + games_url = "https://api-v3.igdb.com/games" + headers = { + "accept": "application/json", + "user-key": "***REMOVED***", + } + if not path.name == "info.json": + return + else: + with path.open("r") as f: + info = json.load(f) + game_id = info["id"] + if database.game_in_db(game_id): + update_game(path) + else: + dir = path.parent + folder = path.parts[-2] + current_app.logger.info(f"start loading game: {info['name']}:{info['id']}") + data = f"fields summary;limit 1;where id={game_id};" + r = requests.get(games_url, headers=headers, data=data).json()[0] + description = "" + if "summary" in r.keys(): + description = r["summary"] + data = f"fields image_id;limit 1;where game={game_id};" + r = requests.get(cover_url, headers=headers, data=data).json() + poster_path = None + if r: + if "image_id" in r[0].keys(): + poster_path = "https://images.igdb.com/igdb/image/upload/t_cover_big/" + r[0]["image_id"] + ".jpg" + windows = False + mac = False + linux = False + if "windows" in info.keys(): + windows = True + if "mac" in info.keys(): + mac = True + if "linux" in info.keys(): + linux = True + game = ( + info["name"], + game_id, + description, + poster_path, + str(dir), + windows, + mac, + linux, + folder, + ) + games.append(game) + games_loaded.send("anonymous", games=games) + current_app.logger.info("finished loading game") + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + + +def update_games(): + current_app.logger.info("start updating game data") + for folder in sorted(os.listdir(GAMES_DIRECTORY), key=str.casefold): + root = pathlib.Path(GAMES_DIRECTORY, folder, "info.json") + update_game(root) + current_app.logger.info("finished updating game data") + + +def update_game(path: pathlib.Path): + try: + if path.name == "info.json" and path.exists(): + with path.open("r") as f: + info = json.load(f) + game_id = info["id"] + windows = False + mac = False + linux = False + if "windows" in info.keys(): + windows = True + if "mac" in info.keys(): + mac = True + if "linux" in info.keys(): + linux = True + database.update_game((game_id, windows, mac, linux)) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) diff --git a/scripts/imdb_import.py b/scripts/imdb_import.py index de463b3..7e98086 100644 --- a/scripts/imdb_import.py +++ b/scripts/imdb_import.py @@ -1,4 +1,6 @@ -import sqlite3, subprocess, os +import os +import sqlite3 +import subprocess RPI_IMDB_DATABASE = "/var/lib/rpiWebApp/" RPI_TSV_DIRECTORY = "/var/lib/imdb-rename/" @@ -15,19 +17,25 @@ CSV_DIRECTORY = RPI_CSV_DIRECTORY if os.path.exists(RPI_CSV_DIRECTORY) else MC_C def create_csv_files(): - print("start create csv") - subprocess.run(["xsv", "input", "-d", "\t", "--no-quoting", "{}title.akas.tsv".format(TSV_DIRECTORY), "-o", "{}title_akas.csv".format(CSV_DIRECTORY)]) - subprocess.run(["xsv", "input", "-d", "\t", "--no-quoting", "{}title.basics.tsv".format(TSV_DIRECTORY), "-o", "{}title_basics.csv".format(CSV_DIRECTORY)]) - subprocess.run(["xsv", "input", "-d", "\t", "--no-quoting", "{}title.episode.tsv".format(TSV_DIRECTORY), "-o", "{}title_episode.csv".format(CSV_DIRECTORY)]) - print("end create csv") + print("start create csv") + subprocess.run( + ["xsv", "input", "-d", "\t", "--no-quoting", "{}title.akas.tsv".format(TSV_DIRECTORY), "-o", "{}title_akas.csv".format(CSV_DIRECTORY)] + ) + subprocess.run( + ["xsv", "input", "-d", "\t", "--no-quoting", "{}title.basics.tsv".format(TSV_DIRECTORY), "-o", "{}title_basics.csv".format(CSV_DIRECTORY)] + ) + subprocess.run( + ["xsv", "input", "-d", "\t", "--no-quoting", "{}title.episode.tsv".format(TSV_DIRECTORY), "-o", "{}title_episode.csv".format(CSV_DIRECTORY)] + ) + print("end create csv") def import_csv_files(): - print("start import csv") - f = open("import_csv.sql").read() - sql_script = f.format(CSV_DIRECTORY) - subprocess.run(["sudo", "-u", "http", "sqlite3", IMDB_DATABASE+"imdb.db"], input=sql_script.encode("utf8")) - print("end import csv") + print("start import csv") + f = open("import_csv.sql").read() + sql_script = f.format(CSV_DIRECTORY) + subprocess.run(["sudo", "-u", "http", "sqlite3", IMDB_DATABASE + "imdb.db"], input=sql_script.encode("utf8")) + print("end import csv") create_csv_files() diff --git a/scripts/tmdb.py b/scripts/tmdb.py index e65f7ca..505006b 100644 --- a/scripts/tmdb.py +++ b/scripts/tmdb.py @@ -1,7 +1,8 @@ -from flask import current_app -import requests import inspect +import requests +from flask import current_app + API_KEY = "***REMOVED***" TMDB_FIND_URL = "https://api.themoviedb.org/3/find/" TMDB_GET_TV_URL = "https://api.themoviedb.org/3/tv/" @@ -10,83 +11,70 @@ TMDB_IMG_URL = "https://image.tmdb.org/t/p/original" def get_movie_data(imdb_id): - try: - data = { - "api_key": API_KEY, - "language": "en-US", - "external_source": "imdb_id" - } - r = requests.get(TMDB_FIND_URL+imdb_id, params=data) - info = dict(r.json()) - if "status_code" in info.keys(): - current_app.logger.info("error getting tmdb movie data, status code: "+str(info["status_code"])+" "+str(info["status_message"])) - return None - if info["movie_results"] == []: - current_app.logger.info("no tmdb results for: " + str(imdb_id)) - return None - current_app.logger.info("tmdb movie title: " + str(info["movie_results"][0]["title"])) - movie_id = info["movie_results"][0]["id"] - overview = info["movie_results"][0]["overview"] - poster_path = info["movie_results"][0]["poster_path"] - backdrop_path = info["movie_results"][0]["backdrop_path"] + try: + data = {"api_key": API_KEY, "language": "en-US", "external_source": "imdb_id"} + r = requests.get(TMDB_FIND_URL + imdb_id, params=data) + info = dict(r.json()) + if "status_code" in info.keys(): + current_app.logger.info("error getting tmdb movie data, status code: " + str(info["status_code"]) + " " + str(info["status_message"])) + return None + if info["movie_results"] == []: + current_app.logger.info("no tmdb results for: " + str(imdb_id)) + return None + current_app.logger.info("tmdb movie title: " + str(info["movie_results"][0]["title"])) + movie_id = info["movie_results"][0]["id"] + overview = info["movie_results"][0]["overview"] + poster_path = info["movie_results"][0]["poster_path"] + backdrop_path = info["movie_results"][0]["backdrop_path"] - return movie_id, overview, poster_path, backdrop_path - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return movie_id, overview, poster_path, backdrop_path + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def get_tv_show_data(imdb_id): - try: - data = { - "api_key": API_KEY, - "language": "en-US", - "external_source": "imdb_id" - } - r = requests.get(TMDB_FIND_URL+imdb_id, params=data) - info = dict(r.json()) - if "status_code" in info.keys(): - current_app.logger.info("error getting tmdb tv show data, status code: " + str(info["status_code"])+" "+str(info["status_message"])) - return None - if info["tv_results"] == []: - current_app.logger.info("no tmdb results for: " + str(imdb_id)) - return None - current_app.logger.info("tmdb tv show title: " + str(info["tv_results"][0]["name"])) - tv_show_id = info["tv_results"][0]["id"] - overview = info["tv_results"][0]["overview"] - poster_path = info["tv_results"][0]["poster_path"] + try: + data = {"api_key": API_KEY, "language": "en-US", "external_source": "imdb_id"} + r = requests.get(TMDB_FIND_URL + imdb_id, params=data) + info = dict(r.json()) + if "status_code" in info.keys(): + current_app.logger.info("error getting tmdb tv show data, status code: " + str(info["status_code"]) + " " + str(info["status_message"])) + return None + if info["tv_results"] == []: + current_app.logger.info("no tmdb results for: " + str(imdb_id)) + return None + current_app.logger.info("tmdb tv show title: " + str(info["tv_results"][0]["name"])) + tv_show_id = info["tv_results"][0]["id"] + overview = info["tv_results"][0]["overview"] + poster_path = info["tv_results"][0]["poster_path"] - return tv_show_id, overview, poster_path - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return tv_show_id, overview, poster_path + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) def get_tv_episode_data(imdb_id): - try: - data = { - "api_key": API_KEY, - "language": "en-US", - "external_source": "imdb_id" - } - r = requests.get(TMDB_FIND_URL+imdb_id, params=data) - episode_info = dict(r.json()) - if "status_code" in episode_info.keys(): - current_app.logger.info("error getting tmdb tv episode data, status code: " + str(episode_info["status_code"])+" "+str(episode_info["status_message"])) - return None - if episode_info["tv_episode_results"] == []: - current_app.logger.info("no tmdb results for: " + str(imdb_id)) - return None - data = { - "api_key": API_KEY, - "language": "en-US" - } - r = requests.get(TMDB_GET_TV_URL+str(episode_info["tv_episode_results"][0]["show_id"]), params=data) - show_name = dict(r.json())["name"] - current_app.logger.info("tmdb tv_episode title: " + show_name + ": " + str(episode_info["tv_episode_results"][0]["name"])) - tv_episode_id = episode_info["tv_episode_results"][0]["id"] - name = episode_info["tv_episode_results"][0]["name"] - overview = episode_info["tv_episode_results"][0]["overview"] - still_path = episode_info["tv_episode_results"][0]["still_path"] + try: + data = {"api_key": API_KEY, "language": "en-US", "external_source": "imdb_id"} + r = requests.get(TMDB_FIND_URL + imdb_id, params=data) + episode_info = dict(r.json()) + if "status_code" in episode_info.keys(): + current_app.logger.info( + "error getting tmdb tv episode data, status code: " + str(episode_info["status_code"]) + " " + str(episode_info["status_message"]) + ) + return None + if episode_info["tv_episode_results"] == []: + current_app.logger.info("no tmdb results for: " + str(imdb_id)) + return None + data = {"api_key": API_KEY, "language": "en-US"} + r = requests.get(TMDB_GET_TV_URL + str(episode_info["tv_episode_results"][0]["show_id"]), params=data) + show_name = dict(r.json())["name"] + current_app.logger.info("tmdb tv_episode title: " + show_name + ": " + str(episode_info["tv_episode_results"][0]["name"])) + tv_episode_id = episode_info["tv_episode_results"][0]["id"] + name = episode_info["tv_episode_results"][0]["name"] + overview = episode_info["tv_episode_results"][0]["overview"] + still_path = episode_info["tv_episode_results"][0]["still_path"] - return tv_episode_id, overview, still_path - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return tv_episode_id, overview, still_path + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) diff --git a/static/images/Seven Seas Entertainment.png b/static/images/Seven Seas Entertainment.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e010ac23feca7205bc1b93efa5f52e4d2bb2f6 GIT binary patch literal 21470 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 literal 0 HcmV?d00001 diff --git a/static/images/default.png b/static/images/default.png index 352dc8bc3fa2caf49a7b506803fc24feedd54540..7a3adac55d7dd64427213c3f783aebaccfd80604 100644 GIT binary patch literal 7026 zcmeHLdtA)v8vo67o6=~+*3_tmTw=QsrW;L}uB$?pP&US0#-t<^qtfN1+;#~=)DaG) z6kTb$V=EO~Qlm01A*pSxT+(^o-^^r&oR2+w{ycwZ=6&Di`&_=y<$Xu&Tn}fmnywm# zVPsbq)_e>jwqTf|8%Y^<LY>Z%F-&2}_66QS^OtX>b2o2Vz0Q9XJ!l(u6@696y44sK za{uMw#fMvrD2@Y*_Z&HuNIe|E<cswF_`RR%X0>ote#^%!7u8V$!%>X=^?C#S{#`1= zf4}Hq!b&P^<*uzS(}~ZmW;bL%%S}i*9Dn3>Mc1@_&17M=>iVO49}}mqy7zMGn2yV@ za)PyQ`Ez|7E{JB&eRoS3J>ArFO#8aCTVB;4JM#K@Ktq$-K2GzR#{O%{iYdB(MCuth zTyD2scwpOV{)}` Hq;#PrF3+6J3GeSFimyQaan^J-z|W|~^g+v$SV^0iGjPgZhR z9rq3MFNWSZedy-n+yvhXnOE4ed>8I>*_NaKkJMpz$m%T1A1kKq)8y*jU;cBXW>1V# z+2pldM&<LyI!&WZ`tjD?4(gWEMD1TRoN|xmWvDM@^^MeiM#|P+KYE<&7R}=Gykml@ zvu4v8+}YP0Mtr}h#AQOvh}E{E-c<FS*sOM?!RyG{IExmSVSVK@&$c^FY`jEOqTkhG zEbVa&ShOw6clURc>=935RH~Y1jSo)wfpfFXn7Zb8{*_ge?vAg$$u_Z^O(Y(lIOX?j zr6<ftOQ-Ie-F(MwDuk&bxAfidF)I8UVH>Mdr&V0;nI`(JJ|`-~{~*ahCu0Hm?Z|0k zqc6vvx3Jh$oIL*5Wrf>&AE>mp&a-l<^(&61tZ*6Q9II>cj=Ey+q$g#r-D+8nyttE6 zf8Jb?tW&R(W#ROwu-CMp>+1WNodKRZOkI+a%l~0^y6m7^y3d&v?-%_PaUnDHn1=O! zw`l#~KEu&HF3y}AS0=b0pWf;aXL2QUe#(`No6gng<y|^om)sEhjH+#V*Kc_H?C^Il z%B(FY?C}(h6PV^Pn=F%qleca@(R7%4yo7r9hBe>q1eNsM-ap<WSbOtNn*&pe%H!<} zpIYY+MDSm9XyrL`JkR}Rd);U%_S*LzJKr_%M5ONd*Ji&O-0kvI`gpQ7A=K^un;!oU z=|8{!C;OF2sMorNOTP$H9CROqxlql_JWIEzBn1j>cG|_*ofnG6_8CSDOwcJZTmNW^ zN2qbH`Rh#s>qD2k@o8+l-xb@JlP%nlP}C6o%If2i<}e#0uL6d#Xv^b;FIFwM-mtf6 z=1b}ZzDBaa;lO!eZYI}#UY>ZDRBPivHVgWYw6zF3qyIRh;DON*Xg1{)KRC0nnb>U1 zZlz08C+w(lT^0von5iN9M+iA%7Yv&!L9XtODvy*@Mp&6R#QJQ(FzutREc*q}h{C$f z$IU6sx^tpSw>>u{S*N%bt13n=+)=aR4t-_Nfmq=zJ3EmAhUJzU%mz*2G-z}NhS75g z7?Z1j@!Fv0(FTcwVHB7G_(m~I03?Yvq>_VBB>BI9A|wB$Raq$V5Bqk9zY3LQW$0MH z2`WeEAWeTADwhi>O>(iyIxJ^Z4l0|*L7M(L6h$(~inJuY&8i&qtHmQYd}IvP=9+e; zbYQ|*_0eFWEoXVyA8S+t08hWH(!c8=4FZet?;(SZ8zY<WC21qR^ft?G)r;;{hPWJ- zhA6nceMrOHD*tR`U*z(^NoHhz9xC#ZJMqeB>7V}$Es1x?B(OtiuBfD=*ru%G14F!+ zY0Hq1?TQw(T03ngS{ds)BR@2!oJ6b9x{J_CE#3@PSRL{xl8z4i*@Xnibux05WQXwg zD*6;aH;aKRZAWIfy$T?&sE?owdxW}2fI~qw1&IV9kxVjv=!6c-&>?ELjN4sT1C<eh zp!^a0?A<&Clt<X`vn^;r*bUNt5-7h~fUy6RVli4Fn*fRfQ2ZEUKKhHQVegtFG*A+* zjuy!By*6KU<oguqQ1FY`YJp-ENQVKB{NOiSTH;AzdmRz2bDM|~cdUag3GlTobr@)J z7$#B8d*39i2$e+!+0MeJi&P+?($$;+wvZ!q1DizT53>69o=_P+cr@e)8c?U$jhYfU ztl_EN2sn(HC~=r~2F$deBmfz_(9XF;@KfMUlaN@OX`za~^5P2B300!1nl&t{rBP@# zmkeI`jB0S+b=WbaKTrd#=&PrF92nUOF50h*+tf?&sQLcV21*P4UlaQKjqB0&T8+LF zdve|q1Yx1ZbRWw6CieP0Y>W{kxY)^!Q0gjTUlx(E#>`Ukih`CN9Z$^Zaw)mw*~`rP zFO_+%XuCN;f!7wbCL(Klkgfv5IB^4*9hTUGQuDAr7lq0?(=){*F%az^4nRDQtq&o< zsw^cy;gTy8%-rhJ840nI3`Hb}m=aroRxza)MN9gRPpUSlvOo1^s-hsq(Qd3d2^gIm zMSs}bLTvNUpJeafd&7#WFSs&Xk-_WMvP5uJ2@SzF@(sI>5sS&@&h(Xg6_<mTsi9@< zd}O&#QwTD}WYuKy)7sGrkcXHh-S>fTEnU86AS$F74$4|y%F+*_=}_v%=9Wb$&n&1j z8Z#&^tZ*R+X=y!0mUtb?pn={+0Ai!K=mWdJ$~mWk(#xURa}^%NsbvzuETvLX-1`su zx_2GZc%rpT8*Lk*?M!v}%FIweRxCo?(f)W`9MtfGgL3Y7%~&=PcBL1^smP@3d%Dts z3nzdl;<-j?VNPnjwx`SF(<anlSmJvL%seMeXloOSwEA_#8%fHk>RfAeR+b8qiFy!2 zL<OtJ5Oj8S;El&P4Puc>634|?DQ@ugb{!&is&UEtn3$!zKF-_vaTW;jvfFpQxvlvS zEHGDUyc;NEfI14yIdF0R#LMOQ^8TR@H6>_=w6Q%NtVUk#jxr4GA!BnVT>W0p03lMm zMxX6=0{(J~5sQ!Zad1g0<pFY<h14*EA^=Mff+?&Hsi6pE&J1y}!87o9WL7iA+oA$y z>G>lhkAy^?F9*7u*MUqS0{o)!4HPX8p4vzYZhzGg<S*XLii|nq$Ep)CuWoNuF<0Uz zz>JWKil1aKjApwq95l)#=hHi+duGyt|9ChS3=fHvxF9x(T$7~MD!T!ELInIPmK1?L zza8#<^wJh*t|(j?$0Mb3R{ne&u~RY?@%b5K>mlnq4@MqTyd2g~{q`m)cesNRJnxSW z#p7g2lZikal2p^6_Xvr?`NFNwpla*VbQCfDkt3AxxhXXwg^H8wX$0LUr3_<MipHS| z`DD}m$CIZOp!&a{_mguHzNHQKZO+Ns80=LsbhXxB@c7Fv{5(#%%pr<=#zC=4rBJkD z-lE<Q7R->$+Ez8@50ycjESUH1vr<)Mh!Y3pr}uFgNwx=M+5dvp#NC^YvSiXRba2w- z4C&=yGUU@J_J&>%V@?|C0QW@dnharyjvK1!7}x5j7YRc(?vN!}CCHhBnpMmyw7Y!q zL>b^^DRcTk6rhPqj}vVWqB%s*ZC|E8k?=Wiifs`LNJ(`9;m~E`mWDmvrO-5+@Z<Hi z9z<MZP(9*ajLEn?@n`uORqi!8BtGNL+-0sR1C-MZNv-X%RK<G^D|Vk1oI^l?XOIvp zsiO*Dh$3>tVRnS@6(tp+jP<D|%?)o$Ro{0=*6?X*yfQ>nOMAbr2CV7Hs#wE{v>yym zY}Tz?b-b(_;`WjjgE^yhYOFMYQU+1F@i>4NdTs!8IVvnE1o=jvxyw0H3@cg_O^`i@ zQ7%h#6j}d!alVk&2R-SN!)e+~X?E%}e-$T=$vt$$7w$nQ@o_7?;NmfjK?dvK0>TTX zbVqO=;x?Ivy-<~8tu$Q`PVIvfAU!O&m~e4;(g+cyI1-(O)9^DVTnx^<4Jj@Ad%~Y5 zs{st|NDTAbp+lU^+PWd_F4<|1NQr0M$@Q9CcwuysFbqqK*5llAeB)M7^r|GkzH1g% zRW+P3zSQRZ@a_ZbjAnA%k{ekd{_)}Qbt-+G;mdQ-8)wdlObg{_zZ6UzF(#h?Pqx16 zy=k5WLgwQQHYASG=++c%#db0{mb&-VgQMOjHlB41LC?aVF(>=I%NT9e<Zv1VJwpj_ z&BF^SR3YxCtTxx{>oid*nbT~Xc^gJabG7Jl0o>=t>m@Zv#%g<NXyOqjxZUJV^w}q^ z2g_f(W`xr+YSEhx<KEE#A*Mf!tvG!g%Xo*FE&~x%A%YW~mW1@_V)F4@;9gtWGCa?c zJ%6~;CH*5F&ZuI*UGFG6sB`gmap}`@cW2sGqEMu*ARVmujgRj4hb`#U4%v}-u3T^m z>E_N>0&A2e#KI}$w}AW|{AKz?yp0w&R=1^%R}KE7Y#tejK0>1VaZ&g0o@jxNvf+TB za4aj4?z;H5itv$%9*&BmYb=nmAolRxz+keaCz4><z+NP^X8IHKBky^6LJ0iI5fJA^ z_r~YJ)gUNY@{VuW3NjdDaiRgJ8D^Y!dr#nS5Uz_4Ot69<UA&e|!wg`6etqw?-h^$S zHKXI;K%vbW<cP4MIj?dX6u39<=FFPlf@*u{Ev*&&T>ywN{e54WkP}FOXR>2cqS~gy zbhv`G)-+ImXh+_hUaPJWm1DJBAD$E$b7?j`@r~w=*VGC7!-_d+I_d|qcJQA>ZzF}% z+QQ41E{Nr)REDjt$-QZz0a}u22Q2*NR55xf3Ek=pzbOn;>LQU61eDCN-FqnjJ^ia6 zE(_buP+n2r2aaac+Hlw$A1I!S8EuQ+8QJbOG>umQC*q1kfp^&Ia6lqdM$+h&<i>8R z7G{&tjSp70N-AtE48~~Kv@oC1Z)vFoXP2K7MTB;86~fOAi_+O@s8nqo=zm7Y>D5$Z zFrW8nS(GeY3olbjt3#AS`^KFRSID>-HHwG0?dOkM2Yv_4PaQLSv8b7^3<dRY=}Pp9 z?TTN%&jK=r>5~HdW?k<a9I$d#C7tbZwJEK~A+8`wOBDQXnRsOWO+_-J68IYIna_G+ zNFQXNs1v03`QLz|?niZvhX&>P<>mYNY#+2L2St&f4OIDqH2oJ)G#I3;;JT!fLK^GW zt;#}CB!jH{4Jb;ul$Al6<YN7*RavMUD}yxsb*LPnQkvvq{i;<t=>Lo7Km9C`BlK?; c5BGGDUt_b`e4q6*V18h(jvlOZ2fy%t1N?zP7ytkO literal 17826 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 No#SUUvyWN?{U5w;z4ibA diff --git a/templates/base.html b/templates/base.html index 135f6b2..f8a9c69 100644 --- a/templates/base.html +++ b/templates/base.html @@ -61,7 +61,7 @@ </div> <script> - var page_num = {{ page }}; + var page_num = {% if page %}{{ page }}{% else %}1{% endif %}; var max_items = {{ max_items }}; var item_count = {{ item_count }}; var start = max_items*(page_num-1); @@ -74,6 +74,10 @@ var page_container = document.getElementById("page-container"); function go_to_page(pagenumber) { + let url = new URL(document.location) + url.searchParams.set("page", pagenumber) + window.history.replaceState(null, null, url.toString()) + {#document.location.search = search#} page_num = pagenumber; start = max_items*(page_num-1); if (item_count < max_items*page_num) { @@ -126,6 +130,7 @@ } } } + window.scrollTo(0, 0) } function offset_page(i) { diff --git a/tv_movies/templates/tv_movies/episodeViewer.html b/tv_movies/templates/tv_movies/episodeViewer.html index 39d6b5b..18b8078 100644 --- a/tv_movies/templates/tv_movies/episodeViewer.html +++ b/tv_movies/templates/tv_movies/episodeViewer.html @@ -1,11 +1,13 @@ {% extends "base.html" %} {% block content %} - <div class="container" style="text-align: center"> + <div class="w3-row" style="text-align: center"> + <div class="w3-col s1 w3-hide-small m1 l2"><p></p></div> + <div class="w3-col s12 m10 l8"> - <video id="player" class="video-js vjs-big-play-centered" style="display: inline-block" controls preload="auto" width="1100" + <video id="player" class="video-js vjs-big-play-centered vjs-16-9" style="display: inline-block; width:100%" controls preload="auto" poster="https://image.tmdb.org/t/p/original{{ episode.still_path }}" data-setup="{}"> - <source src="{{ url_for("tv_movies.index") }}/get_episode/{{ episode.imdb_id }}" type="video/webm"> + <source src="{{ url_for("tv_movies.index") }}/get_episode/{{ episode.tmdb_id }}" type="video/webm"> <p class='vjs-no-js'> To view this video please enable JavaScript, and consider upgrading to a web browser that <a href='https://videojs.com/html5-video-support/' target='_blank'>supports HTML5 video</a> @@ -32,7 +34,7 @@ {% endif %} {% endfor %} <p style="text-align: left">{{ episode.description }}</p> - <a class="btn btn-primary" href="{{ url_for("tv_movies.index") }}/get_episode/{{ episode.imdb_id }}" download="{{ episode.title }}">Download</a> + <a class="btn btn-primary" href="{{ url_for("tv_movies.index") }}/get_episode/{{ episode.tmdb_id }}" download="{{ episode.title }}">Download</a> {% with %} {% set seasons = [] %} @@ -63,7 +65,7 @@ <div class="w3-display-topleft w3-container w3-round" style="background: rgba(105,105,105,0.61);color: white">Episode {{ episode.episode }}</div> <div class="w3-display-bottommiddle w3-container w3-round" style="background: rgba(105,105,105,0.61);color: white">{{ episode.title }}</div> {% for data in user_tv_show_data %} - {% if data.imdb_id == episode.imdb_id and data.finished %} + {% if data.tmdb_id == episode.tmdb_id and data.finished %} <div class="w3-display-topright w3-container" style="background: rgba(105,105,105,0.61); border-radius: 5px 0 0 5px"> <img src="/static/svg/verified.svg" > </div> @@ -71,7 +73,7 @@ {% endfor %} </div> {% for data in user_tv_show_data %} - {% if data.imdb_id == episode.imdb_id and data.time != 0 and data.length != 0 %} + {% if data.tmdb_id == episode.tmdb_id and data.time != 0 and data.length != 0 %} <div class="w3-light-gray"> <div class="w3-red" style="height: 5px; width: {{ (data.time/data.length)*100 }}%"></div> </div> @@ -83,6 +85,8 @@ </form> </div> </div> + <div class="w3-col s1 w3-hide-small m1 l2"><p></p></div> + </div> {% endblock %} @@ -133,7 +137,7 @@ //length = myPlayer.duration(); let oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); - oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ episode.imdb_id }}?time="+(time+5)+"&parent={{ episode.parent_imdb_id }}&length="+length+"&finished="+finished); + oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ episode.tmdb_id }}?time="+(time+5)+"&parent={{ episode.parent_tmdb_id }}&length="+length+"&finished="+finished); oReq.send(); time = myPlayer.currentTime(); } @@ -143,7 +147,7 @@ //length = myPlayer.duration(); let oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); - oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ episode.imdb_id }}?time="+(time+5)+"&parent={{ episode.parent_imdb_id }}&length="+length+"&finished="+finished); + oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ episode.tmdb_id }}?time="+(time+5)+"&parent={{ episode.parent_tmdb_id }}&length="+length+"&finished="+finished); oReq.send(); time = myPlayer.currentTime(); }); diff --git a/tv_movies/templates/tv_movies/index.html b/tv_movies/templates/tv_movies/index.html index cf7cf67..0e6a231 100644 --- a/tv_movies/templates/tv_movies/index.html +++ b/tv_movies/templates/tv_movies/index.html @@ -16,8 +16,8 @@ <div style="text-align: center"> {% include "pagination.html" %} </div> - <div class="container col-10"> - <div id="page-container" class="row justify-content-start"></div> + <div class="w3-container"> + <div id="page-container" style="display: flow-root; text-align: center"></div> </div> <div style="text-align: center"> {% include "pagination.html" %} @@ -34,28 +34,42 @@ function populate_page() { page_container.innerHTML = ""; for (i = start;i < end; i++) { - var anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}">`; + let badge = "" + var anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}">`; if (tv_shows[i][0].extended && tv_shows[i][0].directors_cut) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?extended=True&directors_cut=True">`; + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?extended=True&directors_cut=True">`; } else if (tv_shows[i][0].extended) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?extended=True">`; + badge = `<div class="w3-display-topleft w3-container" style="color: white; background: rgba(105,105,105,0.61); border-radius: 0 5px 5px 0"> + extended + </div>` + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?extended=True">`; } else if (tv_shows[i][0].directors_cut) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?directors_cut=True">`; + badge = `<div class="w3-display-topleft w3-container" style="color: white; background: rgba(105,105,105,0.61); border-radius: 0 5px 5px 0"> + director's cut + </div>` + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?directors_cut=True">`; } + var finished = ``; if (tv_shows[i][1]) { finished = `<div class="w3-display-topright w3-container" style="background: rgba(105,105,105,0.61); border-radius: 5px 0 0 5px"> <img src="/static/svg/verified.svg" > </div>` } - var list_element = `<div class="col-3" style="padding: 10px"> + + var text = "" + if (!tv_shows[i][0].poster_path) { + text = `<div class='w3-display-middle w3-container' style="color: white; text-align: center;font-size: 20px; width: 100%"> +${tv_shows[i][0].title} (${tv_shows[i][0].year})</div>` + } + + var list_element = `<div class="w3-display-container w3-round" style="width: 350px; display: inline-block; padding: 10px"> ${anchor} <div class="card w3-display-container"> - <img class="card-img" src="https://image.tmdb.org/t/p/original${tv_shows[i][0].poster_path}" alt="" onerror="this.src='/static/images/default.png'"> - <div class="card-body"> - ${tv_shows[i][0].title} (${tv_shows[i][0].year}) - </div> + <img class="card-img" src="https://image.tmdb.org/t/p/original${tv_shows[i][0].poster_path}" alt="${tv_shows[i][0].title} (${tv_shows[i][0].year})" title="${tv_shows[i][0].title} (${tv_shows[i][0].year})" onerror="this.src='/static/images/default.png'"> + ${text} ${finished} + ${badge} </div> </a> </div>`; diff --git a/tv_movies/templates/tv_movies/movieViewer.html b/tv_movies/templates/tv_movies/movieViewer.html index b320ae9..e102fe8 100644 --- a/tv_movies/templates/tv_movies/movieViewer.html +++ b/tv_movies/templates/tv_movies/movieViewer.html @@ -1,17 +1,19 @@ {% extends "base.html" %} {% block content %} - <div class="container" style="text-align: center"> - <video id="player" class="video-js vjs-big-play-centered" style="display: inline-block" controls preload="auto" width="1100" + <div class="w3-row" style="text-align: center"> + <div class="w3-col s1 w3-hide-small m1 l2"><p></p></div> + <div class="w3-col s12 m10 l8"> + <video id="player" class="video-js vjs-big-play-centered vjs-16-9" style="display: inline-block" controls preload="auto" poster="https://image.tmdb.org/t/p/original{{ movie.backdrop_path }}" data-setup="{}"> {% if movie.extended and movie.directors_cut %} - <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.imdb_id }}?extended=True&directors_cut=True" type="video/webm"> + <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.tmdb_id }}?extended=True&directors_cut=True" type="video/webm"> {% elif movie.extended %} - <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.imdb_id }}?extended=True" type="video/webm"> + <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.tmdb_id }}?extended=True" type="video/webm"> {% elif movie.directors_cut %} - <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.imdb_id }}?directors_cut=True" type="video/webm"> + <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.tmdb_id }}?directors_cut=True" type="video/webm"> {% else %} - <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.imdb_id }}" type="video/webm"> + <source src="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.tmdb_id }}" type="video/webm"> {% endif %} <p class='vjs-no-js'> To view this video please enable JavaScript, and consider upgrading to a web browser that @@ -20,7 +22,9 @@ </video> <h1>{{ movie.title }}</h1> <p style="text-align: left">{{ movie.description }}</p> - <a class="btn btn-primary" href="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.imdb_id }}{% if movie.extended == 1 %}/extended{% endif %}{% if movie.directors_cut==1 %}/directors_cut{% endif %}" download="{{ movie.title }}">Download</a> + <a class="btn btn-primary" href="{{ url_for("tv_movies.index") }}/get_movie/{{ movie.tmdb_id }}{% if movie.extended == 1 %}/extended{% endif %}{% if movie.directors_cut==1 %}/directors_cut{% endif %}" download="{{ movie.title }}">Download</a> + </div> + <div class="w3-col s1 w3-hide-small m1 l2"><p></p></div> </div> {% endblock %} @@ -60,7 +64,7 @@ length = myPlayer.duration(); let oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); - oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ movie.imdb_id }}?time="+(time)+"&length="+length+"&finished="+finished{% if movie.extended %}+"&extended=True"{% endif %}{% if movie.directors_cut %}+"&directors_cut=True"{% endif %}); + oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ movie.tmdb_id }}?time="+(time)+"&length="+length+"&finished="+finished{% if movie.extended %}+"&extended=True"{% endif %}{% if movie.directors_cut %}+"&directors_cut=True"{% endif %}); oReq.send(); time = myPlayer.currentTime(); } @@ -70,7 +74,7 @@ length = myPlayer.duration(); let oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); - oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ movie.imdb_id }}?time="+(time)+"&length="+length+"&finished="+finished{% if movie.extended %}+"&extended=True"{% endif %}{% if movie.directors_cut %}+"&directors_cut=True"{% endif %}); + oReq.open("POST", "https://rpi.narnian.us/tv_movies/{{ movie.tmdb_id }}?time="+(time)+"&length="+length+"&finished="+finished{% if movie.extended %}+"&extended=True"{% endif %}{% if movie.directors_cut %}+"&directors_cut=True"{% endif %}); oReq.send(); time = myPlayer.currentTime(); }); diff --git a/tv_movies/templates/tv_movies/search.html b/tv_movies/templates/tv_movies/search.html index 74f1207..8ce5778 100644 --- a/tv_movies/templates/tv_movies/search.html +++ b/tv_movies/templates/tv_movies/search.html @@ -16,9 +16,9 @@ <div style="text-align: center"> {% include "pagination.html" %} </div> - <div class="container col-10"> + <div class="w3-container"> {% if movies != [] %} - <div id="page-container" class="row justify-content-start"></div> + <div id="page-container" style="display: flow-root; text-align: center"></div> {% else %} <h1>No results.</h1> {% endif %} @@ -38,28 +38,42 @@ function populate_page() { page_container.innerHTML = ""; for (i = start;i < end; i++) { - var anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}">`; + let badge = "" + var anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}">`; if (tv_shows[i][0].extended && tv_shows[i][0].directors_cut) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?extended=True&directors_cut=True">`; + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?extended=True&directors_cut=True">`; } else if (tv_shows[i][0].extended) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?extended=True">`; + badge = `<div class="w3-display-topleft w3-container" style="color: white; background: rgba(105,105,105,0.61); border-radius: 0 5px 5px 0"> + extended + </div>` + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?extended=True">`; } else if (tv_shows[i][0].directors_cut) { - anchor = `<a href="/tv_movies/${tv_shows[i][0].imdb_id}?directors_cut=True">`; + badge = `<div class="w3-display-topleft w3-container" style="color: white; background: rgba(105,105,105,0.61); border-radius: 0 5px 5px 0"> + director's cut + </div>` + anchor = `<a href="/tv_movies/${tv_shows[i][0].tmdb_id}?directors_cut=True">`; } + var finished = ``; if (tv_shows[i][1]) { finished = `<div class="w3-display-topright w3-container" style="background: rgba(105,105,105,0.61); border-radius: 5px 0 0 5px"> <img src="/static/svg/verified.svg" > </div>` } - var list_element = `<div class="col-3" style="padding: 10px"> + + var text = "" + if (!tv_shows[i][0].poster_path) { + text = `<div class='w3-display-middle w3-container' style="color: white; text-align: center;font-size: 20px; width: 100%"> +${tv_shows[i][0].title} (${tv_shows[i][0].year})</div>` + } + + var list_element = `<div class="w3-display-container w3-round" style="width: 350px; display: inline-block; padding: 10px"> ${anchor} <div class="card w3-display-container"> - <img class="card-img" src="https://image.tmdb.org/t/p/original${tv_shows[i][0].poster_path}" alt="" onerror="this.src='/static/images/default.png'"> - <div class="card-body"> - ${tv_shows[i][0].title} (${tv_shows[i][0].year}) - </div> + <img class="card-img" src="https://image.tmdb.org/t/p/original${tv_shows[i][0].poster_path}" alt="${tv_shows[i][0].title} (${tv_shows[i][0].year})" title="${tv_shows[i][0].title} (${tv_shows[i][0].year})" onerror="this.src='/static/images/default.png'"> + ${text} ${finished} + ${badge} </div> </a> </div>`; diff --git a/tv_movies/tv_movies.py b/tv_movies/tv_movies.py index e308f0a..ed3d97b 100644 --- a/tv_movies/tv_movies.py +++ b/tv_movies/tv_movies.py @@ -1,9 +1,10 @@ -from flask import Blueprint, render_template, request, make_response, send_from_directory, current_app +import datetime +import inspect + +from flask import Blueprint, current_app, make_response, render_template, request, send_from_directory from flask_login import login_required from scripts import database, func -import inspect -import datetime TV_Movies = Blueprint("tv_movies", __name__, template_folder="templates") @@ -11,154 +12,169 @@ TV_Movies = Blueprint("tv_movies", __name__, template_folder="templates") @TV_Movies.route("/tv_movies") @login_required def index(): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - tv = database.get_all_tv_movies() - start = (max_items * (page - 1)) - end = len(tv) if len(tv) < max_items * page else max_items * page - tv_dict = [] - for tv_item in tv: - item = tv_item[0].__dict__ - item.pop('_sa_instance_state', None) - item.pop('path', None) - tv_dict.append((item, tv_item[1])) - return render_template("tv_movies/index.html", title="tv & movies", tv_shows=tv_dict, page=page, max_items=max_items, start=start, end=end, item_count=len(tv)) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e))+" "+str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + tv = database.get_all_tv_movies() + start = max_items * (page - 1) + end = len(tv) if len(tv) < max_items * page else max_items * page + tv_dict = [] + for tv_item in tv: + item = tv_item[0].__dict__ + item.pop("_sa_instance_state", None) + item.pop("path", None) + tv_dict.append((item, tv_item[1])) + return render_template( + "tv_movies/index.html", title="tv & movies", tv_shows=tv_dict, page=page, max_items=max_items, start=start, end=end, item_count=len(tv) + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @TV_Movies.route("/tv_movies/search") @login_required def search(): - try: - page = request.args.get("page", 1, type=int) - max_items = request.args.get("max_items", 30, type=int) - start = 0 - end = 0 - query = request.args.get("q") - tv = [] - if query: - tv = database.db_search_tv_movie(query) - start = (max_items * (page - 1)) - end = len(tv) if len(tv) < max_items * page else max_items * page - tv_dict = [] - for tv_item in tv: - item = tv_item[0].__dict__ - item.pop('_sa_instance_state', None) - item.pop('path', None) - tv_dict.append((item, tv_item[1])) - return render_template("tv_movies/search.html", title="tv_movies", tv_shows=tv_dict, page=page, max_items=max_items, - start=start, end=end, item_count=len(tv)) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e))+" "+str(e) + try: + page = request.args.get("page", 1, type=int) + max_items = request.args.get("max_items", 30, type=int) + start = 0 + end = 0 + query = request.args.get("q") + tv = [] + if query: + tv = database.db_search_tv_movie(query) + start = max_items * (page - 1) + end = len(tv) if len(tv) < max_items * page else max_items * page + tv_dict = [] + for tv_item in tv: + item = tv_item[0].__dict__ + item.pop("_sa_instance_state", None) + item.pop("path", None) + tv_dict.append((item, tv_item[1])) + return render_template( + "tv_movies/search.html", title="tv_movies", tv_shows=tv_dict, page=page, max_items=max_items, start=start, end=end, item_count=len(tv) + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) -@TV_Movies.route("/tv_movies/<imdb_id>", methods=["GET", "POST"]) +@TV_Movies.route("/tv_movies/<tmdb_id>", methods=["GET", "POST"]) @login_required -def tv_movie_viewer(imdb_id): - try: - tv_movie = database.get_tv_movie_by_imdb_id(imdb_id) - if type(tv_movie) is database.Movie: - extended = request.args.get("extended", default=False, type=bool) - directors_cut = request.args.get("directors_cut", default=False, type=bool) - return movie_view(imdb_id, extended=extended, directors_cut=directors_cut) - else: - return episode_viewer(imdb_id) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e))+" "+str(e) +def tv_movie_viewer(tmdb_id): + try: + tv_movie = database.get_tv_movie_by_tmdb_id(tmdb_id) + if type(tv_movie) is database.Movie: + extended = request.args.get("extended", default=False, type=bool) + directors_cut = request.args.get("directors_cut", default=False, type=bool) + return movie_view(tmdb_id, extended=extended, directors_cut=directors_cut) + else: + return episode_viewer(tmdb_id) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @login_required -def movie_view(imdb_id, extended=False, directors_cut=False): - try: - if request.method == "POST": - time = int(request.args.get("time", type=float)) - length = int(request.args.get("length", type=float, default=0)) - finished = True if request.args.get("finished", default="false") == "true" else False - database.update_user_tv_movie_data(imdb_id, None, time, length, finished, extended, directors_cut) - return make_response("", 201) - else: - movie_data = database.db_get_movie_by_imdb_id(imdb_id, extended=extended, directors_cut=directors_cut) - user_data = database.db_get_user_tv_movie_data(movie_data.imdb_id, extended=extended, directors_cut=directors_cut) - chapters = func.get_chapters(movie_data.path) - if not user_data: - user_data = database.update_user_tv_movie_data(movie_data.imdb_id, None, 0, 0) - return render_template("tv_movies/movieViewer.html", title="Movies: " + movie_data.title, movie=movie_data, user_data=user_data, chapters=chapters) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) +def movie_view(tmdb_id, extended=False, directors_cut=False): + try: + if request.method == "POST": + time = int(request.args.get("time", type=float)) + length = int(request.args.get("length", type=float, default=0)) + finished = True if request.args.get("finished", default="false") == "true" else False + database.update_user_tv_movie_data(tmdb_id, None, time, length, finished, extended, directors_cut) + return make_response("", 201) + else: + movie_data = database.db_get_movie_by_tmdb_id(tmdb_id, extended=extended, directors_cut=directors_cut) + user_data = database.db_get_user_tv_movie_data(movie_data.tmdb_id, extended=extended, directors_cut=directors_cut) + chapters = func.get_chapters(movie_data.path) + if not user_data: + user_data = database.update_user_tv_movie_data(movie_data.tmdb_id, None, 0, 0) + return render_template( + "tv_movies/movieViewer.html", title="Movies: " + movie_data.title, movie=movie_data, user_data=user_data, chapters=chapters + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) @login_required -def episode_viewer(imdb_id): - try: - if request.method == "POST": - time = int(request.args.get("time", type=float)) - parent_id = request.args.get("parent", type=str) - length = int(request.args.get("length", type=float, default=0)) - finished = True if request.args.get("finished", default="false") == "true" else False - database.update_user_tv_movie_data(imdb_id, parent_id, time, length, finished) - return make_response("", 201) - else: - tv_show = database.get_tv_show(imdb_id) - episodes = database.get_tv_show_episodes_by_imdb_id(imdb_id) - season_num = request.args.get("season", type=int, default=None) - episode_num = request.args.get("episode", type=int, default=None) - user_tv_show_data = database.db_get_user_tv_show_episodes_data(imdb_id) - if not season_num and not episode_num: - (current_episode, user_data) = database.db_get_current_tv_show_episode_and_data(imdb_id, episodes) - season_num = current_episode.season - episode_num = current_episode.episode - chapters = func.get_chapters(current_episode.path) - if not user_data: - user_data = database.update_user_tv_movie_data(current_episode.imdb_id, imdb_id, 0, 0) - else: - current_episode = episodes[0] - user_data = database.UserTvMovieData(("", "", "", 0, 0, False, datetime.datetime.min)) - for episode in episodes: - if episode.season == season_num and episode.episode == episode_num: - current_episode = episode - user_data = database.db_get_user_tv_movie_data(current_episode.imdb_id) - if not user_data: - user_data = database.update_user_tv_movie_data(current_episode.imdb_id, imdb_id, 0, 0) - break - else: - for episode in episodes: - if episode.season == season_num: - current_episode = episode - episode_num = episode.episode - user_data = database.db_get_user_tv_movie_data(current_episode.imdb_id) - if not user_data: - user_data = database.update_user_tv_movie_data(current_episode.imdb_id, imdb_id, 0, 0) - break - chapters = func.get_chapters(current_episode.path) - return render_template("tv_movies/episodeViewer.html", title="Tv: " + tv_show.title, episodes=episodes, season_num=season_num, episode_num=episode_num, episode=current_episode, user_data=user_data, user_tv_show_data=user_tv_show_data, chapters=chapters) - except Exception as e: - current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) - return str(type(e)) + " " + str(e) +def episode_viewer(tmdb_id): + try: + if request.method == "POST": + time = int(request.args.get("time", type=float)) + parent_id = request.args.get("parent", type=str) + length = int(request.args.get("length", type=float, default=0)) + finished = True if request.args.get("finished", default="false") == "true" else False + database.update_user_tv_movie_data(tmdb_id, parent_id, time, length, finished) + return make_response("", 201) + else: + tv_show = database.get_tv_show(tmdb_id) + episodes = database.get_tv_show_episodes_by_tmdb_id(tmdb_id) + season_num = request.args.get("season", type=int, default=None) + episode_num = request.args.get("episode", type=int, default=None) + user_tv_show_data = database.db_get_user_tv_show_episodes_data(tmdb_id) + if not season_num and not episode_num: + (current_episode, user_data) = database.db_get_current_tv_show_episode_and_data(tmdb_id, episodes) + season_num = current_episode.season + episode_num = current_episode.episode + chapters = func.get_chapters(current_episode.path) + if not user_data: + user_data = database.update_user_tv_movie_data(current_episode.tmdb_id, tmdb_id, 0, 0) + else: + current_episode = episodes[0] + user_data = database.UserTvMovieData(("", "", "", 0, 0, False, datetime.datetime.min, False, False)) + for episode in episodes: + if episode.season == season_num and episode.episode == episode_num: + current_episode = episode + user_data = database.db_get_user_tv_movie_data(current_episode.tmdb_id) + if not user_data: + user_data = database.update_user_tv_movie_data(current_episode.tmdb_id, tmdb_id, 0, 0) + break + else: + for episode in episodes: + if episode.season == season_num: + current_episode = episode + episode_num = episode.episode + user_data = database.db_get_user_tv_movie_data(current_episode.tmdb_id) + if not user_data: + user_data = database.update_user_tv_movie_data(current_episode.tmdb_id, tmdb_id, 0, 0) + break + chapters = func.get_chapters(current_episode.path) + return render_template( + "tv_movies/episodeViewer.html", + title="Tv: " + tv_show.title, + episodes=episodes, + season_num=season_num, + episode_num=episode_num, + episode=current_episode, + user_data=user_data, + user_tv_show_data=user_tv_show_data, + chapters=chapters, + ) + except Exception as e: + current_app.logger.info(inspect.stack()[0][3] + " " + str(type(e)) + " " + str(e)) + return str(type(e)) + " " + str(e) -@TV_Movies.route("/tv_movies/get_movie/<imdb_id>") +@TV_Movies.route("/tv_movies/get_movie/<tmdb_id>") @login_required -def get_movie(imdb_id): - extended = request.args.get("extended", default=False, type=bool) - directors_cut = request.args.get("directors_cut", default=False, type=bool) - movie_data = database.db_get_movie_by_imdb_id(imdb_id, extended=extended, directors_cut=directors_cut) - filename = movie_data.path.replace(func.MOVIES_DIRECTORY, "") - response = make_response(send_from_directory(func.MOVIES_DIRECTORY, filename)) - response.headers["content-type"] = "video/webm" - return response +def get_movie(tmdb_id): + extended = request.args.get("extended", default=False, type=bool) + directors_cut = request.args.get("directors_cut", default=False, type=bool) + movie_data = database.db_get_movie_by_tmdb_id(tmdb_id, extended=extended, directors_cut=directors_cut) + filename = movie_data.path.replace(func.MOVIES_DIRECTORY, "") + response = make_response(send_from_directory(func.MOVIES_DIRECTORY, filename)) + response.headers["content-type"] = "video/webm" + return response -@TV_Movies.route("/tv_movies/get_episode/<imdb_id>") +@TV_Movies.route("/tv_movies/get_episode/<tmdb_id>") @login_required -def get_episode(imdb_id): - episode_data = database.db_get_episode_by_imdb_id(imdb_id) - filename = episode_data.path.replace(func.TV_SHOWS_DIRECTORY, "") - response = make_response(send_from_directory(func.TV_SHOWS_DIRECTORY, filename)) - response.headers["content-type"] = "video/webm" - return response +def get_episode(tmdb_id): + episode_data = database.db_get_episode_by_tmdb_id(tmdb_id) + filename = episode_data.path.replace(func.TV_SHOWS_DIRECTORY, "") + response = make_response(send_from_directory(func.TV_SHOWS_DIRECTORY, filename)) + response.headers["content-type"] = "video/webm" + return response