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:
parent
4ff8be6a2f
commit
12ec43f098
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue