Add screenshot resolution checking

This commit is contained in:
rubenwardy 2022-01-26 02:51:40 +00:00
parent 625e4cf9ee
commit d08710684d
11 changed files with 203 additions and 20 deletions

@ -27,6 +27,7 @@ from app.models import *
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
from app.utils import addNotification, get_system_user from app.utils import addNotification, get_system_user
from app.utils.image import get_image_size
actions = {} actions = {}
@ -54,8 +55,7 @@ def check_releases():
tasks = [] tasks = []
for release in releases: for release in releases:
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"]) tasks.append(checkZipRelease.s(release.id, release.file_path))
tasks.append(checkZipRelease.s(release.id, zippath))
result = group(tasks).apply_async() result = group(tasks).apply_async()
@ -71,8 +71,7 @@ def reimport_packages():
for package in Package.query.filter(Package.state!=PackageState.DELETED).all(): for package in Package.query.filter(Package.state!=PackageState.DELETED).all():
release = package.releases.first() release = package.releases.first()
if release: if release:
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"]) tasks.append(checkZipRelease.s(release.id, release.file_path))
tasks.append(checkZipRelease.s(release.id, zippath))
result = group(tasks).apply_async() result = group(tasks).apply_async()
@ -311,3 +310,16 @@ def remind_video_url():
url_for('users.profile', username=user.username)) url_for('users.profile', username=user.username))
db.session.commit() 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()

@ -17,7 +17,7 @@
from celery import uuid from celery import uuid
from flask import * from flask import *
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy import or_ from sqlalchemy import or_, and_
from app.models import * from app.models import *
from app.querybuilder import QueryBuilder from app.querybuilder import QueryBuilder
@ -168,6 +168,11 @@ def view_user(username=None):
Package.state == PackageState.CHANGES_NEEDED)) \ Package.state == PackageState.CHANGES_NEEDED)) \
.order_by(db.asc(Package.created_at)).all() .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 \ outdated_packages = user.maintained_packages \
.filter(Package.state != PackageState.DELETED, .filter(Package.state != PackageState.DELETED,
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \ 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, return render_template("todo/user.html", current_tab="user", user=user,
unapproved_packages=unapproved_packages, outdated_packages=outdated_packages, 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/<username>/update-configs/apply-all/", methods=["POST"]) @bp.route("/users/<username>/update-configs/apply-all/", methods=["POST"])

@ -67,7 +67,7 @@ is available.
### Meta and packaging ### Meta and packaging
* MUST: `screenshot.png` is present and up-to-date, with a correct aspect ratio (3:2, at least 300x200). * 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. It may be shown cropped to 16:9 aspect ratio, or shorter.
* MUST: mod.conf/game.conf/texture_pack.conf present with: * MUST: mod.conf/game.conf/texture_pack.conf present with:
* name (if mod or game) * name (if mod or game)

@ -6,6 +6,7 @@ from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file from app.logic.uploads import upload_file
from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity
from app.utils import addNotification, addAuditLog 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): 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.url = uploaded_url
ss.approved = package.checkPerm(user, Permission.APPROVE_SCREENSHOT) ss.approved = package.checkPerm(user, Permission.APPROVE_SCREENSHOT)
ss.order = counter 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) db.session.add(ss)
if reason is None: if reason is None:

@ -26,6 +26,7 @@ from sqlalchemy_utils.types import TSVectorType
from . import db from . import db
from .users import Permission, UserRank, User from .users import Permission, UserRank, User
from .. import app
class PackageQuery(BaseQuery, SearchQueryMixin): 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 # 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)") 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): def getAsDictionary(self):
return { return {
"id": self.id, "id": self.id,
@ -986,6 +991,9 @@ class PackageRelease(db.Model):
class PackageScreenshot(db.Model): class PackageScreenshot(db.Model):
HARD_MIN_SIZE = (920, 517)
SOFT_MIN_SIZE = (1280, 720)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False) 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) approved = db.Column(db.Boolean, nullable=False, default=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) 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): def getEditURL(self):
return url_for("packages.edit_screenshot", return url_for("packages.edit_screenshot",
author=self.package.author.username, author=self.package.author.username,

@ -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 .minetestcheck import build_tree, MinetestCheckError, ContentType
from ..logic.LogicError import LogicError from ..logic.LogicError import LogicError
from ..logic.packages import do_edit_package, ALIASES from ..logic.packages import do_edit_package, ALIASES
from ..utils.image import get_image_size
@celery.task() @celery.task()
@ -213,6 +214,10 @@ def importRepoScreenshot(id):
ss.package = package ss.package = package
ss.title = "screenshot.png" ss.title = "screenshot.png"
ss.url = "/uploads/" + filename 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.add(ss)
db.session.commit() db.session.commit()

@ -6,6 +6,10 @@
{% block content %} {% block content %}
<h1>{{ _("Add a screenshot") }}</h1> <h1>{{ _("Add a screenshot") }}</h1>
<p class="mb-4">
{{ _("The recommended resolution is 1920x1080, and screenshots must be at least %(width)dx%(height)d.",
width=920, height=517) }}
</p>
{% from "macros/forms.html" import render_field, render_submit_field %} {% from "macros/forms.html" import render_field, render_submit_field %}
<form method="POST" action="" enctype="multipart/form-data"> <form method="POST" action="" enctype="multipart/form-data">

@ -6,7 +6,7 @@
{% block content %} {% block content %}
{% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %} {% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<a href="{{ package.getURL("packages.create_screenshot") }}" class="btn btn-primary float-right"> <a href="{{ package.getURL('packages.create_screenshot') }}" class="btn btn-primary float-right">
<i class="fas fa-plus mr-1"></i> <i class="fas fa-plus mr-1"></i>
{{ _("Add Image") }} {{ _("Add Image") }}
</a> </a>
@ -26,16 +26,34 @@
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<img class="img-fluid" style="max-height: 64px;" <img class="img-fluid" style="max-height: 64px;" src="{{ ss.getThumbnailURL() }}" />
src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
</div> </div>
<div class="col"> <div class="col">
{{ ss.title }} {{ ss.title }}
{% if not ss.approved %}
<div class="text-muted"> <div class="mt-1 text-muted">
{{ _("Awaiting approval") }} {{ ss.width }} x {{ ss.height }}
</div> {% if ss.is_low_res() %}
{% endif %} {% if ss.is_very_small() %}
<span class="badge badge-danger ml-3">
{{ _("Way too small") }}
</span>
{% elif ss.is_too_small() %}
<span class="badge badge-warning ml-3">
{{ _("Too small") }}
</span>
{% else %}
<span class="badge badge-secondary ml-3">
{{ _("Not HD") }}
</span>
{% endif %}
{% endif %}
{% if not ss.approved %}
<span class="ml-3">
{{ _("Awaiting approval") }}
</span>
{% endif %}
</div>
</div> </div>
<form action="{{ ss.getDeleteURL() }}" method="POST" class="col-auto text-right" role="form"> <form action="{{ ss.getDeleteURL() }}" method="POST" class="col-auto text-right" role="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />

@ -14,6 +14,7 @@
</a> </a>
</div> </div>
{% endif %} {% endif %}
<h2>{{ _("Unapproved Packages Needing Action") }}</h2> <h2>{{ _("Unapproved Packages Needing Action") }}</h2>
<div class="list-group mt-3 mb-5"> <div class="list-group mt-3 mb-5">
{% for package in unapproved_packages %} {% for package in unapproved_packages %}
@ -53,21 +54,75 @@
</form> </form>
{% endif %} {% endif %}
<h2>{{ _("Potentially Outdated Packages") }}</h2> <h2>{{ _("Potentially Outdated Packages") }}</h2>
<p class="alert alert-info">
{{ _("New: Git Update Detection has been set up on all packages to send notifications.") }}<br />
{{ _("Consider changing the update settings to create releases automatically instead.") }}
</p>
<p> <p>
{{ _("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'.") }} {{ _("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 %} {% if outdated_packages %}
{{ _("To remove a package from below, create a release or change the update settings.") }} {{ _("To remove a package from below, create a release or change the update settings.") }}
{% endif %} {% endif %}
</p> </p>
{% from "macros/todo.html" import render_outdated_packages %} {% from "macros/todo.html" import render_outdated_packages %}
{{ render_outdated_packages(outdated_packages, current_user) }} {{ render_outdated_packages(outdated_packages, current_user) }}
<div class="mt-5"></div> <div class="mt-5"></div>
<h2 id="small-screenshots">{{ _("Small Screenshots") }}</h2>
{% if packages_with_small_screenshots %}
<p>
{{ _("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) }}
<span class="badge badge-danger ml-3">
{{ _("Way too small") }}
</span>
<span class="badge badge-warning">
{{ _("Too small") }}
</span>
<span class="badge badge-secondary">
{{ _("Not HD") }}
</span>
</p>
{% endif %}
<div class="list-group mt-3 mb-5">
{% for package in packages_with_small_screenshots %}
<a class="list-group-item list-group-item-action" href="{{ package.getURL('packages.screenshots') }}">
<div class="row">
<div class="col-sm-3 text-muted" style="min-width: 200px;">
<img
class="img-fluid"
style="max-height: 22px; max-width: 22px;"
src="{{ package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
{{ package.title }}
</span>
</div>
<div class="col-sm">
{% for ss in package.screenshots %}
{% if ss.is_low_res() %}
{% if ss.is_very_small() %}
{% set badge_color = "badge-danger" %}
{% elif ss.is_too_small() %}
{% set badge_color = "badge-warning" %}
{% else %}
{% set badge_color = "badge-secondary" %}
{% endif %}
<span class="badge {{ badge_color }} ml-2" title="{{ ss.title }}">
{{ ss.width }} x {{ ss.height }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
</a>
{% else %}
<p class="text-muted">{{ _("Nothing to do :)") }}</p>
{% endfor %}
</div>
<a class="btn btn-secondary float-right" href="{{ url_for('todo.tags', author=user.username) }}"> <a class="btn btn-secondary float-right" href="{{ url_for('todo.tags', author=user.username) }}">
{{_ ("See All") }}</a> {{_ ("See All") }}</a>
<h2>{{ _("Packages Without Tags") }}</h2> <h2>{{ _("Packages Without Tags") }}</h2>

24
app/utils/image.py Normal file

@ -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 <https://www.gnu.org/licenses/>.
from typing import Tuple
from PIL import Image
def get_image_size(path: str) -> Tuple[int,int]:
im = Image.open(path)
return im.size

@ -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')