VisibilityHandler #3

Merged
erth9960 merged 4 commits from visibility into master 2024-03-27 14:54:33 +01:00
22 changed files with 101 additions and 69 deletions

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -1,4 +1,3 @@
import logging
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from pathlib import Path from pathlib import Path

View File

@ -1,5 +1,4 @@
from .handler import Handler from .handler import Handler
from ..exceptions import ValidationException
@Handler.register @Handler.register

View File

@ -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)):

View File

@ -1,5 +1,4 @@
from os import path, rename from os import path, rename
from shutil import rmtree
import ffmpeg import ffmpeg

View File

@ -1,4 +1,3 @@
import logging
from pathlib import Path from pathlib import Path

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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}}}
} }

View File

@ -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

View File

@ -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
View File

@ -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',