server/posts: auto convert GIFs to WEBMs/MP4s

- Default setting is false for both conversions, as this will require
  additional resources of the server, but is bandwidth friendly for
  viewers
- WEBM conversion is slow, but better quality than MP4 conversion with
  a typically smaller file size
- Tags are copied over from the original upload
- Snapshots are generated for the new auto posts
This commit is contained in:
ReAnzu 2018-03-08 00:47:58 -06:00 committed by rr-
parent 4ff8be6a2f
commit 12ec43f098
5 changed files with 138 additions and 6 deletions

View File

@ -27,6 +27,12 @@ thumbnails:
post_height: 300 post_height: 300
convert:
gif:
to_webm: false
to_mp4: false
# used to send password reset e-mails # used to send password reset e-mails
smtp: smtp:
host: # example: localhost host: # example: localhost

View File

@ -1,4 +1,4 @@
from typing import Optional, Dict from typing import Optional, Dict, List
from datetime import datetime from datetime import datetime
from szurubooru import db, model, errors, rest, search from szurubooru import db, model, errors, rest, search
from szurubooru.func import ( from szurubooru.func import (
@ -69,13 +69,26 @@ def create_post(
posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) posts.update_post_thumbnail(post, ctx.get_file('thumbnail'))
ctx.session.add(post) ctx.session.add(post)
ctx.session.flush() ctx.session.flush()
snapshots.create(post, None if anonymous else ctx.user) create_snapshots_for_post(post, new_tags, None if anonymous else ctx.user)
for tag in new_tags: alternate_format_posts = posts.generate_alternate_formats(post, content)
snapshots.create(tag, None if anonymous else ctx.user) 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() ctx.session.commit()
return _serialize_post(ctx, post) 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<post_id>[^/]+)/?') @rest.routes.get('/post/(?P<post_id>[^/]+)/?')
def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
auth.verify_privilege(ctx.user, 'posts:view') auth.verify_privilege(ctx.user, 'posts:view')

View File

@ -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( def _execute(
self, self,
cli: List[str], cli: List[str],

View File

@ -5,7 +5,7 @@ import sqlalchemy as sa
from szurubooru import config, db, model, errors, rest from szurubooru import config, db, model, errors, rest
from szurubooru.func import ( from szurubooru.func import (
users, scores, comments, tags, util, users, scores, comments, tags, util,
mime, images, files, image_hash, serialization) mime, images, files, image_hash, serialization, snapshots)
EMPTY_PIXEL = ( EMPTY_PIXEL = (
@ -364,7 +364,7 @@ def create_post(
update_post_content(post, content) update_post_content(post, content)
new_tags = update_post_tags(post, tag_names) 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: 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) 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: def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
assert post assert post
if not content: if not content:

View File

@ -41,6 +41,16 @@ def create_temp_file(**kwargs: Any) -> Generator:
os.remove(path) 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]: def unalias_dict(source: List[Tuple[List[str], T]]) -> Dict[str, T]:
output_dict = {} # type: Dict[str, T] output_dict = {} # type: Dict[str, T]
for aliases, value in source: for aliases, value in source: