<% if (['image', 'animation'].includes(ctx.post.type)) { %> - + <% } else if (ctx.post.type === 'flash') { %> - + +
Your browser does not support Flash.
<% } else if (ctx.post.type === 'video') { %> @@ -19,6 +20,8 @@ loop: (ctx.post.flags || []).includes('loop'), playsinline: true, autoplay: ctx.autoplay, + preload: 'auto', + poster: ctx.post.originalThumbnailUrl, }, ctx.makeElement('source', { type: ctx.post.mimeType, diff --git a/client/html/post_upload_row.tpl b/client/html/post_upload_row.tpl index 2885e3d7..1318ab2f 100644 --- a/client/html/post_upload_row.tpl +++ b/client/html/post_upload_row.tpl @@ -10,7 +10,7 @@
- diff --git a/client/js/api.js b/client/js/api.js index 5bde6d81..d3f74c26 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -294,11 +294,16 @@ class Api extends events.EventTarget { // transform the request: upload each file, then make the request use // its tokens. data = Object.assign({}, data); + let fileData = {}; let abortFunction = () => {}; let promise = Promise.resolve(); if (files) { for (let key of Object.keys(files)) { const file = files[key]; + if (file === null) { + fileData[key] = null; + continue; + } const fileId = this._getFileId(file); if (fileTokens[fileId]) { data[key + "Token"] = fileTokens[fileId]; @@ -324,7 +329,7 @@ class Api extends events.EventTarget { url, requestFactory, data, - {}, + fileData, options ); abortFunction = () => requestPromise.abort(); @@ -388,7 +393,7 @@ class Api extends events.EventTarget { if (files) { for (let key of Object.keys(files)) { const value = files[key]; - if (value.constructor === String) { + if (value !== null && value.constructor === String) { data[key + "Url"] = value; } else { req.attach(key, value || new Blob()); diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index d54059e8..9e9d1202 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -8,7 +8,7 @@ const PageController = require("../controllers/page_controller.js"); const CommentsPageView = require("../views/comments_page_view.js"); const EmptyView = require("../views/empty_view.js"); -const fields = ["id", "comments", "commentCount", "thumbnailUrl"]; +const fields = ["id", "comments", "commentCount", "thumbnailUrl", "customThumbnailUrl"]; class CommentsController { constructor(ctx) { diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index fdb7b844..ca1a411c 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -14,6 +14,7 @@ const EmptyView = require("../views/empty_view.js"); const fields = [ "id", "thumbnailUrl", + "customThumbnailUrl", "type", "safety", "score", diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index bd338129..3cab18cf 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -178,15 +178,11 @@ class PostMainController extends BasePostController { if (e.detail.relations !== undefined && e.detail.relations !== null) { post.relations = e.detail.relations; } - if (e.detail.content !== undefined && e.detail.content !== null) { - post.newContent = e.detail.content; - } - if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) { - post.newThumbnail = e.detail.thumbnail; - } if (e.detail.source !== undefined && e.detail.source !== null) { post.source = e.detail.source; } + post.newContent = e.detail.content; + post.newThumbnail = e.detail.thumbnail; post.save().then( () => { this._view.sidebarControl.showSuccess("Post saved."); diff --git a/client/js/controls/post_content_control.js b/client/js/controls/post_content_control.js index 8cbd5d89..68951dca 100644 --- a/client/js/controls/post_content_control.js +++ b/client/js/controls/post_content_control.js @@ -5,11 +5,12 @@ const views = require("../util/views.js"); const optimizedResize = require("../util/optimized_resize.js"); class PostContentControl { - constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) { + constructor(hostNode, post, isMediaCached, viewportSizeCalculator, fitFunctionOverride) { this._post = post; this._viewportSizeCalculator = viewportSizeCalculator; this._hostNode = hostNode; this._template = views.getTemplate("post-content"); + this._isMediaCached = isMediaCached; let fitMode = settings.get().fitMode; if (typeof fitFunctionOverride !== "undefined") { @@ -143,9 +144,30 @@ class PostContentControl { post: this._post, autoplay: settings.get().autoplayVideos, }); - if (settings.get().transparencyGrid) { + function load(argument) { + if (settings.get().transparencyGrid) { + newNode.classList.add("transparency-grid"); + } + newNode.firstElementChild.style.backgroundImage = ""; + } + if (["image", "flash"].includes(this._post.type)) { + if (this._post.type !== "image" || !this._isMediaCached) { + newNode.firstElementChild.style.backgroundImage = "url("+this._post.originalThumbnailUrl+")"; + } + } + if (this._post.type == "image" && !this._isMediaCached) { + newNode.firstElementChild.addEventListener("load", load); + } else if (settings.get().transparencyGrid) { newNode.classList.add("transparency-grid"); } + newNode.firstElementChild.addEventListener("error", (e) => { + newNode.classList.add("post-error"); + if (["image", "animation"].includes(this._post.type)) { + newNode.firstElementChild.removeEventListener("load", load); + newNode.firstElementChild.style.backgroundImage = "url("+this._post.originalThumbnailUrl+")"; + newNode.firstElementChild.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + } + }); if (this._postContentNode) { this._hostNode.replaceChild(newNode, this._postContentNode); } else { diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 3b1c16e7..9f2d37da 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -138,10 +138,7 @@ class PostEditSidebarControl extends events.EventTarget { this._thumbnailRemovalLinkNode.addEventListener("click", (e) => this._evtRemoveThumbnailClick(e) ); - this._thumbnailRemovalLinkNode.style.display = this._post - .hasCustomThumbnail - ? "block" - : "none"; + this._thumbnailRemovalLinkUpdate(this._post); } if (this._addNoteLinkNode) { @@ -249,12 +246,25 @@ class PostEditSidebarControl extends events.EventTarget { this._poolsExpander.title = `Pools (${this._post.pools.length})`; } + _thumbnailRemovalLinkUpdate(post) { + if (this._thumbnailRemovalLinkNode) { + this._thumbnailRemovalLinkNode.style.display = post + .customThumbnailUrl + ? "block" + : "none"; + } + } + _evtPostContentChange(e) { this._contentFileDropper.reset(); + this._thumbnailRemovalLinkUpdate(e.detail.post); + this._newPostContent = null; } _evtPostThumbnailChange(e) { this._thumbnailFileDropper.reset(); + this._thumbnailRemovalLinkUpdate(e.detail.post); + this._newPostThumbnail = undefined; } _evtRemoveThumbnailClick(e) { @@ -427,9 +437,7 @@ class PostEditSidebarControl extends events.EventTarget { : undefined, thumbnail: - this._newPostThumbnail !== undefined && this._newPostThumbnail !== null - ? this._newPostThumbnail - : undefined, + this._newPostThumbnail, source: this._sourceInputNode ? this._sourceInputNode.value diff --git a/client/js/models/post.js b/client/js/models/post.js index 01f81bf1..23003979 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -70,6 +70,14 @@ class Post extends events.EventTarget { return this._thumbnailUrl; } + get customThumbnailUrl() { + return this._customThumbnailUrl; + } + + get originalThumbnailUrl() { + return this._originalThumbnailUrl; + } + get source() { return this._source; } @@ -146,10 +154,6 @@ class Post extends events.EventTarget { return this._ownScore; } - get hasCustomThumbnail() { - return this._hasCustomThumbnail; - } - set flags(value) { this._flags = value; } @@ -477,7 +481,9 @@ class Post extends events.EventTarget { response.contentUrl, document.getElementsByTagName("base")[0].href ).href, - _thumbnailUrl: response.thumbnailUrl, + _thumbnailUrl: response.customThumbnailUrl ? response.customThumbnailUrl : response.thumbnailUrl, + _customThumbnailUrl: response.customThumbnailUrl, + _originalThumbnailUrl: response.thumbnailUrl, _source: response.source, _canvasWidth: response.canvasWidth, _canvasHeight: response.canvasHeight, @@ -491,7 +497,6 @@ class Post extends events.EventTarget { _favoriteCount: response.favoriteCount, _ownScore: response.ownScore, _ownFavorite: response.ownFavorite, - _hasCustomThumbnail: response.hasCustomThumbnail, }); for (let obj of [this, this._orig]) { diff --git a/client/js/util/misc.js b/client/js/util/misc.js index 756ad84b..2a71fe10 100644 --- a/client/js/util/misc.js +++ b/client/js/util/misc.js @@ -211,6 +211,15 @@ function getPrettyName(tag) { return tag; } +function isMediaCached(post) { + if (post.type !== "image") { + return false; + } + const img = new Image() + img.src = post.contentUrl; + return img.complete; +} + module.exports = { range: range, formatRelativeTime: formatRelativeTime, @@ -229,4 +238,5 @@ module.exports = { escapeSearchTerm: escapeSearchTerm, dataURItoBlob: dataURItoBlob, getPrettyName: getPrettyName, + isMediaCached: isMediaCached, }; diff --git a/client/js/util/views.js b/client/js/util/views.js index f6280a1c..c0dfd6c5 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -49,7 +49,7 @@ function makeThumbnail(url) { style: `background-image: url(\'${url}\')`, } : { class: "thumbnail empty" }, - makeElement("img", { alt: "thumbnail", src: url }) + makeElement("img", { alt: "thumbnail", src: url, draggable: "false" }) ); } diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index c91363b2..795d6a5a 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -62,6 +62,7 @@ class HomeView { this._postContentControl = new PostContentControl( this._postContainerNode, postInfo.featuredPost, + misc.isMediaCached(postInfo.featuredPost), () => { return [window.innerWidth * 0.8, window.innerHeight * 0.7]; }, diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 5ef7f61e..d5d15c51 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -31,6 +31,7 @@ class PostMainView { this._postContentControl = new PostContentControl( postContainerNode, ctx.post, + misc.isMediaCached(ctx.post), () => { const margin = sidebarNode.getBoundingClientRect().left; diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js index 4ef4c1ad..55b55427 100644 --- a/client/js/views/post_upload_view.js +++ b/client/js/views/post_upload_view.js @@ -401,6 +401,14 @@ class PostUploadView extends events.EventTarget { .addEventListener("click", (e) => this._evtMoveClick(e, uploadable, 1) ); + if (uploadable.type == "video") { + const video = rowNode.querySelector("video"); + if (video) { + video.addEventListener("loadedmetadata", (e) => { + if (!isNaN(video.duration)) video.currentTime = Math.floor(video.duration * 0.3) + }); + } + } } _updateThumbnailNode(uploadable) { diff --git a/server/Dockerfile b/server/Dockerfile index 032ec098..7fa8ae2f 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -33,6 +33,21 @@ RUN pip3 install --no-cache-dir --disable-pip-version-check \ "pillow-avif-plugin~=1.1.0" RUN apk --no-cache del py3-pip +# build and install mozjpeg +RUN apk --no-cache add automake cmake nasm libpng-dev libpng curl +RUN curl -fL "https://github.com/mozilla/mozjpeg/archive/refs/tags/v4.1.1.tar.gz" -o mozjpeg.tar.gz +RUN tar xzf mozjpeg.tar.gz && cd mozjpeg-* \ +&& cmake -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + -DBUILD_SHARED_LIBS=True \ + -DCMAKE_BUILD_TYPE=None \ + -DCMAKE_C_FLAGS="$CFLAGS" \ + -DWITH_JPEG8=1 \ + -DWITH_TURBOJPEG=1 \ + -DENABLE_STATIC=0 \ +&& make install && cd +RUN apk --no-cache del automake nasm cmake curl + COPY ./ /opt/app/ RUN rm -rf /opt/app/szurubooru/tests diff --git a/server/szuru-admin b/server/szuru-admin index 08ba1827..204e8f11 100755 --- a/server/szuru-admin +++ b/server/szuru-admin @@ -13,7 +13,7 @@ from getpass import getpass from sys import stderr from szurubooru import config, db, errors, model -from szurubooru.func import files, images +from szurubooru.func import files, images, mime from szurubooru.func import posts as postfuncs from szurubooru.func import users as userfuncs @@ -95,7 +95,14 @@ def regenerate_thumbnails() -> None: for post in db.session.query(model.Post).all(): print("Generating tumbnail for post %d ..." % post.post_id, end="\r") try: - postfuncs.generate_post_thumbnail(post) + content = files.get(postfuncs.get_post_content_path(post)) + postfuncs.generate_post_thumbnail(postfuncs.get_post_thumbnail_path(post), content, seek=False) + + custom_content = files.get(postfuncs.get_post_custom_content_path(post)) + if custom_content: + generate_post_thumbnail(get_post_custom_thumbnail_path(post), custom_content, seek=True) + elif mime.is_video(post.mime_type): + generate_post_thumbnail(get_post_custom_thumbnail_path(post), content, seek=True) except Exception: pass diff --git a/server/szurubooru/func/files.py b/server/szurubooru/func/files.py index 6a898269..02be1296 100644 --- a/server/szurubooru/func/files.py +++ b/server/szurubooru/func/files.py @@ -1,4 +1,5 @@ import os +import glob from typing import Any, List, Optional from szurubooru import config @@ -24,6 +25,10 @@ def scan(path: str) -> List[Any]: return [] +def find(path: str, pattern: str, recursive: bool = False) -> List[Any]: + return glob.glob(glob.escape(_get_full_path(path) + "/") + pattern, recursive=recursive) + + def move(source_path: str, target_path: str) -> None: os.rename(_get_full_path(source_path), _get_full_path(target_path)) diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index e135d182..93891017 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -24,6 +24,11 @@ def convert_heif_to_png(content: bytes) -> bytes: return img_byte_arr.getvalue() +def check_for_loop(content: bytes) -> bytes: + img = PILImage.open(BytesIO(content)) + return "loop" in img.info + + class Image: def __init__(self, content: bytes) -> None: self.content = content @@ -41,7 +46,7 @@ class Image: def frames(self) -> int: return self.info["streams"][0]["nb_read_frames"] - def resize_fill(self, width: int, height: int) -> None: + def resize_fill(self, width: int, height: int, keep_transparency: bool = True, seek=True) -> None: width_greater = self.width > self.height width, height = (-1, height) if width_greater else (width, -1) @@ -50,8 +55,12 @@ class Image: "{path}", "-f", "image2", - "-filter:v", - "scale='{width}:{height}'".format(width=width, height=height), + "-filter_complex", + ( + "format=rgb32,scale={width}:{height}:flags=bicubic" + if keep_transparency else + "[0:v]format=rgb32,scale={width}:{height}:flags=bicubic[a];color=white[b];[b][a]scale2ref[b][a];[b][a]overlay" + ).format(width=width, height=height), "-map", "0:v:0", "-vframes", @@ -61,7 +70,8 @@ class Image: "-", ] if ( - "duration" in self.info["format"] + seek + and "duration" in self.info["format"] and self.info["format"]["format_name"] != "swf" ): duration = float(self.info["format"]["duration"]) @@ -96,24 +106,13 @@ class Image: def to_jpeg(self) -> bytes: return self._execute( [ - "-f", - "lavfi", - "-i", - "color=white:s=%dx%d" % (self.width, self.height), - "-i", + "-quality", + "85", + "-sample", + "1x1", "{path}", - "-f", - "image2", - "-filter_complex", - "overlay", - "-map", - "0:v:0", - "-vframes", - "1", - "-vcodec", - "mjpeg", - "-", - ] + ], + program="cjpeg", ) def to_webm(self) -> bytes: @@ -274,7 +273,10 @@ class Image: with util.create_temp_file(suffix="." + extension) as handle: handle.write(self.content) handle.flush() - cli = [program, "-loglevel", "32" if get_logs else "24"] + cli + if program in ["ffmpeg", "ffprobe"]: + cli = [program, "-loglevel", "32" if get_logs else "24"] + cli + else: + cli = [program] + cli cli = [part.format(path=handle.name) for part in cli] proc = subprocess.Popen( cli, @@ -285,7 +287,7 @@ class Image: out, err = proc.communicate() if proc.returncode != 0: logger.warning( - "Failed to execute ffmpeg command (cli=%r, err=%r)", + "Failed to execute {program} command (cli=%r, err=%r)".format(program=program), " ".join(shlex.quote(arg) for arg in cli), err, ) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index be2259cf..3244da98 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -117,7 +117,16 @@ def get_post_content_url(post: model.Post) -> str: def get_post_thumbnail_url(post: model.Post) -> str: assert post - return "%s/generated-thumbnails/%d_%s.jpg" % ( + return "%s/generated-thumbnails/sample_%d_%s.jpg" % ( + config.config["data_url"].rstrip("/"), + post.post_id, + get_post_security_hash(post.post_id), + ) + + +def get_post_custom_thumbnail_url(post: model.Post) -> str: + assert post + return "%s/generated-thumbnails/custom-thumbnails/sample_%d_%s.jpg" % ( config.config["data_url"].rstrip("/"), post.post_id, get_post_security_hash(post.post_id), @@ -134,17 +143,26 @@ def get_post_content_path(post: model.Post) -> str: ) -def get_post_thumbnail_path(post: model.Post) -> str: +def get_post_custom_content_path(post: model.Post) -> str: assert post - return "generated-thumbnails/%d_%s.jpg" % ( + assert post.post_id + return "posts/custom-thumbnails/%d_%s.dat" % ( post.post_id, get_post_security_hash(post.post_id), ) -def get_post_thumbnail_backup_path(post: model.Post) -> str: +def get_post_thumbnail_path(post: model.Post) -> str: assert post - return "posts/custom-thumbnails/%d_%s.dat" % ( + return "generated-thumbnails/sample_%d_%s.jpg" % ( + post.post_id, + get_post_security_hash(post.post_id), + ) + + +def get_post_custom_thumbnail_path(post: model.Post) -> str: + assert post + return "generated-thumbnails/custom-thumbnails/sample_%d_%s.jpg" % ( post.post_id, get_post_security_hash(post.post_id), ) @@ -180,6 +198,7 @@ class PostSerializer(serialization.BaseSerializer): "canvasHeight": self.serialize_canvas_height, "contentUrl": self.serialize_content_url, "thumbnailUrl": self.serialize_thumbnail_url, + "customThumbnailUrl": self.serialize_custom_thumbnail_url, "flags": self.serialize_flags, "tags": self.serialize_tags, "relations": self.serialize_relations, @@ -195,7 +214,6 @@ class PostSerializer(serialization.BaseSerializer): "featureCount": self.serialize_feature_count, "lastFeatureTime": self.serialize_last_feature_time, "favoritedBy": self.serialize_favorited_by, - "hasCustomThumbnail": self.serialize_has_custom_thumbnail, "notes": self.serialize_notes, "comments": self.serialize_comments, "pools": self.serialize_pools, @@ -319,8 +337,9 @@ class PostSerializer(serialization.BaseSerializer): for rel in self.post.favorited_by ] - def serialize_has_custom_thumbnail(self) -> Any: - return files.has(get_post_thumbnail_backup_path(self.post)) + def serialize_custom_thumbnail_url(self) -> Any: + if files.has(get_post_custom_thumbnail_path(self.post)): + return get_post_custom_thumbnail_url(self.post) def serialize_notes(self) -> Any: return sorted( @@ -357,7 +376,7 @@ def serialize_micro_post( post: model.Post, auth_user: model.User ) -> Optional[rest.Response]: return serialize_post( - post, auth_user=auth_user, options=["id", "thumbnailUrl"] + post, auth_user=auth_user, options=["id", "thumbnailUrl", "customThumbnailUrl"] ) @@ -462,32 +481,28 @@ def _before_post_delete( ) -> None: if post.post_id: if config.config["delete_source_files"]: - files.delete(get_post_content_path(post)) - files.delete(get_post_thumbnail_path(post)) + pattern = f"{post.post_id}_*" + for file in files.find("posts", "**/" + pattern, recursive=True) + files.find("generated-thumbnails", "**/sample_" + pattern, recursive=True): + files.delete(file) def _sync_post_content(post: model.Post) -> None: - regenerate_thumb = False - if hasattr(post, "__content"): content = getattr(post, "__content") files.save(get_post_content_path(post), content) + generate_post_thumbnail(get_post_thumbnail_path(post), content, seek=False) + if mime.is_video(post.mime_type): + generate_post_thumbnail(get_post_custom_thumbnail_path(post), content, seek=True) delattr(post, "__content") - regenerate_thumb = True if hasattr(post, "__thumbnail"): if getattr(post, "__thumbnail"): - files.save( - get_post_thumbnail_backup_path(post), - getattr(post, "__thumbnail"), - ) + thumbnail = getattr(post, "__thumbnail") + files.save(get_post_custom_content_path(post), thumbnail) + generate_post_thumbnail(get_post_custom_thumbnail_path(post), thumbnail, seek=True) else: - files.delete(get_post_thumbnail_backup_path(post)) + files.delete(get_post_custom_thumbnail_path(post)) delattr(post, "__thumbnail") - regenerate_thumb = True - - if regenerate_thumb: - generate_post_thumbnail(post) def generate_alternate_formats( @@ -677,22 +692,19 @@ def update_post_thumbnail( setattr(post, "__thumbnail", content) -def generate_post_thumbnail(post: model.Post) -> None: - assert post - if files.has(get_post_thumbnail_backup_path(post)): - content = files.get(get_post_thumbnail_backup_path(post)) - else: - content = files.get(get_post_content_path(post)) +def generate_post_thumbnail(path: str, content: bytes, seek=True) -> None: try: assert content image = images.Image(content) image.resize_fill( int(config.config["thumbnails"]["post_width"]), int(config.config["thumbnails"]["post_height"]), + keep_transparency=False, + seek=seek, ) - files.save(get_post_thumbnail_path(post), image.to_jpeg()) + files.save(path, image.to_jpeg()) except errors.ProcessingError: - files.save(get_post_thumbnail_path(post), EMPTY_PIXEL) + files.save(path, EMPTY_PIXEL) def update_post_tags( diff --git a/server/szurubooru/func/users.py b/server/szurubooru/func/users.py index 5cbe3cc0..a2e21da6 100644 --- a/server/szurubooru/func/users.py +++ b/server/szurubooru/func/users.py @@ -311,6 +311,8 @@ def update_user_avatar( image.resize_fill( int(config.config["thumbnails"]["avatar_width"]), int(config.config["thumbnails"]["avatar_height"]), + keep_transparency=False, + seek=False, ) files.save(avatar_path, image.to_png()) else: diff --git a/server/szurubooru/rest/context.py b/server/szurubooru/rest/context.py index 40ba0bcb..393a37e2 100644 --- a/server/szurubooru/rest/context.py +++ b/server/szurubooru/rest/context.py @@ -51,7 +51,7 @@ class Context: use_video_downloader: bool = False, allow_tokens: bool = True, ) -> bytes: - if name in self._files and self._files[name]: + if name in self._files: return self._files[name] if name + "Url" in self._params: diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index fa1b3bb6..55e77bc5 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -41,7 +41,7 @@ def test_get_post_thumbnail_url(input_mime_type, config_injector): post.mime_type = input_mime_type assert ( posts.get_post_thumbnail_url(post) - == "http://example.com/generated-thumbnails/1_244c8840887984c4.jpg" + == "http://example.com/generated-thumbnails/sample_1_244c8840887984c4.jpg" ) @@ -67,18 +67,18 @@ def test_get_post_thumbnail_path(input_mime_type): post.mime_type = input_mime_type assert ( posts.get_post_thumbnail_path(post) - == "generated-thumbnails/1_244c8840887984c4.jpg" + == "generated-thumbnails/sample_1_244c8840887984c4.jpg" ) @pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) -def test_get_post_thumbnail_backup_path(input_mime_type): +def test_get_post_custom_thumbnail_path(input_mime_type): post = model.Post() post.post_id = 1 post.mime_type = input_mime_type assert ( - posts.get_post_thumbnail_backup_path(post) - == "posts/custom-thumbnails/1_244c8840887984c4.dat" + posts.get_post_custom_thumbnail_path(post) + == "generated-thumbnails/custom-thumbnails/sample_1_244c8840887984c4.jpg" ) @@ -226,7 +226,9 @@ def test_serialize_post( "canvasHeight": 300, "contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg", "thumbnailUrl": "http://example.com/" - "generated-thumbnails/1_244c8840887984c4.jpg", + "generated-thumbnails/sample_1_244c8840887984c4.jpg", + "customThumbnailUrl": "http://example.com/" + "generated-thumbnails/custom-thumbnails/sample_1_244c8840887984c4.jpg", "flags": ["loop"], "tags": [ { @@ -270,17 +272,27 @@ def test_serialize_post( "relationCount": 0, "lastFeatureTime": datetime(1999, 1, 1), "favoritedBy": ["fav1"], - "hasCustomThumbnail": True, "mimeType": "image/jpeg", "comments": ["commenter1", "commenter2"], } -def test_serialize_micro_post(post_factory, user_factory): +def test_serialize_micro_post(tmpdir, config_injector, post_factory, user_factory): with patch("szurubooru.func.posts.get_post_thumbnail_url"): posts.get_post_thumbnail_url.return_value = ( "https://example.com/thumb.png" ) + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) auth_user = user_factory() post = post_factory() db.session.add(post) @@ -288,6 +300,7 @@ def test_serialize_micro_post(post_factory, user_factory): assert posts.serialize_micro_post(post, auth_user) == { "id": post.post_id, "thumbnailUrl": "https://example.com/thumb.png", + "customThumbnailUrl": None, } @@ -605,7 +618,7 @@ def test_update_post_thumbnail_to_new_one( assert post.post_id generated_path = ( "{}/data/generated-thumbnails/".format(tmpdir) - + "1_244c8840887984c4.jpg" + + "sample_1_244c8840887984c4.jpg" ) source_path = ( "{}/data/posts/custom-thumbnails/".format(tmpdir) @@ -646,7 +659,7 @@ def test_update_post_thumbnail_to_default( assert post.post_id generated_path = ( "{}/data/generated-thumbnails/".format(tmpdir) - + "1_244c8840887984c4.jpg" + + "sample_1_244c8840887984c4.jpg" ) source_path = ( "{}/data/posts/custom-thumbnails/".format(tmpdir) @@ -686,7 +699,7 @@ def test_update_post_thumbnail_with_broken_thumbnail( assert post.post_id generated_path = ( "{}/data/generated-thumbnails/".format(tmpdir) - + "1_244c8840887984c4.jpg" + + "sample_1_244c8840887984c4.jpg" ) source_path = ( "{}/data/posts/custom-thumbnails/".format(tmpdir) @@ -705,8 +718,8 @@ def test_update_post_thumbnail_with_broken_thumbnail( assert handle.read() == read_asset("png-broken.png") with open(generated_path, "rb") as handle: image = images.Image(handle.read()) - assert image.width == 1 - assert image.height == 1 + assert image.width == 300 + assert image.height == 300 def test_update_post_content_leaving_custom_thumbnail( @@ -731,7 +744,7 @@ def test_update_post_content_leaving_custom_thumbnail( db.session.flush() generated_path = ( "{}/data/generated-thumbnails/".format(tmpdir) - + "1_244c8840887984c4.jpg" + + "sample_1_244c8840887984c4.jpg" ) source_path = ( "{}/data/posts/custom-thumbnails/".format(tmpdir) @@ -763,7 +776,7 @@ def test_update_post_content_convert_heif_to_png_when_processing( db.session.flush() generated_path = ( "{}/data/generated-thumbnails/".format(tmpdir) - + "1_244c8840887984c4.jpg" + + "sample_1_244c8840887984c4.jpg" ) source_path = ( "{}/data/posts/custom-thumbnails/".format(tmpdir)