VisibilityHandler #3
18
README.md
18
README.md
|
@ -183,7 +183,6 @@ Valid top-level keys and their expected values are:
|
||||||
video. Relative to `upload_dir`.
|
video. Relative to `upload_dir`.
|
||||||
Details under the heading [Slides](#slides).
|
Details under the heading [Slides](#slides).
|
||||||
|
|
||||||
|
|
||||||
### Subtitles
|
### Subtitles
|
||||||
|
|
||||||
There are two top-level keys that deal with subtitles: `subtitles` and
|
There are two top-level keys that deal with subtitles: `subtitles` and
|
||||||
|
@ -265,13 +264,18 @@ These are the valid keys for a source object:
|
||||||
only have a `true` value for one stream per presentation. If omitted on
|
only have a `true` value for one stream per presentation. If omitted on
|
||||||
stream creation, this will defauilt to `false`.
|
stream creation, this will defauilt to `false`.
|
||||||
|
|
||||||
|
* `enabled`: `bool`
|
||||||
|
Whether this stream will be displayed in the player. At least one stream
|
||||||
|
must be enabled. If omitted on stream creation, this will deafult to `true`.
|
||||||
|
|
||||||
A `sources` object would look like this:
|
A `sources` object would look like this:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"asourcename": {
|
"asourcename": {
|
||||||
"video": "some/path",
|
"video": "some/path",
|
||||||
"poster": "some/other/path",
|
"poster": "some/other/path",
|
||||||
"playAudio": someBool
|
"playAudio": someBool,
|
||||||
|
"enabled": somebool,
|
||||||
},
|
},
|
||||||
"anothersource": {...},
|
"anothersource": {...},
|
||||||
...
|
...
|
||||||
|
@ -377,12 +381,14 @@ This is a job specification that has all keys and values:
|
||||||
"main": {
|
"main": {
|
||||||
"video": "videos/myvideo.mp4",
|
"video": "videos/myvideo.mp4",
|
||||||
"poster": "aposter.jpg",
|
"poster": "aposter.jpg",
|
||||||
"playAudio": true
|
"playAudio": true,
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
"second": {
|
"second": {
|
||||||
"video": "myothervideo.mp4",
|
"video": "myothervideo.mp4",
|
||||||
"poster": "anotherposter.jpg",
|
"poster": "anotherposter.jpg",
|
||||||
"playAudio": false
|
"playAudio": false,
|
||||||
|
"enabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"slides": {
|
"slides": {
|
||||||
|
@ -580,3 +586,7 @@ A source definition is a JSON object with the following keys:
|
||||||
* `playAudio`: `bool`
|
* `playAudio`: `bool`
|
||||||
A boolean value denoting whether to this stream's audio track. This will
|
A boolean value denoting whether to this stream's audio track. This will
|
||||||
only be set to `true` for one source in a given package.
|
only be set to `true` for one source in a given package.
|
||||||
|
|
||||||
|
* `enabled`: `bool`
|
||||||
|
Whether this stream will be displayed in the player. At least one stream
|
||||||
|
will be enabled.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import multiprocessing as mp
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
|
||||||
from .audio import AudioHandler
|
from .audio import AudioHandler
|
||||||
from .handler import Handler
|
|
||||||
from .metadata import MetadataHandler
|
from .metadata import MetadataHandler
|
||||||
from .poster import PosterHandler
|
from .poster import PosterHandler
|
||||||
from .slides import SlidesHandler
|
from .slides import SlidesHandler
|
||||||
|
@ -9,6 +8,7 @@ from .subtitles_whisper_hack import SubtitlesWhisperHandler
|
||||||
from .subtitles_import import SubtitlesImportHandler
|
from .subtitles_import import SubtitlesImportHandler
|
||||||
from .thumbnail import ThumbnailHandler
|
from .thumbnail import ThumbnailHandler
|
||||||
from .transcode import TranscodeHandler
|
from .transcode import TranscodeHandler
|
||||||
|
from .visibility import VisibilityHandler
|
||||||
from ..ldap import Ldap
|
from ..ldap import Ldap
|
||||||
from ..utils import get_section
|
from ..utils import get_section
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ allHandlers = [AudioHandler,
|
||||||
SubtitlesWhisperHandler,
|
SubtitlesWhisperHandler,
|
||||||
ThumbnailHandler,
|
ThumbnailHandler,
|
||||||
TranscodeHandler,
|
TranscodeHandler,
|
||||||
|
VisibilityHandler,
|
||||||
]
|
]
|
||||||
|
|
||||||
def init_handlers(collector, worker, config):
|
def init_handlers(collector, worker, config):
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
from .handler import Handler
|
|
||||||
from ..exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
@Handler.register
|
|
||||||
class VisibilityHandler(Handler):
|
|
||||||
"""
|
|
||||||
This class handles visibility settings for streams.
|
|
||||||
"""
|
|
||||||
@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.
|
|
||||||
|
|
||||||
In order for a job to be wanted, the field 'sources' must exist and
|
|
||||||
at least one of the source items must contain a 'enabled' field.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _validate(cls, jobspec, existing_package):
|
|
||||||
"""
|
|
||||||
Return True if the job is valid for this handler.
|
|
||||||
|
|
||||||
A job is valid as long as at least one of the package's source items
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _handle(self, jobspec, existing_package, tempdir):
|
|
||||||
pass
|
|
|
@ -1,4 +1,3 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from .handler import Handler
|
from .handler import Handler
|
||||||
from ..exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
@Handler.register
|
@Handler.register
|
||||||
|
|
|
@ -79,7 +79,7 @@ class PosterHandler(Handler):
|
||||||
super()._validate(jobspec, existing_package)
|
super()._validate(jobspec, existing_package)
|
||||||
if uploaded:
|
if uploaded:
|
||||||
if 'upload_dir' not in jobspec:
|
if 'upload_dir' not in jobspec:
|
||||||
raise ValidationException(f"upload_dir missing")
|
raise ValidationException("upload_dir missing")
|
||||||
for name, poster in uploaded.items():
|
for name, poster in uploaded.items():
|
||||||
if not path.isfile(path.join(jobspec['upload_dir'],
|
if not path.isfile(path.join(jobspec['upload_dir'],
|
||||||
poster)):
|
poster)):
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from os import path, rename
|
from os import path, rename
|
||||||
from shutil import rmtree
|
|
||||||
|
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,10 @@ from multiprocessing import Process, Queue
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from threading import Event
|
|
||||||
|
|
||||||
from .handler import Handler
|
from .handler import Handler
|
||||||
from ..exceptions import ConfigurationException, ValidationException
|
from ..exceptions import ValidationException
|
||||||
|
|
||||||
import torch
|
|
||||||
import whisper
|
import whisper
|
||||||
import whisper.utils
|
import whisper.utils
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ def _whisper_processor(inqueue,
|
||||||
if language is None:
|
if language is None:
|
||||||
out_language = result['language']
|
out_language = result['language']
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Detected language '%s' in %s.", out_language, inpath)
|
"Detected language '%s' in %s.", out_language, inpath)
|
||||||
else:
|
else:
|
||||||
out_language = language
|
out_language = language
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from multiprocessing import Process, Queue
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from threading import Event
|
|
||||||
|
|
||||||
from .handler import Handler
|
from .handler import Handler
|
||||||
from ..exceptions import ConfigurationException, ValidationException
|
from ..exceptions import ConfigurationException, ValidationException
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from os import path, remove, rename
|
from os import path, remove, rename
|
||||||
from shutil import rmtree
|
|
||||||
|
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
|
@ -169,7 +168,7 @@ class TranscodeHandler(Handler):
|
||||||
"""
|
"""
|
||||||
super()._validate(jobspec, existing_package)
|
super()._validate(jobspec, existing_package)
|
||||||
if 'upload_dir' not in jobspec:
|
if 'upload_dir' not in jobspec:
|
||||||
raise ValidationException(f"upload_dir missing")
|
raise ValidationException("upload_dir missing")
|
||||||
for name, source in jobspec['sources'].items():
|
for name, source in jobspec['sources'].items():
|
||||||
if not path.isfile(path.join(jobspec['upload_dir'],
|
if not path.isfile(path.join(jobspec['upload_dir'],
|
||||||
source['video'])):
|
source['video'])):
|
||||||
|
@ -228,8 +227,10 @@ class TranscodeHandler(Handler):
|
||||||
else:
|
else:
|
||||||
# Initialize a new source
|
# Initialize a new source
|
||||||
# set playAudio to False by default so we don't conflict
|
# set playAudio to False by default so we don't conflict
|
||||||
# with the audio handler
|
# with the audio handler, set enabled to True so we don't
|
||||||
sources[name] = {'playAudio': False}
|
# conflict with the visibility handler
|
||||||
|
sources[name] = {'playAudio': False,
|
||||||
|
'enabled': True}
|
||||||
|
|
||||||
# Save new source videos
|
# Save new source videos
|
||||||
sources[name]['video'] = variants
|
sources[name]['video'] = variants
|
||||||
|
|
57
pipeline/handlers/visibility.py
Normal file
57
pipeline/handlers/visibility.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import copy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .handler import Handler
|
||||||
|
from ..exceptions import ValidationException
|
||||||
|
from ..package import Package
|
||||||
|
|
||||||
|
@Handler.register
|
||||||
|
class VisibilityHandler(Handler):
|
||||||
|
"""
|
||||||
|
This class handles visibility settings for streams.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def wants(cls, jobspec: dict, existing_package: Package) -> bool | ValidationException:
|
||||||
|
"""
|
||||||
|
Return True if this handler wants to process this jobspec.
|
||||||
|
Raises an exception if the job is wanted but doesn't pass validation.
|
||||||
|
|
||||||
|
In order for a job to be wanted, the field 'sources' must exist and
|
||||||
|
at least one of the source items must contain a 'enabled' field.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if 'sources' in jobspec and any('enabled' in source for source in jobspec['sources'].values()):
|
||||||
|
return cls._validate(jobspec, existing_package)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate(cls, jobspec: dict, existing_package: Package) -> bool | ValidationException:
|
||||||
|
"""
|
||||||
|
Return True if the job is valid for this handler.
|
||||||
|
|
||||||
|
A job is valid as long as at least one of the package's source items
|
||||||
|
has its 'enabled' field set to True.
|
||||||
|
"""
|
||||||
|
sources = copy.deepcopy(existing_package.get('sources', {}))
|
||||||
|
for name, source in jobspec['sources'].items():
|
||||||
|
if name not in sources:
|
||||||
|
sources[name] = {}
|
||||||
|
if 'enabled' in source:
|
||||||
|
sources[name]['enabled'] = source['enabled']
|
||||||
|
for source in sources.values():
|
||||||
|
if 'enabled' in source:
|
||||||
|
return True
|
||||||
|
raise ValidationException("No enabled sources")
|
||||||
|
|
||||||
|
def _handle(self, jobspec: dict, existing_package: Package, tempdir: Path) -> callable:
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def apply_func(package: Package) -> None:
|
||||||
|
sources = package.get('sources', {})
|
||||||
|
for name, source in jobspec['sources'].items():
|
||||||
|
if name not in sources:
|
||||||
|
sources[name] = {}
|
||||||
|
if 'enabled' in source:
|
||||||
|
sources[name]['enabled'] = source['enabled']
|
||||||
|
|
||||||
|
return apply_func
|
|
@ -1,5 +1,4 @@
|
||||||
from ldap3 import Connection, ObjectDef, Reader, Server
|
from ldap3 import Connection, ObjectDef, Reader, Server
|
||||||
from ldap3.core.exceptions import LDAPSocketSendError
|
|
||||||
|
|
||||||
class Ldap:
|
class Ldap:
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
|
@ -2,7 +2,7 @@ import json
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from os import mkdir, path, rename, remove
|
from os import mkdir, path, rename, remove
|
||||||
from shutil import copy, copytree
|
from shutil import copytree
|
||||||
|
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
|
|
|
@ -110,4 +110,4 @@ class CatturaProcessor(Preprocessor):
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
if key.startswith('mediapackage:'):
|
if key.startswith('mediapackage:'):
|
||||||
return data[key]
|
return data[key]
|
||||||
raise KeyError(f"no 'mediapackage' key in job specification")
|
raise KeyError("no 'mediapackage' key in job specification")
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from os import mkdir, path
|
|
||||||
|
|
||||||
from ..queuethread import QueueThread
|
from ..queuethread import QueueThread
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from os import chdir, path
|
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +22,7 @@ canonical_jobspec = {
|
||||||
'source': str}},
|
'source': str}},
|
||||||
'sources': {str: {'poster': str,
|
'sources': {str: {'poster': str,
|
||||||
'playAudio': bool,
|
'playAudio': bool,
|
||||||
|
'enabled': bool,
|
||||||
'video': str}},
|
'video': str}},
|
||||||
'slides': str,
|
'slides': str,
|
||||||
'notification_url': str,
|
'notification_url': str,
|
||||||
|
@ -49,6 +49,7 @@ canonical_manifest = {
|
||||||
'subtitles': {str: str},
|
'subtitles': {str: str},
|
||||||
'sources': {str: {'poster': str,
|
'sources': {str: {'poster': str,
|
||||||
'playAudio': bool,
|
'playAudio': bool,
|
||||||
|
'enabled': bool,
|
||||||
'video': {'720': str,
|
'video': {'720': str,
|
||||||
'1080': str}}}
|
'1080': str}}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import logging
|
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
@ -6,7 +5,7 @@ from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from time import sleep, strftime
|
from time import sleep
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from .queuethread import QueueThread
|
from .queuethread import QueueThread
|
||||||
|
|
|
@ -5,7 +5,6 @@ import sys
|
||||||
|
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from os import path
|
from os import path
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from pipeline import Pipeline
|
from pipeline import Pipeline
|
||||||
|
|
||||||
|
|
23
test.py
23
test.py
|
@ -272,7 +272,7 @@ class PipelineTest(DaemonTest):
|
||||||
print("¤ Contents of invalid notification file ¤")
|
print("¤ Contents of invalid notification file ¤")
|
||||||
print(f.read())
|
print(f.read())
|
||||||
print("¤ End invalid notification file contents ¤")
|
print("¤ End invalid notification file contents ¤")
|
||||||
self.fail(f"Invalid JSON in result file.")
|
self.fail("Invalid JSON in result file.")
|
||||||
|
|
||||||
# Validate that this is the correct resultfile
|
# Validate that this is the correct resultfile
|
||||||
self.assertEqual(jobid, result['jobid'])
|
self.assertEqual(jobid, result['jobid'])
|
||||||
|
@ -300,7 +300,7 @@ class PipelineTest(DaemonTest):
|
||||||
return final_result
|
return final_result
|
||||||
|
|
||||||
def init_job(self, pkgid=False, url=False, subs=False, thumb=False,
|
def init_job(self, pkgid=False, url=False, subs=False, thumb=False,
|
||||||
source_count=0, poster_count=0):
|
source_count=0, disabled_source_count=0, poster_count=0):
|
||||||
jobspec = {}
|
jobspec = {}
|
||||||
|
|
||||||
if url == True:
|
if url == True:
|
||||||
|
@ -333,7 +333,7 @@ class PipelineTest(DaemonTest):
|
||||||
if source_count:
|
if source_count:
|
||||||
# poster_count determines the number of posters to include.
|
# poster_count determines the number of posters to include.
|
||||||
# Any remaining posters will be generated in pipeline
|
# Any remaining posters will be generated in pipeline
|
||||||
self.add_sources(jobspec, source_count, poster_count)
|
self.add_sources(jobspec, source_count, poster_count, disabled_source_count)
|
||||||
|
|
||||||
return jobspec
|
return jobspec
|
||||||
|
|
||||||
|
@ -343,7 +343,8 @@ class PipelineTest(DaemonTest):
|
||||||
str(uuid.uuid4()))
|
str(uuid.uuid4()))
|
||||||
jobspec['upload_dir'] = uldir
|
jobspec['upload_dir'] = uldir
|
||||||
makedirs(uldir)
|
makedirs(uldir)
|
||||||
return uldir
|
return uldir
|
||||||
|
return jobspec['upload_dir']
|
||||||
|
|
||||||
def add_subtitles(self, jobspec):
|
def add_subtitles(self, jobspec):
|
||||||
uldir = self.ensure_uldir(jobspec)
|
uldir = self.ensure_uldir(jobspec)
|
||||||
|
@ -356,16 +357,22 @@ class PipelineTest(DaemonTest):
|
||||||
copyfile(subspath, path.join(uldir, subsfile))
|
copyfile(subspath, path.join(uldir, subsfile))
|
||||||
jobspec['subtitles'] = subspec
|
jobspec['subtitles'] = subspec
|
||||||
|
|
||||||
def add_sources(self, jobspec, count, poster_count=0):
|
def add_sources(self, jobspec, count, poster_count=0, disabled_count=0):
|
||||||
uldir = self.ensure_uldir(jobspec)
|
uldir = self.ensure_uldir(jobspec)
|
||||||
jobspec['sources'] = {}
|
if 'sources' not in jobspec:
|
||||||
|
jobspec['sources'] = {}
|
||||||
posters = 0
|
posters = 0
|
||||||
|
disabled = 0
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
videopath = next(self.videopaths)
|
videopath = next(self.videopaths)
|
||||||
videofile = path.basename(videopath)
|
videofile = path.basename(videopath)
|
||||||
copyfile(videopath, path.join(uldir, videofile))
|
copyfile(videopath, path.join(uldir, videofile))
|
||||||
sourcedef = {'video': videofile,
|
sourcedef = {'video': videofile,
|
||||||
'playAudio': False}
|
'playAudio': False,
|
||||||
|
'enabled': True}
|
||||||
|
if disabled < disabled_count:
|
||||||
|
sourcedef['enabled'] = False
|
||||||
|
disabled += 1
|
||||||
if i == 0:
|
if i == 0:
|
||||||
sourcedef['playAudio'] = True
|
sourcedef['playAudio'] = True
|
||||||
if posters < poster_count:
|
if posters < poster_count:
|
||||||
|
@ -457,7 +464,7 @@ class PipelineTest(DaemonTest):
|
||||||
|
|
||||||
#@unittest.skip("This test is very slow")
|
#@unittest.skip("This test is very slow")
|
||||||
def test_transcoding(self):
|
def test_transcoding(self):
|
||||||
jobspec = self.init_job(source_count=4, poster_count=2)
|
jobspec = self.init_job(source_count=3, poster_count=2, disabled_source_count=1)
|
||||||
|
|
||||||
jobid = self.submit_default_job(jobspec)
|
jobid = self.submit_default_job(jobspec)
|
||||||
result = self.wait_for_result(jobid, ['AudioHandler',
|
result = self.wait_for_result(jobid, ['AudioHandler',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user