mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
34 Commits
8be6aa92b0
...
0bca328c49
Author | SHA1 | Date | |
---|---|---|---|
0bca328c49 | |||
ee7e9ef2a3 | |||
376f687c38 | |||
4fd848abf2 | |||
61b9f81e39 | |||
b721865931 | |||
46e3295003 | |||
031131506e | |||
d102578b54 | |||
6edb25d87b | |||
93fc15f2a4 | |||
4f9d46e1c2 | |||
b72e81850d | |||
c1c695f082 | |||
4b6b231fc8 | |||
6b0c3cfc7f | |||
4ec8cb3ba2 | |||
8d971234a2 | |||
a16bb198ab | |||
3f182a66ad | |||
b52363e82d | |||
3bf45e4c0a | |||
5596f53744 | |||
da425afc49 | |||
d7394d672f | |||
190d795426 | |||
7c92ceaf6a | |||
9e00f37464 | |||
59c497e168 | |||
c292b96f06 | |||
7a82e9d581 | |||
4806bbe0ed | |||
c2fdc2d070 | |||
ffdf115714 |
@ -8,7 +8,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
- 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)
|
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||||
- Post comments
|
- Post comments
|
||||||
- Post notes / annotations, including arbitrary polygons
|
- Post notes / annotations, including arbitrary polygons
|
||||||
- Rich JSON REST API ([see documentation](doc/API.md))
|
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
|
public/
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
|
@ -1,3 +1,26 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM node:lts-alpine as development
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
dumb-init \
|
||||||
|
nginx \
|
||||||
|
git
|
||||||
|
|
||||||
|
RUN ln -sf /opt/app/nginx.conf.docker /etc/nginx/nginx.conf
|
||||||
|
RUN rm -rf /var/www
|
||||||
|
RUN ln -sf /opt/app/public/ /var/www
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
ARG BUILD_INFO="docker-development"
|
||||||
|
ENV BUILD_INFO=${BUILD_INFO}
|
||||||
|
ENV BACKEND_HOST="server"
|
||||||
|
|
||||||
|
CMD ["/opt/app/docker-start-dev.sh"]
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:lts as builder
|
FROM --platform=$BUILDPLATFORM node:lts as builder
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ function makeOutputDirs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function watch() {
|
function watch() {
|
||||||
let wss = new WebSocket.Server({ port: 8080 });
|
let wss = new WebSocket.Server({ port: environment === "development" ? 8081 : 8080 });
|
||||||
const liveReload = !process.argv.includes('--no-live-reload');
|
const liveReload = !process.argv.includes('--no-live-reload');
|
||||||
|
|
||||||
function emitReload() {
|
function emitReload() {
|
||||||
|
@ -127,6 +127,10 @@ $comment-border-color = #DDD
|
|||||||
color: mix($main-color, $inactive-link-color-darktheme)
|
color: mix($main-color, $inactive-link-color-darktheme)
|
||||||
|
|
||||||
.comment-content
|
.comment-content
|
||||||
|
p
|
||||||
|
word-wrap: normal
|
||||||
|
word-break: break-word
|
||||||
|
|
||||||
ul, ol
|
ul, ol
|
||||||
list-style-position: inside
|
list-style-position: inside
|
||||||
margin: 1em 0
|
margin: 1em 0
|
||||||
|
@ -300,10 +300,10 @@ a .access-key
|
|||||||
background-size: 20px 20px
|
background-size: 20px 20px
|
||||||
img
|
img
|
||||||
opacity: 0
|
opacity: 0
|
||||||
width: auto
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
video
|
video
|
||||||
width: auto
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
.flexbox-dummy
|
.flexbox-dummy
|
||||||
|
@ -187,6 +187,9 @@
|
|||||||
vertical-align: top
|
vertical-align: top
|
||||||
@media (max-width: 1000px)
|
@media (max-width: 1000px)
|
||||||
display: block
|
display: block
|
||||||
|
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
|
||||||
|
float: left
|
||||||
|
margin-right: 1em
|
||||||
input
|
input
|
||||||
margin-bottom: 0.25em
|
margin-bottom: 0.25em
|
||||||
margin-right: 0.25em
|
margin-right: 0.25em
|
||||||
|
@ -15,40 +15,44 @@
|
|||||||
border: 0
|
border: 0
|
||||||
outline: 0
|
outline: 0
|
||||||
|
|
||||||
nav.buttons
|
>.sidebar>nav.buttons, >.content nav.buttons
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
article
|
article
|
||||||
flex: 1 0 33%
|
flex: 1 0 33%
|
||||||
a
|
a
|
||||||
display: inline-block
|
display: inline-block
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 0.3em 0
|
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
|
text-align: center
|
||||||
@media (max-width: 800px)
|
vertical-align: middle
|
||||||
margin-top: 2em
|
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
|
>.content
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.post-container
|
.post-container
|
||||||
margin-bottom: 2em
|
margin-bottom: 0.6em
|
||||||
|
|
||||||
.post-content
|
.post-content
|
||||||
margin: 0
|
margin: 0
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-repeat: no-repeat
|
background-repeat: no-repeat
|
||||||
|
|
||||||
|
.after-mobile-controls
|
||||||
|
width: 100%
|
||||||
|
|
||||||
.darktheme .post-view
|
.darktheme .post-view
|
||||||
>.sidebar
|
>.sidebar, >.content
|
||||||
nav.buttons
|
nav.buttons
|
||||||
article
|
article
|
||||||
a:not(.inactive):hover
|
a:not(.inactive):hover
|
||||||
@ -58,6 +62,8 @@
|
|||||||
@media (max-width: 800px)
|
@media (max-width: 800px)
|
||||||
.post-view
|
.post-view
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
>.after-mobile-controls
|
||||||
|
order: 3
|
||||||
>.sidebar
|
>.sidebar
|
||||||
order: 2
|
order: 2
|
||||||
min-width: 100%
|
min-width: 100%
|
||||||
@ -115,7 +121,6 @@
|
|||||||
h1
|
h1
|
||||||
margin-bottom: 0.5em
|
margin-bottom: 0.5em
|
||||||
.thumbnail
|
.thumbnail
|
||||||
background-position: 50% 30%
|
|
||||||
width: 4em
|
width: 4em
|
||||||
height: 3em
|
height: 3em
|
||||||
li
|
li
|
||||||
|
@ -21,10 +21,11 @@
|
|||||||
.details
|
.details
|
||||||
font-size: 90%
|
font-size: 90%
|
||||||
line-height: 130%
|
line-height: 130%
|
||||||
|
.image
|
||||||
|
margin: 0.25em 0.6em 0.25em 0
|
||||||
.thumbnail
|
.thumbnail
|
||||||
width: 3em
|
width: 3em
|
||||||
height: 3em
|
height: 3em
|
||||||
margin: 0.25em 0.6em 0 0
|
|
||||||
|
|
||||||
.darktheme .user-list
|
.darktheme .user-list
|
||||||
ul li
|
ul li
|
||||||
|
17
client/docker-start-dev.sh
Executable file
17
client/docker-start-dev.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/dumb-init /bin/sh
|
||||||
|
|
||||||
|
# Integrate environment variables
|
||||||
|
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||||
|
/etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
nginx &
|
||||||
|
|
||||||
|
# Watch source for changes and build app
|
||||||
|
# FIXME: It's not ergonomic to run `npm i` outside of the build step.
|
||||||
|
# However, the mounting of different directories into the
|
||||||
|
# client container's /opt/app causes node_modules to disappear
|
||||||
|
# (the mounting causes client/Dockerfile's RUN npm install
|
||||||
|
# to silently clobber).
|
||||||
|
# Find a way to move `npm i` into client/Dockerfile.
|
||||||
|
npm i && npm run watch -- --polling
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
# Integrate environment variables
|
# Integrate environment variables
|
||||||
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||||
/etc/nginx/nginx.conf
|
/etc/nginx/nginx.conf
|
||||||
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
|
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
|
||||||
/var/www/index.htm \
|
/var/www/index.htm \
|
||||||
/var/www/manifest.json
|
/var/www/manifest.json
|
||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
exec nginx
|
exec nginx
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'/>
|
<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='theme-color' content='#24aadd'/>
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<span class='vim-nav-hint'>Next post ></span>
|
<span class='vim-nav-hint'>Next post ></span>
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
|
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||||
<article class='edit-post'>
|
<article class='edit-post'>
|
||||||
<% if (ctx.editMode) { %>
|
<% if (ctx.editMode) { %>
|
||||||
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
||||||
@ -36,16 +37,13 @@
|
|||||||
<span class='vim-nav-hint'>Back to view mode</span>
|
<span class='vim-nav-hint'>Back to view mode</span>
|
||||||
</a>
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
<i class='fa fa-pencil'></i>
|
||||||
<% } else { %>
|
<span class='vim-nav-hint'>Edit post</span>
|
||||||
<a class='inactive'>
|
|
||||||
<% } %>
|
|
||||||
<i class='fa fa-pencil'></i>
|
|
||||||
<span class='vim-nav-hint'>Edit post</span>
|
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</article>
|
</article>
|
||||||
|
<% } %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class='sidebar-container'></div>
|
<div class='sidebar-container'></div>
|
||||||
@ -54,13 +52,15 @@
|
|||||||
<div class='content'>
|
<div class='content'>
|
||||||
<div class='post-container'></div>
|
<div class='post-container'></div>
|
||||||
|
|
||||||
<% if (ctx.canListComments) { %>
|
<div class='after-mobile-controls'>
|
||||||
<div class='comments-container'></div>
|
<% if (ctx.canCreateComments) { %>
|
||||||
<% } %>
|
<h2>Add comment</h2>
|
||||||
|
<div class='comment-form-container'></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<% if (ctx.canCreateComments) { %>
|
<% if (ctx.canListComments) { %>
|
||||||
<h2>Add comment</h2>
|
<div class='comments-container'></div>
|
||||||
<div class='comment-form-container'></div>
|
<% } %>
|
||||||
<% } %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
'video/mp4': 'MPEG-4',
|
'video/mp4': 'MPEG-4',
|
||||||
'video/quicktime': 'MOV',
|
'video/quicktime': 'MOV',
|
||||||
'application/x-shockwave-flash': 'SWF',
|
'application/x-shockwave-flash': 'SWF',
|
||||||
}[ctx.post.mimeType] %>
|
}[ctx.post.mimeType] %><!--
|
||||||
</a>
|
--></a>
|
||||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||||
<% if (ctx.post.flags.length) { %><!--
|
<% if (ctx.post.flags.length) { %><!--
|
||||||
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||||
@ -58,7 +58,7 @@
|
|||||||
Search on
|
Search on
|
||||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||||
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
||||||
<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>
|
||||||
|
|
||||||
<section class='social'>
|
<section class='social'>
|
||||||
@ -99,10 +99,10 @@
|
|||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
--><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]) %><!--
|
||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--></a><!--
|
--></a><!--
|
||||||
--><% } %><!--
|
--><% } %> <!--
|
||||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||||
--></li><!--
|
--></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||||
%><span class='append hint'>Tagging with:</span><%
|
%><span class='append hint'>Tagging with:</span><%
|
||||||
%><a href class='mousetrap button append open'>Mass tag</a><%
|
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||||
%><wbr/><%
|
|
||||||
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||||
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||||
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||||
|
@ -91,16 +91,16 @@ class PoolController {
|
|||||||
_evtUpdate(e) {
|
_evtUpdate(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
if (e.detail.names !== undefined) {
|
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||||
e.detail.pool.names = e.detail.names;
|
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;
|
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;
|
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();
|
e.detail.pool.posts.clear();
|
||||||
for (let postId of e.detail.posts) {
|
for (let postId of e.detail.posts) {
|
||||||
e.detail.pool.posts.add(
|
e.detail.pool.posts.add(
|
||||||
|
@ -43,6 +43,8 @@ class PoolListController {
|
|||||||
this._headerView.addEventListener(
|
this._headerView.addEventListener(
|
||||||
"submit",
|
"submit",
|
||||||
(e) => this._evtSubmit(e),
|
(e) => this._evtSubmit(e),
|
||||||
|
);
|
||||||
|
this._headerView.addEventListener(
|
||||||
"navigate",
|
"navigate",
|
||||||
(e) => this._evtNavigate(e)
|
(e) => this._evtNavigate(e)
|
||||||
);
|
);
|
||||||
|
@ -169,22 +169,22 @@ class PostMainController extends BasePostController {
|
|||||||
this._view.sidebarControl.disableForm();
|
this._view.sidebarControl.disableForm();
|
||||||
this._view.sidebarControl.clearMessages();
|
this._view.sidebarControl.clearMessages();
|
||||||
const post = e.detail.post;
|
const post = e.detail.post;
|
||||||
if (e.detail.safety !== undefined) {
|
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||||
post.safety = e.detail.safety;
|
post.safety = e.detail.safety;
|
||||||
}
|
}
|
||||||
if (e.detail.flags !== undefined) {
|
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||||
post.flags = e.detail.flags;
|
post.flags = e.detail.flags;
|
||||||
}
|
}
|
||||||
if (e.detail.relations !== undefined) {
|
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||||
post.relations = e.detail.relations;
|
post.relations = e.detail.relations;
|
||||||
}
|
}
|
||||||
if (e.detail.content !== undefined) {
|
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||||
post.newContent = e.detail.content;
|
post.newContent = e.detail.content;
|
||||||
}
|
}
|
||||||
if (e.detail.thumbnail !== undefined) {
|
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||||
post.newThumbnail = e.detail.thumbnail;
|
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.source = e.detail.source;
|
||||||
}
|
}
|
||||||
post.save().then(
|
post.save().then(
|
||||||
|
@ -95,13 +95,13 @@ class TagController {
|
|||||||
_evtUpdate(e) {
|
_evtUpdate(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
if (e.detail.names !== undefined) {
|
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||||
e.detail.tag.names = e.detail.names;
|
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;
|
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.description = e.detail.description;
|
||||||
}
|
}
|
||||||
e.detail.tag.save().then(
|
e.detail.tag.save().then(
|
||||||
|
@ -175,21 +175,21 @@ class UserController {
|
|||||||
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
||||||
const infix = isLoggedIn ? "self" : "any";
|
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;
|
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;
|
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;
|
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;
|
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;
|
e.detail.user.avatarStyle = e.detail.avatarStyle;
|
||||||
if (e.detail.avatarContent) {
|
if (e.detail.avatarContent) {
|
||||||
e.detail.user.avatarContent = e.detail.avatarContent;
|
e.detail.user.avatarContent = e.detail.avatarContent;
|
||||||
@ -302,7 +302,7 @@ class UserController {
|
|||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
|
|
||||||
if (e.detail.note !== undefined) {
|
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||||
e.detail.userToken.note = e.detail.note;
|
e.detail.userToken.note = e.detail.note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,12 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
this._urlInputNode.addEventListener("keydown", (e) =>
|
this._urlInputNode.addEventListener("keydown", (e) =>
|
||||||
this._evtUrlInputKeyDown(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) {
|
if (this._urlConfirmButtonNode) {
|
||||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
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;
|
this._originalHtml = this._dropperNode.innerHTML;
|
||||||
views.replaceContent(target, source);
|
views.replaceContent(target, source);
|
||||||
}
|
}
|
||||||
@ -129,6 +140,17 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
this._emitFiles(e.dataTransfer.files);
|
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) {
|
_evtUrlInputKeyDown(e) {
|
||||||
if (e.which !== KEY_RETURN) {
|
if (e.which !== KEY_RETURN) {
|
||||||
return;
|
return;
|
||||||
|
@ -103,6 +103,30 @@ class PostContentControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_refreshSize() {
|
_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();
|
this._currentFitFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
thumbnail:
|
thumbnail:
|
||||||
this._newPostThumbnail !== undefined
|
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
|
||||||
? this._newPostThumbnail
|
? this._newPostThumbnail
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
const config = require("./config.js");
|
const config = require("./config.js");
|
||||||
|
|
||||||
if (config.environment == "development") {
|
if (config.environment == "development") {
|
||||||
var ws = new WebSocket("ws://" + location.hostname + ":8080");
|
var ws = new WebSocket("ws://" + location.hostname + ":8081");
|
||||||
ws.addEventListener("open", function (event) {
|
ws.addEventListener("open", function (event) {
|
||||||
console.log("Live-reloading websocket connected.");
|
console.log("Live-reloading websocket connected.");
|
||||||
});
|
});
|
||||||
|
@ -271,7 +271,7 @@ class Post extends events.EventTarget {
|
|||||||
if (this._newContent) {
|
if (this._newContent) {
|
||||||
files.content = this._newContent;
|
files.content = this._newContent;
|
||||||
}
|
}
|
||||||
if (this._newThumbnail !== undefined) {
|
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
|
||||||
files.thumbnail = this._newThumbnail;
|
files.thumbnail = this._newThumbnail;
|
||||||
}
|
}
|
||||||
if (this._source !== this._orig._source) {
|
if (this._source !== this._orig._source) {
|
||||||
|
@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTagLink(name, includeHash, includeCount, tag) {
|
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);
|
let text = misc.getPrettyName(name);
|
||||||
if (includeHash === true) {
|
if (includeHash === true) {
|
||||||
text = "#" + text;
|
text = "#" + text;
|
||||||
}
|
}
|
||||||
if (includeCount === true) {
|
if (includeCount === true) {
|
||||||
text += " (" + (tag ? tag.postCount : 0) + ")";
|
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
|
||||||
}
|
}
|
||||||
return api.hasPrivilege("tags:view")
|
return api.hasPrivilege("tags:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makePoolLink(id, includeHash, includeCount, pool, name) {
|
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(
|
let text = misc.getPrettyName(
|
||||||
name ? name : pool ? pool.names[0] : "unknown"
|
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
|
||||||
);
|
);
|
||||||
if (includeHash === true) {
|
if (includeHash === true) {
|
||||||
text = "#" + text;
|
text = "#" + text;
|
||||||
}
|
}
|
||||||
if (includeCount === true) {
|
if (includeCount === true) {
|
||||||
text += " (" + (pool ? pool.postCount : 0) + ")";
|
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
|
||||||
}
|
}
|
||||||
return api.hasPrivilege("pools:view")
|
return api.hasPrivilege("pools:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
@ -264,7 +264,7 @@ function makeUserLink(user) {
|
|||||||
let text = makeThumbnail(user ? user.avatarUrl : null);
|
let text = makeThumbnail(user ? user.avatarUrl : null);
|
||||||
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
||||||
const link =
|
const link =
|
||||||
user && api.hasPrivilege("users:view")
|
user && user.name && api.hasPrivilege("users:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
"a",
|
"a",
|
||||||
{ href: uri.formatClientLink("user", user.name) },
|
{ href: uri.formatClientLink("user", user.name) },
|
||||||
|
16
doc/API.md
16
doc/API.md
@ -54,7 +54,7 @@
|
|||||||
- [Deleting pool category](#deleting-pool-category)
|
- [Deleting pool category](#deleting-pool-category)
|
||||||
- [Setting default pool category](#setting-default-pool-category)
|
- [Setting default pool category](#setting-default-pool-category)
|
||||||
- Pools
|
- Pools
|
||||||
- [Listing pools](#listing-pool)
|
- [Listing pools](#listing-pools)
|
||||||
- [Creating pool](#creating-pool)
|
- [Creating pool](#creating-pool)
|
||||||
- [Updating pool](#updating-pool)
|
- [Updating pool](#updating-pool)
|
||||||
- [Getting pool](#getting-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
|
accepts a file named `content`, the client should pass
|
||||||
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
|
`{"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
|
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)
|
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
|
||||||
to download content from popular sites such as youtube, gfycat, etc. Access to
|
download content from popular sites such as youtube, gfycat, etc. Access to
|
||||||
youtube-dl can be configured with the `'uploads:use_downloader'` permission
|
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
|
Finally, in some cases the user might want to reuse one file between the
|
||||||
requests to save the bandwidth (for example, reverse search + consecutive
|
requests to save the bandwidth (for example, reverse search + consecutive
|
||||||
@ -323,7 +323,7 @@ data.
|
|||||||
{
|
{
|
||||||
"name": <name>,
|
"name": <name>,
|
||||||
"color": <color>,
|
"color": <color>,
|
||||||
"order": <order> // optional
|
"order": <order>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -789,7 +789,7 @@ data.
|
|||||||
| `fav-time` | alias of `fav-date` |
|
| `fav-time` | alias of `fav-date` |
|
||||||
| `feature-date` | featured at given date |
|
| `feature-date` | featured at given date |
|
||||||
| `feature-time` | alias of `feature-time` |
|
| `feature-time` | alias of `feature-time` |
|
||||||
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
|
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
|
||||||
| `rating` | alias of `safety` |
|
| `rating` | alias of `safety` |
|
||||||
|
|
||||||
**Sort style tokens**
|
**Sort style tokens**
|
||||||
@ -1389,7 +1389,7 @@ data.
|
|||||||
## Creating pool
|
## Creating pool
|
||||||
- **Request**
|
- **Request**
|
||||||
|
|
||||||
`POST /pools/create`
|
`POST /pool`
|
||||||
|
|
||||||
- **Input**
|
- **Input**
|
||||||
|
|
||||||
@ -2491,7 +2491,7 @@ One file together with its metadata posted to the site.
|
|||||||
## Micro post
|
## Micro post
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
|
A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields.
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
**Description**
|
**Description**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
This assumes that you have Docker (version 17.05 or greater)
|
This assumes that you have Docker (version 19.03 or greater)
|
||||||
and Docker Compose (version 1.6.0 or greater) already installed.
|
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
|
||||||
|
|
||||||
### Prepare things
|
### 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:
|
This pulls the latest containers from docker.io:
|
||||||
```console
|
```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
|
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:
|
For first run, it is recommended to start the database separately:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose up -d sql
|
user@host:szuru$ docker compose up -d sql
|
||||||
```
|
```
|
||||||
|
|
||||||
To start all containers:
|
To start all containers:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose up -d
|
user@host:szuru$ docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
To view/monitor the application logs:
|
To view/monitor the application logs:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose logs -f
|
user@host:szuru$ docker compose logs -f
|
||||||
# (CTRL+C to exit)
|
# (CTRL+C to exit)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -84,13 +84,13 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
|||||||
2. Build the containers:
|
2. Build the containers:
|
||||||
|
|
||||||
```console
|
```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`
|
That will attempt to build both containers, but you can specify `client`
|
||||||
or `server` to make it build only one.
|
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
|
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
|
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
|
*Note: If your changes are not taking effect in your builds, consider building
|
||||||
with `--no-cache`.*
|
with `--no-cache`.*
|
||||||
@ -117,7 +117,7 @@ with `--no-cache`.*
|
|||||||
run from docker:
|
run from docker:
|
||||||
|
|
||||||
```console
|
```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.
|
will give you a breakdown on all available commands.
|
||||||
|
54
docker-compose.dev.yml
Normal file
54
docker-compose.dev.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
## Docker Compose configuration for dev iteration
|
||||||
|
##
|
||||||
|
## Data is transient by using named vols.
|
||||||
|
## Run: docker-compose -f ./docker-compose.dev.yml up
|
||||||
|
services:
|
||||||
|
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: ./server
|
||||||
|
target: development
|
||||||
|
depends_on:
|
||||||
|
- sql
|
||||||
|
environment:
|
||||||
|
## These should be the names of the dependent containers listed below,
|
||||||
|
## or FQDNs/IP addresses if these services are running outside of Docker
|
||||||
|
POSTGRES_HOST: sql
|
||||||
|
## Credentials for database:
|
||||||
|
POSTGRES_USER:
|
||||||
|
POSTGRES_PASSWORD:
|
||||||
|
## Commented Values are Default:
|
||||||
|
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
||||||
|
#POSTGRES_PORT: 5432
|
||||||
|
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||||
|
THREADS:
|
||||||
|
volumes:
|
||||||
|
- "data:/data"
|
||||||
|
- "./server/:/opt/app/"
|
||||||
|
|
||||||
|
client:
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
target: development
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
volumes:
|
||||||
|
- "data:/data:ro"
|
||||||
|
- "./client/:/opt/app/"
|
||||||
|
- "/opt/app/public/"
|
||||||
|
ports:
|
||||||
|
- "${PORT}:80"
|
||||||
|
- "8081:8081"
|
||||||
|
|
||||||
|
sql:
|
||||||
|
image: postgres:11-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER:
|
||||||
|
POSTGRES_PASSWORD:
|
||||||
|
volumes:
|
||||||
|
- "sql:/var/lib/postgresql/data"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
sql:
|
@ -1,9 +1,7 @@
|
|||||||
## Example Docker Compose configuration
|
## 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
|
## orchestration services
|
||||||
version: '2'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
server:
|
server:
|
||||||
|
@ -23,15 +23,15 @@ RUN apk --no-cache add \
|
|||||||
py3-pillow \
|
py3-pillow \
|
||||||
py3-pynacl \
|
py3-pynacl \
|
||||||
py3-tz \
|
py3-tz \
|
||||||
py3-pyrfc3339 \
|
py3-pyrfc3339
|
||||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
RUN pip3 install --no-cache-dir --disable-pip-version-check \
|
||||||
"alembic>=0.8.5" \
|
"alembic>=0.8.5" \
|
||||||
"coloredlogs==5.0" \
|
"coloredlogs==5.0" \
|
||||||
"pyheif==0.6.1" \
|
"pyheif==0.6.1" \
|
||||||
"heif-image-plugin>=0.3.2" \
|
"heif-image-plugin>=0.3.2" \
|
||||||
youtube_dl \
|
yt-dlp \
|
||||||
"pillow-avif-plugin>=1.1.0" \
|
"pillow-avif-plugin~=1.1.0"
|
||||||
&& apk --no-cache del py3-pip
|
RUN apk --no-cache del py3-pip
|
||||||
|
|
||||||
COPY ./ /opt/app/
|
COPY ./ /opt/app/
|
||||||
RUN rm -rf /opt/app/szurubooru/tests
|
RUN rm -rf /opt/app/szurubooru/tests
|
||||||
@ -61,7 +61,42 @@ ENTRYPOINT ["pytest", "--tb=short"]
|
|||||||
CMD ["szurubooru/"]
|
CMD ["szurubooru/"]
|
||||||
|
|
||||||
|
|
||||||
|
FROM prereqs as development
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
ARG PUID=1000
|
||||||
|
ARG PGID=1000
|
||||||
|
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
dumb-init \
|
||||||
|
py3-pip \
|
||||||
|
py3-setuptools \
|
||||||
|
py3-waitress \
|
||||||
|
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||||
|
hupper \
|
||||||
|
&& mkdir -p /opt/app /data \
|
||||||
|
&& addgroup -g ${PGID} app \
|
||||||
|
&& adduser -SDH -h /opt/app -g '' -G app -u ${PUID} app \
|
||||||
|
&& chown -R app:app /opt/app /data
|
||||||
|
|
||||||
|
USER app
|
||||||
|
CMD ["/opt/app/docker-start-dev.sh"]
|
||||||
|
|
||||||
|
ARG PORT=6666
|
||||||
|
ENV PORT=${PORT}
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
ARG THREADS=4
|
||||||
|
ENV THREADS=${THREADS}
|
||||||
|
|
||||||
|
VOLUME ["/data/"]
|
||||||
|
|
||||||
|
|
||||||
FROM prereqs as release
|
FROM prereqs as release
|
||||||
|
|
||||||
|
COPY ./ /opt/app/
|
||||||
|
RUN rm -rf /opt/app/szurubooru/tests
|
||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
ARG PUID=1000
|
ARG PUID=1000
|
||||||
|
8
server/docker-start-dev.sh
Executable file
8
server/docker-start-dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/dumb-init /bin/sh
|
||||||
|
set -e
|
||||||
|
cd /opt/app
|
||||||
|
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||||
|
exec hupper -m waitress --port ${PORT} --threads ${THREADS} szurubooru.facade:app
|
@ -3,7 +3,7 @@ certifi>=2017.11.5
|
|||||||
coloredlogs==5.0
|
coloredlogs==5.0
|
||||||
heif-image-plugin==0.3.2
|
heif-image-plugin==0.3.2
|
||||||
numpy>=1.8.2
|
numpy>=1.8.2
|
||||||
pillow-avif-plugin>=1.1.0
|
pillow-avif-plugin~=1.1.0
|
||||||
pillow>=4.3.0
|
pillow>=4.3.0
|
||||||
psycopg2-binary>=2.6.1
|
psycopg2-binary>=2.6.1
|
||||||
pyheif==0.6.1
|
pyheif==0.6.1
|
||||||
@ -12,4 +12,4 @@ pyRFC3339>=1.0
|
|||||||
pytz>=2018.3
|
pytz>=2018.3
|
||||||
pyyaml>=3.11
|
pyyaml>=3.11
|
||||||
SQLAlchemy>=1.0.12, <1.4
|
SQLAlchemy>=1.0.12, <1.4
|
||||||
youtube_dl
|
yt-dlp
|
||||||
|
@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
|
|||||||
return left
|
return left
|
||||||
|
|
||||||
|
|
||||||
def _docker_config() -> Dict:
|
def _container_config() -> Dict:
|
||||||
if "TEST_ENVIRONMENT" not in os.environ:
|
if "TEST_ENVIRONMENT" not in os.environ:
|
||||||
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
||||||
if key not in os.environ:
|
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 {}
|
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:
|
def _read_config() -> Dict:
|
||||||
ret = _file_config("config.yaml.dist")
|
ret = _file_config("config.yaml.dist")
|
||||||
if os.path.isfile("config.yaml"):
|
if os.path.isfile("config.yaml"):
|
||||||
@ -57,8 +66,8 @@ def _read_config() -> Dict:
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
"'config.yaml' should be a file, not a directory, skipping"
|
"'config.yaml' should be a file, not a directory, skipping"
|
||||||
)
|
)
|
||||||
if os.path.exists("/.dockerenv"):
|
if _running_inside_container():
|
||||||
ret = _merge(ret, _docker_config())
|
ret = _merge(ret, _container_config())
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def _get_youtube_dl_content_url(url: str) -> str:
|
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"]:
|
if config.config["user_agent"]:
|
||||||
cmd.extend(["--user-agent", config.config["user_agent"]])
|
cmd.extend(["--user-agent", config.config["user_agent"]])
|
||||||
cmd.extend(["--get-url", url])
|
cmd.extend(["--get-url", url])
|
||||||
|
@ -122,6 +122,34 @@ def _pool_filter(
|
|||||||
)(query, criterion, negated)
|
)(query, criterion, negated)
|
||||||
|
|
||||||
|
|
||||||
|
def _category_filter(
|
||||||
|
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
|
||||||
|
) -> SaQuery:
|
||||||
|
assert criterion
|
||||||
|
|
||||||
|
# Step 1. find the id for the category
|
||||||
|
q1 = db.session.query(model.TagCategory.tag_category_id).filter(
|
||||||
|
model.TagCategory.name == criterion.value
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2. find the tags with that category
|
||||||
|
q2 = db.session.query(model.Tag.tag_id).filter(
|
||||||
|
model.Tag.category_id.in_(q1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3. find all posts that have at least one of those tags
|
||||||
|
q3 = db.session.query(model.PostTag.post_id).filter(
|
||||||
|
model.PostTag.tag_id.in_(q2)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4. profit
|
||||||
|
expr = model.Post.post_id.in_(q3)
|
||||||
|
if negated:
|
||||||
|
expr = ~expr
|
||||||
|
|
||||||
|
return query.filter(expr)
|
||||||
|
|
||||||
|
|
||||||
class PostSearchConfig(BaseSearchConfig):
|
class PostSearchConfig(BaseSearchConfig):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.user = None # type: Optional[model.User]
|
self.user = None # type: Optional[model.User]
|
||||||
@ -349,6 +377,7 @@ class PostSearchConfig(BaseSearchConfig):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
(["pool"], _pool_filter),
|
(["pool"], _pool_filter),
|
||||||
|
(["category"], _category_filter),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -863,3 +863,55 @@ def test_tumbleweed(
|
|||||||
db.session.flush()
|
db.session.flush()
|
||||||
verify_unpaged("special:tumbleweed", [4])
|
verify_unpaged("special:tumbleweed", [4])
|
||||||
verify_unpaged("-special:tumbleweed", [1, 2, 3])
|
verify_unpaged("-special:tumbleweed", [1, 2, 3])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input,expected_post_ids",
|
||||||
|
[
|
||||||
|
("category:cat1", [1, 2, 3]),
|
||||||
|
("category:cat2", [3, 4]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_search_by_tag_category(
|
||||||
|
verify_unpaged,
|
||||||
|
post_factory,
|
||||||
|
tag_factory,
|
||||||
|
tag_category_factory,
|
||||||
|
input,
|
||||||
|
expected_post_ids,
|
||||||
|
):
|
||||||
|
cat1 = tag_category_factory(name="cat1")
|
||||||
|
cat2 = tag_category_factory(name="cat2")
|
||||||
|
tag1 = tag_factory(names=["t1"], category=cat1)
|
||||||
|
tag2 = tag_factory(names=["t2"], category=cat1)
|
||||||
|
tag3 = tag_factory(names=["t3"], category=cat2)
|
||||||
|
|
||||||
|
post1 = post_factory(id=1)
|
||||||
|
post1.tags.append(tag1)
|
||||||
|
|
||||||
|
post2 = post_factory(id=2)
|
||||||
|
post2.tags.append(tag2)
|
||||||
|
|
||||||
|
post3 = post_factory(id=3)
|
||||||
|
post3.tags.append(tag1)
|
||||||
|
post3.tags.append(tag3)
|
||||||
|
|
||||||
|
post4 = post_factory(id=4)
|
||||||
|
post4.tags.append(tag3)
|
||||||
|
|
||||||
|
post5 = post_factory(id=5)
|
||||||
|
|
||||||
|
db.session.add_all(
|
||||||
|
[
|
||||||
|
tag1,
|
||||||
|
tag2,
|
||||||
|
tag3,
|
||||||
|
post1,
|
||||||
|
post2,
|
||||||
|
post3,
|
||||||
|
post4,
|
||||||
|
post5,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.session.flush()
|
||||||
|
verify_unpaged(input, expected_post_ids)
|
||||||
|
Reference in New Issue
Block a user