61 Commits

Author SHA1 Message Date
Eva
c705e9b9f1 Merge 9f533882bf into 376f687c38 2025-03-31 23:49:06 +00:00
Eva
9f533882bf client/css: focused active tab styling 2025-04-01 01:48:59 +02:00
Eva
6035278c7e client/css: dark theme comment styling 2025-04-01 01:00:12 +02:00
Eva
acb1eddb53 client/css: text input focus outline 2025-03-30 08:05:47 +02:00
Eva
b3fa26b3fb client/api: set json mime type for metadata 2025-03-29 07:16:08 +01:00
Eva
d3c2da4f91 server/upload: disable default loop flag for videos, detect gif loop 2025-03-29 07:13:56 +01:00
Eva
8acd6ab776 server/search: add safety search synonyms 2025-03-29 07:13:24 +01:00
Eva
2ed19e013a server/posts: more thorough error exception 2025-03-29 07:13:10 +01:00
2b06e1cafa client: fix weird Chrome bug where video controls are invisible under div class="transparent" 2025-03-29 07:12:01 +01:00
7f6211e0cc client: fix displaying 0 in number inputs 2025-03-29 07:11:28 +01:00
Eva
86bbd429f7 client: apply theme as soon as possible
Fixes the default theme flashing on reload
2025-03-29 07:10:55 +01:00
Eva
0b02826e6d client: apply dark theme without a page reload
I'm really confused why the user was expected to reload the page,
there's no technical reason that I can find for it to be this way.
2025-03-29 07:08:33 +01:00
ea215901af client: add more keybinds, prevent typing when focusing input 2025-03-29 07:08:08 +01:00
Eva
9884161297 client/views: prevent errors on slow connections 2025-03-29 07:07:42 +01:00
Eva
b2501b7ee2 client/users: default user icon to site favicon for anonymous uploads 2025-03-29 07:07:09 +01:00
Eva
7f2a8b5b07 client/upload: check "Add relation" box by default 2025-03-29 07:06:49 +01:00
Eva
de9b8fdce3 client/snapshots: display pool name 2025-03-29 07:05:55 +01:00
Eva
ae85605531 client/posts: filter out empty post source lines 2025-03-29 07:05:34 +01:00
Eva
ae72d75631 client/posts: redirect away from /edit when no post edit permissions 2025-03-29 07:03:48 +01:00
Eva
ce613c5ade client/posts: page exit confirmation for bulk edit actions 2025-03-29 07:03:30 +01:00
Eva
e05252f67e client/posts: hide edit icon when unprivileged 2025-03-29 07:03:09 +01:00
Eva
20e03e1397 client/posts: force file download for download button
So that it works regardless of server configuration.
2025-03-29 07:02:47 +01:00
Eva
a9d870eaa8 client/posts: don't crash on edit mode when no permissions 2025-03-29 07:00:49 +01:00
Eva
08bd3bb890 client/posts: better drag alt text on chrome 2025-03-29 07:00:33 +01:00
Eva
b8b51ded15 client/pools: display error message on merge page when no pool selected
Also fixes a js error.
2025-03-29 06:59:37 +01:00
Eva
f590dc6a41 client/pools, client/tags: obvious button to access posts list 2025-03-29 06:59:22 +01:00
Eva
486fc345fe client/pools, client/tags: move description before details 2025-03-29 06:59:22 +01:00
Eva
ae9e596095 client/notes: remove interaction with main overlay area
Right click Copy would not work on Chrome and Firefox.
Right click Save would not work on Firefox.
2025-03-29 06:58:27 +01:00
Eva
5d1dbb291c client/markdown: allow markdown headers
This regex caught more than was necessary, and in the process made it
impossible to use headers. Only replace instances that will actually
trigger tag permalinks.
2025-03-29 06:58:12 +01:00
Eva
a8b6c143eb client/css: use text color for home footer bullets 2025-03-29 06:57:43 +01:00
Eva
4c6d1a216b client/css: space out similar posts on upload page 2025-03-29 06:57:27 +01:00
Eva
9588d11cb0 client/css: gray out disabled radio button circle 2025-03-29 06:52:46 +01:00
Eva
b08c6eca26 client/comments: prevent avatar focus outline from being clipped 2025-03-29 06:50:56 +01:00
376f687c38 chore: questionable is not a recognized rating 2025-02-11 21:50:27 +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
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
55 changed files with 343 additions and 159 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 [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 notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md))

View File

@ -4,6 +4,9 @@ $comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD
.comments-container
margin-top: 2em
.comment-container
padding: 0 0 0 60px
@ -16,7 +19,13 @@ $comment-border-color = #DDD
width: 40px
height: 40px
a
outline: none
display: inline-block
position: relative
top: -2px
border: 2px solid transparent
&:focus
border: 2px solid $main-color
nav:not(.active), .tab:not(.active)
display: none
@ -114,19 +123,36 @@ $comment-border-color = #DDD
.messages
margin: 1em 0
.darktheme .comment-container .comment header
background: $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
.darktheme .comment-container
.comment
border: 1px solid $comment-header-background-color-darktheme
header
background: $comment-header-background-color-darktheme
border-bottom: 1px solid $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
&:before
border-right: 0
&:after
border-right: 0.75em solid $comment-header-background-color-darktheme
.comment-content
p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside
margin: 1em 0

View File

@ -125,6 +125,9 @@ input[type=radio]:checked + .radio:after,
input[type=checkbox]:checked + .checkbox:after
opacity: 1
input[type=radio]:disabled + .radio:after
background: $input-disabled-text-color
input[type=radio]:disabled + .radio:before,
input[type=checkbox]:disabled + .checkbox:before,
input[type=radio]:disabled + .radio:after,
@ -203,6 +206,7 @@ input[type=number]
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
outline: none
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
@ -211,7 +215,7 @@ input[type=number]
color: $input-disabled-text-color
&:focus
border-color: $main-color
border-color: $main-color !important
&[readonly]
border: 2px solid $input-disabled-border-color

View File

@ -106,7 +106,12 @@ form .fa-question-circle-o
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
>.content-wrapper:not(.transparent)
li[data-name=view]
background: $button-enabled-background-color
margin-right: 1em
a
color: $button-enabled-text-color
>.content-wrapper:not(.transparent-container)
background: $top-navigation-color
padding: 1.8em
@media (max-width: 1000px)
@ -117,7 +122,7 @@ form .fa-question-circle-o
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
>.content-wrapper:not(.transparent-container)
background: $top-navigation-color-darktheme
hr
@ -161,6 +166,8 @@ nav
li.active a
background: $active-tab-background-color
color: $active-tab-text-color
li.active:has(:focus)
background: $active-tab-background-color
:focus
background: $focused-tab-background-color
outline: 0
@ -236,6 +243,8 @@ nav
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
li.active:has(:focus)
background: $active-tab-background-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
@ -300,10 +309,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

@ -61,7 +61,10 @@
word-spacing: 1.1em
background-repeat: no-repeat
background-position: 50% 50%
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23000000'/></svg>")
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23111111'/></svg>")
.thumbnail
margin-right: 0.4em
.darktheme #home footer ul .sep
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23e6e6e6'/></svg>")

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

@ -18,6 +18,8 @@
right: 0
top: 0
bottom: 0
&[data-state=read-only]
pointer-events: none
.notes-overlay
g

View File

@ -145,6 +145,7 @@ $cancel-button-color = tomato
clear: both
margin: 1em 0 0 0
padding-left: 7em
padding-bottom: 1em;
font-size: 90%
.thumbnail-wrapper

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

@ -34,6 +34,16 @@ shortcuts:</p>
<td>Focus first post in post list</td>
</tr>
<tr>
<td><kbd>T</kbd></td>
<td>(In edit mode) Focus tag input</td>
</tr>
<tr>
<td><kbd>Command/Ctrl+S</kbd></td>
<td>(In edit mode) Save post</td>
</tr>
<tr>
<td><kbd>Delete</kbd></td>
<td>Delete post (while in edit mode)</td>

View File

@ -1,4 +1,4 @@
<div class='content-wrapper transparent' id='home'>
<div class='content-wrapper transparent-container' id='home'>
<div class='messages'></div>
<header>
<h1><%- ctx.name %></h1>

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

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='view'><a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'>View</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!--

View File

@ -1,4 +1,10 @@
<div class='content-wrapper pool-summary'>
<section class='description'>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<hr/>
</section>
<section class='details'>
<section>
Category:
@ -14,10 +20,4 @@
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section>
</div>

View File

@ -1,7 +1,7 @@
<div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>' draggable='false'/>
<% } else if (ctx.post.type === 'flash') { %>

View File

@ -1,4 +1,4 @@
<div class='content-wrapper transparent post-view'>
<div class='content-wrapper transparent-container post-view'>
<aside class='sidebar'>
<nav class='buttons'>
<article class='previous-post'>
@ -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

@ -1,7 +1,7 @@
<div class='readonly-sidebar'>
<article class='details'>
<section class='download'>
<a rel='external' href='<%- ctx.post.contentUrl %>'>
<a href='<%- ctx.post.contentUrl %>' download>
<i class='fa fa-download'></i><!--
--><%= ctx.makeFileSize(ctx.post.fileSize) %> <!--
--><%- {
@ -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://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 +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]) %>&#32;<!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View File

@ -85,7 +85,7 @@
<div class='controls'>
<%= ctx.makeCheckbox({text: 'Copy tags', name: 'copy-tags'}) %>
<br/>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation'}) %>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation', checked: true}) %>
</div>
</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

@ -10,28 +10,27 @@
<span class='type' data-type='<%- post.type %>'>
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
<span class='icon'><i class='fa fa-film'></i></span>
<% } else { %>
<%- post.type %>
<% } %>
<%- post.type %>
</span>
<% if (post.score || post.favoriteCount || post.commentCount) { %>
<span class='stats'>
<% if (post.score) { %>
<span class='icon'>
<i class='fa fa-thumbs-up'></i>
<%- post.score %>
<span style="display: none">score:</span><%- post.score %>
</span>
<% } %>
<% if (post.favoriteCount) { %>
<span class='icon'>
<i class='fa fa-heart'></i>
<%- post.favoriteCount %>
<span style="display: none">favorites:</span><%- post.favoriteCount %>
</span>
<% } %>
<% if (post.commentCount) { %>
<span class='icon'>
<i class='fa fa-commenting'></i>
<%- post.commentCount %>
<span style="display: none">comments:</span><%- post.commentCount %>
</span>
<% } %>
</span>

View File

@ -28,7 +28,6 @@
name: 'dark-theme',
checked: ctx.browsingSettings.darkTheme,
}) %>
<p class='hint'>Changing this setting will require you to refresh the page for it to apply.</p>
</li>
<li>

View File

@ -12,7 +12,7 @@
<%= item.operation %>
<%= ctx.makeResourceLink(item.type, item.id) %>
<%= ctx.makeResourceLink(item.type, item.id, item.data) %>
</div>
<div class='details'><!--

View File

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='view'><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'>View</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'edit') %>'>Edit</a></li><!--

View File

@ -1,4 +1,10 @@
<div class='content-wrapper tag-summary'>
<section class='description'>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<hr/>
</section>
<section class='details'>
<section>
Category:
@ -32,10 +38,4 @@
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
</section>
</div>

View File

@ -398,7 +398,7 @@ class Api extends events.EventTarget {
if (data) {
if (files && Object.keys(files).length) {
req.attach("metadata", new Blob([JSON.stringify(data)]));
req.attach("metadata", new Blob([JSON.stringify(data)], { type: "application/json" }));
} else {
req.set("Content-Type", "application/json");
req.send(data);

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(
@ -281,7 +281,16 @@ module.exports = (router) => {
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
const canEditPosts = api.hasPrivilege("posts:edit");
if (canEditPosts) {
ctx.controller = new PostMainController(ctx, true);
} else {
router.show(uri.formatClientLink(
"post",
ctx.parameters.id,
ctx.parameters ? { query: ctx.parameters.query } : {}
));
}
});
router.enter(["post", ":id"], (ctx, next) => {
// restore parameters from history state

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

@ -4,6 +4,7 @@ const api = require("../api.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const keyboard = require("../util/keyboard.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");
const TagInputControl = require("./tag_input_control.js");
@ -224,10 +225,12 @@ class PostEditSidebarControl extends events.EventTarget {
});
}
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
if (this._tagControl) {
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
}
if (this._noteTextareaNode) {
this._noteTextareaNode.addEventListener("change", (e) =>
@ -241,6 +244,16 @@ class PostEditSidebarControl extends events.EventTarget {
this._syncExpanderTitles();
});
}
keyboard.bind(["command+s", "ctrl+s"], (e) => this._evtSubmit(e));
if (this._tagInputNode) {
const realTagInput = this._formNode.querySelector(".tag-input input");
keyboard.bindElement(realTagInput, ["command+s", "ctrl+s"], (e) => this._evtSubmit(e));
keyboard.bind("t", (e) => {
e.preventDefault();
realTagInput.focus();
});
}
}
_syncExpanderTitles() {
@ -427,7 +440,7 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined,
thumbnail:
this._newPostThumbnail !== undefined
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
? this._newPostThumbnail
: undefined,

View File

@ -42,6 +42,10 @@ const pools = require("./pools.js");
const api = require("./api.js");
const settings = require("./models/settings.js");
if (settings.get().darkTheme) {
document.body.classList.add("darktheme");
}
Promise.resolve()
.then(() => api.fetchConfig())
.then(

View File

@ -75,7 +75,7 @@ class Post extends events.EventTarget {
}
get sourceSplit() {
return this._source.split("\n");
return this._source.split("\n").filter((s) => s);
}
get canvasWidth() {
@ -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

@ -40,6 +40,11 @@ class Settings extends events.EventTarget {
save(newSettings, silent) {
newSettings = Object.assign(this.cache, newSettings);
localStorage.setItem("settings", JSON.stringify(newSettings));
if (newSettings.darkTheme) {
document.body.classList.add("darktheme");
} else {
document.body.classList.remove("darktheme");
}
this.cache = this._getFromLocalStorage();
if (silent !== true) {
this.dispatchEvent(

View File

@ -22,12 +22,21 @@ function bind(hotkey, func) {
return false;
}
function bindElement(element, hotkey, func) {
if (settings.get().keyboardShortcuts) {
mousetrap(element).bind(hotkey, func);
return true;
}
return false;
}
function unbind(hotkey) {
mousetrap.unbind(hotkey);
}
module.exports = {
bind: bind,
bindElement: bindElement,
unbind: unbind,
pause: () => {
paused = true;

View File

@ -54,7 +54,7 @@ class TildeWrapper extends BaseMarkdownWrapper {
// prevent ^#... from being treated as headers, due to tag permalinks
class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
preprocess(text) {
return text.replace(/^#/g, "%%%#");
return text.replace(/^#(?=[a-zA-Z0-9_-])/g, "%%%#");
}
postprocess(text) {

View File

@ -5,7 +5,8 @@ const keyboard = require("../util/keyboard.js");
const views = require("./views.js");
function searchInputNodeFocusHelper(inputNode) {
keyboard.bind("q", () => {
keyboard.bind("q", (e) => {
e.preventDefault();
inputNode.focus();
inputNode.setSelectionRange(
inputNode.value.length,

View File

@ -111,7 +111,7 @@ function makeSelect(options) {
}
function makeInput(options) {
options.value = options.value || "";
options.value = options.value === 0 ? 0 : options.value || "";
return _makeLabel(options) + makeElement("input", options);
}
@ -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(
@ -261,10 +261,10 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
}
function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
let text = makeThumbnail(user ? user.avatarUrl : "img/favicon.png");
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) },
@ -300,6 +300,8 @@ function _serializeElement(name, attributes) {
attributes[key] === undefined
) {
return "";
} else if (attributes[key] === 0) {
return `${key}="0"`;
}
const attribute = misc.escapeHtml(attributes[key] || "");
return `${key}="${attribute}"`;
@ -321,6 +323,7 @@ function emptyContent(target) {
}
function replaceContent(target, source) {
if (!target) return;
emptyContent(target);
if (source instanceof NodeList) {
for (let child of [...source]) {

View File

@ -58,6 +58,11 @@ class PoolMergeView extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
if (!this._targetPoolId) {
this.clearMessages();
this.showError("You must select a pool name from autocomplete.");
return;
}
this.dispatchEvent(
new CustomEvent("submit", {
detail: {

View File

@ -60,12 +60,14 @@ class BulkSafetyEditor extends BulkEditor {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}
@ -130,6 +132,7 @@ class BulkTagEditor extends BulkEditor {
this.toggleOpen(true);
this.focus();
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
@ -138,6 +141,7 @@ class BulkTagEditor extends BulkEditor {
this.toggleOpen(false);
this.blur();
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}
@ -160,12 +164,14 @@ class BulkDeleteEditor extends BulkEditor {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}

View File

@ -29,7 +29,7 @@ function _formatBasicChange(diff, text) {
return lines;
}
function _makeResourceLink(type, id) {
function _makeResourceLink(type, id, data) {
if (type === "post") {
return views.makePostLink(id, true);
} else if (type === "tag") {
@ -37,7 +37,7 @@ function _makeResourceLink(type, id) {
} else if (type === "tag_category") {
return 'category "' + id + '"';
} else if (type === "pool") {
return views.makePoolLink(id, true);
return views.makePoolLink(id, false, false, data);
}
}

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
@ -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 `questionable`) or `unsafe`. |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
| `rating` | alias of `safety` |
**Sort style tokens**
@ -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

@ -23,15 +23,15 @@ RUN apk --no-cache add \
py3-pillow \
py3-pynacl \
py3-tz \
py3-pyrfc3339 \
&& pip3 install --no-cache-dir --disable-pip-version-check \
py3-pyrfc3339
RUN 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" \
youtube_dl \
"pillow-avif-plugin>=1.1.0" \
&& apk --no-cache del py3-pip
yt-dlp \
"pillow-avif-plugin~=1.1.0"
RUN 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
youtube_dl
yt-dlp

View File

@ -21,7 +21,7 @@ _search_executor = search.Executor(_search_executor_config)
def _get_post_id(params: Dict[str, str]) -> int:
try:
return int(params["post_id"])
except TypeError:
except (TypeError, ValueError):
raise posts.InvalidPostIdError(
"Invalid post ID: %r." % params["post_id"]
)

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

@ -24,6 +24,11 @@ def convert_heif_to_png(content: bytes) -> bytes:
return img_byte_arr.getvalue()
def check_for_loop(content: bytes) -> bytes:
img = PILImage.open(BytesIO(content))
return "loop" in img.info
class Image:
def __init__(self, content: bytes) -> None:
self.content = content

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])

View File

@ -531,8 +531,10 @@ def generate_alternate_formats(
def get_default_flags(content: bytes) -> List[str]:
assert content
ret = []
if mime.is_video(mime.get_mime_type(content)):
ret.append(model.Post.FLAG_LOOP)
if mime.is_animated_gif(content):
if images.check_for_loop(content):
ret.append(model.Post.FLAG_LOOP)
elif mime.is_video(mime.get_mime_type(content)):
if images.Image(content).check_for_sound():
ret.append(model.Post.FLAG_SOUND)
return ret

View File

@ -32,9 +32,13 @@ def _type_transformer(value: str) -> str:
def _safety_transformer(value: str) -> str:
available_values = {
"safe": model.Post.SAFETY_SAFE,
"s": model.Post.SAFETY_SAFE,
"sketchy": model.Post.SAFETY_SKETCHY,
"questionable": model.Post.SAFETY_SKETCHY,
"q": model.Post.SAFETY_SKETCHY,
"unsafe": model.Post.SAFETY_UNSAFE,
"explicit": model.Post.SAFETY_UNSAFE,
"e": model.Post.SAFETY_UNSAFE,
}
return search_util.enum_transformer(available_values, value)