diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..48e3403 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Shell scripts require LF +*.sh text eol=lf diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 3dda368..c688a5f 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -1,5 +1,8 @@ name: Build Docker containers -on: [push] +on: + push: + branches: + - master jobs: build-client: name: Build and push client/ Docker container diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c793e75..7b550ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,29 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: mixed-line-ending + - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.9 + rev: v1.1.10 hooks: - id: remove-tabs - repo: https://github.com/psf/black - rev: 20.8b1 + rev: '22.3.0' hooks: - id: black files: 'server/' types: [python] - language_version: python3.8 + language_version: python3.9 -- repo: https://github.com/timothycrosley/isort - rev: '5.4.2' +- repo: https://github.com/PyCQA/isort + rev: '5.10.1' hooks: - id: isort files: 'server/' @@ -31,8 +32,8 @@ repos: additional_dependencies: - toml -- repo: https://github.com/prettier/prettier - rev: '2.1.1' +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.5.0 hooks: - id: prettier files: client/js/ @@ -40,7 +41,7 @@ repos: args: ['--config', 'client/.prettierrc.yml'] - repo: https://github.com/pre-commit/mirrors-eslint - rev: v7.8.0 + rev: v8.3.0 hooks: - id: eslint files: client/js/ @@ -48,8 +49,8 @@ repos: additional_dependencies: - eslint-config-prettier -- repo: https://gitlab.com/pycqa/flake8 - rev: '3.8.3' +- repo: https://gitlab.com/PyCQA/flake8 + rev: '4.0.1' hooks: - id: flake8 files: server/szurubooru/ diff --git a/README.md b/README.md index a86ef79..9307055 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Szurubooru is an image board engine inspired by services such as Danbooru, Gelbooru and Moebooru dedicated for small and medium communities. Its name [has its roots in Polish language and has onomatopeic meaning of scraping or -scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. +scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. ## Features diff --git a/client/Dockerfile b/client/Dockerfile index b0f6b48..dbb6659 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -2,7 +2,6 @@ FROM node:lts as builder WORKDIR /opt/app COPY package.json package-lock.json ./ -RUN npm install -g npm RUN npm install COPY . ./ diff --git a/client/css/post-upload.styl b/client/css/post-upload.styl index 3836293..ea79fca 100644 --- a/client/css/post-upload.styl +++ b/client/css/post-upload.styl @@ -14,9 +14,11 @@ $cancel-button-color = tomato &.inactive input[type=submit], &.inactive .skip-duplicates &.inactive .always-upload-similar + &.inactive .pause-remain-on-error &.uploading input[type=submit], &.uploading .skip-duplicates, &.uploading .always-upload-similar + &.uploading .pause-remain-on-error &:not(.uploading) .cancel display: none @@ -44,6 +46,9 @@ $cancel-button-color = tomato .always-upload-similar margin-left: 1em + .pause-remain-on-error + margin-left: 1em + form>.messages margin-top: 1em diff --git a/client/html/post_upload.tpl b/client/html/post_upload.tpl index 6374fe8..3c1b238 100644 --- a/client/html/post_upload.tpl +++ b/client/html/post_upload.tpl @@ -7,7 +7,7 @@ <%= ctx.makeCheckbox({ - text: 'Skip duplicates', + text: 'Skip duplicate', name: 'skip-duplicates', checked: false, }) %> @@ -15,12 +15,20 @@ <%= ctx.makeCheckbox({ - text: 'Always upload similar', + text: 'Force upload similar', name: 'always-upload-similar', checked: false, }) %> + + <%= ctx.makeCheckbox({ + text: 'Pause on error', + name: 'pause-remain-on-error', + checked: true, + }) %> + + diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js index a54baec..720a116 100644 --- a/client/js/controllers/post_upload_controller.js +++ b/client/js/controllers/post_upload_controller.js @@ -90,21 +90,30 @@ class PostUploadController { uploadable ); } + if (e.detail.pauseRemainOnError) { + return Promise.reject(); + } }) ), Promise.resolve() ) .then(() => { if (anyFailures) { - this._view.showError(genericErrorMessage); - this._view.enableForm(); - } else { + return Promise.reject(); + } + }) + .then( + () => { this._view.clearMessages(); misc.disableExitConfirmation(); const ctx = router.show(uri.formatClientLink("posts")); ctx.controller.showSuccess("Posts uploaded."); + }, + (error) => { + this._view.showError(genericErrorMessage); + this._view.enableForm(); } - }); + ); } _uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) { diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js index 326736b..068d329 100644 --- a/client/js/controllers/user_controller.js +++ b/client/js/controllers/user_controller.js @@ -31,9 +31,8 @@ class UserController { userTokenPromise = UserToken.get(userName).then( (userTokens) => { return userTokens.map((token) => { - token.isCurrentAuthToken = api.isCurrentAuthToken( - token - ); + token.isCurrentAuthToken = + api.isCurrentAuthToken(token); return token; }); }, diff --git a/client/js/controls/expander_control.js b/client/js/controls/expander_control.js index 11ad3ef..ffb0e90 100644 --- a/client/js/controls/expander_control.js +++ b/client/js/controls/expander_control.js @@ -45,9 +45,8 @@ class ExpanderControl { // eslint-disable-next-line accessor-pairs set title(newTitle) { if (this._expanderNode) { - this._expanderNode.querySelector( - "header span" - ).textContent = newTitle; + this._expanderNode.querySelector("header span").textContent = + newTitle; } } diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index b8ad9da..eabb98a 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -203,9 +203,8 @@ class PostEditSidebarControl extends events.EventTarget { ); if (this._formNode) { - const inputNodes = this._formNode.querySelectorAll( - "input, textarea" - ); + const inputNodes = + this._formNode.querySelectorAll("input, textarea"); for (let node of inputNodes) { node.addEventListener("change", (e) => this.dispatchEvent(new CustomEvent("change")) diff --git a/client/js/controls/post_notes_overlay_control.js b/client/js/controls/post_notes_overlay_control.js index 030f7f2..e9aad04 100644 --- a/client/js/controls/post_notes_overlay_control.js +++ b/client/js/controls/post_notes_overlay_control.js @@ -727,9 +727,8 @@ class PostNotesOverlayControl extends events.EventTarget { } _showNoteText(note) { - this._textNode.querySelector( - ".wrapper" - ).innerHTML = misc.formatMarkdown(note.text); + this._textNode.querySelector(".wrapper").innerHTML = + misc.formatMarkdown(note.text); this._textNode.style.display = "block"; const bodyRect = document.body.getBoundingClientRect(); const noteRect = this._textNode.getBoundingClientRect(); diff --git a/client/js/util/markdown.js b/client/js/util/markdown.js index 22cdae5..e71e326 100644 --- a/client/js/util/markdown.js +++ b/client/js/util/markdown.js @@ -65,17 +65,6 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper { // post, user and tags permalinks class EntityPermalinkWrapper extends BaseMarkdownWrapper { preprocess(text) { - // URL-based permalinks - text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1"); - text = text.replace( - new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"), - "#$1" - ); - text = text.replace( - new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"), - "+$1" - ); - text = text.replace( /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, "$1[$2]($2)" @@ -136,12 +125,8 @@ function createRenderer() { const renderer = new marked.Renderer(); renderer.image = (href, title, alt) => { - let [ - _, - url, - width, - height, - ] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href); + let [_, url, width, height] = + /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href); let res = '' + sanitize(alt);
         if (width) {
             res += '=0.10.0" + "node": ">= 12" } }, "node_modules/md5.js": { @@ -3108,9 +3108,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/mkdirp": { @@ -3385,9 +3385,9 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "node_modules/path-platform": { @@ -3983,12 +3983,6 @@ "minimist": "^1.1.0" } }, - "node_modules/subarg/node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "node_modules/superagent": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", @@ -6053,9 +6047,9 @@ "dev": true }, "cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", + "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", "dev": true }, "call-bind": { @@ -7280,9 +7274,9 @@ "dev": true }, "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", + "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==" }, "md5.js": { "version": "1.3.4", @@ -7364,9 +7358,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { @@ -7607,9 +7601,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-platform": { @@ -8116,14 +8110,6 @@ "dev": true, "requires": { "minimist": "^1.1.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } }, "superagent": { diff --git a/client/package.json b/client/package.json index a83a9cb..9d0005c 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ "font-awesome": "^4.7.0", "ios-inner-height": "^1.0.3", "js-cookie": "^2.2.0", - "marked": "^0.7.0", + "marked": "^4.0.10", "mousetrap": "^1.6.2", "nprogress": "^0.2.0", "superagent": "^3.8.3" diff --git a/doc/API.md b/doc/API.md index 3d280fd..63a50a2 100644 --- a/doc/API.md +++ b/doc/API.md @@ -37,6 +37,7 @@ - [Creating post](#creating-post) - [Updating post](#updating-post) - [Getting post](#getting-post) + - [Getting around post](#getting-around-post) - [Deleting post](#deleting-post) - [Merging posts](#merging-posts) - [Rating post](#rating-post) @@ -951,6 +952,29 @@ data. Retrieves information about an existing post. +## Getting around post +- **Request** + + `GET /post//around` + +- **Output** + + ```json5 + { + "prev": , + "next": + } + ``` + +- **Errors** + + - the post does not exist + - privileges are too low + +- **Description** + + Retrieves information about posts that are before or after an existing post. + ## Deleting post - **Request** diff --git a/doc/INSTALL.md b/doc/INSTALL.md index d978e4a..ca0212b 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -34,33 +34,79 @@ and Docker Compose (version 1.6.0 or greater) already installed. Read the comments to guide you. Note that `.env` should be in the root directory of this repository. -### Running the Application +4. Pull the containers: -Download containers: -```console -user@host:szuru$ docker-compose pull -``` + This pulls the latest containers from docker.io: + ```console + user@host:szuru$ docker-compose pull + ``` -For first run, it is recommended to start the database separately: -```console -user@host:szuru$ docker-compose up -d sql -``` + If you have modified the application's source and would like to manually + build it, follow the instructions in [**Building**](#Building) instead, + then read here once you're done. -To start all containers: -```console -user@host:szuru$ docker-compose up -d -``` +5. Run it! -To view/monitor the application logs: -```console -user@host:szuru$ docker-compose logs -f -# (CTRL+C to exit) -``` + For first run, it is recommended to start the database separately: + ```console + user@host:szuru$ docker-compose up -d sql + ``` + + To start all containers: + ```console + user@host:szuru$ docker-compose up -d + ``` + + To view/monitor the application logs: + ```console + user@host:szuru$ docker-compose logs -f + # (CTRL+C to exit) + ``` + +### Building + +1. Edit `docker-compose.yml` to tell Docker to build instead of pull containers: + + ```diff yaml + ... + server: + - image: szurubooru/server:latest + + build: server + ... + client: + - image: szurubooru/client:latest + + build: client + ... + ``` + + You can choose to build either one from source. + +2. Build the containers: + + ```console + 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: + + ``` + 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 + ``` + + ...you will need to export Docker BuildKit flags: + + ```console + user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1 + ``` + + ...and run `docker-compose build` again. + +*Note: If your changes are not taking effect in your builds, consider building +with `--no-cache`.* -To stop all containers: -```console -user@host:szuru$ docker-compose down -``` ### Additional Features diff --git a/doc/example.env b/doc/example.env index 59e1e85..303a25e 100644 --- a/doc/example.env +++ b/doc/example.env @@ -10,6 +10,12 @@ BUILD_INFO=latest # otherwise the port specified here will be publicly accessible PORT=8080 +# How many waitress threads to start +# 4 is the default amount of threads. If you experience performance +# degradation with a large number of posts, increasing this may +# improve performance, since waitress is most likely clogging up with Tasks. +THREADS=4 + # URL base to run szurubooru under # See "Additional Features" section in INSTALL.md BASE_URL=/ diff --git a/docker-compose.yml b/docker-compose.yml index 5edca67..b26e6ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,14 +22,15 @@ services: #POSTGRES_DB: defaults to same as POSTGRES_USER #POSTGRES_PORT: 5432 #LOG_SQL: 0 (1 for verbose SQL logs) + THREADS: volumes: - "${MOUNT_DATA}:/data" - "./server/config.yaml:/opt/app/config.yaml" client: #image: szurubooru/client:latest - image: szuruclient:latest - #build: ./client + #image: szuruclient:latest + build: ./client restart: unless-stopped depends_on: - server diff --git a/server/Dockerfile b/server/Dockerfile index 205c8e4..487f192 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -7,8 +7,13 @@ WORKDIR /opt/app RUN apk --no-cache add \ python3 \ python3-dev \ - ffmpeg \ py3-pip \ + build-base \ + libheif \ + libheif-dev \ + libavif \ + libavif-dev \ + ffmpeg \ # from requirements.txt: py3-yaml \ py3-psycopg2 \ @@ -19,18 +24,13 @@ RUN apk --no-cache add \ py3-pynacl \ py3-tz \ py3-pyrfc3339 \ - build-base \ - && apk --no-cache add \ - libheif \ - libavif \ - libheif-dev \ - libavif-dev \ && pip3 install --no-cache-dir --disable-pip-version-check \ - alembic \ + "alembic>=0.8.5" \ "coloredlogs==5.0" \ + "pyheif==0.6.1" \ + "heif-image-plugin>=0.3.2" \ youtube_dl \ - pillow-avif-plugin \ - pyheif-pillow-opener \ + "pillow-avif-plugin>=1.1.0" \ && apk --no-cache del py3-pip COPY ./ /opt/app/ @@ -83,6 +83,9 @@ ARG PORT=6666 ENV PORT=${PORT} EXPOSE ${PORT} +ARG THREADS=4 +ENV THREADS=${THREADS} + VOLUME ["/data/"] ARG DOCKER_REPO diff --git a/server/docker-start.sh b/server/docker-start.sh index 34a0e49..eebef1c 100755 --- a/server/docker-start.sh +++ b/server/docker-start.sh @@ -4,5 +4,5 @@ cd /opt/app alembic upgrade head -echo "Starting szurubooru API on port ${PORT}" -exec waitress-serve-3 --port ${PORT} szurubooru.facade:app +echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads" +exec waitress-serve-3 --port ${PORT} --threads ${THREADS} szurubooru.facade:app diff --git a/server/requirements.txt b/server/requirements.txt index 2a09b24..16b29ff 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,14 +1,15 @@ alembic>=0.8.5 -pyyaml>=3.11 -psycopg2-binary>=2.6.1 -SQLAlchemy>=1.0.12, <1.4 -coloredlogs==5.0 certifi>=2017.11.5 +coloredlogs==5.0 +heif-image-plugin==0.3.2 numpy>=1.8.2 -pillow>=4.3.0 -pynacl>=1.2.1 -pytz>=2018.3 -pyRFC3339>=1.0 pillow-avif-plugin>=1.1.0 -pyheif-pillow-opener>=0.1.0 +pillow>=4.3.0 +psycopg2-binary>=2.6.1 +pyheif==0.6.1 +pynacl>=1.2.1 +pyRFC3339>=1.0 +pytz>=2018.3 +pyyaml>=3.11 +SQLAlchemy>=1.0.12, <1.4 youtube_dl diff --git a/server/szuru-admin b/server/szuru-admin index 004a751..08ba182 100755 --- a/server/szuru-admin +++ b/server/szuru-admin @@ -91,6 +91,15 @@ def reset_filenames() -> None: rename_in_dir("posts/custom-thumbnails/") +def regenerate_thumbnails() -> None: + for post in db.session.query(model.Post).all(): + print("Generating tumbnail for post %d ..." % post.post_id, end="\r") + try: + postfuncs.generate_post_thumbnail(post) + except Exception: + pass + + def main() -> None: parser_top = ArgumentParser( description="Collection of CLI commands for an administrator to use", @@ -114,6 +123,12 @@ def main() -> None: help="reset and rename the content and thumbnail " "filenames in case of a lost/changed secret key", ) + parser.add_argument( + "--regenerate-thumbnails", + action="store_true", + help="regenerate the thumbnails for posts if the " + "thumbnail files are missing", + ) command = parser_top.parse_args() try: @@ -123,6 +138,8 @@ def main() -> None: check_audio() elif command.reset_filenames: reset_filenames() + elif command.regenerate_thumbnails: + regenerate_thumbnails() except errors.BaseError as e: print(e, file=stderr) diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index 1515a54..8f87642 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -33,7 +33,7 @@ def _docker_config() -> Dict: "show_sql": int(os.getenv("LOG_SQL", 0)), "data_url": os.getenv("DATA_URL", "data/"), "data_dir": "/data/", - "database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s" + "database": "postgresql://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s" % { "user": os.getenv("POSTGRES_USER"), "pass": os.getenv("POSTGRES_PASSWORD"), diff --git a/server/szurubooru/facade.py b/server/szurubooru/facade.py index a7e4844..4c8084f 100644 --- a/server/szurubooru/facade.py +++ b/server/szurubooru/facade.py @@ -135,7 +135,7 @@ _live_migrations = ( def create_app() -> Callable[[Any, Any], Any]: - """ Create a WSGI compatible App object. """ + """Create a WSGI compatible App object.""" validate_config() coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s") if config.config["debug"]: diff --git a/server/szurubooru/func/auth.py b/server/szurubooru/func/auth.py index d013775..17d25f7 100644 --- a/server/szurubooru/func/auth.py +++ b/server/szurubooru/func/auth.py @@ -25,7 +25,7 @@ RANK_MAP = OrderedDict( def get_password_hash(salt: str, password: str) -> Tuple[str, int]: - """ Retrieve argon2id password hash. """ + """Retrieve argon2id password hash.""" return ( pwhash.argon2id.str( (config.config["secret"] + salt + password).encode("utf8") @@ -37,7 +37,7 @@ def get_password_hash(salt: str, password: str) -> Tuple[str, int]: def get_sha256_legacy_password_hash( salt: str, password: str ) -> Tuple[str, int]: - """ Retrieve old-style sha256 password hash. """ + """Retrieve old-style sha256 password hash.""" digest = hashlib.sha256() digest.update(config.config["secret"].encode("utf8")) digest.update(salt.encode("utf8")) @@ -46,7 +46,7 @@ def get_sha256_legacy_password_hash( def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]: - """ Retrieve old-style sha1 password hash. """ + """Retrieve old-style sha1 password hash.""" digest = hashlib.sha1() digest.update(b"1A2/$_4xVa") digest.update(salt.encode("utf8")) @@ -125,7 +125,7 @@ def verify_privilege(user: model.User, privilege_name: str) -> None: def generate_authentication_token(user: model.User) -> str: - """ Generate nonguessable challenge (e.g. links in password reminder). """ + """Generate nonguessable challenge (e.g. links in password reminder).""" assert user digest = hashlib.md5() digest.update(config.config["secret"].encode("utf8")) diff --git a/server/szurubooru/func/image_hash.py b/server/szurubooru/func/image_hash.py index 05b27a4..76d5a84 100644 --- a/server/szurubooru/func/image_hash.py +++ b/server/szurubooru/func/image_hash.py @@ -4,16 +4,13 @@ from datetime import datetime from io import BytesIO from typing import Any, Callable, List, Optional, Set, Tuple +import HeifImagePlugin import numpy as np import pillow_avif -import pyheif from PIL import Image -from pyheif_pillow_opener import register_heif_opener from szurubooru import config, errors -register_heif_opener() - logger = logging.getLogger(__name__) # Math based on paper from H. Chi Wong, Marshall Bern and David Goldberg diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index de41222..e135d18 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -7,6 +7,8 @@ import subprocess from io import BytesIO from typing import List +import HeifImagePlugin +import pillow_avif from PIL import Image as PILImage from szurubooru import errors @@ -277,10 +279,10 @@ class Image: proc = subprocess.Popen( cli, stdout=subprocess.PIPE, - stdin=subprocess.PIPE, + stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, ) - out, err = proc.communicate(input=self.content) + out, err = proc.communicate() if proc.returncode != 0: logger.warning( "Failed to execute ffmpeg command (cli=%r, err=%r)", diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py index 3be43f7..ee10b9d 100644 --- a/server/szurubooru/func/mime.py +++ b/server/szurubooru/func/mime.py @@ -36,7 +36,7 @@ def get_mime_type(content: bytes) -> str: if content[0:4] == b"\x1A\x45\xDF\xA3": return "video/webm" - if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42", b"ftypM4V "): + if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypiso6", b"ftypmp42", b"ftypM4V "): return "video/mp4" return "application/octet-stream" diff --git a/server/szurubooru/func/net.py b/server/szurubooru/func/net.py index 3f085a0..c53a62e 100644 --- a/server/szurubooru/func/net.py +++ b/server/szurubooru/func/net.py @@ -39,7 +39,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes: length_tally = 0 try: with urllib.request.urlopen(request) as handle: - while (chunk := handle.read(_dl_chunk_size)) : + while chunk := handle.read(_dl_chunk_size): length_tally += len(chunk) if length_tally > config.config["max_dl_filesize"]: raise DownloadTooLargeError( diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py index f839136..453e121 100644 --- a/server/szurubooru/func/util.py +++ b/server/szurubooru/func/util.py @@ -83,12 +83,12 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]: def is_valid_email(email: Optional[str]) -> bool: - """ Return whether given email address is valid or empty. """ + """Return whether given email address is valid or empty.""" return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None class dotdict(dict): - """ dot.notation access to dictionary attributes. """ + """dot.notation access to dictionary attributes.""" def __getattr__(self, attr: str) -> Any: return self.get(attr) @@ -98,7 +98,7 @@ class dotdict(dict): def parse_time_range(value: str) -> Tuple[datetime, datetime]: - """ Return tuple containing min/max time for given text representation. """ + """Return tuple containing min/max time for given text representation.""" one_day = timedelta(days=1) one_second = timedelta(seconds=1) almost_one_day = one_day - one_second diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index e73b235..436543b 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -7,7 +7,7 @@ from szurubooru.rest.errors import HttpBadRequest def _authenticate_basic_auth(username: str, password: str) -> model.User: - """ Try to authenticate user. Throw AuthError for invalid users. """ + """Try to authenticate user. Throw AuthError for invalid users.""" user = users.get_user_by_name(username) if not auth.is_valid_password(user, password): raise errors.AuthError("Invalid password.") @@ -17,7 +17,7 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User: def _authenticate_token( username: str, token: str ) -> Tuple[model.User, model.UserToken]: - """ Try to authenticate user. Throw AuthError for invalid users. """ + """Try to authenticate user. Throw AuthError for invalid users.""" user = users.get_user_by_name(username) user_token = user_tokens.get_by_user_and_token(user, token) if not auth.is_valid_token(user_token): @@ -72,7 +72,7 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: def process_request(ctx: rest.Context) -> None: - """ Bind the user to request. Update last login time if needed. """ + """Bind the user to request. Update last login time if needed.""" bump_login = ctx.get_param_as_bool("bump-login", default=False) auth_user = _get_user(ctx, bump_login) if auth_user: diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py index a6f10fb..c098bd0 100644 --- a/server/szurubooru/rest/app.py +++ b/server/szurubooru/rest/app.py @@ -11,7 +11,7 @@ from szurubooru.rest import context, errors, middleware, routes def _json_serializer(obj: Any) -> str: - """ JSON serializer for objects not serializable by default JSON code """ + """JSON serializer for objects not serializable by default JSON code""" if isinstance(obj, datetime): serial = obj.isoformat("T") + "Z" return serial