29 Commits

Author SHA1 Message Date
fb70171b17 Merge 0825866dce into 4fd848abf2 2025-02-11 21:35:23 +01:00
4fd848abf2 doc: use docker compose instead of docker-compose
The minimum version requirements are rough guesses, in practice any decently modern docker installation should work.
2025-02-11 21:25:10 +01:00
0825866dce client+server/docker: add .psd support 2025-02-07 18:28:31 +01:00
61b9f81e39 Fixed the google search option in the post details view 2024-11-17 16:48:24 +01:00
b721865931 server/config: generalize container support
Allow running in Kubernetes, podman, and LXC, besides plain docker-compose,
without having to fake out /.dockerenv in non-Docker environments.
2024-11-10 15:44:39 +01:00
Neo
46e3295003 Upload from clipboard (#414)
client/upload: upload from clipboard

Co-authored-by: Eva <evauwu@riseup.net>
2024-09-29 14:54:53 +02:00
031131506e client/css: fix comment word-break
`break-all` makes it hard to read actual comments.
2024-09-29 13:48:06 +02:00
Neo
d102578b54 Merge pull request #647 from po5/null-checks
client: add null checks
2024-04-27 21:23:16 +02:00
Neo
6edb25d87b Merge pull request #641 from po5/mobile
Mobile improvements
2024-04-26 22:56:58 +02:00
Neo
93fc15f2a4 Merge pull request #642 from po5/better-links 2024-04-26 22:37:54 +02:00
Neo
4f9d46e1c2 Merge branch 'master' into better-links 2024-04-26 22:16:37 +02:00
Eva
b72e81850d client: add null checks 2024-03-28 13:31:48 +01:00
Eva
c1c695f082 client/css: stack bulk tagging toggles horizontally on mobile 2024-03-21 22:26:49 +01:00
Eva
4b6b231fc8 client/posts: reorder elements in mobile layout
Navigation is always right below the image, and comments are always
at the very bottom, to minimize scrolling for common actions.
2024-03-21 22:26:28 +01:00
Eva
6b0c3cfc7f client/html: allow mobile browsers to zoom in 2024-03-21 22:23:45 +01:00
Eva
4ec8cb3ba2 client/css: constrain thumbnails to parent to prevent overextended links 2024-03-21 22:19:46 +01:00
Eva
8d971234a2 client/views: better pool name fallback 2024-03-21 22:16:05 +01:00
Eva
a16bb198ab client/views: more thorough link fallbacks
Prevents a bunch of errors that can happen when a resource is deleted.
2024-03-21 21:53:11 +01:00
Eva
3f182a66ad client/posts: fix overextended tag link 2024-03-21 21:52:52 +01:00
Eva
b52363e82d client/posts: fix overextended download link 2024-03-21 21:52:49 +01:00
Eva
3bf45e4c0a client/users: fix overextended avatar links 2024-03-21 21:52:39 +01:00
5596f53744 posts page ugly horizontal bar fix
fixes ugly horizontal scrollbar appearing when a post with extremely wide image is present in the posts list
2024-02-29 20:56:27 +01:00
da425afc49 Pin pillow-avif-plugin to compatible version range 2024-02-21 17:47:27 +01:00
d7394d672f Fix Pool Search 2024-02-21 01:27:00 +01:00
190d795426 doc: fix small error in pool API docs 2023-12-05 21:31:23 +01:00
7c92ceaf6a fix overflow on comments, prevents ugly unnecesary horizontal scroll 2023-11-05 12:27:03 +01:00
Neo
9e00f37464 Merge pull request #597 from zakame/use-yt-dlp
server/net: use yt-dlp instead of youtube-dl
2023-11-05 12:22:03 +01:00
59c497e168 doc: update for yt-dlp 2023-08-17 20:58:09 +08:00
c292b96f06 server/net: use yt-dlp instead of youtube-dl
youtube-dl no longer even gets URLs properly, so switch to yt-dlp as a
drop-in replacement for it.
2023-08-17 20:41:50 +08:00
32 changed files with 247 additions and 124 deletions

6
.gitignore vendored
View File

@ -13,3 +13,9 @@ server/**/lib/
server/**/bin/
server/**/pyvenv.cfg
__pycache__/
# Developer Tools
.vscode/
.idea/
*.sublime-project
*.sublime-workspace

View File

@ -7,8 +7,8 @@ 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 [youtube-dl](https://github.com/ytdl-org/youtube-dl)
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations, project files (PSD)
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments
- Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md))

View File

@ -127,6 +127,10 @@ $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

@ -300,10 +300,10 @@ a .access-key
background-size: 20px 20px
img
opacity: 0
width: auto
width: 100%
height: 100%
video
width: auto
width: 100%
height: 100%
.flexbox-dummy

View File

@ -187,6 +187,9 @@
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,38 +15,42 @@
border: 0
outline: 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%
>.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
text-align: center
@media (max-width: 800px)
margin-top: 2em
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
>.content
width: 100%
.post-container
margin-bottom: 2em
margin-bottom: 0.6em
.post-content
margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
@ -56,6 +60,8 @@
@media (max-width: 800px)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
@ -113,7 +119,6 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
li

View File

@ -21,10 +21,11 @@
.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, maximum-scale=1'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<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,7 +1,11 @@
<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 %>'/>
<% if (ctx.post.mimeType === 'image/vnd.adobe.photoshop') { %>
<img class='resize-listener' alt='' src='<%- ctx.post.thumbnailUrl %>'/>
<% } else { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<% } %>
<% } else if (ctx.post.type === 'flash') { %>

View File

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

View File

@ -13,12 +13,13 @@
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'image/vnd.adobe.photoshop': 'PSD',
'video/webm': 'WEBM',
'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 +59,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://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -99,10 +100,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]) %>&#32;<!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View File

@ -16,7 +16,6 @@
%><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

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

View File

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

View File

@ -169,22 +169,22 @@ class PostMainController extends BasePostController {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
const post = e.detail.post;
if (e.detail.safety !== undefined) {
if (e.detail.safety !== undefined && e.detail.safety !== null) {
post.safety = e.detail.safety;
}
if (e.detail.flags !== undefined) {
if (e.detail.flags !== undefined && e.detail.flags !== null) {
post.flags = e.detail.flags;
}
if (e.detail.relations !== undefined) {
if (e.detail.relations !== undefined && e.detail.relations !== null) {
post.relations = e.detail.relations;
}
if (e.detail.content !== undefined) {
if (e.detail.content !== undefined && e.detail.content !== null) {
post.newContent = e.detail.content;
}
if (e.detail.thumbnail !== undefined) {
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
post.newThumbnail = e.detail.thumbnail;
}
if (e.detail.source !== undefined) {
if (e.detail.source !== undefined && e.detail.source !== null) {
post.source = e.detail.source;
}
post.save().then(

View File

@ -95,13 +95,13 @@ class TagController {
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined) {
if (e.detail.names !== undefined && e.detail.names !== null) {
e.detail.tag.names = e.detail.names;
}
if (e.detail.category !== undefined) {
if (e.detail.category !== undefined && e.detail.category !== null) {
e.detail.tag.category = e.detail.category;
}
if (e.detail.description !== undefined) {
if (e.detail.description !== undefined && e.detail.description !== null) {
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) {
if (e.detail.name !== undefined && e.detail.name !== null) {
e.detail.user.name = e.detail.name;
}
if (e.detail.email !== undefined) {
if (e.detail.email !== undefined && e.detail.email !== null) {
e.detail.user.email = e.detail.email;
}
if (e.detail.rank !== undefined) {
if (e.detail.rank !== undefined && e.detail.rank !== null) {
e.detail.user.rank = e.detail.rank;
}
if (e.detail.password !== undefined) {
if (e.detail.password !== undefined && e.detail.password !== null) {
e.detail.user.password = e.detail.password;
}
if (e.detail.avatarStyle !== undefined) {
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
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) {
if (e.detail.note !== undefined && e.detail.note !== null) {
e.detail.userToken.note = e.detail.note;
}

View File

@ -48,6 +48,12 @@ 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) =>
@ -55,6 +61,11 @@ 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);
}
@ -129,6 +140,17 @@ 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

@ -103,6 +103,30 @@ 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();
}

View File

@ -427,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined,
thumbnail:
this._newPostThumbnail !== undefined
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
? this._newPostThumbnail
: undefined,

View File

@ -271,7 +271,7 @@ class Post extends events.EventTarget {
if (this._newContent) {
files.content = this._newContent;
}
if (this._newThumbnail !== undefined) {
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
files.thumbnail = this._newThumbnail;
}
if (this._source !== this._orig._source) {

View File

@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
}
function makeTagLink(name, includeHash, includeCount, tag) {
const category = tag ? tag.category : "unknown";
const category = tag && tag.category ? tag.category : "unknown";
let text = misc.getPrettyName(name);
if (includeHash === true) {
text = "#" + text;
}
if (includeCount === true) {
text += " (" + (tag ? tag.postCount : 0) + ")";
text += " (" + (tag && tag.postCount ? 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 : "unknown";
const category = pool && pool.category ? pool.category : "unknown";
let text = misc.getPrettyName(
name ? name : pool ? pool.names[0] : "unknown"
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
);
if (includeHash === true) {
text = "#" + text;
}
if (includeCount === true) {
text += " (" + (pool ? pool.postCount : 0) + ")";
text += " (" + (pool && pool.postCount ? 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 && api.hasPrivilege("users:view")
user && user.name && api.hasPrivilege("users:view")
? makeElement(
"a",
{ href: uri.formatClientLink("user", user.name) },

View File

@ -20,6 +20,7 @@ function _mimeTypeToPostType(mimeType) {
"image/avif": "image",
"image/heif": "image",
"image/heic": "image",
"image/vnd.adobe.photoshop": "image",
"video/mp4": "video",
"video/webm": "video",
"video/quicktime": "video",
@ -165,7 +166,7 @@ class PostUploadView extends events.EventTarget {
this._contentInputNode,
{
extraText:
"Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf, .avif, .heif, .heic",
"Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf, .avif, .heif, .heic, .psd",
allowUrls: true,
allowMultiple: true,
lock: false,

View File

@ -54,7 +54,7 @@
- [Deleting pool category](#deleting-pool-category)
- [Setting default pool category](#setting-default-pool-category)
- Pools
- [Listing pools](#listing-pool)
- [Listing pools](#listing-pools)
- [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 [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
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
Finally, in some cases the user might want to reuse one file between the
requests to save the bandwidth (for example, reverse search + consecutive
@ -1389,7 +1389,7 @@ data.
## Creating pool
- **Request**
`POST /pools/create`
`POST /pool`
- **Input**

View File

@ -1,5 +1,5 @@
This assumes that you have Docker (version 17.05 or greater)
and Docker Compose (version 1.6.0 or greater) already installed.
This assumes that you have Docker (version 19.03 or greater)
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
### Prepare things
@ -38,7 +38,7 @@ and Docker Compose (version 1.6.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 Docker Compose (version 1.6.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 Docker Compose (version 1.6.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 Docker Compose (version 1.6.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,9 +1,7 @@
## 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

@ -1,9 +1,12 @@
ARG ALPINE_VERSION=3.13
ARG ALPINE_VERSION=3.16
FROM alpine:$ALPINE_VERSION as prereqs
#######################################
# Base Stage (prereqs)
#######################################
FROM alpine:$ALPINE_VERSION AS prereqs
WORKDIR /opt/app
# Install system dependencies (Python, development packages, and runtime libraries)
RUN apk --no-cache add \
python3 \
python3-dev \
@ -13,31 +16,47 @@ RUN apk --no-cache add \
libheif-dev \
libavif \
libavif-dev \
freetype-dev \
ffmpeg \
lapack \
lapack-dev \
# from requirements.txt:
py3-yaml \
py3-psycopg2 \
py3-sqlalchemy \
py3-certifi \
py3-numpy \
py3-pillow \
py3-pynacl \
py3-tz \
py3-pyrfc3339 \
&& pip3 install --no-cache-dir --disable-pip-version-check \
py3-pyrfc3339
# Install pip-only packages, using a wheel to avoid building scikit-image
RUN pip3 install --no-cache-dir wheel
RUN pip3 install --no-cache-dir --disable-pip-version-check \
--extra-index-url https://alpine-wheels.github.io/index \
"aggdraw==1.3.12" \
"alembic>=0.8.5" \
"attrs==25.1.0" \
"coloredlogs==5.0" \
"pyheif==0.6.1" \
"heif-image-plugin>=0.3.2" \
youtube_dl \
"pillow-avif-plugin>=1.1.0" \
&& apk --no-cache del py3-pip
yt-dlp \
"pillow>=6.1.0" \
"pillow-avif-plugin~=1.1.0" \
"psd-tools==1.10.4" \
"numpy==1.21.6" \
"scikit-image==0.19.3" \
"scipy==1.8.0" \
"sqlalchemy==1.3.21"
RUN apk --no-cache del py3-pip
COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests
FROM --platform=$BUILDPLATFORM prereqs as testing
#######################################
# Testing Stage
#######################################
FROM --platform=$BUILDPLATFORM prereqs AS testing
WORKDIR /opt/app
RUN apk --no-cache add \
@ -60,8 +79,10 @@ USER app
ENTRYPOINT ["pytest", "--tb=short"]
CMD ["szurubooru/"]
FROM prereqs as release
#######################################
# Release Stage
#######################################
FROM prereqs AS release
WORKDIR /opt/app
ARG PUID=1000

View File

@ -1,15 +1,20 @@
aggdraw==1.3.12
alembic>=0.8.5
attrs==25.1.0
certifi>=2017.11.5
coloredlogs==5.0
heif-image-plugin==0.3.2
numpy>=1.8.2
pillow-avif-plugin>=1.1.0
pillow>=4.3.0
heif-image-plugin>=0.3.2
numpy>=1.21.6
pillow-avif-plugin~=1.1.0
pillow>=6.1.0
psd-tools==1.10.4
psycopg2-binary>=2.6.1
pyheif==0.6.1
pynacl>=1.2.1
pyRFC3339>=1.0
pytz>=2018.3
pyyaml>=3.11
SQLAlchemy>=1.0.12, <1.4
youtube_dl
scikit-image==0.19.3
scipy==1.8.0
SQLAlchemy==1.3.21
yt-dlp

View File

@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
return left
def _docker_config() -> Dict:
def _container_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,6 +49,15 @@ 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"):
@ -57,8 +66,8 @@ def _read_config() -> Dict:
logger.warning(
"'config.yaml' should be a file, not a directory, skipping"
)
if os.path.exists("/.dockerenv"):
ret = _merge(ret, _docker_config())
if _running_inside_container():
ret = _merge(ret, _container_config())
return ret

View File

@ -7,13 +7,14 @@ import subprocess
from io import BytesIO
from typing import List
import HeifImagePlugin
import pillow_avif
from PIL import Image as PILImage
from psd_tools import PSDImage
from szurubooru import errors
from szurubooru.func import mime, util
logger = logging.getLogger(__name__)
@ -24,6 +25,15 @@ def convert_heif_to_png(content: bytes) -> bytes:
return img_byte_arr.getvalue()
def convert_psd_to_png(content: bytes) -> bytes:
psd_object = PSDImage.open(BytesIO(content))
img = psd_object.composite()
img_byte_arr = BytesIO()
img.save(img_byte_arr, format="PNG")
return img_byte_arr.getvalue()
class Image:
def __init__(self, content: bytes) -> None:
self.content = content
@ -265,10 +275,13 @@ class Image:
get_logs: bool = False,
) -> bytes:
mime_type = mime.get_mime_type(self.content)
# FFmpeg does not support HEIF or PSD.
# https://trac.ffmpeg.org/ticket/6521
# https://ffmpeg.org/pipermail/ffmpeg-devel/2016-July/196477.html
if mime.is_heif(mime_type):
# FFmpeg does not support HEIF.
# https://trac.ffmpeg.org/ticket/6521
self.content = convert_heif_to_png(self.content)
elif mime_type == "image/vnd.adobe.photoshop":
self.content = convert_psd_to_png(self.content)
extension = mime.get_extension(mime_type)
assert extension
with util.create_temp_file(suffix="." + extension) as handle:

View File

@ -33,6 +33,9 @@ def get_mime_type(content: bytes) -> str:
if content[4:12] in (b"ftypheic", b"ftypheix"):
return "image/heic"
if content[0:4] == b"8BPS":
return "image/vnd.adobe.photoshop"
if content[0:4] == b"\x1A\x45\xDF\xA3":
return "video/webm"
@ -56,6 +59,7 @@ def get_extension(mime_type: str) -> Optional[str]:
"image/avif": "avif",
"image/heif": "heif",
"image/heic": "heic",
"image/vnd.adobe.photoshop": "psd",
"video/mp4": "mp4",
"video/quicktime": "mov",
"video/webm": "webm",
@ -87,6 +91,7 @@ def is_image(mime_type: str) -> bool:
"image/avif",
"image/heif",
"image/heic",
"image/vnd.adobe.photoshop",
)

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 = ["youtube-dl", "--format", "best", "--no-playlist"]
cmd = ["yt-dlp", "--format", "best", "--no-playlist"]
if config.config["user_agent"]:
cmd.extend(["--user-agent", config.config["user_agent"]])
cmd.extend(["--get-url", url])