mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Merge 14a4386561
into ee7e9ef2a3
This commit is contained in:
@ -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
|
||||
@ -287,6 +287,7 @@ a .access-key
|
||||
background-size: cover
|
||||
background-position: center
|
||||
display: inline-block
|
||||
overflow: hidden
|
||||
width: 20px
|
||||
height: 20px
|
||||
&.empty
|
||||
@ -298,13 +299,12 @@ a .access-key
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
|
||||
background-repeat: repeat
|
||||
background-size: 20px 20px
|
||||
img
|
||||
img, video
|
||||
opacity: 0
|
||||
object-fit: cover
|
||||
width: 100%
|
||||
height: 100%
|
||||
video
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: block
|
||||
|
||||
.flexbox-dummy
|
||||
height: 0 !important
|
||||
|
@ -1,14 +1,16 @@
|
||||
@import colors
|
||||
|
||||
.post-container
|
||||
.post-content.transparency-grid img
|
||||
.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-size: 20px 20px
|
||||
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
|
||||
|
@ -124,6 +124,8 @@
|
||||
li
|
||||
margin: 0 0.3em 0.3em 0
|
||||
display: inline-block
|
||||
a
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
|
||||
|
||||
.tags
|
||||
margin-top: 2em
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
|
@ -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) {
|
||||
|
@ -14,6 +14,7 @@ const EmptyView = require("../views/empty_view.js");
|
||||
const fields = [
|
||||
"id",
|
||||
"thumbnailUrl",
|
||||
"customThumbnailUrl",
|
||||
"type",
|
||||
"safety",
|
||||
"score",
|
||||
|
@ -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.");
|
||||
|
@ -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,
|
||||
});
|
||||
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 {
|
||||
|
@ -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
|
||||
|
@ -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]) {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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" })
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
},
|
||||
|
@ -31,6 +31,7 @@ class PostMainView {
|
||||
this._postContentControl = new PostContentControl(
|
||||
postContainerNode,
|
||||
ctx.post,
|
||||
misc.isMediaCached(ctx.post),
|
||||
() => {
|
||||
const margin = sidebarNode.getBoundingClientRect().left;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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()
|
||||
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,
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user