1 Commits

Author SHA1 Message Date
1daa31886e build(deps-dev): bump browserify-sign from 4.0.4 to 4.2.2 in /client
Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.0.4 to 4.2.2.
- [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.0.4...v4.2.2)

---
updated-dependencies:
- dependency-name: browserify-sign
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 21:16:03 +00:00
46 changed files with 372 additions and 472 deletions

View File

@ -8,7 +8,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
- Post comments
- Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md))

View File

@ -127,10 +127,6 @@ $comment-border-color = #DDD
color: mix($main-color, $inactive-link-color-darktheme)
.comment-content
p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside
margin: 1em 0

View File

@ -106,11 +106,6 @@ 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
@ -219,6 +214,8 @@ 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
@ -247,6 +244,9 @@ nav
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
.messages
margin: 0 auto
text-align: left
@ -287,7 +287,6 @@ nav
background-size: cover
background-position: center
display: inline-block
overflow: hidden
width: 20px
height: 20px
&.empty
@ -299,12 +298,13 @@ nav
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: 20px 20px
img, video
img
opacity: 0
object-fit: cover
width: 100%
width: auto
height: 100%
video
width: auto
height: 100%
display: block
.flexbox-dummy
height: 0 !important

View File

@ -1,16 +1,14 @@
@import colors
.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
.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
text-align: center
.post-content
@ -19,8 +17,6 @@
position: relative
.resize-listener
background-repeat: no-repeat
background-size: cover
position: absolute
left: 0
right: 0
@ -31,14 +27,3 @@
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

@ -187,9 +187,6 @@
vertical-align: top
@media (max-width: 1000px)
display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input
margin-bottom: 0.25em
margin-right: 0.25em

View File

@ -15,42 +15,38 @@
border: 0
outline: 0
>.sidebar>nav.buttons, >.content nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
article
flex: 1 0 33%
a
display: inline-block
width: 100%
padding: 0.3em 0
nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
article
flex: 1 0 33%
a
display: inline-block
width: 100%
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
@media (max-width: 800px)
margin-top: 2em
>.content
width: 100%
.post-container
margin-bottom: 0.6em
margin-bottom: 2em
.post-content
margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
>.sidebar
nav.buttons
article
a:not(.inactive):hover
@ -60,8 +56,6 @@
@media (max-width: 800px)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
@ -119,13 +113,12 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
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

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

@ -21,11 +21,10 @@
.details
font-size: 90%
line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail
width: 3em
height: 3em
margin: 0.25em 0.6em 0 0
.darktheme .user-list
ul li

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
<meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>

View File

@ -1,14 +1,13 @@
<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 %>' draggable='false' fetchPriority='high'/>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<% } 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='transparent'/>
<param name='wmode' value='opaque'/>
<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') { %>
@ -20,8 +19,6 @@
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

@ -29,7 +29,6 @@
<span class='vim-nav-hint'>Next post &gt;</span>
</a>
</article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'>
<% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
@ -37,13 +36,16 @@
<span class='vim-nav-hint'>Back to view mode</span>
</a>
<% } else { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<% } else { %>
<a class='inactive'>
<% } %>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a>
<% } %>
</article>
<% } %>
</nav>
<div class='sidebar-container'></div>
@ -52,15 +54,13 @@
<div class='content'>
<div class='post-container'></div>
<div class='after-mobile-controls'>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
</div>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
</div>
</div>

View File

@ -17,8 +17,8 @@
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %><!--
--></a>
}[ctx.post.mimeType] %>
</a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
@ -58,7 +58,7 @@
Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -99,10 +99,10 @@
--><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><%- ctx.getPrettyName(tag.names[0]) %>&#32;<!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %>&#32;<!--
--><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View File

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

View File

@ -16,6 +16,7 @@
%><form class='horizontal bulk-edit bulk-edit-tags'><%
%><span class='append hint'>Tagging with:</span><%
%><a href class='mousetrap button append open'>Mass tag</a><%
%><wbr/><%
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append close'>Stop tagging</a><%

View File

@ -294,16 +294,11 @@ 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];
@ -329,7 +324,7 @@ class Api extends events.EventTarget {
url,
requestFactory,
data,
fileData,
{},
options
);
abortFunction = () => requestPromise.abort();
@ -393,7 +388,7 @@ class Api extends events.EventTarget {
if (files) {
for (let key of Object.keys(files)) {
const value = files[key];
if (value !== null && value.constructor === String) {
if (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", "customThumbnailUrl"];
const fields = ["id", "comments", "commentCount", "thumbnailUrl"];
class CommentsController {
constructor(ctx) {

View File

@ -91,16 +91,16 @@ class PoolController {
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) {
if (e.detail.names !== undefined) {
e.detail.pool.names = e.detail.names;
}
if (e.detail.category !== undefined && e.detail.category !== null) {
if (e.detail.category !== undefined) {
e.detail.pool.category = e.detail.category;
}
if (e.detail.description !== undefined && e.detail.description !== null) {
if (e.detail.description !== undefined) {
e.detail.pool.description = e.detail.description;
}
if (e.detail.posts !== undefined && e.detail.posts !== null) {
if (e.detail.posts !== undefined) {
e.detail.pool.posts.clear();
for (let postId of e.detail.posts) {
e.detail.pool.posts.add(

View File

@ -43,8 +43,6 @@ class PoolListController {
this._headerView.addEventListener(
"submit",
(e) => this._evtSubmit(e),
);
this._headerView.addEventListener(
"navigate",
(e) => this._evtNavigate(e)
);

View File

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

View File

@ -169,20 +169,24 @@ class PostMainController extends BasePostController {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
const post = e.detail.post;
if (e.detail.safety !== undefined && e.detail.safety !== null) {
if (e.detail.safety !== undefined) {
post.safety = e.detail.safety;
}
if (e.detail.flags !== undefined && e.detail.flags !== null) {
if (e.detail.flags !== undefined) {
post.flags = e.detail.flags;
}
if (e.detail.relations !== undefined && e.detail.relations !== null) {
if (e.detail.relations !== undefined) {
post.relations = e.detail.relations;
}
if (e.detail.source !== undefined && e.detail.source !== null) {
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

@ -95,13 +95,13 @@ class TagController {
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) {
if (e.detail.names !== undefined) {
e.detail.tag.names = e.detail.names;
}
if (e.detail.category !== undefined && e.detail.category !== null) {
if (e.detail.category !== undefined) {
e.detail.tag.category = e.detail.category;
}
if (e.detail.description !== undefined && e.detail.description !== null) {
if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description;
}
e.detail.tag.save().then(

View File

@ -175,21 +175,21 @@ class UserController {
const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? "self" : "any";
if (e.detail.name !== undefined && e.detail.name !== null) {
if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name;
}
if (e.detail.email !== undefined && e.detail.email !== null) {
if (e.detail.email !== undefined) {
e.detail.user.email = e.detail.email;
}
if (e.detail.rank !== undefined && e.detail.rank !== null) {
if (e.detail.rank !== undefined) {
e.detail.user.rank = e.detail.rank;
}
if (e.detail.password !== undefined && e.detail.password !== null) {
if (e.detail.password !== undefined) {
e.detail.user.password = e.detail.password;
}
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
if (e.detail.avatarStyle !== undefined) {
e.detail.user.avatarStyle = e.detail.avatarStyle;
if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent;
@ -302,7 +302,7 @@ class UserController {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.note !== undefined && e.detail.note !== null) {
if (e.detail.note !== undefined) {
e.detail.userToken.note = e.detail.note;
}

View File

@ -48,12 +48,6 @@ class FileDropperControl extends events.EventTarget {
this._urlInputNode.addEventListener("keydown", (e) =>
this._evtUrlInputKeyDown(e)
);
this._urlInputNode.addEventListener("paste", (e) => {
// document.onpaste is used on the post-upload page.
// And this event is used on the post edit page.
if (document.getElementById("post-upload")) return;
this._evtPaste(e)
});
}
if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener("click", (e) =>
@ -61,11 +55,6 @@ class FileDropperControl extends events.EventTarget {
);
}
document.onpaste = (e) => {
if (!document.getElementById("post-upload")) return;
this._evtPaste(e)
}
this._originalHtml = this._dropperNode.innerHTML;
views.replaceContent(target, source);
}
@ -140,17 +129,6 @@ class FileDropperControl extends events.EventTarget {
this._emitFiles(e.dataTransfer.files);
}
_evtPaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
if (!this._options.allowMultiple && fileList.length > 1) {
window.alert("Cannot select multiple files.");
} else if (fileList.length > 0) {
this._emitFiles(fileList);
}
}
_evtUrlInputKeyDown(e) {
if (e.which !== KEY_RETURN) {
return;

View File

@ -5,12 +5,11 @@ const views = require("../util/views.js");
const optimizedResize = require("../util/optimized_resize.js");
class PostContentControl {
constructor(hostNode, post, isMediaCached, viewportSizeCalculator, fitFunctionOverride) {
constructor(hostNode, post, 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") {
@ -104,30 +103,6 @@ class PostContentControl {
}
_refreshSize() {
if (window.innerWidth <= 800) {
const buttons = document.querySelector(".sidebar > .buttons");
if (buttons) {
const content = document.querySelector(".content");
content.insertBefore(buttons, content.querySelector(".post-container + *"));
const afterControls = document.querySelector(".content > .after-mobile-controls");
if (afterControls) {
afterControls.parentElement.parentElement.appendChild(afterControls);
}
}
} else {
const buttons = document.querySelector(".content > .buttons");
if (buttons) {
const sidebar = document.querySelector(".sidebar");
sidebar.insertBefore(buttons, sidebar.firstElementChild);
}
const afterControls = document.querySelector(".content + .after-mobile-controls");
if (afterControls) {
document.querySelector(".content").appendChild(afterControls);
}
}
this._currentFitFunction();
}
@ -144,30 +119,9 @@ 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) {
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,7 +138,10 @@ class PostEditSidebarControl extends events.EventTarget {
this._thumbnailRemovalLinkNode.addEventListener("click", (e) =>
this._evtRemoveThumbnailClick(e)
);
this._thumbnailRemovalLinkUpdate(this._post);
this._thumbnailRemovalLinkNode.style.display = this._post
.hasCustomThumbnail
? "block"
: "none";
}
if (this._addNoteLinkNode) {
@ -246,25 +249,12 @@ 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) {
@ -437,7 +427,9 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined,
thumbnail:
this._newPostThumbnail,
this._newPostThumbnail !== undefined
? this._newPostThumbnail
: undefined,
source: this._sourceInputNode
? this._sourceInputNode.value

View File

@ -70,14 +70,6 @@ class Post extends events.EventTarget {
return this._thumbnailUrl;
}
get customThumbnailUrl() {
return this._customThumbnailUrl;
}
get originalThumbnailUrl() {
return this._originalThumbnailUrl;
}
get source() {
return this._source;
}
@ -154,6 +146,10 @@ class Post extends events.EventTarget {
return this._ownScore;
}
get hasCustomThumbnail() {
return this._hasCustomThumbnail;
}
set flags(value) {
this._flags = value;
}
@ -275,7 +271,7 @@ class Post extends events.EventTarget {
if (this._newContent) {
files.content = this._newContent;
}
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
if (this._newThumbnail !== undefined) {
files.thumbnail = this._newThumbnail;
}
if (this._source !== this._orig._source) {
@ -481,9 +477,7 @@ class Post extends events.EventTarget {
response.contentUrl,
document.getElementsByTagName("base")[0].href
).href,
_thumbnailUrl: response.customThumbnailUrl ? response.customThumbnailUrl : response.thumbnailUrl,
_customThumbnailUrl: response.customThumbnailUrl,
_originalThumbnailUrl: response.thumbnailUrl,
_thumbnailUrl: response.thumbnailUrl,
_source: response.source,
_canvasWidth: response.canvasWidth,
_canvasHeight: response.canvasHeight,
@ -497,6 +491,7 @@ 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

@ -211,15 +211,6 @@ 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,
@ -238,5 +229,4 @@ module.exports = {
escapeSearchTerm: escapeSearchTerm,
dataURItoBlob: dataURItoBlob,
getPrettyName: getPrettyName,
isMediaCached: isMediaCached,
};

View File

@ -49,7 +49,7 @@ function makeThumbnail(url) {
style: `background-image: url(\'${url}\')`,
}
: { class: "thumbnail empty" },
makeElement("img", { alt: "thumbnail", src: url, draggable: "false" })
makeElement("img", { alt: "thumbnail", src: url })
);
}
@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
}
function makeTagLink(name, includeHash, includeCount, tag) {
const category = tag && tag.category ? tag.category : "unknown";
const category = tag ? tag.category : "unknown";
let text = misc.getPrettyName(name);
if (includeHash === true) {
text = "#" + text;
}
if (includeCount === true) {
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
text += " (" + (tag ? tag.postCount : 0) + ")";
}
return api.hasPrivilege("tags:view")
? makeElement(
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
}
function makePoolLink(id, includeHash, includeCount, pool, name) {
const category = pool && pool.category ? pool.category : "unknown";
const category = pool ? pool.category : "unknown";
let text = misc.getPrettyName(
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
name ? name : pool ? pool.names[0] : "unknown"
);
if (includeHash === true) {
text = "#" + text;
}
if (includeCount === true) {
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
text += " (" + (pool ? pool.postCount : 0) + ")";
}
return api.hasPrivilege("pools:view")
? makeElement(
@ -264,7 +264,7 @@ function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
const link =
user && user.name && api.hasPrivilege("users:view")
user && api.hasPrivilege("users:view")
? makeElement(
"a",
{ href: uri.formatClientLink("user", user.name) },

View File

@ -62,7 +62,6 @@ class HomeView {
this._postContentControl = new PostContentControl(
this._postContainerNode,
postInfo.featuredPost,
misc.isMediaCached(postInfo.featuredPost),
() => {
return [window.innerWidth * 0.8, window.innerHeight * 0.7];
},

View File

@ -31,7 +31,6 @@ class PostMainView {
this._postContentControl = new PostContentControl(
postContainerNode,
ctx.post,
misc.isMediaCached(ctx.post),
() => {
const margin = sidebarNode.getBoundingClientRect().left;

View File

@ -401,14 +401,6 @@ 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) {

196
client/package-lock.json generated
View File

@ -477,14 +477,15 @@
}
},
"node_modules/asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dev": true,
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/assert": {
@ -1404,30 +1405,87 @@
}
},
"node_modules/browserify-rsa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
"integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
"dev": true,
"dependencies": {
"bn.js": "^4.1.0",
"bn.js": "^5.0.0",
"randombytes": "^2.0.1"
}
},
"node_modules/browserify-rsa/node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
},
"node_modules/browserify-sign": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
"integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dev": true,
"dependencies": {
"bn.js": "^4.1.1",
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.2",
"elliptic": "^6.0.0",
"inherits": "^2.0.1",
"parse-asn1": "^5.0.0"
"bn.js": "^5.2.1",
"browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"elliptic": "^6.5.4",
"inherits": "^2.0.4",
"parse-asn1": "^5.1.6",
"readable-stream": "^3.6.2",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 4"
}
},
"node_modules/browserify-sign/node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
},
"node_modules/browserify-sign/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/browserify-sign/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/browserify-sign/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
@ -3305,16 +3363,16 @@
}
},
"node_modules/parse-asn1": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
"integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
"dev": true,
"dependencies": {
"asn1.js": "^4.0.0",
"asn1.js": "^5.2.0",
"browserify-aes": "^1.0.0",
"create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.0",
"pbkdf2": "^3.0.3"
"pbkdf2": "^3.0.3",
"safe-buffer": "^5.1.1"
}
},
"node_modules/parse-bmfont-ascii": {
@ -5031,14 +5089,15 @@
}
},
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dev": true,
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"assert": {
@ -5940,28 +5999,69 @@
}
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
"integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
"dev": true,
"requires": {
"bn.js": "^4.1.0",
"bn.js": "^5.0.0",
"randombytes": "^2.0.1"
},
"dependencies": {
"bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
}
}
},
"browserify-sign": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
"integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dev": true,
"requires": {
"bn.js": "^4.1.1",
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.2",
"elliptic": "^6.0.0",
"inherits": "^2.0.1",
"parse-asn1": "^5.0.0"
"bn.js": "^5.2.1",
"browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"elliptic": "^6.5.4",
"inherits": "^2.0.4",
"parse-asn1": "^5.1.6",
"readable-stream": "^3.6.2",
"safe-buffer": "^5.2.1"
},
"dependencies": {
"bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
}
},
"browserify-zlib": {
@ -7513,16 +7613,16 @@
}
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
"integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
"dev": true,
"requires": {
"asn1.js": "^4.0.0",
"asn1.js": "^5.2.0",
"browserify-aes": "^1.0.0",
"create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.0",
"pbkdf2": "^3.0.3"
"pbkdf2": "^3.0.3",
"safe-buffer": "^5.1.1"
}
},
"parse-bmfont-ascii": {

View File

@ -54,7 +54,7 @@
- [Deleting pool category](#deleting-pool-category)
- [Setting default pool category](#setting-default-pool-category)
- Pools
- [Listing pools](#listing-pools)
- [Listing pools](#listing-pool)
- [Creating pool](#creating-pool)
- [Updating pool](#updating-pool)
- [Getting pool](#getting-pool)
@ -165,9 +165,9 @@ way. The files, however, should be passed as regular fields appended with a
accepts a file named `content`, the client should pass
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
body. When creating or updating post content using this method, the server can
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
download content from popular sites such as youtube, gfycat, etc. Access to
yt-dlp can be configured with the `'uploads:use_downloader'` permission
also be configured to employ [youtube-dl](https://github.com/ytdl-org/youtube-dl)
to download content from popular sites such as youtube, gfycat, etc. Access to
youtube-dl can be configured with the `'uploads:use_downloader'` permission
Finally, in some cases the user might want to reuse one file between the
requests to save the bandwidth (for example, reverse search + consecutive
@ -789,7 +789,7 @@ data.
| `fav-time` | alias of `fav-date` |
| `feature-date` | featured at given date |
| `feature-time` | alias of `feature-time` |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
| `rating` | alias of `safety` |
**Sort style tokens**
@ -1389,7 +1389,7 @@ data.
## Creating pool
- **Request**
`POST /pool`
`POST /pools/create`
- **Input**

View File

@ -1,5 +1,5 @@
This assumes that you have Docker (version 19.03 or greater)
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
This assumes that you have Docker (version 17.05 or greater)
and Docker Compose (version 1.6.0 or greater) already installed.
### Prepare things
@ -38,7 +38,7 @@ and the Docker Compose CLI (version 1.27.0 or greater) already installed.
This pulls the latest containers from docker.io:
```console
user@host:szuru$ docker compose pull
user@host:szuru$ docker-compose pull
```
If you have modified the application's source and would like to manually
@ -49,17 +49,17 @@ and the Docker Compose CLI (version 1.27.0 or greater) already installed.
For first run, it is recommended to start the database separately:
```console
user@host:szuru$ docker compose up -d sql
user@host:szuru$ docker-compose up -d sql
```
To start all containers:
```console
user@host:szuru$ docker compose up -d
user@host:szuru$ docker-compose up -d
```
To view/monitor the application logs:
```console
user@host:szuru$ docker compose logs -f
user@host:szuru$ docker-compose logs -f
# (CTRL+C to exit)
```
@ -84,13 +84,13 @@ and the Docker Compose CLI (version 1.27.0 or greater) already installed.
2. Build the containers:
```console
user@host:szuru$ docker compose build
user@host:szuru$ docker-compose build
```
That will attempt to build both containers, but you can specify `client`
or `server` to make it build only one.
If `docker compose build` spits out:
If `docker-compose build` spits out:
```
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
@ -102,7 +102,7 @@ and the Docker Compose CLI (version 1.27.0 or greater) already installed.
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
```
...and run `docker compose build` again.
...and run `docker-compose build` again.
*Note: If your changes are not taking effect in your builds, consider building
with `--no-cache`.*
@ -117,7 +117,7 @@ with `--no-cache`.*
run from docker:
```console
user@host:szuru$ docker compose run server ./szuru-admin --help
user@host:szuru$ docker-compose run server ./szuru-admin --help
```
will give you a breakdown on all available commands.

View File

@ -1,7 +1,9 @@
## Example Docker Compose configuration
##
## Use this as a template to set up docker compose, or as guide to set up other
## Use this as a template to set up docker-compose, or as guide to set up other
## orchestration services
version: '2'
services:
server:

View File

@ -23,30 +23,15 @@ RUN apk --no-cache add \
py3-pillow \
py3-pynacl \
py3-tz \
py3-pyrfc3339
RUN pip3 install --no-cache-dir --disable-pip-version-check \
py3-pyrfc3339 \
&& pip3 install --no-cache-dir --disable-pip-version-check \
"alembic>=0.8.5" \
"coloredlogs==5.0" \
"pyheif==0.6.1" \
"heif-image-plugin>=0.3.2" \
yt-dlp \
"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
youtube_dl \
"pillow-avif-plugin>=1.1.0" \
&& apk --no-cache del py3-pip
COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests

View File

@ -3,7 +3,7 @@ certifi>=2017.11.5
coloredlogs==5.0
heif-image-plugin==0.3.2
numpy>=1.8.2
pillow-avif-plugin~=1.1.0
pillow-avif-plugin>=1.1.0
pillow>=4.3.0
psycopg2-binary>=2.6.1
pyheif==0.6.1
@ -12,4 +12,4 @@ pyRFC3339>=1.0
pytz>=2018.3
pyyaml>=3.11
SQLAlchemy>=1.0.12, <1.4
yt-dlp
youtube_dl

View File

@ -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, mime
from szurubooru.func import files, images
from szurubooru.func import posts as postfuncs
from szurubooru.func import users as userfuncs
@ -95,14 +95,7 @@ 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:
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)
postfuncs.generate_post_thumbnail(post)
except Exception:
pass

View File

@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
return left
def _container_config() -> Dict:
def _docker_config() -> Dict:
if "TEST_ENVIRONMENT" not in os.environ:
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
if key not in os.environ:
@ -49,15 +49,6 @@ def _file_config(filename: str) -> Dict:
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
def _running_inside_container() -> bool:
env = os.environ.keys()
return (
os.path.exists("/.dockerenv")
or "KUBERNETES_SERVICE_HOST" in env
or "container" in env # set by lxc/podman
)
def _read_config() -> Dict:
ret = _file_config("config.yaml.dist")
if os.path.isfile("config.yaml"):
@ -66,8 +57,8 @@ def _read_config() -> Dict:
logger.warning(
"'config.yaml' should be a file, not a directory, skipping"
)
if _running_inside_container():
ret = _merge(ret, _container_config())
if os.path.exists("/.dockerenv"):
ret = _merge(ret, _docker_config())
return ret

View File

@ -1,5 +1,4 @@
import os
import glob
from typing import Any, List, Optional
from szurubooru import config
@ -25,10 +24,6 @@ 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,11 +24,6 @@ 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
@ -46,7 +41,7 @@ class Image:
def frames(self) -> int:
return self.info["streams"][0]["nb_read_frames"]
def resize_fill(self, width: int, height: int, keep_transparency: bool = True, seek=True) -> None:
def resize_fill(self, width: int, height: int) -> None:
width_greater = self.width > self.height
width, height = (-1, height) if width_greater else (width, -1)
@ -55,12 +50,8 @@ class Image:
"{path}",
"-f",
"image2",
"-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),
"-filter:v",
"scale='{width}:{height}'".format(width=width, height=height),
"-map",
"0:v:0",
"-vframes",
@ -70,8 +61,7 @@ class Image:
"-",
]
if (
seek
and "duration" in self.info["format"]
"duration" in self.info["format"]
and self.info["format"]["format_name"] != "swf"
):
duration = float(self.info["format"]["duration"])
@ -106,13 +96,24 @@ class Image:
def to_jpeg(self) -> bytes:
return self._execute(
[
"-quality",
"85",
"-sample",
"1x1",
"-f",
"lavfi",
"-i",
"color=white:s=%dx%d" % (self.width, self.height),
"-i",
"{path}",
],
program="cjpeg",
"-f",
"image2",
"-filter_complex",
"overlay",
"-map",
"0:v:0",
"-vframes",
"1",
"-vcodec",
"mjpeg",
"-",
]
)
def to_webm(self) -> bytes:
@ -273,10 +274,7 @@ 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 = [program, "-loglevel", "32" if get_logs else "24"] + cli
cli = [part.format(path=handle.name) for part in cli]
proc = subprocess.Popen(
cli,
@ -287,7 +285,7 @@ class Image:
out, err = proc.communicate()
if proc.returncode != 0:
logger.warning(
"Failed to execute {program} command (cli=%r, err=%r)".format(program=program),
"Failed to execute ffmpeg command (cli=%r, err=%r)",
" ".join(shlex.quote(arg) for arg in cli),
err,
)

View File

@ -64,7 +64,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
def _get_youtube_dl_content_url(url: str) -> str:
cmd = ["yt-dlp", "--format", "best", "--no-playlist"]
cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
if config.config["user_agent"]:
cmd.extend(["--user-agent", config.config["user_agent"]])
cmd.extend(["--get-url", url])

View File

@ -117,16 +117,7 @@ def get_post_content_url(post: model.Post) -> str:
def get_post_thumbnail_url(post: model.Post) -> str:
assert post
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" % (
return "%s/generated-thumbnails/%d_%s.jpg" % (
config.config["data_url"].rstrip("/"),
post.post_id,
get_post_security_hash(post.post_id),
@ -143,26 +134,17 @@ 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/sample_%d_%s.jpg" % (
return "generated-thumbnails/%d_%s.jpg" % (
post.post_id,
get_post_security_hash(post.post_id),
)
def get_post_custom_thumbnail_path(post: model.Post) -> str:
def get_post_thumbnail_backup_path(post: model.Post) -> str:
assert post
return "generated-thumbnails/custom-thumbnails/sample_%d_%s.jpg" % (
return "posts/custom-thumbnails/%d_%s.dat" % (
post.post_id,
get_post_security_hash(post.post_id),
)
@ -198,7 +180,6 @@ 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,
@ -214,6 +195,7 @@ 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,
@ -337,9 +319,8 @@ class PostSerializer(serialization.BaseSerializer):
for rel in self.post.favorited_by
]
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_has_custom_thumbnail(self) -> Any:
return files.has(get_post_thumbnail_backup_path(self.post))
def serialize_notes(self) -> Any:
return sorted(
@ -376,7 +357,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", "customThumbnailUrl"]
post, auth_user=auth_user, options=["id", "thumbnailUrl"]
)
@ -481,28 +462,32 @@ def _before_post_delete(
) -> None:
if post.post_id:
if config.config["delete_source_files"]:
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)
files.delete(get_post_content_path(post))
files.delete(get_post_thumbnail_path(post))
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"):
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)
files.save(
get_post_thumbnail_backup_path(post),
getattr(post, "__thumbnail"),
)
else:
files.delete(get_post_custom_thumbnail_path(post))
files.delete(get_post_thumbnail_backup_path(post))
delattr(post, "__thumbnail")
regenerate_thumb = True
if regenerate_thumb:
generate_post_thumbnail(post)
def generate_alternate_formats(
@ -692,19 +677,22 @@ def update_post_thumbnail(
setattr(post, "__thumbnail", content)
def generate_post_thumbnail(path: str, content: bytes, seek=True) -> None:
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))
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(path, image.to_jpeg())
files.save(get_post_thumbnail_path(post), image.to_jpeg())
except errors.ProcessingError:
files.save(path, EMPTY_PIXEL)
files.save(get_post_thumbnail_path(post), EMPTY_PIXEL)
def update_post_tags(

View File

@ -311,8 +311,6 @@ 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:

View File

@ -51,7 +51,7 @@ class Context:
use_video_downloader: bool = False,
allow_tokens: bool = True,
) -> bytes:
if name in self._files:
if name in self._files and self._files[name]:
return self._files[name]
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
assert (
posts.get_post_thumbnail_url(post)
== "http://example.com/generated-thumbnails/sample_1_244c8840887984c4.jpg"
== "http://example.com/generated-thumbnails/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/sample_1_244c8840887984c4.jpg"
== "generated-thumbnails/1_244c8840887984c4.jpg"
)
@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"])
def test_get_post_custom_thumbnail_path(input_mime_type):
def test_get_post_thumbnail_backup_path(input_mime_type):
post = model.Post()
post.post_id = 1
post.mime_type = input_mime_type
assert (
posts.get_post_custom_thumbnail_path(post)
== "generated-thumbnails/custom-thumbnails/sample_1_244c8840887984c4.jpg"
posts.get_post_thumbnail_backup_path(post)
== "posts/custom-thumbnails/1_244c8840887984c4.dat"
)
@ -226,9 +226,7 @@ def test_serialize_post(
"canvasHeight": 300,
"contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg",
"thumbnailUrl": "http://example.com/"
"generated-thumbnails/sample_1_244c8840887984c4.jpg",
"customThumbnailUrl": "http://example.com/"
"generated-thumbnails/custom-thumbnails/sample_1_244c8840887984c4.jpg",
"generated-thumbnails/1_244c8840887984c4.jpg",
"flags": ["loop"],
"tags": [
{
@ -272,27 +270,17 @@ 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(tmpdir, config_injector, post_factory, user_factory):
def test_serialize_micro_post(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)
@ -300,7 +288,6 @@ def test_serialize_micro_post(tmpdir, config_injector, post_factory, user_factor
assert posts.serialize_micro_post(post, auth_user) == {
"id": post.post_id,
"thumbnailUrl": "https://example.com/thumb.png",
"customThumbnailUrl": None,
}
@ -618,7 +605,7 @@ def test_update_post_thumbnail_to_new_one(
assert post.post_id
generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir)
+ "sample_1_244c8840887984c4.jpg"
+ "1_244c8840887984c4.jpg"
)
source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -659,7 +646,7 @@ def test_update_post_thumbnail_to_default(
assert post.post_id
generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir)
+ "sample_1_244c8840887984c4.jpg"
+ "1_244c8840887984c4.jpg"
)
source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -699,7 +686,7 @@ def test_update_post_thumbnail_with_broken_thumbnail(
assert post.post_id
generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir)
+ "sample_1_244c8840887984c4.jpg"
+ "1_244c8840887984c4.jpg"
)
source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -718,8 +705,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 == 300
assert image.height == 300
assert image.width == 1
assert image.height == 1
def test_update_post_content_leaving_custom_thumbnail(
@ -744,7 +731,7 @@ def test_update_post_content_leaving_custom_thumbnail(
db.session.flush()
generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir)
+ "sample_1_244c8840887984c4.jpg"
+ "1_244c8840887984c4.jpg"
)
source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir)
@ -776,7 +763,7 @@ def test_update_post_content_convert_heif_to_png_when_processing(
db.session.flush()
generated_path = (
"{}/data/generated-thumbnails/".format(tmpdir)
+ "sample_1_244c8840887984c4.jpg"
+ "1_244c8840887984c4.jpg"
)
source_path = (
"{}/data/posts/custom-thumbnails/".format(tmpdir)