From d08710684dd1b377465b47f28122a000636f1e55 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Wed, 26 Jan 2022 02:51:40 +0000 Subject: [PATCH] Add screenshot resolution checking --- app/blueprints/admin/actions.py | 20 +++++-- app/blueprints/todo/__init__.py | 11 +++- app/flatpages/help/featured.md | 2 +- app/logic/screenshots.py | 8 +++ app/models/packages.py | 24 ++++++++ app/tasks/importtasks.py | 5 ++ app/templates/packages/screenshot_new.html | 4 ++ app/templates/packages/screenshots.html | 34 ++++++++--- app/templates/todo/user.html | 65 ++++++++++++++++++++-- app/utils/image.py | 24 ++++++++ migrations/versions/f6ef5f35abca_.py | 26 +++++++++ 11 files changed, 203 insertions(+), 20 deletions(-) create mode 100644 app/utils/image.py create mode 100644 migrations/versions/f6ef5f35abca_.py diff --git a/app/blueprints/admin/actions.py b/app/blueprints/admin/actions.py index 6cad529..8a742f6 100644 --- a/app/blueprints/admin/actions.py +++ b/app/blueprints/admin/actions.py @@ -27,6 +27,7 @@ from app.models import * from app.tasks.forumtasks import importTopicList, checkAllForumAccounts from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates from app.utils import addNotification, get_system_user +from app.utils.image import get_image_size actions = {} @@ -54,8 +55,7 @@ def check_releases(): tasks = [] for release in releases: - zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"]) - tasks.append(checkZipRelease.s(release.id, zippath)) + tasks.append(checkZipRelease.s(release.id, release.file_path)) result = group(tasks).apply_async() @@ -71,8 +71,7 @@ def reimport_packages(): for package in Package.query.filter(Package.state!=PackageState.DELETED).all(): release = package.releases.first() if release: - zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"]) - tasks.append(checkZipRelease.s(release.id, zippath)) + tasks.append(checkZipRelease.s(release.id, release.file_path)) result = group(tasks).apply_async() @@ -311,3 +310,16 @@ def remind_video_url(): url_for('users.profile', username=user.username)) db.session.commit() + + +@action("Update screenshot sizes") +def remind_video_url(): + import sys + + for screenshot in PackageScreenshot.query.all(): + width, height = get_image_size(screenshot.file_path) + print(f"{screenshot.url}: {width}, {height}", file=sys.stderr) + screenshot.width = width + screenshot.height = height + + db.session.commit() diff --git a/app/blueprints/todo/__init__.py b/app/blueprints/todo/__init__.py index be9aa80..8f1dfac 100644 --- a/app/blueprints/todo/__init__.py +++ b/app/blueprints/todo/__init__.py @@ -17,7 +17,7 @@ from celery import uuid from flask import * from flask_login import current_user, login_required -from sqlalchemy import or_ +from sqlalchemy import or_, and_ from app.models import * from app.querybuilder import QueryBuilder @@ -168,6 +168,11 @@ def view_user(username=None): Package.state == PackageState.CHANGES_NEEDED)) \ .order_by(db.asc(Package.created_at)).all() + packages_with_small_screenshots = user.maintained_packages \ + .filter(Package.screenshots.any(and_(PackageScreenshot.width < PackageScreenshot.SOFT_MIN_SIZE[0], + PackageScreenshot.height < PackageScreenshot.SOFT_MIN_SIZE[1]))) \ + .all() + outdated_packages = user.maintained_packages \ .filter(Package.state != PackageState.DELETED, Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \ @@ -185,7 +190,9 @@ def view_user(username=None): return render_template("todo/user.html", current_tab="user", user=user, unapproved_packages=unapproved_packages, outdated_packages=outdated_packages, - needs_tags=needs_tags, topics_to_add=topics_to_add) + needs_tags=needs_tags, topics_to_add=topics_to_add, + packages_with_small_screenshots=packages_with_small_screenshots, + screenshot_min_size=PackageScreenshot.HARD_MIN_SIZE, screenshot_rec_size=PackageScreenshot.SOFT_MIN_SIZE) @bp.route("/users//update-configs/apply-all/", methods=["POST"]) diff --git a/app/flatpages/help/featured.md b/app/flatpages/help/featured.md index babf8d3..eb871ec 100644 --- a/app/flatpages/help/featured.md +++ b/app/flatpages/help/featured.md @@ -67,7 +67,7 @@ is available. ### Meta and packaging * MUST: `screenshot.png` is present and up-to-date, with a correct aspect ratio (3:2, at least 300x200). -* MUST: Have a high resolution cover image on ContentDB (at least 1280x768 pixels). +* MUST: Have a high resolution cover image on ContentDB (at least 1280x720 pixels). It may be shown cropped to 16:9 aspect ratio, or shorter. * MUST: mod.conf/game.conf/texture_pack.conf present with: * name (if mod or game) diff --git a/app/logic/screenshots.py b/app/logic/screenshots.py index 17981df..e8bdc34 100644 --- a/app/logic/screenshots.py +++ b/app/logic/screenshots.py @@ -6,6 +6,7 @@ from app.logic.LogicError import LogicError from app.logic.uploads import upload_file from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity from app.utils import addNotification, addAuditLog +from app.utils.image import get_image_size def do_create_screenshot(user: User, package: Package, title: str, file, reason: str = None): @@ -27,6 +28,13 @@ def do_create_screenshot(user: User, package: Package, title: str, file, reason: ss.url = uploaded_url ss.approved = package.checkPerm(user, Permission.APPROVE_SCREENSHOT) ss.order = counter + ss.width, ss.height = get_image_size(uploaded_path) + + if ss.is_too_small(): + raise LogicError(429, + lazy_gettext("Screenshot is too small, it should be at least %(width)s by %(height)s pixels", + width=PackageScreenshot.HARD_MIN_SIZE[0], height=PackageScreenshot.HARD_MIN_SIZE[1])) + db.session.add(ss) if reason is None: diff --git a/app/models/packages.py b/app/models/packages.py index b32d8e7..047cbdc 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -26,6 +26,7 @@ from sqlalchemy_utils.types import TSVectorType from . import db from .users import Permission, UserRank, User +from .. import app class PackageQuery(BaseQuery, SearchQueryMixin): @@ -885,6 +886,10 @@ class PackageRelease(db.Model): # If the release is approved, then the task_id must be null and the url must be present CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)") + @property + def file_path(self): + return self.url.replace("/uploads/", app.config["UPLOAD_DIR"]) + def getAsDictionary(self): return { "id": self.id, @@ -986,6 +991,9 @@ class PackageRelease(db.Model): class PackageScreenshot(db.Model): + HARD_MIN_SIZE = (920, 517) + SOFT_MIN_SIZE = (1280, 720) + id = db.Column(db.Integer, primary_key=True) package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False) @@ -997,6 +1005,22 @@ class PackageScreenshot(db.Model): approved = db.Column(db.Boolean, nullable=False, default=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) + width = db.Column(db.Integer, nullable=False) + height = db.Column(db.Integer, nullable=False) + + def is_very_small(self): + return self.width < 720 or self.height < 405 + + def is_too_small(self): + return self.width < PackageScreenshot.HARD_MIN_SIZE[0] or self.height < PackageScreenshot.HARD_MIN_SIZE[1] + + def is_low_res(self): + return self.width < PackageScreenshot.SOFT_MIN_SIZE[0] or self.height < PackageScreenshot.SOFT_MIN_SIZE[1] + + @property + def file_path(self): + return self.url.replace("/uploads/", app.config["UPLOAD_DIR"]) + def getEditURL(self): return url_for("packages.edit_screenshot", author=self.package.author.username, diff --git a/app/tasks/importtasks.py b/app/tasks/importtasks.py index 80f0f6f..141157c 100644 --- a/app/tasks/importtasks.py +++ b/app/tasks/importtasks.py @@ -27,6 +27,7 @@ from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_tem from .minetestcheck import build_tree, MinetestCheckError, ContentType from ..logic.LogicError import LogicError from ..logic.packages import do_edit_package, ALIASES +from ..utils.image import get_image_size @celery.task() @@ -213,6 +214,10 @@ def importRepoScreenshot(id): ss.package = package ss.title = "screenshot.png" ss.url = "/uploads/" + filename + ss.width, ss.height = get_image_size(destPath) + if ss.is_too_small(): + return None + db.session.add(ss) db.session.commit() diff --git a/app/templates/packages/screenshot_new.html b/app/templates/packages/screenshot_new.html index d45d201..6ac156b 100644 --- a/app/templates/packages/screenshot_new.html +++ b/app/templates/packages/screenshot_new.html @@ -6,6 +6,10 @@ {% block content %}

{{ _("Add a screenshot") }}

+

+ {{ _("The recommended resolution is 1920x1080, and screenshots must be at least %(width)dx%(height)d.", + width=920, height=517) }} +

{% from "macros/forms.html" import render_field, render_submit_field %}
diff --git a/app/templates/packages/screenshots.html b/app/templates/packages/screenshots.html index d4bbda6..3cdc230 100644 --- a/app/templates/packages/screenshots.html +++ b/app/templates/packages/screenshots.html @@ -6,7 +6,7 @@ {% block content %} {% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %} - + {{ _("Add Image") }} @@ -26,16 +26,34 @@
- {{ ss.title }} +
{{ ss.title }} - {% if not ss.approved %} -
- {{ _("Awaiting approval") }} -
- {% endif %} + +
+ {{ ss.width }} x {{ ss.height }} + {% if ss.is_low_res() %} + {% if ss.is_very_small() %} + + {{ _("Way too small") }} + + {% elif ss.is_too_small() %} + + {{ _("Too small") }} + + {% else %} + + {{ _("Not HD") }} + + {% endif %} + {% endif %} + {% if not ss.approved %} + + {{ _("Awaiting approval") }} + + {% endif %} +
diff --git a/app/templates/todo/user.html b/app/templates/todo/user.html index 2679e78..e0a3294 100644 --- a/app/templates/todo/user.html +++ b/app/templates/todo/user.html @@ -14,6 +14,7 @@ {% endif %} +

{{ _("Unapproved Packages Needing Action") }}

{% for package in unapproved_packages %} @@ -53,21 +54,75 @@ {% endif %}

{{ _("Potentially Outdated Packages") }}

-

- {{ _("New: Git Update Detection has been set up on all packages to send notifications.") }}
- {{ _("Consider changing the update settings to create releases automatically instead.") }} -

{{ _("Instead of marking packages as outdated, you can automatically create releases when New Commits or New Tags are pushed to Git by clicking 'Update Settings'.") }} {% if outdated_packages %} {{ _("To remove a package from below, create a release or change the update settings.") }} {% endif %}

- {% from "macros/todo.html" import render_outdated_packages %} {{ render_outdated_packages(outdated_packages, current_user) }} +
+

{{ _("Small Screenshots") }}

+ {% if packages_with_small_screenshots %} +

+ {{ _("These packages have screenshots that are too small, and should be replaced.") }} + {{ _("Red and orange are screenshots below the limit, and grey screenshots are below the recommended resolution.") }} + {{ _("The recommended resolution is 1920x1080, and screenshots must be at least %(width)dx%(height)d.", + width=920, height=517) }} + + + {{ _("Way too small") }} + + + {{ _("Too small") }} + + + {{ _("Not HD") }} + +

+ {% endif %} + + + {{_ ("See All") }}

{{ _("Packages Without Tags") }}

diff --git a/app/utils/image.py b/app/utils/image.py new file mode 100644 index 0000000..54e13ba --- /dev/null +++ b/app/utils/image.py @@ -0,0 +1,24 @@ +# ContentDB +# Copyright (C) 2022 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from typing import Tuple +from PIL import Image + + +def get_image_size(path: str) -> Tuple[int,int]: + im = Image.open(path) + return im.size diff --git a/migrations/versions/f6ef5f35abca_.py b/migrations/versions/f6ef5f35abca_.py new file mode 100644 index 0000000..ec59b10 --- /dev/null +++ b/migrations/versions/f6ef5f35abca_.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: f6ef5f35abca +Revises: 011e42c52d21 +Create Date: 2022-01-26 00:10:46.610784 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'f6ef5f35abca' +down_revision = '011e42c52d21' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('package_screenshot', sa.Column('height', sa.Integer(), nullable=False, server_default="0")) + op.add_column('package_screenshot', sa.Column('width', sa.Integer(), nullable=False, server_default="0")) + + +def downgrade(): + op.drop_column('package_screenshot', 'width') + op.drop_column('package_screenshot', 'height')