diff --git a/config.yaml.dist b/config.yaml.dist index 7273ea7..79e1723 100644 --- a/config.yaml.dist +++ b/config.yaml.dist @@ -27,6 +27,12 @@ thumbnails: post_height: 300 +convert: + gif: + to_webm: false + to_mp4: false + + # used to send password reset e-mails smtp: host: # example: localhost diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index 27f10c1..aaa7437 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict +from typing import Optional, Dict, List from datetime import datetime from szurubooru import db, model, errors, rest, search from szurubooru.func import ( @@ -69,13 +69,26 @@ def create_post( posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) ctx.session.add(post) ctx.session.flush() - snapshots.create(post, None if anonymous else ctx.user) - for tag in new_tags: - snapshots.create(tag, None if anonymous else ctx.user) + create_snapshots_for_post(post, new_tags, None if anonymous else ctx.user) + alternate_format_posts = posts.generate_alternate_formats(post, content) + for alternate_post, alternate_post_new_tags in alternate_format_posts: + create_snapshots_for_post( + alternate_post, + alternate_post_new_tags, + None if anonymous else ctx.user) ctx.session.commit() return _serialize_post(ctx, post) +def create_snapshots_for_post( + post: model.Post, + new_tags: List[model.Tag], + user: Optional[model.User]): + snapshots.create(post, user) + for tag in new_tags: + snapshots.create(tag, user) + + @rest.routes.get('/post/(?P[^/]+)/?') def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: auth.verify_privilege(ctx.user, 'posts:view') diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index 9e62aef..b3df55e 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -79,6 +79,68 @@ class Image: '-', ]) + def to_webm(self) -> bytes: + with util.create_temp_file_path(suffix='.log') as phase_log_path: + # Pass 1 + self._execute([ + '-i', '{path}', + '-pass', '1', + '-passlogfile', phase_log_path, + '-vcodec', 'libvpx-vp9', + '-crf', '4', + '-b:v', '2500K', + '-acodec', 'libvorbis', + '-f', 'webm', + '-y', '/dev/null' + ]) + + # Pass 2 + return self._execute([ + '-i', '{path}', + '-pass', '2', + '-passlogfile', phase_log_path, + '-vcodec', 'libvpx-vp9', + '-crf', '4', + '-b:v', '2500K', + '-acodec', 'libvorbis', + '-f', 'webm', + '-' + ]) + + def to_mp4(self) -> bytes: + with util.create_temp_file_path(suffix='.dat') as mp4_temp_path: + width = self.width + height = self.height + altered_dimensions = False + + if self.width % 2 != 0: + width = self.width - 1 + altered_dimensions = True + + if self.height % 2 != 0: + height = self.height - 1 + altered_dimensions = True + + args = [ + '-i', '{path}', + '-vcodec', 'libx264', + '-preset', 'slow', + '-crf', '22', + '-b:v', '200K', + '-profile:v', 'main', + '-pix_fmt', 'yuv420p', + '-acodec', 'aac', + '-f', 'mp4' + ] + + if altered_dimensions: + args += ['-filter:v', 'scale=\'%d:%d\'' % (width, height)] + + self._execute(args + ['-y', mp4_temp_path]) + + with open(mp4_temp_path, 'rb') as mp4_temp: + return mp4_temp.read() + def _execute( self, cli: List[str], diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 406f6e1..219c832 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -5,7 +5,7 @@ import sqlalchemy as sa from szurubooru import config, db, model, errors, rest from szurubooru.func import ( users, scores, comments, tags, util, - mime, images, files, image_hash, serialization) + mime, images, files, image_hash, serialization, snapshots) EMPTY_PIXEL = ( @@ -364,7 +364,7 @@ def create_post( update_post_content(post, content) new_tags = update_post_tags(post, tag_names) - return (post, new_tags) + return post, new_tags def update_post_safety(post: model.Post, safety: str) -> None: @@ -429,6 +429,47 @@ def _sync_post_content(post: model.Post) -> None: generate_post_thumbnail(post) +def generate_alternate_formats(post: model.Post, content: bytes) \ + -> List[Tuple[model.Post, List[model.Tag]]]: + assert post + assert content + new_posts = [] + if mime.is_animated_gif(content): + tag_names = [ + tag_name.name + for tag_name in [tag.names for tag in post.tags]] + + if config.config['convert']['gif']['to_mp4']: + mp4_post, new_tags = create_post( + images.Image(content).to_mp4(), + tag_names, + post.user) + update_post_flags(mp4_post, ['loop']) + update_post_safety(mp4_post, post.safety) + update_post_source(mp4_post, post.source) + new_posts += [(mp4_post, new_tags)] + + if config.config['convert']['gif']['to_webm']: + webm_post, new_tags = create_post( + images.Image(content).to_webm(), + tag_names, + post.user) + update_post_flags(webm_post, ['loop']) + update_post_safety(webm_post, post.safety) + update_post_source(webm_post, post.source) + new_posts += [(webm_post, new_tags)] + + db.session.flush() + + new_posts = [p for p in new_posts if p[0] is not None] + + new_relations = [p[0].post_id for p in new_posts] + if len(new_relations) > 0: + update_post_relations(post, new_relations) + + return new_posts + + def update_post_content(post: model.Post, content: Optional[bytes]) -> None: assert post if not content: diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py index 61e975b..ba2d4dc 100644 --- a/server/szurubooru/func/util.py +++ b/server/szurubooru/func/util.py @@ -41,6 +41,16 @@ def create_temp_file(**kwargs: Any) -> Generator: os.remove(path) +@contextmanager +def create_temp_file_path(**kwargs: Any) -> Generator: + (descriptor, path) = tempfile.mkstemp(**kwargs) + os.close(descriptor) + try: + yield path + finally: + os.remove(path) + + def unalias_dict(source: List[Tuple[List[str], T]]) -> Dict[str, T]: output_dict = {} # type: Dict[str, T] for aliases, value in source: