This commit is contained in:
Eva
2025-03-28 16:17:24 +00:00
committed by GitHub
23 changed files with 242 additions and 124 deletions

View File

@ -106,6 +106,11 @@ form .fa-question-circle-o
background-color: $scrollbar-bg-color background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb &::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color 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) >.content-wrapper:not(.transparent)
background: $top-navigation-color background: $top-navigation-color
padding: 1.8em padding: 1.8em
@ -214,8 +219,6 @@ nav
ul li[data-name=settings], ul li[data-name=settings],
ul li[data-name=help] ul li[data-name=help]
float: none float: none
.access-key
text-decoration: underline
.thumbnail .thumbnail
width: 1.5em width: 1.5em
height: 1.5em height: 1.5em
@ -244,9 +247,6 @@ nav
#mobile-navigation-toggle #mobile-navigation-toggle
color: $text-color-darktheme color: $text-color-darktheme
a .access-key
text-decoration: underline
.messages .messages
margin: 0 auto margin: 0 auto
text-align: left text-align: left
@ -287,6 +287,7 @@ a .access-key
background-size: cover background-size: cover
background-position: center background-position: center
display: inline-block display: inline-block
overflow: hidden
width: 20px width: 20px
height: 20px height: 20px
&.empty &.empty
@ -298,13 +299,12 @@ a .access-key
background-position: 0 0, 0 10px, 10px -10px, -10px 0px background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat background-repeat: repeat
background-size: 20px 20px background-size: 20px 20px
img img, video
opacity: 0 opacity: 0
object-fit: cover
width: 100% width: 100%
height: 100% height: 100%
video display: block
width: 100%
height: 100%
.flexbox-dummy .flexbox-dummy
height: 0 !important height: 0 !important

View File

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

@ -124,6 +124,8 @@
li li
margin: 0 0.3em 0.3em 0 margin: 0 0.3em 0.3em 0
display: inline-block display: inline-block
a
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
.tags .tags
margin-top: 2em margin-top: 2em

View File

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

View File

@ -1,13 +1,14 @@
<div class='post-content post-type-<%- ctx.post.type %>'> <div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(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') { %> <% } else if (ctx.post.type === 'flash') { %>
<object class='resize-listener' width='<%- ctx.post.canvasWidth %>' height='<%- ctx.post.canvasHeight %>' data='<%- ctx.post.contentUrl %>'> <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 %>'/> <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> </object>
<% } else if (ctx.post.type === 'video') { %> <% } else if (ctx.post.type === 'video') { %>
@ -19,6 +20,8 @@
loop: (ctx.post.flags || []).includes('loop'), loop: (ctx.post.flags || []).includes('loop'),
playsinline: true, playsinline: true,
autoplay: ctx.autoplay, autoplay: ctx.autoplay,
preload: 'auto',
poster: ctx.post.originalThumbnailUrl,
}, },
ctx.makeElement('source', { ctx.makeElement('source', {
type: ctx.post.mimeType, type: ctx.post.mimeType,

View File

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

View File

@ -294,11 +294,16 @@ class Api extends events.EventTarget {
// transform the request: upload each file, then make the request use // transform the request: upload each file, then make the request use
// its tokens. // its tokens.
data = Object.assign({}, data); data = Object.assign({}, data);
let fileData = {};
let abortFunction = () => {}; let abortFunction = () => {};
let promise = Promise.resolve(); let promise = Promise.resolve();
if (files) { if (files) {
for (let key of Object.keys(files)) { for (let key of Object.keys(files)) {
const file = files[key]; const file = files[key];
if (file === null) {
fileData[key] = null;
continue;
}
const fileId = this._getFileId(file); const fileId = this._getFileId(file);
if (fileTokens[fileId]) { if (fileTokens[fileId]) {
data[key + "Token"] = fileTokens[fileId]; data[key + "Token"] = fileTokens[fileId];
@ -324,7 +329,7 @@ class Api extends events.EventTarget {
url, url,
requestFactory, requestFactory,
data, data,
{}, fileData,
options options
); );
abortFunction = () => requestPromise.abort(); abortFunction = () => requestPromise.abort();
@ -388,7 +393,7 @@ class Api extends events.EventTarget {
if (files) { if (files) {
for (let key of Object.keys(files)) { for (let key of Object.keys(files)) {
const value = files[key]; const value = files[key];
if (value.constructor === String) { if (value !== null && value.constructor === String) {
data[key + "Url"] = value; data[key + "Url"] = value;
} else { } else {
req.attach(key, value || new Blob()); 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 CommentsPageView = require("../views/comments_page_view.js");
const EmptyView = require("../views/empty_view.js"); const EmptyView = require("../views/empty_view.js");
const fields = ["id", "comments", "commentCount", "thumbnailUrl"]; const fields = ["id", "comments", "commentCount", "thumbnailUrl", "customThumbnailUrl"];
class CommentsController { class CommentsController {
constructor(ctx) { constructor(ctx) {

View File

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

View File

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

View File

@ -143,9 +143,28 @@ class PostContentControl {
post: this._post, post: this._post,
autoplay: settings.get().autoplayVideos, 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.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) { if (this._postContentNode) {
this._hostNode.replaceChild(newNode, this._postContentNode); this._hostNode.replaceChild(newNode, this._postContentNode);
} else { } else {

View File

@ -138,10 +138,7 @@ class PostEditSidebarControl extends events.EventTarget {
this._thumbnailRemovalLinkNode.addEventListener("click", (e) => this._thumbnailRemovalLinkNode.addEventListener("click", (e) =>
this._evtRemoveThumbnailClick(e) this._evtRemoveThumbnailClick(e)
); );
this._thumbnailRemovalLinkNode.style.display = this._post this._thumbnailRemovalLinkUpdate(this._post);
.hasCustomThumbnail
? "block"
: "none";
} }
if (this._addNoteLinkNode) { if (this._addNoteLinkNode) {
@ -249,12 +246,25 @@ class PostEditSidebarControl extends events.EventTarget {
this._poolsExpander.title = `Pools (${this._post.pools.length})`; this._poolsExpander.title = `Pools (${this._post.pools.length})`;
} }
_thumbnailRemovalLinkUpdate(post) {
if (this._thumbnailRemovalLinkNode) {
this._thumbnailRemovalLinkNode.style.display = post
.customThumbnailUrl
? "block"
: "none";
}
}
_evtPostContentChange(e) { _evtPostContentChange(e) {
this._contentFileDropper.reset(); this._contentFileDropper.reset();
this._thumbnailRemovalLinkUpdate(e.detail.post);
this._newPostContent = null;
} }
_evtPostThumbnailChange(e) { _evtPostThumbnailChange(e) {
this._thumbnailFileDropper.reset(); this._thumbnailFileDropper.reset();
this._thumbnailRemovalLinkUpdate(e.detail.post);
this._newPostThumbnail = undefined;
} }
_evtRemoveThumbnailClick(e) { _evtRemoveThumbnailClick(e) {
@ -427,9 +437,7 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined, : undefined,
thumbnail: thumbnail:
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null this._newPostThumbnail,
? this._newPostThumbnail
: undefined,
source: this._sourceInputNode source: this._sourceInputNode
? this._sourceInputNode.value ? this._sourceInputNode.value

View File

@ -70,6 +70,14 @@ class Post extends events.EventTarget {
return this._thumbnailUrl; return this._thumbnailUrl;
} }
get customThumbnailUrl() {
return this._customThumbnailUrl;
}
get originalThumbnailUrl() {
return this._originalThumbnailUrl;
}
get source() { get source() {
return this._source; return this._source;
} }
@ -146,10 +154,6 @@ class Post extends events.EventTarget {
return this._ownScore; return this._ownScore;
} }
get hasCustomThumbnail() {
return this._hasCustomThumbnail;
}
set flags(value) { set flags(value) {
this._flags = value; this._flags = value;
} }
@ -477,7 +481,9 @@ class Post extends events.EventTarget {
response.contentUrl, response.contentUrl,
document.getElementsByTagName("base")[0].href document.getElementsByTagName("base")[0].href
).href, ).href,
_thumbnailUrl: response.thumbnailUrl, _thumbnailUrl: response.customThumbnailUrl ? response.customThumbnailUrl : response.thumbnailUrl,
_customThumbnailUrl: response.customThumbnailUrl,
_originalThumbnailUrl: response.thumbnailUrl,
_source: response.source, _source: response.source,
_canvasWidth: response.canvasWidth, _canvasWidth: response.canvasWidth,
_canvasHeight: response.canvasHeight, _canvasHeight: response.canvasHeight,
@ -491,7 +497,6 @@ class Post extends events.EventTarget {
_favoriteCount: response.favoriteCount, _favoriteCount: response.favoriteCount,
_ownScore: response.ownScore, _ownScore: response.ownScore,
_ownFavorite: response.ownFavorite, _ownFavorite: response.ownFavorite,
_hasCustomThumbnail: response.hasCustomThumbnail,
}); });
for (let obj of [this, this._orig]) { for (let obj of [this, this._orig]) {

View File

@ -49,7 +49,7 @@ function makeThumbnail(url) {
style: `background-image: url(\'${url}\')`, style: `background-image: url(\'${url}\')`,
} }
: { class: "thumbnail empty" }, : { 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) => .addEventListener("click", (e) =>
this._evtMoveClick(e, uploadable, 1) 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) { _updateThumbnailNode(uploadable) {

View File

@ -33,6 +33,21 @@ RUN pip3 install --no-cache-dir --disable-pip-version-check \
"pillow-avif-plugin~=1.1.0" "pillow-avif-plugin~=1.1.0"
RUN apk --no-cache del py3-pip 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/ COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests RUN rm -rf /opt/app/szurubooru/tests

View File

@ -13,7 +13,7 @@ from getpass import getpass
from sys import stderr from sys import stderr
from szurubooru import config, db, errors, model 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 posts as postfuncs
from szurubooru.func import users as userfuncs from szurubooru.func import users as userfuncs
@ -95,7 +95,14 @@ def regenerate_thumbnails() -> None:
for post in db.session.query(model.Post).all(): for post in db.session.query(model.Post).all():
print("Generating tumbnail for post %d ..." % post.post_id, end="\r") print("Generating tumbnail for post %d ..." % post.post_id, end="\r")
try: 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: except Exception:
pass pass

View File

@ -1,4 +1,5 @@
import os import os
import glob
from typing import Any, List, Optional from typing import Any, List, Optional
from szurubooru import config from szurubooru import config
@ -24,6 +25,10 @@ def scan(path: str) -> List[Any]:
return [] 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: def move(source_path: str, target_path: str) -> None:
os.rename(_get_full_path(source_path), _get_full_path(target_path)) os.rename(_get_full_path(source_path), _get_full_path(target_path))

View File

@ -24,6 +24,11 @@ def convert_heif_to_png(content: bytes) -> bytes:
return img_byte_arr.getvalue() return img_byte_arr.getvalue()
def check_for_loop(content: bytes) -> bytes:
img = PILImage.open(BytesIO(content))
return "loop" in img.info
class Image: class Image:
def __init__(self, content: bytes) -> None: def __init__(self, content: bytes) -> None:
self.content = content self.content = content
@ -41,7 +46,7 @@ class Image:
def frames(self) -> int: def frames(self) -> int:
return self.info["streams"][0]["nb_read_frames"] 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_greater = self.width > self.height
width, height = (-1, height) if width_greater else (width, -1) width, height = (-1, height) if width_greater else (width, -1)
@ -50,8 +55,12 @@ class Image:
"{path}", "{path}",
"-f", "-f",
"image2", "image2",
"-filter:v", "-filter_complex",
"scale='{width}:{height}'".format(width=width, height=height), (
"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", "-map",
"0:v:0", "0:v:0",
"-vframes", "-vframes",
@ -61,7 +70,8 @@ class Image:
"-", "-",
] ]
if ( if (
"duration" in self.info["format"] seek
and "duration" in self.info["format"]
and self.info["format"]["format_name"] != "swf" and self.info["format"]["format_name"] != "swf"
): ):
duration = float(self.info["format"]["duration"]) duration = float(self.info["format"]["duration"])
@ -96,24 +106,13 @@ class Image:
def to_jpeg(self) -> bytes: def to_jpeg(self) -> bytes:
return self._execute( return self._execute(
[ [
"-f", "-quality",
"lavfi", "85",
"-i", "-sample",
"color=white:s=%dx%d" % (self.width, self.height), "1x1",
"-i",
"{path}", "{path}",
"-f", ],
"image2", program="cjpeg",
"-filter_complex",
"overlay",
"-map",
"0:v:0",
"-vframes",
"1",
"-vcodec",
"mjpeg",
"-",
]
) )
def to_webm(self) -> bytes: def to_webm(self) -> bytes:
@ -274,7 +273,10 @@ class Image:
with util.create_temp_file(suffix="." + extension) as handle: with util.create_temp_file(suffix="." + extension) as handle:
handle.write(self.content) handle.write(self.content)
handle.flush() 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] cli = [part.format(path=handle.name) for part in cli]
proc = subprocess.Popen( proc = subprocess.Popen(
cli, cli,
@ -285,7 +287,7 @@ class Image:
out, err = proc.communicate() out, err = proc.communicate()
if proc.returncode != 0: if proc.returncode != 0:
logger.warning( 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), " ".join(shlex.quote(arg) for arg in cli),
err, err,
) )

View File

@ -117,7 +117,16 @@ def get_post_content_url(post: model.Post) -> str:
def get_post_thumbnail_url(post: model.Post) -> str: def get_post_thumbnail_url(post: model.Post) -> str:
assert post 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("/"), config.config["data_url"].rstrip("/"),
post.post_id, post.post_id,
get_post_security_hash(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 assert post
return "generated-thumbnails/%d_%s.jpg" % ( assert post.post_id
return "posts/custom-thumbnails/%d_%s.dat" % (
post.post_id, post.post_id,
get_post_security_hash(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 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, post.post_id,
get_post_security_hash(post.post_id), get_post_security_hash(post.post_id),
) )
@ -180,6 +198,7 @@ class PostSerializer(serialization.BaseSerializer):
"canvasHeight": self.serialize_canvas_height, "canvasHeight": self.serialize_canvas_height,
"contentUrl": self.serialize_content_url, "contentUrl": self.serialize_content_url,
"thumbnailUrl": self.serialize_thumbnail_url, "thumbnailUrl": self.serialize_thumbnail_url,
"customThumbnailUrl": self.serialize_custom_thumbnail_url,
"flags": self.serialize_flags, "flags": self.serialize_flags,
"tags": self.serialize_tags, "tags": self.serialize_tags,
"relations": self.serialize_relations, "relations": self.serialize_relations,
@ -195,7 +214,6 @@ class PostSerializer(serialization.BaseSerializer):
"featureCount": self.serialize_feature_count, "featureCount": self.serialize_feature_count,
"lastFeatureTime": self.serialize_last_feature_time, "lastFeatureTime": self.serialize_last_feature_time,
"favoritedBy": self.serialize_favorited_by, "favoritedBy": self.serialize_favorited_by,
"hasCustomThumbnail": self.serialize_has_custom_thumbnail,
"notes": self.serialize_notes, "notes": self.serialize_notes,
"comments": self.serialize_comments, "comments": self.serialize_comments,
"pools": self.serialize_pools, "pools": self.serialize_pools,
@ -319,8 +337,9 @@ class PostSerializer(serialization.BaseSerializer):
for rel in self.post.favorited_by for rel in self.post.favorited_by
] ]
def serialize_has_custom_thumbnail(self) -> Any: def serialize_custom_thumbnail_url(self) -> Any:
return files.has(get_post_thumbnail_backup_path(self.post)) if files.has(get_post_custom_thumbnail_path(self.post)):
return get_post_custom_thumbnail_url(self.post)
def serialize_notes(self) -> Any: def serialize_notes(self) -> Any:
return sorted( return sorted(
@ -357,7 +376,7 @@ def serialize_micro_post(
post: model.Post, auth_user: model.User post: model.Post, auth_user: model.User
) -> Optional[rest.Response]: ) -> Optional[rest.Response]:
return serialize_post( 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: ) -> None:
if post.post_id: if post.post_id:
if config.config["delete_source_files"]: if config.config["delete_source_files"]:
files.delete(get_post_content_path(post)) pattern = f"{post.post_id}_*"
files.delete(get_post_thumbnail_path(post)) 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: def _sync_post_content(post: model.Post) -> None:
regenerate_thumb = False
if hasattr(post, "__content"): if hasattr(post, "__content"):
content = getattr(post, "__content") content = getattr(post, "__content")
files.save(get_post_content_path(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") delattr(post, "__content")
regenerate_thumb = True
if hasattr(post, "__thumbnail"): if hasattr(post, "__thumbnail"):
if getattr(post, "__thumbnail"): if getattr(post, "__thumbnail"):
files.save( thumbnail = getattr(post, "__thumbnail")
get_post_thumbnail_backup_path(post), files.save(get_post_custom_content_path(post), thumbnail)
getattr(post, "__thumbnail"), generate_post_thumbnail(get_post_custom_thumbnail_path(post), thumbnail, seek=True)
)
else: else:
files.delete(get_post_thumbnail_backup_path(post)) files.delete(get_post_custom_thumbnail_path(post))
delattr(post, "__thumbnail") delattr(post, "__thumbnail")
regenerate_thumb = True
if regenerate_thumb:
generate_post_thumbnail(post)
def generate_alternate_formats( def generate_alternate_formats(
@ -677,22 +692,19 @@ def update_post_thumbnail(
setattr(post, "__thumbnail", content) setattr(post, "__thumbnail", content)
def generate_post_thumbnail(post: model.Post) -> None: def generate_post_thumbnail(path: str, content: bytes, seek=True) -> 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))
try: try:
assert content assert content
image = images.Image(content) image = images.Image(content)
image.resize_fill( image.resize_fill(
int(config.config["thumbnails"]["post_width"]), int(config.config["thumbnails"]["post_width"]),
int(config.config["thumbnails"]["post_height"]), 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: except errors.ProcessingError:
files.save(get_post_thumbnail_path(post), EMPTY_PIXEL) files.save(path, EMPTY_PIXEL)
def update_post_tags( def update_post_tags(

View File

@ -311,6 +311,8 @@ def update_user_avatar(
image.resize_fill( image.resize_fill(
int(config.config["thumbnails"]["avatar_width"]), int(config.config["thumbnails"]["avatar_width"]),
int(config.config["thumbnails"]["avatar_height"]), int(config.config["thumbnails"]["avatar_height"]),
keep_transparency=False,
seek=False,
) )
files.save(avatar_path, image.to_png()) files.save(avatar_path, image.to_png())
else: else:

View File

@ -51,7 +51,7 @@ class Context:
use_video_downloader: bool = False, use_video_downloader: bool = False,
allow_tokens: bool = True, allow_tokens: bool = True,
) -> bytes: ) -> bytes:
if name in self._files and self._files[name]: if name in self._files:
return self._files[name] return self._files[name]
if name + "Url" in self._params: if name + "Url" in self._params:

View File

@ -41,7 +41,7 @@ def test_get_post_thumbnail_url(input_mime_type, config_injector):
post.mime_type = input_mime_type post.mime_type = input_mime_type
assert ( assert (
posts.get_post_thumbnail_url(post) 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 post.mime_type = input_mime_type
assert ( assert (
posts.get_post_thumbnail_path(post) 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"]) @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 = model.Post()
post.post_id = 1 post.post_id = 1
post.mime_type = input_mime_type post.mime_type = input_mime_type
assert ( assert (
posts.get_post_thumbnail_backup_path(post) posts.get_post_custom_thumbnail_path(post)
== "posts/custom-thumbnails/1_244c8840887984c4.dat" == "generated-thumbnails/custom-thumbnails/sample_1_244c8840887984c4.jpg"
) )
@ -226,7 +226,9 @@ def test_serialize_post(
"canvasHeight": 300, "canvasHeight": 300,
"contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg", "contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg",
"thumbnailUrl": "http://example.com/" "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"], "flags": ["loop"],
"tags": [ "tags": [
{ {
@ -270,17 +272,27 @@ def test_serialize_post(
"relationCount": 0, "relationCount": 0,
"lastFeatureTime": datetime(1999, 1, 1), "lastFeatureTime": datetime(1999, 1, 1),
"favoritedBy": ["fav1"], "favoritedBy": ["fav1"],
"hasCustomThumbnail": True,
"mimeType": "image/jpeg", "mimeType": "image/jpeg",
"comments": ["commenter1", "commenter2"], "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"): with patch("szurubooru.func.posts.get_post_thumbnail_url"):
posts.get_post_thumbnail_url.return_value = ( posts.get_post_thumbnail_url.return_value = (
"https://example.com/thumb.png" "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() auth_user = user_factory()
post = post_factory() post = post_factory()
db.session.add(post) 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) == { assert posts.serialize_micro_post(post, auth_user) == {
"id": post.post_id, "id": post.post_id,
"thumbnailUrl": "https://example.com/thumb.png", "thumbnailUrl": "https://example.com/thumb.png",
"customThumbnailUrl": None,
} }
@ -605,7 +618,7 @@ def test_update_post_thumbnail_to_new_one(
assert post.post_id assert post.post_id
generated_path = ( generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir) "{}/data/generated-thumbnails/".format(tmpdir)
+ "1_244c8840887984c4.jpg" + "sample_1_244c8840887984c4.jpg"
) )
source_path = ( source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir) "{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -646,7 +659,7 @@ def test_update_post_thumbnail_to_default(
assert post.post_id assert post.post_id
generated_path = ( generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir) "{}/data/generated-thumbnails/".format(tmpdir)
+ "1_244c8840887984c4.jpg" + "sample_1_244c8840887984c4.jpg"
) )
source_path = ( source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir) "{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -686,7 +699,7 @@ def test_update_post_thumbnail_with_broken_thumbnail(
assert post.post_id assert post.post_id
generated_path = ( generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir) "{}/data/generated-thumbnails/".format(tmpdir)
+ "1_244c8840887984c4.jpg" + "sample_1_244c8840887984c4.jpg"
) )
source_path = ( source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir) "{}/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") assert handle.read() == read_asset("png-broken.png")
with open(generated_path, "rb") as handle: with open(generated_path, "rb") as handle:
image = images.Image(handle.read()) image = images.Image(handle.read())
assert image.width == 1 assert image.width == 300
assert image.height == 1 assert image.height == 300
def test_update_post_content_leaving_custom_thumbnail( def test_update_post_content_leaving_custom_thumbnail(
@ -731,7 +744,7 @@ def test_update_post_content_leaving_custom_thumbnail(
db.session.flush() db.session.flush()
generated_path = ( generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir) "{}/data/generated-thumbnails/".format(tmpdir)
+ "1_244c8840887984c4.jpg" + "sample_1_244c8840887984c4.jpg"
) )
source_path = ( source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir) "{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -763,7 +776,7 @@ def test_update_post_content_convert_heif_to_png_when_processing(
db.session.flush() db.session.flush()
generated_path = ( generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir) "{}/data/generated-thumbnails/".format(tmpdir)
+ "1_244c8840887984c4.jpg" + "sample_1_244c8840887984c4.jpg"
) )
source_path = ( source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir) "{}/data/posts/custom-thumbnails/".format(tmpdir)