.tar.gz
."
+ ),
+ label=_("Generate voctomix lower thirds"),
+ required=False,
+ )
broadcast_tools_room_info_lower_content = ChoiceField(
choices=(
diff --git a/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po b/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
index d5dd4be..b7b5017 100644
--- a/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
+++ b/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-31 21:10+0100\n"
+"POT-Creation-Date: 2024-11-03 13:40+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME .tar.gz
."
+msgstr ""
+"Wenn aktiviert, wird pretalx automatisch voctomix-kompatible bauchbinden "
+"generieren und diese als .tar.gz
zur Verfügung stellen."
+
+#: forms.py:32
+msgid "Generate voctomix lower thirds"
+msgstr "Erzeuge voctomix-Bauchbinden"
+
+#: forms.py:47
msgid ""
"If a talk is running, the room info page will always show the talk title and "
"the list of speakers. The content below is configurable here."
@@ -92,11 +107,11 @@ msgstr ""
"und die Liste der Vortragenden an. Der Inhalt unterhalb dessen ist hier "
"konfigurierbar."
-#: forms.py:41
+#: forms.py:51
msgid "lower content"
msgstr "Unterer Inhalt"
-#: forms.py:46
+#: forms.py:56
msgid ""
"If no talk is running in the room, show the time and title of the next talk "
"in the room."
@@ -104,11 +119,11 @@ msgstr ""
"Wenn derzeit kein Vortrag läuft, soll die Startzeit und der Titel des "
"nächsten Vortrags angezeigt werden."
-#: forms.py:49
+#: forms.py:59
msgid "Show next talk"
msgstr "Zeige nächsten Vortrag"
-#: forms.py:55
+#: forms.py:65
msgid ""
"If checked, the value of the 'internal notes' field in a submission will get "
"added to the pdf export."
@@ -116,22 +131,22 @@ msgstr ""
"Wenn aktiviert, wird der Inhalt des Feldes 'Interne Notizen' im PDF-Export "
"angezeigt."
-#: forms.py:58
+#: forms.py:68
msgid "Show internal notes in pdf export"
msgstr "Zeige interne Notizen im PDF-Export"
-#: forms.py:63
+#: forms.py:73
msgid ""
"If checked, 'do not record' talks will not generate a page in the pdf export."
msgstr ""
"Wenn aktiviert, werden Vorträge mit 'Zeichnet meinen Vortrag nicht auf'-Flag "
"keine Seite im PDF-Export generieren."
-#: forms.py:66
+#: forms.py:76
msgid "Ignore 'do not record' talks when generating pdf"
msgstr "Ignoriere 'Zeichnet meinen Vortrag nicht auf'-Vorträge im PDF-Export"
-#: forms.py:71
+#: forms.py:81
msgid ""
"Comma-Separated list of question ids to include in pdf export. If empty, no "
"questions will get added."
@@ -139,11 +154,11 @@ msgstr ""
"Komma-separierte Liste von Fragen, die im PDF-Export eingebunden werden. "
"Falls dieses Feld leer ist, werden keine Fragen im PDF angezeigt."
-#: forms.py:74
+#: forms.py:84
msgid "Questions to include"
msgstr "Eingebundene Fragen"
-#: forms.py:79
+#: forms.py:89
msgid ""
"Additional content to print onto the PDF export. Will get printed as-is. You "
"may use the place holders mentioned below."
@@ -151,7 +166,7 @@ msgstr ""
"Zusätzlicher Inhalt, der auf dem PDF-Export angezeigt wird. Wird wie hier "
"eingegeben angezeigt. Du kannst die oben genannten Platzhalter benutzen."
-#: forms.py:83
+#: forms.py:93
msgid "Additional text"
msgstr "Zusätzlicher Text"
@@ -167,36 +182,40 @@ msgstr ""
msgid "broadcasting tools"
msgstr "Broadcasting-Tools"
-#: templates/pretalx_broadcast_tools/orga.html:14
+#: templates/pretalx_broadcast_tools/orga.html:13
msgid "room"
msgstr "Raum"
-#: templates/pretalx_broadcast_tools/orga.html:15
+#: templates/pretalx_broadcast_tools/orga.html:14
msgid "Feature"
msgstr "Funktion"
-#: templates/pretalx_broadcast_tools/orga.html:22
+#: templates/pretalx_broadcast_tools/orga.html:21
#, fuzzy
#| msgid "Lower thirds"
msgid "Lower Thirds"
msgstr "Bauchbinden"
-#: templates/pretalx_broadcast_tools/orga.html:23
+#: templates/pretalx_broadcast_tools/orga.html:22
#, fuzzy
#| msgid "Room info"
msgid "Room Info"
msgstr "Raum-Information"
-#: templates/pretalx_broadcast_tools/orga.html:30
+#: templates/pretalx_broadcast_tools/orga.html:27
+msgid "Download voctomix-compatible lower thirds images"
+msgstr "Lade voctomix-kompatible Bauchbinden-Bilder herunter"
+
+#: templates/pretalx_broadcast_tools/orga.html:29
msgid "Placeholders"
msgstr "Platzhalter"
-#: templates/pretalx_broadcast_tools/orga.html:31
+#: templates/pretalx_broadcast_tools/orga.html:30
msgid ""
"pretalx will automatically replace some placeholders in your custom content:"
msgstr "pretalx ersetzt automatisch einige Platzhalter in deinen Texten:"
-#: templates/pretalx_broadcast_tools/orga.html:34
+#: templates/pretalx_broadcast_tools/orga.html:33
msgid ""
"talk code (MUX9U3
for example) - most useful in combination "
"with pretalx-proposal-redirects or something like that"
@@ -204,54 +223,46 @@ msgstr ""
"Vortrags-Slug (zum Beispiel MUX9U3
) - am hilfreichsten in "
"Kombination mit pretalx-proposal-redirects oder ähnlichen Plugins"
-#: templates/pretalx_broadcast_tools/orga.html:37
+#: templates/pretalx_broadcast_tools/orga.html:36
msgid "The event slug"
msgstr "Der Event-Slug"
-#: templates/pretalx_broadcast_tools/orga.html:40
+#: templates/pretalx_broadcast_tools/orga.html:39
msgid "URL to the talk feedback page."
msgstr "Adresse der Vortrags-Feedback-Seite"
-#: templates/pretalx_broadcast_tools/orga.html:43
+#: templates/pretalx_broadcast_tools/orga.html:42
msgid "The talk slug"
msgstr "Der Vortrags-Slug"
-#: templates/pretalx_broadcast_tools/orga.html:46
+#: templates/pretalx_broadcast_tools/orga.html:45
msgid "URL to the talk detail page."
msgstr "Adresse der Vortrags-Seite"
-#: templates/pretalx_broadcast_tools/orga.html:48
+#: templates/pretalx_broadcast_tools/orga.html:47
msgid "or"
msgstr "oder"
-#: templates/pretalx_broadcast_tools/orga.html:49
+#: templates/pretalx_broadcast_tools/orga.html:48
msgid "Track name in plain text or coloured using the track colour."
msgstr "Track-Name in einfachem Text oder in der Track-Farbe eingefärbt."
-#: templates/pretalx_broadcast_tools/orga.html:52
+#: templates/pretalx_broadcast_tools/orga.html:51
msgid "Settings"
msgstr "Einstellungen"
-#: templates/pretalx_broadcast_tools/orga.html:55
+#: templates/pretalx_broadcast_tools/orga.html:54
msgid "Lower thirds"
msgstr "Bauchbinden"
-#: templates/pretalx_broadcast_tools/orga.html:59
-msgid ""
-"The info line will be shown on the bottom right side of your lower third. If "
-"you set it to an empty string, it will automatically hide itself."
-msgstr ""
-"Die Info-Zeile wird unten rechts in den Bauchbinden angezeigt. Wenn das Feld "
-"leer ist, wird die Zeile automatisch ausgeblendet."
-
-#: templates/pretalx_broadcast_tools/orga.html:63
+#: templates/pretalx_broadcast_tools/orga.html:62
msgid "Room info"
msgstr "Raum-Information"
-#: templates/pretalx_broadcast_tools/orga.html:70
+#: templates/pretalx_broadcast_tools/orga.html:69
msgid "PDF export"
msgstr "PDF-Export"
-#: templates/pretalx_broadcast_tools/orga.html:86
+#: templates/pretalx_broadcast_tools/orga.html:85
msgid "Save"
msgstr "Speichern"
diff --git a/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po b/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
index 04efdec..0b98d27 100644
--- a/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
+++ b/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-31 21:10+0100\n"
+"POT-Creation-Date: 2024-11-03 13:40+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME .tar.gz
."
+msgstr ""
+
+#: forms.py:32
+msgid "Generate voctomix lower thirds"
+msgstr ""
+
+#: forms.py:47
msgid ""
"If a talk is running, the room info page will always show the talk title and "
"the list of speakers. The content below is configurable here."
msgstr ""
-#: forms.py:41
+#: forms.py:51
msgid "lower content"
msgstr ""
-#: forms.py:46
+#: forms.py:56
msgid ""
"If no talk is running in the room, show the time and title of the next talk "
"in the room."
msgstr ""
-#: forms.py:49
+#: forms.py:59
msgid "Show next talk"
msgstr ""
-#: forms.py:55
+#: forms.py:65
msgid ""
"If checked, the value of the 'internal notes' field in a submission will get "
"added to the pdf export."
msgstr ""
-#: forms.py:58
+#: forms.py:68
msgid "Show internal notes in pdf export"
msgstr ""
-#: forms.py:63
+#: forms.py:73
msgid ""
"If checked, 'do not record' talks will not generate a page in the pdf export."
msgstr ""
-#: forms.py:66
+#: forms.py:76
msgid "Ignore 'do not record' talks when generating pdf"
msgstr ""
-#: forms.py:71
+#: forms.py:81
msgid ""
"Comma-Separated list of question ids to include in pdf export. If empty, no "
"questions will get added."
msgstr ""
-#: forms.py:74
+#: forms.py:84
msgid "Questions to include"
msgstr ""
-#: forms.py:79
+#: forms.py:89
msgid ""
"Additional content to print onto the PDF export. Will get printed as-is. You "
"may use the place holders mentioned below."
msgstr ""
-#: forms.py:83
+#: forms.py:93
msgid "Additional text"
msgstr ""
@@ -149,83 +160,81 @@ msgstr ""
msgid "broadcasting tools"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:14
+#: templates/pretalx_broadcast_tools/orga.html:13
msgid "room"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:15
+#: templates/pretalx_broadcast_tools/orga.html:14
msgid "Feature"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:22
+#: templates/pretalx_broadcast_tools/orga.html:21
msgid "Lower Thirds"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:23
+#: templates/pretalx_broadcast_tools/orga.html:22
msgid "Room Info"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:30
+#: templates/pretalx_broadcast_tools/orga.html:27
+msgid "Download voctomix-compatible lower thirds images"
+msgstr ""
+
+#: templates/pretalx_broadcast_tools/orga.html:29
msgid "Placeholders"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:31
+#: templates/pretalx_broadcast_tools/orga.html:30
msgid ""
"pretalx will automatically replace some placeholders in your custom content:"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:34
+#: templates/pretalx_broadcast_tools/orga.html:33
msgid ""
"talk code (MUX9U3
for example) - most useful in combination "
"with pretalx-proposal-redirects or something like that"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:37
+#: templates/pretalx_broadcast_tools/orga.html:36
msgid "The event slug"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:40
+#: templates/pretalx_broadcast_tools/orga.html:39
msgid "URL to the talk feedback page."
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:43
+#: templates/pretalx_broadcast_tools/orga.html:42
msgid "The talk slug"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:46
+#: templates/pretalx_broadcast_tools/orga.html:45
msgid "URL to the talk detail page."
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:48
+#: templates/pretalx_broadcast_tools/orga.html:47
msgid "or"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:49
+#: templates/pretalx_broadcast_tools/orga.html:48
msgid "Track name in plain text or coloured using the track colour."
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:52
+#: templates/pretalx_broadcast_tools/orga.html:51
msgid "Settings"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:55
+#: templates/pretalx_broadcast_tools/orga.html:54
msgid "Lower thirds"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:59
-msgid ""
-"The info line will be shown on the bottom right side of your lower third. If "
-"you set it to an empty string, it will automatically hide itself."
-msgstr ""
-
-#: templates/pretalx_broadcast_tools/orga.html:63
+#: templates/pretalx_broadcast_tools/orga.html:62
msgid "Room info"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:70
+#: templates/pretalx_broadcast_tools/orga.html:69
msgid "PDF export"
msgstr ""
-#: templates/pretalx_broadcast_tools/orga.html:86
+#: templates/pretalx_broadcast_tools/orga.html:85
msgid "Save"
msgstr ""
diff --git a/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py b/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py
new file mode 100644
index 0000000..87f1bd5
--- /dev/null
+++ b/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py
@@ -0,0 +1,336 @@
+import logging
+import tarfile
+from pathlib import Path
+
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError
+from django_scopes import scope, scopes_disabled
+from PIL import Image, ImageDraw, ImageFont
+from pretalx.agenda.management.commands.export_schedule_html import delete_directory
+from pretalx.event.models import Event
+
+from pretalx_broadcast_tools.utils.placeholders import placeholders
+
+IMG_WIDTH = 1920 # px
+IMG_HEIGHT = 1080 # px
+
+FONT_SIZE_TITLE = 30 # px
+FONT_SIZE_SPEAKER = 25 # px
+FONT_SIZE_INFOLINE = 18 # px
+FONT_FILE = (
+ Path(__file__).resolve().parent.parent.parent
+ / "assets"
+ / "titilium-web-regular.ttf"
+)
+
+BOX_PADDING = 10 # px
+
+BOX_WIDTH = int(IMG_WIDTH * 0.8)
+BOX_BOTTOM = int(IMG_HEIGHT - (IMG_HEIGHT * 0.1))
+BOX_LEFT = int((IMG_WIDTH - BOX_WIDTH) / 2)
+
+
+def get_export_path(event):
+ return settings.HTMLEXPORT_ROOT / event.slug / "broadcast-tools"
+
+
+def get_export_targz_path(event):
+ return get_export_path(event).with_suffix(".voctomix.tar.gz")
+
+
+def make_targz(generated_files, targz_path):
+ tmp_name = targz_path.with_suffix(".tmp")
+ tmp_name.unlink(missing_ok=True)
+ with tarfile.open(tmp_name, "w:gz") as tar:
+ for file in generated_files:
+ tar.add(file, arcname=file.name)
+ tmp_name.rename(targz_path)
+
+
+class VoctomixLowerThirdsExporter:
+ def __init__(self, event, tmp_dir):
+ self.log = logging.getLogger(event.slug)
+ self.event = event
+ self.tmp_dir = tmp_dir
+ self.exported = set()
+
+ if event.primary_color:
+ self.primary_colour = self._hex2rgb(event.primary_color)
+ else:
+ # pretalx.settings.DEFAULT_EVENT_PRIMARY_COLOR
+ self.primary__color = (58, 165, 124)
+
+ self.infoline = event.settings.broadcast_tools_lower_thirds_info_string or ""
+
+ self.font_title = ImageFont.truetype(
+ FONT_FILE,
+ size=FONT_SIZE_TITLE,
+ encoding="unic",
+ )
+ self.font_speaker = ImageFont.truetype(
+ FONT_FILE,
+ size=FONT_SIZE_SPEAKER,
+ encoding="unic",
+ )
+ self.font_infoline = ImageFont.truetype(
+ FONT_FILE,
+ size=FONT_SIZE_INFOLINE,
+ encoding="unic",
+ )
+
+ def _hex2rgb(self, hex_value):
+ hex_value = hex_value.lstrip("#")
+ # black insists this should have spaces around the :, but flake8
+ # complains about spaces around the :, soooooo ....
+ return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4)) # NOQA
+
+ def _fit_text(self, input_text, font, max_width):
+ words = [i.strip() for i in input_text.split()]
+ lines = []
+ line = []
+ for word in words:
+ new_line = " ".join([*line, word])
+ _, _, w, _ = font.getbbox(new_line)
+ if w > max_width:
+ # append old line to list of lines, then start new line with
+ # current word
+ lines.append(" ".join(line))
+ line = [word]
+ elif word.endswith(":"):
+ lines.append(new_line)
+ line = []
+ else:
+ line.append(word)
+
+ if line:
+ lines.append(" ".join(line))
+ return lines
+
+ def _get_infoline(self, talk):
+ infoline = self.infoline.localize(self.event.locale).format(
+ **placeholders(
+ self.event,
+ talk,
+ )
+ )
+ _, _, w, _ = self.font_infoline.getbbox(infoline)
+ return w, infoline
+
+ def export_speaker(self, talk, speaker):
+ img = Image.new(
+ mode="RGBA",
+ size=(IMG_WIDTH, IMG_HEIGHT),
+ color=(0, 0, 0, 0),
+ )
+
+ speaker_text = self._fit_text(
+ speaker.get_display_name(),
+ self.font_speaker,
+ BOX_WIDTH,
+ )
+ infoline_width, infoline_text = self._get_infoline(talk)
+
+ y_pos = BOX_BOTTOM - BOX_PADDING
+ if speaker_text:
+ y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
+ if infoline_text:
+ y_pos -= FONT_SIZE_INFOLINE
+
+ draw = ImageDraw.Draw(img)
+ draw.rectangle(
+ [
+ (BOX_LEFT - BOX_PADDING, y_pos),
+ (BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
+ ],
+ fill=self.primary_colour,
+ )
+ if talk.submission.track and talk.submission.track.color:
+ draw.rectangle(
+ [
+ (BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
+ (
+ BOX_LEFT + BOX_WIDTH + BOX_PADDING,
+ BOX_BOTTOM + (BOX_PADDING * 2),
+ ),
+ ],
+ fill=self._hex2rgb(talk.submission.track.color),
+ )
+
+ for line in speaker_text:
+ draw.text(
+ (BOX_LEFT, y_pos),
+ line,
+ font=self.font_speaker,
+ fill=(255, 255, 255),
+ )
+ y_pos += FONT_SIZE_SPEAKER
+
+ if infoline_text:
+ draw.text(
+ (BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
+ infoline_text,
+ font=self.font_infoline,
+ fill=(255, 255, 255),
+ )
+
+ filename = self.tmp_dir / f"event_{talk.submission_id}_person_{speaker.id}.png"
+ img.save(filename)
+ self.log.debug(
+ f"Generated single-speaker image for {speaker.get_display_name()!r} "
+ "of talk {talk.submission.title!r}, saved as {filename}"
+ )
+ return filename
+
+ def export_talk(self, talk):
+ img = Image.new(
+ mode="RGBA",
+ size=(IMG_WIDTH, IMG_HEIGHT),
+ color=(0, 0, 0, 0),
+ )
+
+ title_text = self._fit_text(talk.submission.title, self.font_title, BOX_WIDTH)
+ speaker_text = self._fit_text(
+ ", ".join(
+ [person.get_display_name() for person in talk.submission.speakers.all()]
+ ),
+ self.font_speaker,
+ BOX_WIDTH,
+ )
+ infoline_width, infoline_text = self._get_infoline(talk)
+
+ y_pos = BOX_BOTTOM - BOX_PADDING
+ if title_text:
+ y_pos -= len(title_text) * FONT_SIZE_TITLE
+ if speaker_text:
+ y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
+ if title_text and speaker_text:
+ y_pos -= BOX_PADDING
+ if infoline_text:
+ y_pos -= FONT_SIZE_INFOLINE
+
+ draw = ImageDraw.Draw(img)
+ draw.rectangle(
+ [
+ (BOX_LEFT - BOX_PADDING, y_pos),
+ (BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
+ ],
+ fill=self.primary_colour,
+ )
+ if talk.submission.track and talk.submission.track.color:
+ draw.rectangle(
+ [
+ (BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
+ (
+ BOX_LEFT + BOX_WIDTH + BOX_PADDING,
+ BOX_BOTTOM + (BOX_PADDING * 2),
+ ),
+ ],
+ fill=self._hex2rgb(talk.submission.track.color),
+ )
+
+ for line in title_text:
+ draw.text(
+ (BOX_LEFT, y_pos),
+ line,
+ font=self.font_title,
+ fill=(255, 255, 255),
+ )
+ y_pos += FONT_SIZE_TITLE
+
+ if title_text and speaker_text:
+ y_pos += BOX_PADDING
+
+ for line in speaker_text:
+ draw.text(
+ (BOX_LEFT, y_pos),
+ line,
+ font=self.font_speaker,
+ fill=(255, 255, 255),
+ )
+ y_pos += FONT_SIZE_SPEAKER
+
+ if infoline_text:
+ draw.text(
+ (BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
+ infoline_text,
+ font=self.font_infoline,
+ fill=(255, 255, 255),
+ )
+
+ filename = self.tmp_dir / f"event_{talk.submission_id}_persons.png"
+ img.save(filename)
+ self.log.debug(
+ f"Generated image for talk {talk.submission.title!r}, saved as {filename}"
+ )
+ return filename
+
+ def export(self):
+ generated_files = set()
+ if not self.event.current_schedule:
+ raise CommandError(
+ f"event {self.event.slug} ({self.event.name}) does not have a schedule to be exported!"
+ )
+
+ self.log.info(
+ f"Generating voctomix-compatible lower thirds for event {self.event.name}"
+ )
+
+ for talk in self.event.current_schedule.talks.filter(
+ is_visible=True
+ ).select_related("submission"):
+ if talk.id in self.exported:
+ # account for talks that are scheduled multiple times
+ self.log.warning(
+ f"Talk {talk.submission.title!r} was already generated, skipping. "
+ "(Possibly scheduled multiple times?)"
+ )
+ continue
+
+ self.log.info(f"Generating image(s) for talk {talk.submission.title!r}")
+ generated_files.add(self.export_talk(talk))
+ for speaker in talk.submission.speakers.all():
+ generated_files.add(self.export_speaker(talk, speaker))
+ self.exported.add(talk.id)
+
+ return generated_files
+
+
+class Command(BaseCommand):
+ def add_arguments(self, parser):
+ super().add_arguments(parser)
+ parser.add_argument("event", type=str)
+ parser.add_argument("--no-delete-source-files", action="store_true")
+
+ def handle(self, *args, **options):
+ event_slug = options.get("event")
+
+ with scopes_disabled():
+ try:
+ event = Event.objects.get(slug__iexact=event_slug)
+ except Event.DoesNotExist:
+ raise CommandError(f"could not find event with slug {event_slug!r}")
+
+ with scope(event=event):
+ logging.info(f"Exporting {event.name}")
+
+ export_dir = get_export_path(event)
+ targz_path = get_export_targz_path(event)
+
+ delete_directory(export_dir)
+ # for the first export of the conference, the "broadcast-tools"
+ # directory does not exist
+ export_dir.mkdir(parents=True)
+
+ try:
+ exporter = VoctomixLowerThirdsExporter(event, export_dir)
+ generated_files = exporter.export()
+ make_targz(generated_files, targz_path)
+ except Exception:
+ logging.exception(f"Export of {event.name} failed")
+ else:
+ logging.info(
+ f"Export of {event.name} succeeded, export available at {targz_path}"
+ )
+ finally:
+ if not options.get("no_delete_source_files"):
+ delete_directory(export_dir)
diff --git a/pretalx_broadcast_tools/tasks.py b/pretalx_broadcast_tools/tasks.py
new file mode 100644
index 0000000..c09d14b
--- /dev/null
+++ b/pretalx_broadcast_tools/tasks.py
@@ -0,0 +1,78 @@
+import logging
+from datetime import timedelta
+
+from django.dispatch import receiver
+from django.utils.timezone import now
+from django_scopes import scope, scopes_disabled
+from pretalx.celery_app import app
+from pretalx.common.signals import periodic_task
+from pretalx.event.models import Event
+
+LOG = logging.getLogger(__name__)
+
+
+@app.task(name="pretalx_broadcast_tools.export_voctomix_lower_thirds")
+def export_voctomix_lower_thirds(*, event_id):
+ from django.core.management import call_command
+
+ with scopes_disabled():
+ event = Event.objects.filter(pk=event_id).first()
+ if not event:
+ LOG.error(f"Could not find event {event_id=} for export")
+ return
+
+ with scope(event=event):
+ if not event.current_schedule:
+ LOG.error(f"event {event.slug} does not have schedule, can't export")
+ return
+
+ call_command(
+ "export_voctomix_lower_thirds",
+ event.slug,
+ )
+
+
+@app.task(name="pretalx_broadcast_tools.periodic_voctomix_export")
+def task_periodic_voctomix_export(*, event_slug):
+ from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
+ get_export_targz_path,
+ )
+
+ with scopes_disabled():
+ event = Event.objects.filter(slug=event_slug).first()
+
+ with scope(event=event):
+ if (
+ not event.settings.broadcast_tools_lower_thirds_export_voctomix
+ or not event.current_schedule
+ ):
+ return
+
+ targz_path = get_export_targz_path(event)
+ needs_rebuild = False
+ last_rebuild = event.cache.get("broadcast_tools_last_voctomix_export")
+ _now = now()
+ if not targz_path.exists():
+ needs_rebuild = True
+ if not last_rebuild or _now - last_rebuild >= timedelta(hours=1):
+ needs_rebuild = True
+ if event.cache.get("broadcast_tools_force_new_voctomix_export"):
+ needs_rebuild = True
+
+ if needs_rebuild:
+ event.cache.delete("broadcast_tools_force_new_voctomix_export")
+ event.cache.set("broadcast_tools_last_voctomix_export", _now, None)
+ export_voctomix_lower_thirds.apply_async(
+ kwargs={"event_id": event.id}, ignore_result=True
+ )
+
+
+@receiver(periodic_task)
+def periodic_event_services(sender, **kwargs):
+ for event in Event.objects.all():
+ with scope(event=event):
+ if (event.date_to + timedelta(days=2)) < now().date():
+ continue
+ task_periodic_voctomix_export.apply_async(
+ kwargs={"event_slug": event.slug}, ignore_result=True
+ )
diff --git a/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html b/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html
index 1804bbf..a184c51 100644
--- a/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html
+++ b/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html
@@ -24,6 +24,7 @@
{% endfor %}
+ {% translate "Download voctomix-compatible lower thirds images" %}
{% translate "pretalx will automatically replace some placeholders in your custom content:" %}
@@ -54,7 +55,7 @@ {{ form.broadcast_tools_lower_thirds_no_talk_info.as_field_group }} {{ form.broadcast_tools_lower_thirds_info_string.as_field_group }} -{% translate "The info line will be shown on the bottom right side of your lower third. If you set it to an empty string, it will automatically hide itself." %}
+ {{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}