1
0
Fork 0
mirror of https://github.com/Kunsi/pretalx-plugin-broadcast-tools synced 2024-12-03 17:12:43 +00:00

add downloadable lower thirds images to be used in voctomix

This commit is contained in:
Franzi 2024-11-03 13:46:47 +01:00
parent f459c6c498
commit c0b3bdb55e
Signed by: kunsi
GPG key ID: 12E3D2136B818350
11 changed files with 651 additions and 83 deletions

View file

@ -22,3 +22,4 @@ class PluginApp(AppConfig):
def ready(self):
from . import signals # NOQA
from . import tasks # NOQA

View file

@ -0,0 +1,94 @@
Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students
of MA course of Visual design. Some rights reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -16,12 +16,22 @@ class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
broadcast_tools_lower_thirds_info_string = I18nFormField(
help_text=_(
"Will only be shown if there's a talk running. You may use "
"the place holders mentioned below."
"the place holders mentioned below. The info line will be shown "
"on the bottom right side of the lower third. Setting this to an "
"empty string will hide the line entirely."
),
label=_("Info line"),
required=False,
widget=I18nTextInput,
)
broadcast_tools_lower_thirds_export_voctomix = BooleanField(
help_text=_(
"If checked, pretalx will periodically generate voctomix-compatible "
"lower thirds images and make them available as <code>.tar.gz</code>."
),
label=_("Generate voctomix lower thirds"),
required=False,
)
broadcast_tools_room_info_lower_content = ChoiceField(
choices=(

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -74,16 +74,31 @@ msgstr "\"Derzeit kein Vortrag\"-Informationen"
#: forms.py:18
msgid ""
"Will only be shown if there's a talk running. You may use the place holders "
"mentioned below."
"mentioned below. The info line will be shown on the bottom right side of the "
"lower third. Setting this to an empty string will hide the line entirely."
msgstr ""
"Wird nur angezeigt, wenn derzeit ein Vortrag läuft. Du kannst die oben "
"genannten Platzhalter benutzen."
"genannten Platzhalter benutzen. Die Info-Zeile wird unten rechts in den "
"Bauchbinden angezeigt. Wenn das Feld leer ist, wird die Zeile automatisch "
"ausgeblendet."
#: forms.py:21
#: forms.py:23
msgid "Info line"
msgstr "Info-Zeile"
#: forms.py:37
#: forms.py:29
msgid ""
"If checked, pretalx will periodically generate voctomix-compatible lower "
"thirds images and make them available as <code>.tar.gz</code>."
msgstr ""
"Wenn aktiviert, wird pretalx automatisch voctomix-kompatible bauchbinden "
"generieren und diese als <code>.tar.gz</code> 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 (<code>MUX9U3</code> for example) - most useful in combination "
"with pretalx-proposal-redirects or something like that"
@ -204,54 +223,46 @@ msgstr ""
"Vortrags-Slug (zum Beispiel <code>MUX9U3</code>) - 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"

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -71,69 +71,80 @@ msgstr ""
#: forms.py:18
msgid ""
"Will only be shown if there's a talk running. You may use the place holders "
"mentioned below."
"mentioned below. The info line will be shown on the bottom right side of the "
"lower third. Setting this to an empty string will hide the line entirely."
msgstr ""
#: forms.py:21
#: forms.py:23
msgid "Info line"
msgstr ""
#: forms.py:37
#: forms.py:29
msgid ""
"If checked, pretalx will periodically generate voctomix-compatible lower "
"thirds images and make them available as <code>.tar.gz</code>."
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 (<code>MUX9U3</code> 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 ""

View file

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

View file

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

View file

@ -24,6 +24,7 @@
{% endfor %}
</tbody>
</table>
<p><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds_voctomix_download' request.event.slug %}">{% translate "Download voctomix-compatible lower thirds images" %}</a></p>
<h2>{% translate "Placeholders" %}</h2>
<p>{% translate "pretalx will automatically replace some placeholders in your custom content:" %}</p>
@ -54,7 +55,7 @@
</legend>
{{ form.broadcast_tools_lower_thirds_no_talk_info.as_field_group }}
{{ form.broadcast_tools_lower_thirds_info_string.as_field_group }}
<p>{% 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." %}</p>
{{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}
</fieldset>
<fieldset>
<legend>

View file

@ -5,6 +5,7 @@ from .views.orga import BroadcastToolsOrgaView
from .views.qr import BroadcastToolsFeedbackQrCodeSvg, BroadcastToolsPublicQrCodeSvg
from .views.schedule import BroadcastToolsScheduleView
from .views.static_html import BroadcastToolsLowerThirdsView, BroadcastToolsRoomInfoView
from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView
urlpatterns = [
path(
@ -26,6 +27,11 @@ urlpatterns = [
BroadcastToolsLowerThirdsView.as_view(),
name="lowerthirds",
),
path(
"lower-thirds.voctomix.tar.gz",
BroadcastToolsLowerThirdsVoctomixDownloadView.as_view(),
name="lowerthirds_voctomix_download",
),
path(
"feedback-qr/<talk>.svg",
BroadcastToolsFeedbackQrCodeSvg.as_view(),

View file

@ -0,0 +1,22 @@
from django.http import FileResponse, Http404
from django.views import View
from pretalx.common.text.path import safe_filename
from pretalx.common.views.mixins import EventPermissionRequired
from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
get_export_targz_path,
)
class BroadcastToolsLowerThirdsVoctomixDownloadView(EventPermissionRequired, View):
permission_required = "agenda.view_schedule"
def get(self, request, *args, **kwargs):
targz_path = get_export_targz_path(self.request.event)
if not targz_path.exists():
raise Http404()
response = FileResponse(open(targz_path, "rb"), as_attachment=True)
response["Content-Disposition"] = (
f"attachment; filename={safe_filename(targz_path.name)}"
)
return response