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.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()

@ -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/<username>/update-configs/apply-all/", methods=["POST"])

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

@ -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:

@ -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,

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

@ -6,6 +6,10 @@
{% block content %}
<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 %}
<form method="POST" action="" enctype="multipart/form-data">

@ -6,7 +6,7 @@
{% block content %}
{% 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>
{{ _("Add Image") }}
</a>
@ -26,16 +26,34 @@
<i class="fas fa-bars"></i>
</div>
<div class="col-auto">
<img class="img-fluid" style="max-height: 64px;"
src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
<img class="img-fluid" style="max-height: 64px;" src="{{ ss.getThumbnailURL() }}" />
</div>
<div class="col">
{{ ss.title }}
{% if not ss.approved %}
<div class="text-muted">
{{ _("Awaiting approval") }}
</div>
<div class="mt-1 text-muted">
{{ ss.width }} x {{ ss.height }}
{% if ss.is_low_res() %}
{% 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>
<form action="{{ ss.getDeleteURL() }}" method="POST" class="col-auto text-right" role="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />

@ -14,6 +14,7 @@
</a>
</div>
{% endif %}
<h2>{{ _("Unapproved Packages Needing Action") }}</h2>
<div class="list-group mt-3 mb-5">
{% for package in unapproved_packages %}
@ -53,21 +54,75 @@
</form>
{% endif %}
<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>
{{ _("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 %}
</p>
{% from "macros/todo.html" import render_outdated_packages %}
{{ render_outdated_packages(outdated_packages, current_user) }}
<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) }}">
{{_ ("See All") }}</a>
<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')