20 Commits

Author SHA1 Message Date
2fb2ffb2e1 server/posts: fix custom thumbnails 2025-03-27 02:31:47 +01:00
3f42037e2b server/posts: fix source file deletion 2025-03-27 02:27:42 +01:00
Eva
7972c34448 client/posts: make discard thumbnail link delete existing custom thumb
I can see the intent, sadly this was always broken in the case where the
post already has a custom thumbnail from initial load, and we don't drag
any new files. It did not actually remove the existing thumbnail.
Before 12c4542bb2482fac89aae9a04b15984a56bb8fb0 it would actually crash,
but this now makes it behave as expected.
Also properly syncs internal state with what's displayed to the user.
2024-03-28 13:18:02 +01:00
Eva
a496e8980f server/rest: allow files with empty content 2024-03-28 13:15:06 +01:00
Eva
096b6bc61e client/css: fix overextended broken thumbnail 2024-03-22 00:04:32 +01:00
Eva
41a681b254 client/posts: prioritize main image load 2024-03-22 00:04:32 +01:00
Eva
c843bbb35e client/posts: use original thumbnail for video poster 2024-03-22 00:04:32 +01:00
Eva
74eaa22662 client, server: rework custom thumbnails
Saving custom thumbnails separately allows us to display them in search
results etc while also displaying a thumbnail of the final content
during loading.
2024-03-22 00:04:31 +01:00
Eva
ad622c4d99 client/posts: more robust fallbacks on error
Fallback cascade: original content, thumbnail, transparency grid
Implementation is very ugly but handles all cases nicely.
2024-03-21 22:11:11 +01:00
Eva
3d27bcaab5 client/upload: restore video previews 2024-03-21 22:05:35 +01:00
Eva
2d71ea0e05 client/upload: fix empty thumbnail size 2024-03-21 22:05:18 +01:00
Eva
ac303db9e6 client/posts: display first video frame when available 2024-03-21 22:04:23 +01:00
Eva
7405593101 client/posts: thumbnail as video poster 2024-03-21 22:04:19 +01:00
Eva
5877ad9463 client/posts: display thumbnail while original image is loading 2024-03-21 22:04:16 +01:00
Eva
f956d8033c client/css: prevent thumbnail dragging, fix ff upload thumbnail outline 2024-03-21 22:03:52 +01:00
Eva
571cd90fd2 client/upload: remove duplicate id 2024-03-21 22:02:11 +01:00
Eva
436a693be1 client/upload: preview video time that will be used for final thumbnail 2024-03-21 22:02:01 +01:00
Eva
4220ae708d server/images: use white background for non-transparent images 2024-03-21 21:58:43 +01:00
Eva
64c5eec3d2 server/images: resize images in rgb, explicitly use bicubic
Indexed color PNGs would use their palette during scaling, leading to
very ugly dithering.
Convert to RGB32/RGB24, depending on if we intend to keep transparency.
For RGB24 this sets background color from the palette if there was one,
black otherwise although that may be undesirable.
Will have to find a way to fall back to a nicer color, or always use
the same color that we configure ourselves.
2024-03-21 21:58:35 +01:00
Eva
80840b9509 server/images: use mozjpeg/libjpeg-turbo for jpeg conversion 2024-03-21 21:58:13 +01:00
19 changed files with 207 additions and 119 deletions

View File

@ -106,6 +106,11 @@ form .fa-question-circle-o
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
li[data-name=view]
background: $button-enabled-background-color
margin-right: 1em
a
color: $button-enabled-text-color
>.content-wrapper:not(.transparent)
background: $top-navigation-color
padding: 1.8em
@ -214,8 +219,6 @@ nav
ul li[data-name=settings],
ul li[data-name=help]
float: none
.access-key
text-decoration: underline
.thumbnail
width: 1.5em
height: 1.5em
@ -244,9 +247,6 @@ nav
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
.messages
margin: 0 auto
text-align: left
@ -284,28 +284,25 @@ a .access-key
/*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle
background-repeat: no-repeat
background-size: cover
background-size: contain
background-position: center
display: inline-block
overflow: hidden
width: 20px
height: 20px
&.empty
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: 20px 20px
img
repeating-linear-gradient(45deg, $window-color, $window-color 10px, #e6e6e6 10px, #e6e6e6 20px)
img, video
opacity: 0
width: auto
height: 100%
video
width: auto
object-fit: cover
width: 100%
height: 100%
.darktheme .thumbnail.empty
background-image:
repeating-linear-gradient(45deg, $window-color-darktheme, $window-color-darktheme 10px, #333 10px, #333 20px)
.flexbox-dummy
height: 0 !important
padding-top: 0 !important

View File

@ -1,14 +1,16 @@
@import colors
.post-container
.post-content.transparency-grid img
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-size: 20px 20px
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
.post-content
&.transparency-grid, &.post-error
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: 20px 20px
text-align: center
.post-content
@ -17,6 +19,8 @@
position: relative
.resize-listener
background-repeat: no-repeat
background-size: cover
position: absolute
left: 0
right: 0
@ -27,3 +31,14 @@
img
image-orientation: from-image
.darktheme .post-container .post-content
&.transparency-grid, &.post-error
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: 20px 20px

View File

@ -62,22 +62,22 @@ $cancel-button-color = tomato
margin: 0 0 1.2em 0
padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper
float: left
width: 12em
height: 8em
margin: 0 0 0 -13em
a
display: block
height: 100%
width: 100%
.thumbnail
width: 100%
height: 100%
video
opacity: 1
img, video
object-fit: contain
.uploadable
border: 1px solid $upload-border-color

View File

@ -1,13 +1,14 @@
<div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>' draggable='false' fetchPriority='high'/>
<% } else if (ctx.post.type === 'flash') { %>
<object class='resize-listener' width='<%- ctx.post.canvasWidth %>' height='<%- ctx.post.canvasHeight %>' data='<%- ctx.post.contentUrl %>'>
<param name='wmode' value='opaque'/>
<param name='wmode' value='transparent'/>
<param name='movie' value='<%- ctx.post.contentUrl %>'/>
<div class='messages'><div class='message-wrapper'><div class='message error'>Your browser does not support Flash.</div></div></div>
</object>
<% } 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,

View File

@ -10,7 +10,7 @@
<div class='thumbnail'>
<a href='<%= ctx.uploadable.previewUrl %>'>
<video id='video' nocontrols muted>
<video nocontrols muted>
<source type='<%- ctx.uploadable.mimeType %>' src='<%- ctx.uploadable.previewUrl %>'/>
</video>
</a>

View File

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

View File

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

View File

@ -14,6 +14,7 @@ const EmptyView = require("../views/empty_view.js");
const fields = [
"id",
"thumbnailUrl",
"customThumbnailUrl",
"type",
"safety",
"score",

View File

@ -178,15 +178,11 @@ class PostMainController extends BasePostController {
if (e.detail.relations !== undefined) {
post.relations = e.detail.relations;
}
if (e.detail.content !== undefined) {
post.newContent = e.detail.content;
}
if (e.detail.thumbnail !== undefined) {
post.newThumbnail = e.detail.thumbnail;
}
if (e.detail.source !== undefined) {
post.source = e.detail.source;
}
post.newContent = e.detail.content;
post.newThumbnail = e.detail.thumbnail;
post.save().then(
() => {
this._view.sidebarControl.showSuccess("Post saved.");

View File

@ -119,9 +119,28 @@ 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)) {
newNode.firstElementChild.style.backgroundImage = "url("+this._post.originalThumbnailUrl+")";
}
if (this._post.type == "image") {
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 {

View File

@ -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
: undefined,
this._newPostThumbnail,
source: this._sourceInputNode
? this._sourceInputNode.value

View File

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

View File

@ -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" })
);
}

View File

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

View File

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

View File

@ -24,10 +24,18 @@ 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
self._reload_info()
if self.info["format"]["format_name"] == "swf":
self.content = self.swf_to_png()
self._reload_info()
@property
def width(self) -> int:
@ -41,7 +49,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 +58,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",
@ -60,10 +72,7 @@ class Image:
"png",
"-",
]
if (
"duration" in self.info["format"]
and self.info["format"]["format_name"] != "swf"
):
if seek and "duration" in self.info["format"]:
duration = float(self.info["format"]["duration"])
if duration > 3:
cli = [
@ -76,6 +85,19 @@ class Image:
self.content = content
self._reload_info()
def swf_to_png(self) -> bytes:
return self._execute(
[
"--silent",
"-g",
"gl",
"--",
"{path}",
"-",
],
program="exporter",
)
def to_png(self) -> bytes:
return self._execute(
[
@ -96,24 +118,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 +285,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 +299,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,
)
@ -315,7 +329,7 @@ class Image:
)
assert "format" in self.info
assert "streams" in self.info
if len(self.info["streams"]) < 1:
if len(self.info["streams"]) < 1 and self.info["format"]["format_name"] != "swf":
logger.warning("The video contains no video streams.")
raise errors.ProcessingError(
"The video contains no video streams."

View File

@ -124,6 +124,15 @@ def get_post_thumbnail_url(post: model.Post) -> str:
)
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),
)
def get_post_content_path(post: model.Post) -> str:
assert post
assert post.post_id
@ -134,6 +143,15 @@ def get_post_content_path(post: model.Post) -> str:
)
def get_post_custom_content_path(post: model.Post) -> str:
assert post
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_path(post: model.Post) -> str:
assert post
return "generated-thumbnails/%d_%s.jpg" % (
@ -142,9 +160,9 @@ def get_post_thumbnail_path(post: model.Post) -> str:
)
def get_post_thumbnail_backup_path(post: model.Post) -> str:
def get_post_custom_thumbnail_path(post: model.Post) -> str:
assert post
return "posts/custom-thumbnails/%d_%s.dat" % (
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(

View File

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

View File

@ -72,12 +72,12 @@ def test_get_post_thumbnail_path(input_mime_type):
@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.get_post_custom_thumbnail_path(post)
== "posts/custom-thumbnails/1_244c8840887984c4.dat"
)