From 9d54df53c8cba36d27028a1cb7f9f32e65c28f1b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 5 Aug 2023 17:42:54 -0400 Subject: [PATCH] feat: add theme completion report (#130) --- .github/workflows/CI.yml | 7 + .github/workflows/localize.yml | 75 ++++ .gitignore | 4 + Contents/Code/__init__.py | 30 +- Contents/Code/constants.py | 27 ++ Contents/Code/default_prefs.py | 6 +- Contents/Code/plex_api_helper.py | 91 ++++- Contents/Code/webapp.py | 340 ++++++++++++++++++ Contents/Code/youtube_dl_helper.py | 2 - Contents/DefaultPrefs.json | 37 ++ Contents/Resources/favicon.ico | Bin 114227 -> 0 bytes Contents/Resources/icon-default.png | Bin 15514 -> 8281 bytes Contents/Resources/web/css/custom.css | 165 +++++++++ Contents/Resources/web/images/favicon.ico | Bin 0 -> 109762 bytes Contents/Resources/web/images/icon.png | Bin 0 -> 8281 bytes Contents/Resources/web/js/translations.js | 32 ++ Contents/Resources/web/templates/base.html | 47 +++ Contents/Resources/web/templates/home.html | 128 +++++++ Contents/Resources/web/templates/navbar.html | 64 ++++ .../Resources/web/templates/translations.html | 1 + Contents/Strings/themerr-plex.po | 64 ++++ Dockerfile | 9 + README.rst | 2 +- crowdin.yml | 22 ++ docs/source/about/usage.rst | 65 ++++ docs/source/code_docs/webapp.rst | 9 + docs/source/conf.py | 4 +- docs/source/contributing/build.rst | 20 ++ docs/source/contributing/testing.rst | 2 +- docs/source/toc.rst | 1 + package.json | 8 + requirements.txt | 4 + scripts/_locale.py | 123 +++++++ scripts/babel.cfg | 2 + tests/conftest.py | 16 + tests/functional/__init__.py | 0 tests/functional/test_webapp.py | 41 +++ 37 files changed, 1407 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/localize.yml create mode 100644 Contents/Code/webapp.py delete mode 100644 Contents/Resources/favicon.ico create mode 100644 Contents/Resources/web/css/custom.css create mode 100644 Contents/Resources/web/images/favicon.ico create mode 100644 Contents/Resources/web/images/icon.png create mode 100644 Contents/Resources/web/js/translations.js create mode 100644 Contents/Resources/web/templates/base.html create mode 100644 Contents/Resources/web/templates/home.html create mode 100644 Contents/Resources/web/templates/navbar.html create mode 100644 Contents/Resources/web/templates/translations.html create mode 100644 Contents/Strings/themerr-plex.po create mode 100644 crowdin.yml create mode 100644 docs/source/code_docs/webapp.rst create mode 100644 package.json create mode 100644 scripts/_locale.py create mode 100644 scripts/babel.cfg create mode 100644 tests/functional/__init__.py create mode 100644 tests/functional/test_webapp.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 69be6381..c7ddeeb8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -59,6 +59,12 @@ jobs: python -m pip install --upgrade --target=./Contents/Libraries/Shared -r \ requirements.txt --no-warn-script-location + - name: Install npm packages + working-directory: Themerr-plex.bundle + run: | + npm install + mv ./node_modules ./Contents/Resources/web + - name: Build plist working-directory: Themerr-plex.bundle env: @@ -89,6 +95,7 @@ jobs: !**/Themerr-plex.bundle/Dockerfile !**/Themerr-plex.bundle/docs !**/Themerr-plex.bundle/scripts + !**/Themerr-plex.bundle/tests - name: Package Release shell: bash diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml new file mode 100644 index 00000000..5bd3480d --- /dev/null +++ b/.github/workflows/localize.yml @@ -0,0 +1,75 @@ +--- +name: localize + +on: + push: + branches: [nightly] + paths: # prevents workflow from running unless these files change + - '.github/workflows/localize.yml' + - 'Contents/Strings/Themerr-plex.po' + - 'Contents/Code/**.py' + - 'Contents/Resources/web/templates/**' + workflow_dispatch: + +jobs: + localize: + name: Update Localization + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Python 2.7 + uses: LizardByte/.github/actions/setup_python2@nightly + + - name: Set up Python Dependencies + run: | + python -m pip install --upgrade pip setuptools + python -m pip install -r requirements.txt + + - name: Update Strings + run: | + python ./scripts/_locale.py --extract + + - name: git diff + run: | + # disable the pager + git config --global pager.diff false + + # print the git diff + git diff Contents/Scripts/plugger.po + + # set the variable with minimal output, replacing `\t` with ` ` + OUTPUT=$(git diff --numstat Contents/Strings/themerr-plex.po | sed -e "s#\t# #g") + echo "git_diff=${OUTPUT}" >> $GITHUB_ENV + + - name: git reset + if: ${{ env.git_diff == '1 1 Contents/Scripts/plugger.po' }} # only run if more than 1 line changed + run: | + git reset --hard + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Create/Update Pull Request + uses: peter-evans/create-pull-request@v4 + with: + add-paths: | + Contents/Scripts/*.po + token: ${{ secrets.GH_BOT_TOKEN }} # must trigger PR tests + commit-message: New localization template + branch: localize/update + delete-branch: true + base: nightly + title: New Babel Updates + body: | + Update report + - Updated ${{ steps.date.outputs.date }} + - Auto-generated by [create-pull-request][1] + + [1]: https://github.com/peter-evans/create-pull-request + labels: | + babel + l10n diff --git a/.gitignore b/.gitignore index da380237..0eaec38e 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,7 @@ plexhints-temp # Remove python modules Contents/Libraries/Shared/ + +# npm +node_modules/ +package-lock.json diff --git a/Contents/Code/__init__.py b/Contents/Code/__init__.py index 30be7a3c..7b53bc7b 100644 --- a/Contents/Code/__init__.py +++ b/Contents/Code/__init__.py @@ -2,7 +2,6 @@ # standard imports import re -import sys # plex debugging try: @@ -27,16 +26,11 @@ from typing import Optional # local imports -if sys.version_info.major < 3: - from default_prefs import default_prefs - from constants import issue_url_games, issue_url_movies - from plex_api_helper import add_themes, get_plex_item, plex_listener - from youtube_dl_helper import process_youtube -else: - from .default_prefs import default_prefs - from .constants import issue_url_games, issue_url_movies - from .plex_api_helper import add_themes, get_plex_item, plex_listener - from .youtube_dl_helper import process_youtube +from default_prefs import default_prefs +from constants import contributes_to, issue_url_games, issue_url_movies +from plex_api_helper import add_themes, get_plex_item, plex_listener +from youtube_dl_helper import process_youtube +from webapp import start_server def ValidatePrefs(): @@ -112,7 +106,7 @@ def Start(): for more information. First preferences are validated using the ``ValidatePrefs()`` method. Then the ``plex_api_helper.plex_listener()`` - method is started to handle updating theme songs for the new Plex Movie agent. + method is started to handle updating theme songs for the new Plex Movie agent. Finally, the web server is started. Examples -------- @@ -124,12 +118,13 @@ def Start(): if prefs_valid.header == 'Error': Log.Warn('Themerr-plex plug-in preferences are not valid.') - Log.Debug('Themerr-plex plug-in started.') - # start watching plex plex_listener() Log.Debug('plex_listener started, watching for activity from new Plex Movie agent.') + start_server() # start the web server if it is not running + Log.Debug('plug-in started.') + @handler(prefix='/music/themerr-plex', name='Themerr-plex', thumb='attribution.png') def main(): @@ -194,12 +189,7 @@ class Themerr(Agent.Movies): primary_provider = False fallback_agent = False accepts_from = [] - contributes_to = [ - 'com.plexapp.agents.imdb', - 'com.plexapp.agents.themoviedb', - # 'com.plexapp.agents.thetvdb', # not available as movie agent - 'dev.lizardbyte.retroarcher-plex' - ] + contributes_to = contributes_to @staticmethod def search(results, media, lang, manual): diff --git a/Contents/Code/constants.py b/Contents/Code/constants.py index 55b28439..79cc983a 100644 --- a/Contents/Code/constants.py +++ b/Contents/Code/constants.py @@ -1,3 +1,30 @@ +# -*- coding: utf-8 -*- + +# standard imports +import os + +# plex debugging +try: + import plexhints # noqa: F401 +except ImportError: + pass +else: # the code is running outside of Plex + from plexhints.core_kit import Core # core kit + +app_support_directory = Core.app_support_path +plugin_identifier = 'dev.lizardbyte.themerr-plex' +plugin_support_directory = os.path.join(app_support_directory, 'Plug-in Support') +plugin_support_data_directory = os.path.join(plugin_support_directory, 'Data') +themerr_data_directory = os.path.join(plugin_support_data_directory, plugin_identifier, 'DataItems') + +contributes_to = [ + 'tv.plex.agents.movie', + 'com.plexapp.agents.imdb', + 'com.plexapp.agents.themoviedb', + # 'com.plexapp.agents.thetvdb', # not available as movie agent + 'dev.lizardbyte.retroarcher-plex' +] + guid_map = dict( imdb='imdb', tmdb='themoviedb', diff --git a/Contents/Code/default_prefs.py b/Contents/Code/default_prefs.py index 33ae7200..b597425d 100644 --- a/Contents/Code/default_prefs.py +++ b/Contents/Code/default_prefs.py @@ -5,5 +5,9 @@ int_plexapi_upload_retries_max='3', int_plexapi_upload_threads='3', str_youtube_user='', - str_youtube_passwd='' + str_youtube_passwd='', + enum_locale='en', + str_http_host='0.0.0.0', + int_http_port='9494', + bool_log_werkzeug_messages='False' ) diff --git a/Contents/Code/plex_api_helper.py b/Contents/Code/plex_api_helper.py index 2108fb8b..03dd4e1d 100644 --- a/Contents/Code/plex_api_helper.py +++ b/Contents/Code/plex_api_helper.py @@ -4,7 +4,6 @@ import hashlib import os import shutil -import sys import time import threading @@ -14,7 +13,6 @@ except ImportError: pass else: # the code is running outside of Plex - from plexhints.core_kit import Core # core kit from plexhints.log_kit import Log # log kit from plexhints.parse_kit import JSON # parse kit from plexhints.prefs_kit import Prefs # prefs kit @@ -22,7 +20,7 @@ # imports from Libraries\Shared from future.moves import queue import requests -from typing import Optional +from typing import Optional, Tuple import urllib3 from urllib3.exceptions import InsecureRequestWarning from plexapi.alert import AlertListener @@ -31,12 +29,8 @@ from plexapi.utils import reverseSearchType # local imports -if sys.version_info.major < 3: - from constants import guid_map, issue_url_movies - from youtube_dl_helper import process_youtube -else: - from .constants import guid_map, issue_url_movies - from .youtube_dl_helper import process_youtube +from constants import app_support_directory, guid_map, issue_url_movies +from youtube_dl_helper import process_youtube plex = None @@ -45,7 +39,6 @@ processing_completed = [] # constants -app_support_directory = Core.app_support_path metadata_movie_directory = os.path.join(app_support_directory, 'Metadata', 'Movies') @@ -141,6 +134,35 @@ def add_themes(rating_key, theme_files=None, theme_urls=None): return uploaded +def get_theme_upload_path(plex_item): + # type: (any) -> str + """ + Get the path to the theme upload directory. + + Get the hashed path of the theme upload directory for the item specified by the ``plex_item``. + + Parameters + ---------- + plex_item : any + The item to get the theme upload path for. + + Returns + ------- + str + The path to the theme upload directory. + + Examples + -------- + >>> get_theme_upload_path(plex_item=...) + "...bundle/Uploads/themes..." + """ + guid = plex_item.guid + full_hash = hashlib.sha1(guid).hexdigest() + theme_upload_path = os.path.join( + metadata_movie_directory, full_hash[0], full_hash[1:] + '.bundle', 'Uploads', 'themes') + return theme_upload_path + + def remove_uploaded_themes(plex_item): # type: (any) -> None """ @@ -163,10 +185,7 @@ def remove_uploaded_themes(plex_item): >>> remove_uploaded_themes(plex_item=...) ... """ - guid = plex_item.guid - full_hash = hashlib.sha1(guid).hexdigest() - theme_upload_path = os.path.join( - metadata_movie_directory, full_hash[0], full_hash[1:] + '.bundle', 'Uploads', 'themes') + theme_upload_path = get_theme_upload_path(plex_item=plex_item) if os.path.isdir(theme_upload_path): shutil.rmtree(path=theme_upload_path, ignore_errors=True, onerror=remove_uploaded_themes_error_handler) @@ -234,6 +253,50 @@ def upload_theme(plex_item, filepath=None, url=None): return False +def get_database_id(item): + # type: (any) -> Tuple[Optional[str], Optional[str]] + agent = None + database_id = None + imdb_id = None # if this gets set we need to do additional processing + + if item.guids: # guids is a blank list for items from legacy agents, only available for new agent items + agent = 'tv.plex.agents.movie' + for guid in item.guids: + split_guid = guid.id.split('://') + database = guid_map[split_guid[0]] + database_id = split_guid[1] + + if database == 'igdb': + imdb_id = database_id + database_id = None + + if database == 'themoviedb': + imdb_id = None # reset this as we won't need to process it + break + elif item.guid: + split_guid = item.guid.split('://') + agent = split_guid[0] + if agent == 'dev.lizardbyte.retroarcher-plex': + # dev.lizardbyte.retroarcher-plex://{igdb-1638}{platform-4}{(USA)}?lang=en + database_id = item.guid.split('igdb-')[1].split('}')[0] + elif agent == 'com.plexapp.agents.themoviedb': + # com.plexapp.agents.themoviedb://363088?lang=en + database_id = item.guid.split('://')[1].split('?')[0] + elif agent == 'com.plexapp.agents.imdb': + # com.plexapp.agents.imdb://tt0113189?lang=en + imdb_id = item.guid.split('://')[1].split('?')[0] + + if imdb_id: + themerr_url = 'https://app.lizardbyte.dev/ThemerrDB/%s/%s/%s.json' % ('movies', 'imdb', imdb_id) + themerr_response = requests.get(url=themerr_url) + + if themerr_response.status_code == requests.codes.ok: + themerr_json = themerr_response.json() + database_id = themerr_json['id'] + + return agent, database_id + + def get_plex_item(rating_key): # type: (int) -> any """ diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py new file mode 100644 index 00000000..4e2f7b8c --- /dev/null +++ b/Contents/Code/webapp.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- + +# future imports +from __future__ import division # fix float division for python2 + +# standard imports +import json +import logging +import os +from threading import Thread + +# plex debugging +try: + import plexhints # noqa: F401 +except ImportError: + pass +else: # the code is running outside of Plex + from plexhints.core_kit import Core # core kit + from plexhints.log_kit import Log # log kit + from plexhints.prefs_kit import Prefs # prefs kit + +# lib imports +import flask +from flask import Flask, Response, render_template, request, send_from_directory +from flask_babel import Babel +import polib +from werkzeug.utils import secure_filename + +# local imports +from constants import contributes_to, issue_url_games, issue_url_movies, plugin_identifier +from plex_api_helper import get_database_id, get_theme_upload_path, setup_plexapi + +bundle_path = Core.bundle_path +if bundle_path.endswith('test.bundle'): + # use current directory instead, to allow for testing outside of Plex + bundle_path = os.getcwd() + +# setup flask app +app = Flask( + import_name=__name__, + root_path=os.path.join(bundle_path, 'Contents', 'Resources', 'web'), + static_folder=os.path.join(bundle_path, 'Contents', 'Resources', 'web'), + template_folder=os.path.join(bundle_path, 'Contents', 'Resources', 'web', 'templates') + ) + +# remove extra lines rendered jinja templates +app.jinja_env.trim_blocks = True +app.jinja_env.lstrip_blocks = True + +# localization +babel = Babel( + app=app, + default_locale='en', + default_timezone='UTC', + default_domain='themerr-plex', + configure_jinja=True +) + +app.config['BABEL_TRANSLATION_DIRECTORIES'] = os.path.join(bundle_path, 'Contents', 'Strings') + +# setup logging for flask +Log.Info('Adding flask log handlers to plex plugin logger') + +# get the plugin logger +plugin_logger = logging.getLogger(plugin_identifier) + +# replace the app.logger handlers with the plugin logger handlers +app.logger.handlers = plugin_logger.handlers +app.logger.setLevel(plugin_logger.level) + +# test message +app.logger.info('flask app logger test message') + +try: + Prefs['bool_log_werkzeug_messages'] +except KeyError: + # this fails when building docs + pass +else: + if Prefs['bool_log_werkzeug_messages']: + # get the werkzeug logger + werkzeug_logger = logging.getLogger('werkzeug') + + # replace the werkzeug logger handlers with the plugin logger handlers + werkzeug_logger.handlers = plugin_logger.handlers + + # use the same log level as the plugin logger + werkzeug_logger.setLevel(plugin_logger.level) + + # test message + werkzeug_logger.info('werkzeug logger test message') + + +# mime type map +mime_type_map = { + 'gif': 'image/gif', + 'ico': 'image/vnd.microsoft.icon', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'svg': 'image/svg+xml', +} + + +@babel.localeselector +def get_locale(): + # type: () -> str + """ + Get the locale from the config. + + Get the locale specified in the config. This does not need to be called as it is done so automatically by `babel`. + + Returns + ------- + str + The locale. + + See Also + -------- + pyra.locales.get_locale : Use this function instead. + + Examples + -------- + >>> get_locale() + en + """ + return Prefs['enum_locale'] + + +def start_server(): + # use threading to start the flask app... or else web server seems to be killed after a couple of minutes + flask_thread = Thread( + target=app.run, + kwargs=dict( + host=Prefs['str_http_host'], + port=Prefs['int_http_port'], + debug=False, + use_reloader=False # reloader doesn't work when running in a separate thread + ) + ) + + # start flask application + flask_thread.start() + + +def stop_server(): + # stop flask server + # todo - this doesn't work + request.environ.get('werkzeug.server.shutdown') + + +@app.route('/', methods=["GET"]) +@app.route('/home', methods=["GET"]) +def home(): + # type: () -> render_template + """ + Serve the webapp home page. + + This page serves the Themerr completion report for supported Plex libraries. + + Returns + ------- + render_template + The rendered page. + + Notes + ----- + The following routes trigger this function. + + - `/` + - `/home` + + Examples + -------- + >>> home() + """ + # get all Plex items from supported metadata agents + plex_server = setup_plexapi() + plex_library = plex_server.library + + sections = plex_library.sections() + + items = dict() + + for section in sections: + if section.agent not in contributes_to: + # todo - there is a small chance that a library with an unsupported agent could still have + # a individual items that was matched with a supported agent... + continue # skip unsupported metadata agents + + # get all items in the section + all_items = section.all() + + # get all items in the section with theme songs + items_with_themes = section.all(theme__exists=True) + + # add each section to the items dict + items[section.key] = dict( + title=section.title, + agent=section.agent, + items=[], + percent_complete=int(len(items_with_themes) / len(all_items) * 100) if len(items_with_themes) else 0 + ) + + for item in all_items: + # build the issue url + database_info = get_database_id(item=item) + item_agent = database_info[0] + database_id = database_info[1] + + item_issue_url = None + if item_agent == 'dev.lizardbyte.retroarcher-plex': + issue_url = issue_url_games + elif item_agent in contributes_to: + issue_url = issue_url_movies + else: + issue_url = None + + if issue_url: + issue_title = '%s (%s)' % (item.title, item.year) + item_issue_url = issue_url % (issue_title, database_id) + + theme_status = 'missing' # default status + issue_action = 'add' # default action + + if item.theme: + theme_status = 'complete' + issue_action = 'edit' + + theme_upload_path = get_theme_upload_path(plex_item=item) + if not os.path.isdir(theme_upload_path) or not os.listdir(theme_upload_path): + theme_status = 'error' + issue_action = 'add' + + items[section.key]['items'].append(dict( + title=item.title, + agent=item_agent, + database_id=database_id, + issue_action=issue_action, + issue_url=item_issue_url, + theme=True if item.theme else False, + theme_status=theme_status, + year=item.year, + )) + + return render_template('home.html', title='Home', items=items) + + +@app.route("/", methods=["GET"]) +def image(img): + # type: (str) -> flask.send_from_directory + """ + Get image from static/images directory. + + Returns + ------- + flask.send_from_directory + The image. + + Notes + ----- + The following routes trigger this function. + + - `/favicon.ico` + + Examples + -------- + >>> image('favicon.ico') + """ + directory = os.path.join(app.static_folder, 'images') + filename = os.path.basename(secure_filename(filename=img)) # sanitize the input + + if os.path.isfile(os.path.join(directory, filename)): + file_extension = filename.rsplit('.', 1)[-1] + if file_extension in mime_type_map: + return send_from_directory(directory=directory, filename=filename, mimetype=mime_type_map[file_extension]) + else: + return Response(response='Invalid file type', status=400, mimetype='text/plain') + else: + return Response(response='Image not found', status=404, mimetype='text/plain') + + +@app.route('/status', methods=["GET"]) +def status(): + # type: () -> dict + """ + Check the status of Themerr-plex. + + This can be used to test if the plugin is still running. It could be used as part of a healthcheck for Docker, + and may have many other uses in the future. + + Returns + ------- + dict + A dictionary of the status. + + Examples + -------- + >>> status() + """ + web_status = {'result': 'success', 'message': 'Ok'} + return web_status + + +@app.route("/translations", methods=["GET"]) +def translations(): + # type: () -> Response + """ + Serve the translations. + + Returns + ------- + Response + The translations. + + Examples + -------- + >>> translations() + """ + locale = get_locale() + + po_files = [ + '%s/%s/LC_MESSAGES/plugger.po' % (app.config['BABEL_TRANSLATION_DIRECTORIES'], locale), # selected locale + '%s/plugger.po' % app.config['BABEL_TRANSLATION_DIRECTORIES'], # fallback to default domain + ] + + for po_file in po_files: + if os.path.isfile(po_file): + po = polib.pofile(po_file) + + # convert the po to json + data = dict() + for entry in po: + if entry.msgid: + data[entry.msgid] = entry.msgstr + Log.Debug('Translation: %s -> %s' % (entry.msgid, entry.msgstr)) + + return Response(response=json.dumps(data), + status=200, + mimetype='application/json') diff --git a/Contents/Code/youtube_dl_helper.py b/Contents/Code/youtube_dl_helper.py index 7a51d2e7..ef803038 100644 --- a/Contents/Code/youtube_dl_helper.py +++ b/Contents/Code/youtube_dl_helper.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -# standard imports - # plex debugging try: import plexhints # noqa: F401 diff --git a/Contents/DefaultPrefs.json b/Contents/DefaultPrefs.json index 8ac594af..f496a37a 100644 --- a/Contents/DefaultPrefs.json +++ b/Contents/DefaultPrefs.json @@ -48,5 +48,42 @@ "default": "", "option": "hidden", "secure": "true" + }, + { + "id": "enum_locale", + "type": "enum", + "label": "Web UI Locale", + "default": "en", + "values": [ + "de", + "en", + "en_GB", + "en_US", + "es", + "fr", + "it", + "ru" + ] + }, + { + "id": "str_http_host", + "type": "text", + "label": "Web UI Host Address (requires Plex Media Server restart)", + "default": "0.0.0.0", + "secure": "false" + }, + { + "id": "int_http_port", + "type": "text", + "label": "Web UI Port (requires Plex Media Server restart)", + "default": "9494", + "secure": "false" + }, + { + "id": "bool_log_werkzeug_messages", + "type": "bool", + "label": "Log all web server messages (requires Plex Media Server restart)", + "default": "False", + "secure": "false" } ] diff --git a/Contents/Resources/favicon.ico b/Contents/Resources/favicon.ico deleted file mode 100644 index 79620bf5b76dbc9bd4c49dd68c0e9fecac911f2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114227 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 z 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 F{}1*5D6;?n literal 15514 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%0A?ieL=tpYG7j3ByQ2)Yj5auFkD!-x1lX;X?)v>(1 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 BSycc4 literal 0 HcmV?d00001 diff --git a/Contents/Resources/web/images/icon.png b/Contents/Resources/web/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3d0061848234e8e5b3c060ac104316e0632685a6 GIT binary patch literal 8281 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 F{}1*5D6;?n literal 0 HcmV?d00001 diff --git a/Contents/Resources/web/js/translations.js b/Contents/Resources/web/js/translations.js new file mode 100644 index 00000000..fe1a239b --- /dev/null +++ b/Contents/Resources/web/js/translations.js @@ -0,0 +1,32 @@ +let translations = null + +let getTranslation = function(string) { + // download translations + if (translations === null) { + $.ajax({ + async: false, + url: "/translations/", + type: "GET", + dataType: "json", + success: function (result) { + translations = result + } + }) + } + + if (translations) { + try { + if (translations[string]) { + return translations[string] + } else { + return string + } + } catch (err) { + return string + } + } + else { + // could not download translations + return string + } +} diff --git a/Contents/Resources/web/templates/base.html b/Contents/Resources/web/templates/base.html new file mode 100644 index 00000000..7a0a6ad2 --- /dev/null +++ b/Contents/Resources/web/templates/base.html @@ -0,0 +1,47 @@ + + + + {% if title %} + Themerr-plex - {{ title }} + {% else %} + Themerr-plex + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% block modals %}{% endblock %} +
+ + {% include 'navbar.html' %} + + + {% block content %}{% endblock %} +
+ {% block scripts %}{% endblock %} + + + diff --git a/Contents/Resources/web/templates/home.html b/Contents/Resources/web/templates/home.html new file mode 100644 index 00000000..197bb41b --- /dev/null +++ b/Contents/Resources/web/templates/home.html @@ -0,0 +1,128 @@ +{% extends 'base.html' %} +{% block modals %} +{% endblock modals %} + +{% block content %} +
+
+ + {# constants #} + {% set title_column_width = 'col-8' %} + {% set year_column_width = 'col-2' %} + {% set contribute_column_width = 'col-2' %} + + {% for section in items %} + +
+
+
+ +

{{ items[section]['title'] }}

+ +

+
+
+ + + {% if items[section]['percent_complete'] == 100 %} + {% set progress_bar_color = 'bg-success' %} + {% elif items[section]['percent_complete'] > 50 %} + {% set progress_bar_color = 'bg-warning' %} + {% else %} + {% set progress_bar_color = 'bg-danger' %} + {% endif %} +
+
+
+
+ {{ items[section]['percent_complete'] }}% +
+ +
+
+
+
+
+ + +
+
+
+ + + + + + + + + {% for item in items[section]['items'] %} + {% if item['theme_status'] == 'complete' %} + {% set theme_status_color = 'bg-success' %} + {% elif item['theme_status'] == 'missing' %} + {% set theme_status_color = 'bg-danger' %} + {% else %} + {% set theme_status_color = 'bg-danger' %} + {% endif %} + + {% if item['issue_action'] == 'add' %} + {% set contribute_button_color = 'btn-warning' %} + {% set contribute_button_text = _('Add') %} + {% elif item['issue_action'] == 'edit' %} + {% set contribute_button_color = 'btn-info' %} + {% set contribute_button_text = _('Edit') %} + {% else %} + {% set contribute_button_color = 'btn-danger' %} + {% endif %} + + + + + + {% endfor %} + +
{{ items[section]['title'] }}
{{ _('Title') }}{{ _('Year') }}{{ _('Contribute') }}
{{ item['title'] }}{{ item['year'] }} + {% if item['issue_action'] %} + {{ contribute_button_text }} + {% endif %} +
+
+
+
+ + +
+ {% endfor %} + +
+
+{% endblock content %} + +{% block scripts %} + + +{% endblock scripts %} diff --git a/Contents/Resources/web/templates/navbar.html b/Contents/Resources/web/templates/navbar.html new file mode 100644 index 00000000..d29ef6a9 --- /dev/null +++ b/Contents/Resources/web/templates/navbar.html @@ -0,0 +1,64 @@ + diff --git a/Contents/Resources/web/templates/translations.html b/Contents/Resources/web/templates/translations.html new file mode 100644 index 00000000..516f9176 --- /dev/null +++ b/Contents/Resources/web/templates/translations.html @@ -0,0 +1 @@ +{# This template is used to extract translations used in javascript files #} diff --git a/Contents/Strings/themerr-plex.po b/Contents/Strings/themerr-plex.po new file mode 100644 index 00000000..f69b5a21 --- /dev/null +++ b/Contents/Strings/themerr-plex.po @@ -0,0 +1,64 @@ +# Translations template for Themerr-plex. +# Copyright (C) 2023 Themerr-plex +# This file is distributed under the same license as the Themerr-plex +# project. +# FIRST AUTHOR , 2023. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Themerr-plex v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-plex\n" +"POT-Creation-Date: 2023-08-05 13:12-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.1\n" + +#: Contents/Resources/web/templates/home.html:65 +msgid "Title" +msgstr "" + +#: Contents/Resources/web/templates/home.html:66 +msgid "Year" +msgstr "" + +#: Contents/Resources/web/templates/home.html:67 +msgid "Contribute" +msgstr "" + +#: Contents/Resources/web/templates/home.html:81 +msgid "Add" +msgstr "" + +#: Contents/Resources/web/templates/home.html:84 +msgid "Edit" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:23 +msgid "Donate" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:28 +msgid "GitHub Sponsors" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:44 +msgid "Support" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:49 +msgid "Docs" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:53 +msgid "Support Center" +msgstr "" + +#: Contents/Resources/web/templates/navbar.html:59 +msgid "Home" +msgstr "" + diff --git a/Dockerfile b/Dockerfile index 5400ceed..bdcb9513 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ RUN <<_DEPS set -e apt-get update -y apt-get install -y --no-install-recommends \ + npm=8.5.* \ python2=2.7.18* \ python-pip=20.3.4* apt-get clean @@ -48,6 +49,14 @@ python2 -m pip --no-python-version-warning --disable-pip-version-check install - python2 ./scripts/build_plist.py _BUILD +# setup npm and dependencies +RUN <<_NPM +#!/bin/bash +set -e +npm install +mv ./node_modules ./Contents/Resources/web +_NPM + # clean RUN <<_CLEAN #!/bin/bash diff --git a/README.rst b/README.rst index 6bd387fd..5a2c076f 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ Themerr-plex is a metadata agent plug-in for Plex Media Player. The plug-in adds This plugin contributes to the following metadata agents. - - Plex Movie + - Plex Movie - `tv.plex.agents.movie` - Plex Movie (Legacy) - `com.plexapp.agents.imdb` - The Movie Database - `com.plexapp.agents.themoviedb` - `RetroArcher `_ - `dev.lizardbyte.retroarcher-plex` diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..e9165198 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,22 @@ +--- +"base_path": "." +"base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) +"preserve_hierarchy": false # flatten tree on crowdin +"pull_request_labels": [ + "crowdin", + "l10n" +] + +"files": [ + { + "source": "/Contents/Strings/*.po", + "translation": "/Contents/Strings/%two_letters_code%/LC_MESSAGES/%original_file_name%", + "languages_mapping": { + "two_letters_code": { + # map non-two letter codes here, left side is crowdin designation, right side is babel designation + "en-GB": "en_GB", + "en-US": "en_US" + } + } + } +] diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index 70b780d8..f4adee06 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -22,6 +22,34 @@ Minimal setup is required to use Themerr-plex. In addition to the installation, .. Attention:: It may take several minutes after completing a metadata refresh for a theme song to be available. +Web UI +------ + +A web interface is provided by the plugin. Currently the web ui only provides a couple of end points. + +/ +^ + +This endpoint will display a report showing the theme song status for each item in a library supported by Themerr-plex. +A supported library is any that has the default agent as one supported by Themerr-plex. + +The report provides an easy means to contribute to `ThemerrDB `_ by providing +`Add/Edit` buttons for items that can be added to ThemerrDB. + +/status +^^^^^^^ + +An endpoint that provides a JSON response. If a valid response is returned, Themerr-plex is running. + +**Example Response** + +.. code-block:: json + + { + "message":"Ok", + "result":"success" + } + Preferences ----------- @@ -103,3 +131,40 @@ Description Default None + +Web UI Locale +^^^^^^^^^^^^^ + +Description + The localization value to use for translations. + +Default + ``en`` + +Web UI Host Address +^^^^^^^^^^^^^^^^^^^ + +Description + The host address to bind the Web UI to. + +Default + ``0.0.0.0`` + +Web UI Port +^^^^^^^^^^^ + +Description + The port to bind the Web UI to. + +Default + ``9494`` + +Log all web server messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Description + If set to ``True``, all web server messages will be logged. This will include logging requests and status codes when + requesting any resource. It is recommended to keep this disabled unless debugging. + +Default + ``False`` diff --git a/docs/source/code_docs/webapp.rst b/docs/source/code_docs/webapp.rst new file mode 100644 index 00000000..1b5893d2 --- /dev/null +++ b/docs/source/code_docs/webapp.rst @@ -0,0 +1,9 @@ +:github_url: https://github.com/LizardByte/Themerr-plex/tree/nightly/Contents/Code/webapp.py + +.. include:: ../global.rst + +:modname:`webapp` +---------------------------- +.. automodule:: Code.webapp + :members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index c2c6a65c..74148f9e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,8 +72,8 @@ # -- Options for HTML output ------------------------------------------------- # images -html_favicon = os.path.join(root_dir, 'Contents', 'Resources', 'favicon.ico') -html_logo = os.path.join(root_dir, 'Contents', 'Resources', 'attribution.png') +html_favicon = os.path.join(root_dir, 'Contents', 'Resources', 'web', 'images', 'favicon.ico') +html_logo = os.path.join(root_dir, 'Contents', 'Resources', 'icon-default.png') # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/source/contributing/build.rst b/docs/source/contributing/build.rst index 1334c980..20d01130 100644 --- a/docs/source/contributing/build.rst +++ b/docs/source/contributing/build.rst @@ -36,6 +36,26 @@ Build Plist python ./scripts/build_plist.py +npm dependencies +---------------- +Install nodejs and npm. Downloads available `here `_. + +Install npm dependencies. + .. code-block:: bash + + npm install + +Move modules directory. + Linux/macOS + .. code-block:: bash + + mv ./node_modules ./Contents/Resources/web + + Windows + .. code-block:: batch + + move .\node_modules .\Contents\Resources\web + Remote Build ------------ It may be beneficial to build remotely in some cases. This will enable easier building on different operating systems. diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index 45b74946..0e415d96 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -5,7 +5,7 @@ Testing Flake8 ------ -Themerr-plex uses `Flake8 `_ for enforcing consistent code styling. Flake is included +Themerr-plex uses `Flake8 `_ for enforcing consistent code styling. Flake8 is included in the ``requirements-dev.txt``. The config file for flake8 is ``.flake8``. This is already included in the root of the repo and should not be modified. diff --git a/docs/source/toc.rst b/docs/source/toc.rst index 38f5c908..96d1765e 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -25,4 +25,5 @@ code_docs/main code_docs/plex_api_helper + code_docs/webapp code_docs/youtube_dl_helper diff --git a/package.json b/package.json new file mode 100644 index 00000000..5412b30c --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@fontsource/open-sans": "4.5.13", + "@fortawesome/fontawesome-free": "6.2.0", + "bootstrap": "5.2.2", + "jquery": "3.7.0" + } +} diff --git a/requirements.txt b/requirements.txt index ea494ae1..e628e77f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ # these requirements must support python 2.7 # it is doubtful that Plex will ever update to Python 3+ +flask==1.1.4;python_version<"3" +flask-babel==2.0.0;python_version<"3" future==0.18.3 +polib==1.2.0;python_version<"3" requests==2.27.1;python_version<"3" # 2.27 is last version supporting Python 2.7 typing==3.10.0.0 +werkzeug==1.0.1;python_version<"3" # youtube_dl is not capable or willing to create a new release so have to install from git # youtube_dl==2021.12.17 diff --git a/scripts/_locale.py b/scripts/_locale.py new file mode 100644 index 00000000..af1c4460 --- /dev/null +++ b/scripts/_locale.py @@ -0,0 +1,123 @@ +# coding=utf-8 +""" +.. + _locale.py + +Functions related to building, initializing, updating, and compiling localization translations. +""" +# standard imports +import argparse +import os +import subprocess + +project_name = 'Themerr-plex' + +script_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.dirname(script_dir) +locale_dir = os.path.join(root_dir, 'Contents', 'Strings') + +# target locales +target_locales = [ + 'de', # Deutsch + 'en', # English + 'en_GB', # English (United Kingdom) + 'en_US', # English (United States) + 'es', # español + 'fr', # français + 'it', # italiano + 'ru', # русский +] + + +def babel_extract(): + """Executes `pybabel extract` in subprocess.""" + commands = [ + 'pybabel', + 'extract', + '-F', os.path.join(script_dir, 'babel.cfg'), + '-o', os.path.join(locale_dir, '%s.po' % project_name.lower()), + '--sort-by-file', + '--msgid-bugs-address=github.com/%s' % project_name.lower(), + '--copyright-holder=%s' % project_name, + '--project=%s' % project_name, + '--version=v0', + '--add-comments=NOTE', + './Contents/Resources/web' + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_init(locale_code): + # type: (str) -> None + """Executes `pybabel init` in subprocess. + + :param locale_code: str - locale code + """ + commands = [ + 'pybabel', + 'init', + '-i', os.path.join(locale_dir, '%s.po' % project_name.lower()), + '-d', locale_dir, + '-D', project_name.lower(), + '-l', locale_code + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_update(): + """Executes `pybabel update` in subprocess.""" + commands = [ + 'pybabel', + 'update', + '-i', os.path.join(locale_dir, '%s.po' % project_name.lower()), + '-d', locale_dir, + '-D', project_name.lower(), + '--update-header-comment' + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_compile(): + """Executes `pybabel compile` in subprocess.""" + commands = [ + 'pybabel', + 'compile', + '-d', locale_dir, + '-D', project_name.lower() + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +if __name__ == '__main__': + # Set up and gather command line arguments + parser = argparse.ArgumentParser( + description='Script helps update locale translations. Translations must be done manually.') + + parser.add_argument('--extract', action='store_true', help='Extract messages from python files and templates.') + parser.add_argument('--init', action='store_true', help='Initialize any new locales specified in target locales.') + parser.add_argument('--update', action='store_true', help='Update existing locales.') + parser.add_argument('--compile', action='store_true', help='Compile translated locales.') + + args = parser.parse_args() + + if args.extract: + babel_extract() + + if args.init: + for locale_id in target_locales: + if not os.path.isdir(os.path.join(locale_dir, locale_id)): + babel_init(locale_code=locale_id) + + if args.update: + babel_update() + + if args.compile: + babel_compile() diff --git a/scripts/babel.cfg b/scripts/babel.cfg new file mode 100644 index 00000000..759e805a --- /dev/null +++ b/scripts/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] +[jinja2: **/templates/**.html] diff --git a/tests/conftest.py b/tests/conftest.py index dbdff526..100b5ccb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ # local imports from Code import Themerr + from Code import webapp else: raise Exception('Contents directory not found') @@ -20,3 +21,18 @@ def agent(): # type: () -> Agent return Themerr() + + +@pytest.fixture +def test_client(scope='function'): + """Create a test client for testing webapp endpoints""" + app = webapp.app + app.config['TESTING'] = True + + client = app.test_client() + + # Create a test client using the Flask application configured for testing + with client as test_client: + # Establish an application context + with app.app_context(): + yield test_client # this is where the testing happens! diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/test_webapp.py b/tests/functional/test_webapp.py new file mode 100644 index 00000000..87a8ded6 --- /dev/null +++ b/tests/functional/test_webapp.py @@ -0,0 +1,41 @@ +# lib imports +import pytest + + +def test_home(test_client): + """ + WHEN the '/' page is requested (GET) + THEN check that the response is valid + + Repeat for '/home' + """ + try: + response = test_client.get('/') + except AttributeError: + pytest.skip("cannot access Plex token/server") + else: + assert response.status_code == 200 + + response = test_client.get('/home') + assert response.status_code == 200 + + +def test_favicon(test_client): + """ + WHEN the '/favicon.ico' file is requested (GET) + THEN check that the response is valid + THEN check the content type is 'image/vnd.microsoft.icon' + """ + response = test_client.get('favicon.ico') + assert response.status_code == 200 + assert response.content_type == 'image/vnd.microsoft.icon' + + +def test_status(test_client): + """ + WHEN the '/status' page is requested (GET) + THEN check that the response is valid + """ + response = test_client.get('/status') + assert response.status_code == 200 + assert response.content_type == 'application/json'