f455f95ce0
This guarantees that a poster can be generated for any valid stream instead of breaking on extremely short streams (<1s).
158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
from os import path, remove, rename
|
|
from shutil import copy2
|
|
|
|
import ffmpeg
|
|
|
|
from .handler import Handler
|
|
from .transcode import TranscodeHandler
|
|
from ..exceptions import ValidationException
|
|
|
|
|
|
def make_poster(inpath, outdir):
|
|
"""
|
|
Create a poster from the video file at inpath.
|
|
"""
|
|
indir, infile = path.split(inpath)
|
|
infile_noext, _ = path.splitext(infile)
|
|
outfile = f"{infile_noext}-poster.jpg"
|
|
outpath = path.join(outdir, outfile)
|
|
|
|
ffmpeg.input(inpath) \
|
|
.output(outpath, vframes=1) \
|
|
.run(quiet=True)
|
|
return (outdir, outfile)
|
|
|
|
|
|
def _needs_poster(source):
|
|
"""
|
|
Returns True if a poster should be generated for this source,
|
|
otherwise False.
|
|
|
|
Poster should be generated if:
|
|
- a poster key has been passed with falsy value
|
|
- a video has been uploaded without a corresponding poster file
|
|
"""
|
|
if 'poster' in source and not source['poster']:
|
|
return True
|
|
if 'video' in source and not source.get('poster', None):
|
|
return True
|
|
return False
|
|
|
|
|
|
@Handler.register
|
|
class PosterHandler(Handler):
|
|
"""
|
|
This class generates poster files and imports uploaded ones.
|
|
"""
|
|
@classmethod
|
|
def wants(cls, jobspec, existing_package):
|
|
"""
|
|
Return True if this handler wants to process this jobspec.
|
|
Raises an exception if the job is wanted but doesn't pass validation.
|
|
|
|
A job is wanted if any of:
|
|
- a 'poster' key exists in a source
|
|
- a video has been uploaded for a source without a
|
|
corresponding poster
|
|
"""
|
|
if 'sources' in jobspec:
|
|
# A poster has been uploaded
|
|
uploaded = {name: source['poster']
|
|
for name, source in jobspec['sources'].items()
|
|
if 'poster' in source and source['poster']}
|
|
# A poster is to be generated
|
|
generate = [_needs_poster(source)
|
|
for source in jobspec['sources'].values()]
|
|
if any(uploaded) or any(generate):
|
|
return cls._validate(jobspec, existing_package, uploaded)
|
|
return False
|
|
|
|
@classmethod
|
|
def _validate(cls, jobspec, existing_package, uploaded):
|
|
"""
|
|
Return True if the job is valid for this handler.
|
|
|
|
Validity requirements are:
|
|
- 'upload_dir' must exist if a 'poster' value is truthy
|
|
- all truthy 'poster' values must point to valid files
|
|
"""
|
|
super()._validate(jobspec, existing_package)
|
|
if uploaded:
|
|
if 'upload_dir' not in jobspec:
|
|
raise ValidationException("upload_dir missing")
|
|
for name, poster in uploaded.items():
|
|
if not path.isfile(path.join(jobspec['upload_dir'],
|
|
poster)):
|
|
raise ValidationException(
|
|
f"poster file invalid for source {name}")
|
|
return True
|
|
|
|
@classmethod
|
|
def apply_after(cls, handlerclass):
|
|
"""
|
|
Return True if this handler must be applied after
|
|
the supplied handler.
|
|
|
|
This handler must be applied after the transcode handler,
|
|
so posters can be generated from the right files.
|
|
"""
|
|
if issubclass(handlerclass, TranscodeHandler):
|
|
return True
|
|
return False
|
|
|
|
def _handle(self, jobspec, existing_package, tempdir):
|
|
"""
|
|
Return a function to apply changes to the stored package.
|
|
|
|
The returned function generates posters as appropriate, moves poster
|
|
files into the package's basedir and updates the package metadata.
|
|
Replaced posters are deleted.
|
|
"""
|
|
def apply_func(package):
|
|
uploaded = {name: source['poster']
|
|
for name, source in jobspec['sources'].items()
|
|
if 'poster' in source and source['poster']}
|
|
for name, poster in uploaded.items():
|
|
if name in package['sources']:
|
|
oldposter = package['sources'][name].get('poster', None)
|
|
if oldposter:
|
|
remove(path.join(package.basedir, oldposter))
|
|
else:
|
|
# Initialize a new source
|
|
# set playAudio to False by default so we don't conflict
|
|
# with the audio handler
|
|
package['sources'][name] = {'playAudio': False}
|
|
# Don't rename - the file may be used by multiple sources.
|
|
posterfile = path.basename(poster)
|
|
copy2(path.join(jobspec['upload_dir'], poster),
|
|
path.join(package.basedir, posterfile))
|
|
package['sources'][name]['poster'] = posterfile
|
|
|
|
generate = [name for name, source in jobspec['sources'].items()
|
|
if _needs_poster(source)]
|
|
for name in generate:
|
|
if name not in package['sources']:
|
|
# A job could request thumbnail generation for a stream
|
|
# that doesn't exist. Ignore it, since there is nothing
|
|
# to generate from.
|
|
continue
|
|
packagesource = package['sources'][name]
|
|
|
|
oldposter = packagesource.get('poster', None)
|
|
if oldposter:
|
|
remove(path.join(package.basedir, oldposter))
|
|
|
|
# We generate the poster from the largest resolution available
|
|
largest = str(max(int(height) for height
|
|
in packagesource['video'].keys()))
|
|
videoname = packagesource['video'][largest]
|
|
_, postername = make_poster(path.join(package.basedir,
|
|
videoname),
|
|
tempdir)
|
|
# We can rename here because the files are unique.
|
|
rename(path.join(tempdir, postername),
|
|
path.join(package.basedir, postername))
|
|
packagesource['poster'] = postername
|
|
|
|
return apply_func
|