From 540a649967f84d744dc8f5a2a8aff1c5623e501c Mon Sep 17 00:00:00 2001 From: nenzen Date: Wed, 27 Mar 2024 12:35:18 +0100 Subject: [PATCH 1/4] Add visibility handler --- pipeline/handlers/__init__.py | 3 +- pipeline/handlers/activation.py | 32 ------------------ pipeline/handlers/transcode.py | 9 +++--- pipeline/handlers/visibility.py | 57 +++++++++++++++++++++++++++++++++ pipeline/utils.py | 2 +- 5 files changed, 65 insertions(+), 38 deletions(-) delete mode 100644 pipeline/handlers/activation.py create mode 100644 pipeline/handlers/visibility.py diff --git a/pipeline/handlers/__init__.py b/pipeline/handlers/__init__.py index 092579a..c28dfb9 100644 --- a/pipeline/handlers/__init__.py +++ b/pipeline/handlers/__init__.py @@ -1,7 +1,6 @@ import multiprocessing as mp from .audio import AudioHandler -from .handler import Handler from .metadata import MetadataHandler from .poster import PosterHandler from .slides import SlidesHandler @@ -9,6 +8,7 @@ from .subtitles_whisper_hack import SubtitlesWhisperHandler from .subtitles_import import SubtitlesImportHandler from .thumbnail import ThumbnailHandler from .transcode import TranscodeHandler +from .visibility import VisibilityHandler from ..ldap import Ldap from ..utils import get_section @@ -20,6 +20,7 @@ allHandlers = [AudioHandler, SubtitlesWhisperHandler, ThumbnailHandler, TranscodeHandler, + VisibilityHandler, ] def init_handlers(collector, worker, config): diff --git a/pipeline/handlers/activation.py b/pipeline/handlers/activation.py deleted file mode 100644 index e69b083..0000000 --- a/pipeline/handlers/activation.py +++ /dev/null @@ -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 diff --git a/pipeline/handlers/transcode.py b/pipeline/handlers/transcode.py index a97aba5..1aba1c0 100644 --- a/pipeline/handlers/transcode.py +++ b/pipeline/handlers/transcode.py @@ -1,7 +1,6 @@ import logging from os import path, remove, rename -from shutil import rmtree import ffmpeg @@ -169,7 +168,7 @@ class TranscodeHandler(Handler): """ super()._validate(jobspec, existing_package) if 'upload_dir' not in jobspec: - raise ValidationException(f"upload_dir missing") + raise ValidationException("upload_dir missing") for name, source in jobspec['sources'].items(): if not path.isfile(path.join(jobspec['upload_dir'], source['video'])): @@ -228,8 +227,10 @@ class TranscodeHandler(Handler): else: # Initialize a new source # set playAudio to False by default so we don't conflict - # with the audio handler - sources[name] = {'playAudio': False} + # with the audio handler, set enabled to True so we don't + # conflict with the visibility handler + sources[name] = {'playAudio': False, + 'enabled': True} # Save new source videos sources[name]['video'] = variants diff --git a/pipeline/handlers/visibility.py b/pipeline/handlers/visibility.py new file mode 100644 index 0000000..ce1042b --- /dev/null +++ b/pipeline/handlers/visibility.py @@ -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 diff --git a/pipeline/utils.py b/pipeline/utils.py index 454f12a..456ed21 100644 --- a/pipeline/utils.py +++ b/pipeline/utils.py @@ -1,4 +1,3 @@ -from os import chdir, path from types import FunctionType @@ -23,6 +22,7 @@ canonical_jobspec = { 'source': str}}, 'sources': {str: {'poster': str, 'playAudio': bool, + 'enabled': bool, 'video': str}}, 'slides': str, 'notification_url': str, -- 2.39.5 From 83b59fffec702763524e1e5f1f8e8a720ffb7dd4 Mon Sep 17 00:00:00 2001 From: nenzen Date: Wed, 27 Mar 2024 12:35:51 +0100 Subject: [PATCH 2/4] Ruff --fix --- pipeline/__init__.py | 1 - pipeline/handlers/handler.py | 1 - pipeline/handlers/metadata.py | 1 - pipeline/handlers/poster.py | 2 +- pipeline/handlers/slides.py | 1 - pipeline/handlers/subtitles_import.py | 1 - pipeline/handlers/subtitles_whisper_hack.py | 6 ++---- pipeline/handlers/subtitles_whisper_serial.py | 1 - pipeline/ldap.py | 1 - pipeline/notifier.py | 1 - pipeline/package.py | 2 +- pipeline/preprocessors/cattura.py | 2 +- pipeline/preprocessors/preprocessor.py | 1 - pipeline/workthread.py | 3 +-- play-daemon.py | 1 - test.py | 2 +- 16 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pipeline/__init__.py b/pipeline/__init__.py index a5b21e3..3d7b70e 100644 --- a/pipeline/__init__.py +++ b/pipeline/__init__.py @@ -1,6 +1,5 @@ import logging import logging.handlers -import multiprocessing as mp import os import shutil diff --git a/pipeline/handlers/handler.py b/pipeline/handlers/handler.py index a6452ce..63a4737 100644 --- a/pipeline/handlers/handler.py +++ b/pipeline/handlers/handler.py @@ -1,4 +1,3 @@ -import logging from abc import ABCMeta, abstractmethod from pathlib import Path diff --git a/pipeline/handlers/metadata.py b/pipeline/handlers/metadata.py index f00e1fa..4e1839c 100644 --- a/pipeline/handlers/metadata.py +++ b/pipeline/handlers/metadata.py @@ -1,5 +1,4 @@ from .handler import Handler -from ..exceptions import ValidationException @Handler.register diff --git a/pipeline/handlers/poster.py b/pipeline/handlers/poster.py index 9bb4bf7..f4a740e 100644 --- a/pipeline/handlers/poster.py +++ b/pipeline/handlers/poster.py @@ -79,7 +79,7 @@ class PosterHandler(Handler): super()._validate(jobspec, existing_package) if uploaded: if 'upload_dir' not in jobspec: - raise ValidationException(f"upload_dir missing") + raise ValidationException("upload_dir missing") for name, poster in uploaded.items(): if not path.isfile(path.join(jobspec['upload_dir'], poster)): diff --git a/pipeline/handlers/slides.py b/pipeline/handlers/slides.py index 6e10459..28490db 100644 --- a/pipeline/handlers/slides.py +++ b/pipeline/handlers/slides.py @@ -1,5 +1,4 @@ from os import path, rename -from shutil import rmtree import ffmpeg diff --git a/pipeline/handlers/subtitles_import.py b/pipeline/handlers/subtitles_import.py index 1d5da49..099cd46 100644 --- a/pipeline/handlers/subtitles_import.py +++ b/pipeline/handlers/subtitles_import.py @@ -1,4 +1,3 @@ -import logging from pathlib import Path diff --git a/pipeline/handlers/subtitles_whisper_hack.py b/pipeline/handlers/subtitles_whisper_hack.py index a9293f7..5aa1954 100644 --- a/pipeline/handlers/subtitles_whisper_hack.py +++ b/pipeline/handlers/subtitles_whisper_hack.py @@ -6,12 +6,10 @@ from multiprocessing import Process, Queue from pathlib import Path from queue import Empty from time import sleep -from threading import Event from .handler import Handler -from ..exceptions import ConfigurationException, ValidationException +from ..exceptions import ValidationException -import torch import whisper import whisper.utils @@ -58,7 +56,7 @@ def _whisper_processor(inqueue, if language is None: out_language = result['language'] logger.info( - f"Detected language '%s' in %s.", out_language, inpath) + "Detected language '%s' in %s.", out_language, inpath) else: out_language = language diff --git a/pipeline/handlers/subtitles_whisper_serial.py b/pipeline/handlers/subtitles_whisper_serial.py index ffdcd29..f3c7d4f 100644 --- a/pipeline/handlers/subtitles_whisper_serial.py +++ b/pipeline/handlers/subtitles_whisper_serial.py @@ -5,7 +5,6 @@ from multiprocessing import Process, Queue from pathlib import Path from queue import Empty from time import sleep -from threading import Event from .handler import Handler from ..exceptions import ConfigurationException, ValidationException diff --git a/pipeline/ldap.py b/pipeline/ldap.py index 364729d..ce55624 100644 --- a/pipeline/ldap.py +++ b/pipeline/ldap.py @@ -1,5 +1,4 @@ from ldap3 import Connection, ObjectDef, Reader, Server -from ldap3.core.exceptions import LDAPSocketSendError class Ldap: def __init__(self, conf): diff --git a/pipeline/notifier.py b/pipeline/notifier.py index cf906a1..36d7f07 100644 --- a/pipeline/notifier.py +++ b/pipeline/notifier.py @@ -1,5 +1,4 @@ import json -import logging from dataclasses import asdict, dataclass from pprint import pformat diff --git a/pipeline/package.py b/pipeline/package.py index 2697e79..cd2b4d9 100644 --- a/pipeline/package.py +++ b/pipeline/package.py @@ -2,7 +2,7 @@ import json from copy import deepcopy from os import mkdir, path, rename, remove -from shutil import copy, copytree +from shutil import copytree import ffmpeg diff --git a/pipeline/preprocessors/cattura.py b/pipeline/preprocessors/cattura.py index 88a736b..898b5a2 100644 --- a/pipeline/preprocessors/cattura.py +++ b/pipeline/preprocessors/cattura.py @@ -110,4 +110,4 @@ class CatturaProcessor(Preprocessor): for key in data.keys(): if key.startswith('mediapackage:'): return data[key] - raise KeyError(f"no 'mediapackage' key in job specification") + raise KeyError("no 'mediapackage' key in job specification") diff --git a/pipeline/preprocessors/preprocessor.py b/pipeline/preprocessors/preprocessor.py index 748f45a..24b93d7 100644 --- a/pipeline/preprocessors/preprocessor.py +++ b/pipeline/preprocessors/preprocessor.py @@ -1,5 +1,4 @@ from abc import ABCMeta, abstractmethod -from os import mkdir, path from ..queuethread import QueueThread diff --git a/pipeline/workthread.py b/pipeline/workthread.py index dd88eb1..4efb351 100644 --- a/pipeline/workthread.py +++ b/pipeline/workthread.py @@ -1,4 +1,3 @@ -import logging import multiprocessing as mp from collections import deque @@ -6,7 +5,7 @@ from collections.abc import Iterable from dataclasses import dataclass from pprint import pformat from threading import Event -from time import sleep, strftime +from time import sleep from typing import Callable from .queuethread import QueueThread diff --git a/play-daemon.py b/play-daemon.py index 5870874..350e7d5 100755 --- a/play-daemon.py +++ b/play-daemon.py @@ -5,7 +5,6 @@ import sys from configparser import ConfigParser from os import path -from time import sleep from pipeline import Pipeline diff --git a/test.py b/test.py index dc972cf..11d44f8 100755 --- a/test.py +++ b/test.py @@ -272,7 +272,7 @@ class PipelineTest(DaemonTest): print("¤ Contents of invalid notification file ¤") print(f.read()) 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 self.assertEqual(jobid, result['jobid']) -- 2.39.5 From 919914ce58fa5811a38811835d4e134863729465 Mon Sep 17 00:00:00 2001 From: nenzen Date: Wed, 27 Mar 2024 14:19:48 +0100 Subject: [PATCH 3/4] Add documentation --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b504d0a..d1b97bf 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,6 @@ Valid top-level keys and their expected values are: video. Relative to `upload_dir`. Details under the heading [Slides](#slides). - ### Subtitles 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 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: ```json { "asourcename": { "video": "some/path", "poster": "some/other/path", - "playAudio": someBool + "playAudio": someBool, + "enabled": somebool, }, "anothersource": {...}, ... @@ -377,12 +381,14 @@ This is a job specification that has all keys and values: "main": { "video": "videos/myvideo.mp4", "poster": "aposter.jpg", - "playAudio": true + "playAudio": true, + "enabled": true }, "second": { "video": "myothervideo.mp4", "poster": "anotherposter.jpg", - "playAudio": false + "playAudio": false, + "enabled": false } }, "slides": { @@ -580,3 +586,7 @@ A source definition is a JSON object with the following keys: * `playAudio`: `bool` 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. + + * `enabled`: `bool` + Whether this stream will be displayed in the player. At least one stream + will be enabled. -- 2.39.5 From a258fdf523b968e9eff1320f4f23de6fd1e49760 Mon Sep 17 00:00:00 2001 From: nenzen Date: Wed, 27 Mar 2024 14:47:54 +0100 Subject: [PATCH 4/4] Create test, fix package --- pipeline/utils.py | 1 + test.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pipeline/utils.py b/pipeline/utils.py index 456ed21..c34f771 100644 --- a/pipeline/utils.py +++ b/pipeline/utils.py @@ -49,6 +49,7 @@ canonical_manifest = { 'subtitles': {str: str}, 'sources': {str: {'poster': str, 'playAudio': bool, + 'enabled': bool, 'video': {'720': str, '1080': str}}} } diff --git a/test.py b/test.py index 11d44f8..ff95249 100755 --- a/test.py +++ b/test.py @@ -300,7 +300,7 @@ class PipelineTest(DaemonTest): return final_result 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 = {} if url == True: @@ -333,7 +333,7 @@ class PipelineTest(DaemonTest): if source_count: # poster_count determines the number of posters to include. # 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 @@ -343,7 +343,8 @@ class PipelineTest(DaemonTest): str(uuid.uuid4())) jobspec['upload_dir'] = uldir makedirs(uldir) - return uldir + return uldir + return jobspec['upload_dir'] def add_subtitles(self, jobspec): uldir = self.ensure_uldir(jobspec) @@ -356,16 +357,22 @@ class PipelineTest(DaemonTest): copyfile(subspath, path.join(uldir, subsfile)) 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) - jobspec['sources'] = {} + if 'sources' not in jobspec: + jobspec['sources'] = {} posters = 0 + disabled = 0 for i in range(count): videopath = next(self.videopaths) videofile = path.basename(videopath) copyfile(videopath, path.join(uldir, videofile)) sourcedef = {'video': videofile, - 'playAudio': False} + 'playAudio': False, + 'enabled': True} + if disabled < disabled_count: + sourcedef['enabled'] = False + disabled += 1 if i == 0: sourcedef['playAudio'] = True if posters < poster_count: @@ -457,7 +464,7 @@ class PipelineTest(DaemonTest): #@unittest.skip("This test is very slow") 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) result = self.wait_for_result(jobid, ['AudioHandler', -- 2.39.5