mirror of
https://github.com/Kunsi/pretalx-plugin-broadcast-tools
synced 2025-04-04 23:34:36 +00:00
Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5613bf5acb | ||
![]() |
b07745b2ea | ||
![]() |
a05cbfc0d4 | ||
![]() |
1c951c6f65 | ||
![]() |
3af67e5e53 | ||
a3435e518f | |||
ade18a033e | |||
283686feb3 | |||
![]() |
baa8ce043c | ||
![]() |
de0c4721ed | ||
fec30e79ff | |||
![]() |
7fead51cb6 | ||
![]() |
65b8b87c8d | ||
6a6fc0be9e | |||
632c756245 | |||
929e41f0f8 | |||
0cc256d134 | |||
![]() |
3addc8b19e | ||
f63603d6de | |||
07090626e9 | |||
cb09f7c65a | |||
9d92add067 | |||
c0b3bdb55e | |||
f459c6c498 | |||
ed960358a4 | |||
dfa0945632 | |||
6a3b1b309e | |||
9ebcde7ab1 | |||
cd77ee8b91 | |||
73bd7f6c96 | |||
d1d3283c8e | |||
8dde19105d | |||
29db37bca5 | |||
410ca28b79 | |||
90c50c9652 | |||
bad650d5b9 | |||
443c7ce85a | |||
86a6075c30 | |||
620b2fb85e | |||
529f7f1eee | |||
3235a91ff5 | |||
d295ae18c3 | |||
![]() |
18cf1cd77a | ||
![]() |
d14f6e78f3 | ||
0b0df8b600 | |||
![]() |
74078695ed |
29 changed files with 1507 additions and 182 deletions
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright 2021 Franziska 'kunsi' Kunsmann
|
||||
Copyright 2021-2024 Franziska 'kunsi' Kunsmann
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
Pretalx: Broadcast Tools (and more)
|
||||
===================================
|
||||
|
||||
.. image:: http://translate.pretalx.com/widget/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/svg-badge.svg
|
||||
:alt: Translation status
|
||||
:target: http://translate.pretalx.com/engage/pretalx-plugin-broadcast-tools/
|
||||
|
||||
This is a plugin for `pretalx`_.
|
||||
|
||||
This adds the following features to your pretalx instance:
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "2.2.1"
|
||||
__version__ = "2.4.0"
|
||||
|
|
|
@ -22,3 +22,4 @@ class PluginApp(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
from . import tasks # NOQA
|
||||
|
|
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal file
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal 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.
|
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
Binary file not shown.
|
@ -1,6 +1,7 @@
|
|||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_noop
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pretalx.schedule.exporters import ScheduleData
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER
|
||||
|
@ -23,6 +24,10 @@ A4_WIDTH, A4_HEIGHT = A4
|
|||
PAGE_PADDING = 10 * mm
|
||||
|
||||
|
||||
def _(text):
|
||||
return LazyI18nString.from_gettext(gettext_noop(text))
|
||||
|
||||
|
||||
class PDFInfoPage(Flowable):
|
||||
def __init__(self, event, schedule, fahrplan_day, room_details, talk, style):
|
||||
super().__init__()
|
||||
|
@ -38,9 +43,9 @@ class PDFInfoPage(Flowable):
|
|||
def _questions(self):
|
||||
return {
|
||||
int(i.strip())
|
||||
for i in self.event.settings.broadcast_tools_pdf_questions_to_include.split(
|
||||
","
|
||||
)
|
||||
for i in (
|
||||
self.event.settings.broadcast_tools_pdf_questions_to_include or ""
|
||||
).split(",")
|
||||
if i
|
||||
}
|
||||
|
||||
|
@ -78,6 +83,9 @@ class PDFInfoPage(Flowable):
|
|||
def _space(self):
|
||||
self._add(Spacer(1, PAGE_PADDING / 2))
|
||||
|
||||
def _localize(self, translation):
|
||||
return translation.localize(self.event.locale)
|
||||
|
||||
def draw(self):
|
||||
if hasattr(self.talk, "local_start"):
|
||||
talk_start = self.talk.local_start
|
||||
|
@ -112,7 +120,10 @@ class PDFInfoPage(Flowable):
|
|||
|
||||
if self.talk.submission.do_not_record:
|
||||
self._add(
|
||||
Paragraph("DO NOT RECORD - DO NOT STREAM", style=self.style["Warning"]),
|
||||
Paragraph(
|
||||
self._localize(_("DO NOT RECORD - DO NOT STREAM")),
|
||||
style=self.style["Warning"],
|
||||
),
|
||||
gap=0,
|
||||
)
|
||||
self._space()
|
||||
|
@ -121,8 +132,8 @@ class PDFInfoPage(Flowable):
|
|||
Paragraph(
|
||||
" | ".join(
|
||||
[
|
||||
self.event.name.localize(self.event.locale),
|
||||
self.room["name"].localize(self.event.locale),
|
||||
self._localize(self.event.name),
|
||||
self._localize(self.room["name"]),
|
||||
talk_start.strftime("%F"),
|
||||
f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}',
|
||||
],
|
||||
|
@ -142,14 +153,21 @@ class PDFInfoPage(Flowable):
|
|||
)
|
||||
self._space()
|
||||
|
||||
if self.talk.submission.track:
|
||||
self._add(
|
||||
Paragraph(
|
||||
f"{self._localize(_('Track:'))} {self._localize(self.talk.submission.track.name)}",
|
||||
style=self.style["Meta"],
|
||||
)
|
||||
)
|
||||
self._add(
|
||||
Table(
|
||||
[
|
||||
(
|
||||
"Duration",
|
||||
"Language",
|
||||
"Type",
|
||||
"Code",
|
||||
self._localize(_("Duration")),
|
||||
self._localize(_("Language")),
|
||||
self._localize(_("Type")),
|
||||
self._localize(_("Code")),
|
||||
),
|
||||
(
|
||||
self.talk.export_duration,
|
||||
|
@ -183,7 +201,7 @@ class PDFInfoPage(Flowable):
|
|||
self._add(
|
||||
Paragraph(
|
||||
self.event.settings.broadcast_tools_pdf_additional_content.format(
|
||||
**placeholders(self.schedule, self.talk)
|
||||
**placeholders(self.schedule.event, self.talk)
|
||||
),
|
||||
style=self.style["Meta"],
|
||||
)
|
||||
|
@ -193,24 +211,35 @@ class PDFInfoPage(Flowable):
|
|||
self._space()
|
||||
self._add(
|
||||
Paragraph(
|
||||
"Questions",
|
||||
self._localize(_("Questions")),
|
||||
style=self.style["Heading"],
|
||||
)
|
||||
)
|
||||
for answer in self.talk.submission.answers.order_by("question"):
|
||||
|
||||
for answer in self.talk.submission.answers.order_by("question__position"):
|
||||
if answer.question.id not in self._questions:
|
||||
continue
|
||||
self._question_text(
|
||||
answer.question.question.localize(self.event.locale),
|
||||
self._localize(answer.question.question),
|
||||
answer.answer,
|
||||
style=self.style["Question"],
|
||||
)
|
||||
|
||||
for spk in self.talk.submission.speakers.all():
|
||||
for answer in spk.answers.order_by("question__position"):
|
||||
if answer.question.id not in self._questions:
|
||||
continue
|
||||
self._question_text(
|
||||
f"{self._localize(answer.question.question)} ({spk.get_display_name()})",
|
||||
answer.answer,
|
||||
style=self.style["Question"],
|
||||
)
|
||||
|
||||
if self.talk.submission.notes:
|
||||
self._space()
|
||||
self._add(
|
||||
Paragraph(
|
||||
"Notes",
|
||||
self._localize(_("Notes")),
|
||||
style=self.style["Heading"],
|
||||
)
|
||||
)
|
||||
|
@ -232,7 +261,7 @@ class PDFInfoPage(Flowable):
|
|||
self._space()
|
||||
self._add(
|
||||
Paragraph(
|
||||
"Internal Notes",
|
||||
self._localize(_("Internal Notes")),
|
||||
style=self.style["Heading"],
|
||||
)
|
||||
)
|
||||
|
@ -339,10 +368,9 @@ class PDFExporter(ScheduleData):
|
|||
)
|
||||
doc.build(self._add_pages(doc))
|
||||
f.seek(0)
|
||||
timestamp = now().strftime("%Y-%m-%d-%H%M")
|
||||
|
||||
return (
|
||||
f"{self.event.slug}_broadcast_tools_{timestamp}.pdf",
|
||||
f"{self.event.slug}_broadcast_tools_{self.schedule.version}.pdf",
|
||||
"application/pdf",
|
||||
f.read(),
|
||||
)
|
||||
|
|
|
@ -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=(
|
||||
|
|
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal file
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,274 @@
|
|||
# pretalx-broadcast-tools
|
||||
# Copyright (C) 2024 Franziska 'kunsi' Kunsmann
|
||||
# This file is distributed under the same license as the pretalx-broadcast-tools package.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 2.4.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
|
||||
"PO-Revision-Date: 2024-11-13 05:41+0000\n"
|
||||
"Last-Translator: Franziska <pretalx@kunsmann.eu>\n"
|
||||
"Language-Team: German <http://translate.pretalx.com/projects/"
|
||||
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/de/>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.6.2\n"
|
||||
|
||||
#: apps.py:12
|
||||
msgid "Broadcasting Tools"
|
||||
msgstr ""
|
||||
|
||||
#: apps.py:15
|
||||
msgid ""
|
||||
"Some tools which can be used for supporting a broadcasting software, for "
|
||||
"example a 'lower third' page which can be embedded into your broadcasting "
|
||||
"software"
|
||||
msgstr ""
|
||||
"Einige Helfer, die zur Unterstützung von Broadcasting-Software benutzt "
|
||||
"werden kann, zum Beispiel eine 'Bauchbinden'-Seite, die in deine "
|
||||
"Broadcasting-Software eingebunden werden kann"
|
||||
|
||||
#: exporter.py:124
|
||||
msgid "DO NOT RECORD - DO NOT STREAM"
|
||||
msgstr "NICHT AUFNEHMEN - NICHT STREAMEN"
|
||||
|
||||
#: exporter.py:167
|
||||
msgid "Duration"
|
||||
msgstr "Dauer"
|
||||
|
||||
#: exporter.py:168
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: exporter.py:169
|
||||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
#: exporter.py:170
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: exporter.py:214
|
||||
msgid "Questions"
|
||||
msgstr "Fragen"
|
||||
|
||||
#: exporter.py:242
|
||||
msgid "Notes"
|
||||
msgstr "Notizen"
|
||||
|
||||
#: exporter.py:264
|
||||
msgid "Internal Notes"
|
||||
msgstr "Interne Notizen"
|
||||
|
||||
#: forms.py:10
|
||||
msgid "Will be shown as talk title if there's currently no talk running."
|
||||
msgstr "Wird angezeigt, wenn derzeit kein Vortrag läuft."
|
||||
|
||||
#: forms.py:12
|
||||
msgid "\"No talk running\" information"
|
||||
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. 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. Die Info-Zeile wird unten rechts in den "
|
||||
"Bauchbinden angezeigt. Wenn das Feld leer ist, wird die Zeile automatisch "
|
||||
"ausgeblendet."
|
||||
|
||||
#: forms.py:23
|
||||
msgid "Info line"
|
||||
msgstr "Info-Zeile"
|
||||
|
||||
#: 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."
|
||||
msgstr ""
|
||||
"Wenn ein Vortrag läuft, zeigt die Raum-Info-Seite immer den Vortrags-Titel "
|
||||
"und die Liste der Vortragenden an. Der Inhalt unterhalb dessen ist hier "
|
||||
"konfigurierbar."
|
||||
|
||||
#: forms.py:51
|
||||
msgid "lower content"
|
||||
msgstr "Unterer Inhalt"
|
||||
|
||||
#: 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 ""
|
||||
"Wenn derzeit kein Vortrag läuft, soll die Startzeit und der Titel des "
|
||||
"nächsten Vortrags angezeigt werden."
|
||||
|
||||
#: forms.py:59
|
||||
msgid "Show next talk"
|
||||
msgstr "Zeige nächsten Vortrag"
|
||||
|
||||
#: forms.py:65
|
||||
msgid ""
|
||||
"If checked, the value of the 'internal notes' field in a submission will get "
|
||||
"added to the pdf export."
|
||||
msgstr ""
|
||||
"Wenn aktiviert, wird der Inhalt des Feldes 'Interne Notizen' im PDF-Export "
|
||||
"angezeigt."
|
||||
|
||||
#: forms.py:68
|
||||
msgid "Show internal notes in pdf export"
|
||||
msgstr "Zeige interne Notizen im PDF-Export"
|
||||
|
||||
#: 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:76
|
||||
msgid "Ignore 'do not record' talks when generating pdf"
|
||||
msgstr "Ignoriere 'Zeichnet meinen Vortrag nicht auf'-Vorträge im PDF-Export"
|
||||
|
||||
#: forms.py:81
|
||||
msgid ""
|
||||
"Comma-Separated list of question ids to include in pdf export. If empty, no "
|
||||
"questions will get added."
|
||||
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:84
|
||||
msgid "Questions to include"
|
||||
msgstr "Eingebundene Fragen"
|
||||
|
||||
#: 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 ""
|
||||
"Zusätzlicher Inhalt, der auf dem PDF-Export angezeigt wird. Wird wie hier "
|
||||
"eingegeben angezeigt. Du kannst die oben genannten Platzhalter benutzen."
|
||||
|
||||
#: forms.py:93
|
||||
msgid "Additional text"
|
||||
msgstr "Zusätzlicher Text"
|
||||
|
||||
#: signals.py:13
|
||||
msgid "Sorry, there's currently no talk running"
|
||||
msgstr "Leider läuft aktuell kein Vortrag"
|
||||
|
||||
#: signals.py:27
|
||||
msgid "broadcast tools"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:8
|
||||
msgid "broadcasting tools"
|
||||
msgstr "Broadcasting-Tools"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:13
|
||||
msgid "room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:14
|
||||
msgid "Feature"
|
||||
msgstr "Funktion"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:21
|
||||
msgid "Lower Thirds"
|
||||
msgstr "Bauchbinden"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:22
|
||||
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||
msgid "Room Info"
|
||||
msgstr "Raum-Information"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:23
|
||||
msgid "Room Timer"
|
||||
msgstr "Raum-Timer"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:28
|
||||
msgid "Download voctomix-compatible lower thirds images"
|
||||
msgstr "Lade voctomix-kompatible Bauchbinden-Bilder herunter"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:30
|
||||
msgid "Placeholders"
|
||||
msgstr "Platzhalter"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:31
|
||||
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
|
||||
msgid ""
|
||||
"talk code (<code>MUX9U3</code> for example) - most useful in combination "
|
||||
"with pretalx-proposal-redirects or something like that"
|
||||
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
|
||||
msgid "The event slug"
|
||||
msgstr "Der Event-Slug"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:40
|
||||
msgid "URL to the talk feedback page."
|
||||
msgstr "Adresse der Vortrags-Feedback-Seite"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:43
|
||||
msgid "The talk slug"
|
||||
msgstr "Der Vortrags-Slug"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:46
|
||||
msgid "URL to the talk detail page."
|
||||
msgstr "Adresse der Vortrags-Seite"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:48
|
||||
msgid "or"
|
||||
msgstr "oder"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:49
|
||||
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
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||
msgid "PDF Export"
|
||||
msgstr "PDF-Export"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:86
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:55
|
||||
msgid "Lower thirds"
|
||||
msgstr "Bauchbinden"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||
msgid "Room info"
|
||||
msgstr "Raum-Information"
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||
msgid "PDF export"
|
||||
msgstr "PDF-Export"
|
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal file
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,240 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
|
||||
"PO-Revision-Date: 2025-02-05 12:56+0000\n"
|
||||
"Last-Translator: Harrissou Sant-anna <delazj@gmail.com>\n"
|
||||
"Language-Team: French <http://translate.pretalx.com/projects/"
|
||||
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/fr/>\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 5.6.2\n"
|
||||
|
||||
#: apps.py:12
|
||||
msgid "Broadcasting Tools"
|
||||
msgstr ""
|
||||
|
||||
#: apps.py:15
|
||||
msgid ""
|
||||
"Some tools which can be used for supporting a broadcasting software, for "
|
||||
"example a 'lower third' page which can be embedded into your broadcasting "
|
||||
"software"
|
||||
msgstr ""
|
||||
|
||||
#: exporter.py:124
|
||||
msgid "DO NOT RECORD - DO NOT STREAM"
|
||||
msgstr "NE PAS ENREGISTRER - NE PAS DIFFUSER EN CONTINU"
|
||||
|
||||
#: exporter.py:167
|
||||
msgid "Duration"
|
||||
msgstr "Durée"
|
||||
|
||||
#: exporter.py:168
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
|
||||
#: exporter.py:169
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: exporter.py:170
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: exporter.py:214
|
||||
msgid "Questions"
|
||||
msgstr "Questions"
|
||||
|
||||
#: exporter.py:242
|
||||
msgid "Notes"
|
||||
msgstr "Notes"
|
||||
|
||||
#: exporter.py:264
|
||||
msgid "Internal Notes"
|
||||
msgstr "Notes internes"
|
||||
|
||||
#: forms.py:10
|
||||
msgid "Will be shown as talk title if there's currently no talk running."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:12
|
||||
msgid "\"No talk running\" information"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:18
|
||||
msgid ""
|
||||
"Will only be shown if there's a talk running. You may use 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."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:23
|
||||
msgid "Info line"
|
||||
msgstr ""
|
||||
|
||||
#: 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:51
|
||||
msgid "lower content"
|
||||
msgstr ""
|
||||
|
||||
#: 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:59
|
||||
msgid "Show next talk"
|
||||
msgstr ""
|
||||
|
||||
#: 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:68
|
||||
msgid "Show internal notes in pdf export"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:73
|
||||
msgid ""
|
||||
"If checked, 'do not record' talks will not generate a page in the pdf export."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:76
|
||||
msgid "Ignore 'do not record' talks when generating pdf"
|
||||
msgstr ""
|
||||
|
||||
#: 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:84
|
||||
msgid "Questions to include"
|
||||
msgstr ""
|
||||
|
||||
#: 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:93
|
||||
msgid "Additional text"
|
||||
msgstr ""
|
||||
|
||||
#: signals.py:13
|
||||
msgid "Sorry, there's currently no talk running"
|
||||
msgstr ""
|
||||
|
||||
#: signals.py:27
|
||||
msgid "broadcast tools"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:8
|
||||
msgid "broadcasting tools"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:13
|
||||
msgid "room"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:14
|
||||
msgid "Feature"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:21
|
||||
#: templates/pretalx_broadcast_tools/orga.html:55
|
||||
msgid "Lower Thirds"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:22
|
||||
#: templates/pretalx_broadcast_tools/orga.html:63
|
||||
msgid "Room Info"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:23
|
||||
msgid "Room Timer"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:28
|
||||
msgid "Download voctomix-compatible lower thirds images"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:30
|
||||
msgid "Placeholders"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:31
|
||||
msgid ""
|
||||
"pretalx will automatically replace some placeholders in your custom content:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:34
|
||||
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
|
||||
msgid "The event slug"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:40
|
||||
msgid "URL to the talk feedback page."
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:43
|
||||
msgid "The talk slug"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:46
|
||||
msgid "URL to the talk detail page."
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:48
|
||||
msgid "or"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:49
|
||||
msgid "Track name in plain text or coloured using the track colour."
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:52
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:70
|
||||
msgid "PDF Export"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pretalx_broadcast_tools/orga.html:86
|
||||
msgid "Save"
|
||||
msgstr ""
|
|
@ -0,0 +1,343 @@
|
|||
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
|
||||
|
||||
if talk.submission is None:
|
||||
self.log.info(
|
||||
f"Talk {talk.id} has no associated submission, this is a break. "
|
||||
"Skipping."
|
||||
)
|
||||
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)
|
|
@ -37,7 +37,7 @@ def navbar_info(sender, request, **kwargs):
|
|||
]
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdfexporter")
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdf")
|
||||
def register_data_exporter(sender, **kwargs):
|
||||
from .exporter import PDFExporter
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ body {
|
|||
background-color: black;
|
||||
}
|
||||
|
||||
/* room info *********************************************/
|
||||
#broadcast_tools_room_info {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
@ -23,19 +24,19 @@ body {
|
|||
}
|
||||
|
||||
#broadcast_tools_room_info_roomname {
|
||||
font-size: 2em;
|
||||
font-size: 5vh;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_info_title {
|
||||
font-size: 6em;
|
||||
font-size: 8vh;
|
||||
margin-bottom: 0.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_info_speaker {
|
||||
font-size: 3em;
|
||||
font-size: 6vh;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
@ -45,13 +46,14 @@ body {
|
|||
|
||||
#broadcast_tools_room_info_qr img {
|
||||
background-color: white;
|
||||
height: calc(100% - 2em);
|
||||
height: calc(100% - 2vh);
|
||||
}
|
||||
|
||||
#broadcast_tools_room_info_qr p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/* lower thirds ******************************************/
|
||||
#broadcast_tools_lower_thirds_box {
|
||||
width: 1020px;
|
||||
|
||||
|
@ -66,16 +68,58 @@ body {
|
|||
}
|
||||
|
||||
#broadcast_tools_lower_thirds_title {
|
||||
font-size: 30px;
|
||||
font-size: 3vh;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#broadcast_tools_lower_thirds_speaker {
|
||||
font-size: 20px;
|
||||
font-size: 2vh;
|
||||
}
|
||||
|
||||
#broadcast_tools_lower_thirds_infoline {
|
||||
font-size: 16px;
|
||||
font-size: 1.8vh;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* room timer ********************************************/
|
||||
#broadcast_tools_room_timer_header {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_title {
|
||||
font-size: 8vh;
|
||||
margin-bottom: 0.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_speaker {
|
||||
font-size: 6vh;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_scheduledata {
|
||||
font-size: 3vh;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_timeleft {
|
||||
font-size: 6vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_timeleft_timer {
|
||||
font-size: 35vh;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_progressbar, #broadcast_tools_room_timer_progressbar_bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
#broadcast_tools_room_timer_progressbar {
|
||||
right: 0;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
schedule = null;
|
||||
event_info = null;
|
||||
req = {};
|
||||
|
||||
function get_current_talk(max_offset) {
|
||||
room_name = get_room_name();
|
||||
|
@ -60,14 +61,16 @@ function get_next_talk() {
|
|||
}
|
||||
|
||||
function get_room_name() {
|
||||
room_name = null;
|
||||
try {
|
||||
hash = decodeURIComponent(window.location.hash.substring(1));
|
||||
room_name = hash;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return room_name;
|
||||
if (event_info && event_info["rooms"].hasOwnProperty(hash)) {
|
||||
return event_info["rooms"][hash];
|
||||
}
|
||||
// XXX remove fallback when releasing 3.0.0
|
||||
return hash;
|
||||
}
|
||||
|
||||
function format_time_from_pretalx(from_pretalx) {
|
||||
|
@ -87,11 +90,32 @@ function format_time_from_pretalx(from_pretalx) {
|
|||
return h + ':' + m;
|
||||
}
|
||||
|
||||
function xhr_get(url, callback_func) {
|
||||
req[url] = new XMLHttpRequest();
|
||||
req[url].timeout = 10000;
|
||||
req[url].onreadystatechange = () => {
|
||||
if (req[url].readyState === 4) {
|
||||
if (req[url].status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback_func(req[url].responseText);
|
||||
}
|
||||
};
|
||||
req[url].open('GET', url);
|
||||
req[url].setRequestHeader('Accept', 'application/json');
|
||||
req[url].setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
req[url].send();
|
||||
}
|
||||
|
||||
function update_schedule() {
|
||||
$.getJSON('../event.json', function(data) {
|
||||
event_info = data;
|
||||
xhr_get('../event.json', function(text) {
|
||||
console.debug("events: " + text);
|
||||
event_info = JSON.parse(text);
|
||||
});
|
||||
$.getJSON('../schedule.json', function(data) {
|
||||
xhr_get('../schedule.json', function(text) {
|
||||
console.debug("schedule: " + text);
|
||||
data = JSON.parse(text);
|
||||
if ('error' in data) {
|
||||
console.error(data['error']);
|
||||
} else {
|
||||
|
|
|
@ -6,42 +6,47 @@ function update_lower_third() {
|
|||
return
|
||||
}
|
||||
|
||||
$('#broadcast_tools_lower_thirds_box').css('background-color', event_info['color']);
|
||||
box = document.getElementById('broadcast_tools_lower_thirds_box');
|
||||
title = document.getElementById('broadcast_tools_lower_thirds_title');
|
||||
speaker = document.getElementById('broadcast_tools_lower_thirds_speaker');
|
||||
infoline = document.getElementById('broadcast_tools_lower_thirds_infoline');
|
||||
|
||||
box.style.backgroundColor = event_info['color'];
|
||||
|
||||
if (!schedule) {
|
||||
$('#broadcast_tools_lower_thirds_title').text('Waiting for schedule ...')
|
||||
title.innerHTML = 'Waiting for schedule ...';
|
||||
return
|
||||
}
|
||||
|
||||
if ('error' in schedule) {
|
||||
$('#broadcast_tools_lower_thirds_title').text('Error')
|
||||
$('#broadcast_tools_lower_thirds_speaker').html(schedule['error'].join('<br>'));
|
||||
$('#broadcast_tools_lower_thirds_infoline').text('');
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = schedule['error'].join('<br>');
|
||||
infoline.innerHTML = '';
|
||||
return
|
||||
}
|
||||
|
||||
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||
$('#broadcast_tools_lower_thirds_title').text('Error')
|
||||
$('#broadcast_tools_lower_thirds_speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
|
||||
$('#broadcast_tools_lower_thirds_infoline').text('');
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||
infoline.innerHTML = '';
|
||||
return
|
||||
}
|
||||
|
||||
current_talk = get_current_talk(5);
|
||||
if (current_talk) {
|
||||
$('#broadcast_tools_lower_thirds_title').text(current_talk['title']);
|
||||
$('#broadcast_tools_lower_thirds_speaker').text(current_talk['persons'].join(', '));
|
||||
$('#broadcast_tools_lower_thirds_infoline').text(current_talk['infoline']);
|
||||
title.innerHTML = current_talk['title'];
|
||||
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||
infoline.innerHTML = current_talk['infoline'];
|
||||
} else {
|
||||
$('#broadcast_tools_lower_thirds_title').text(event_info['no_talk']);
|
||||
$('#broadcast_tools_lower_thirds_speaker').text('');
|
||||
$('#broadcast_tools_lower_thirds_infoline').text('');
|
||||
title.innerHTML = event_info['no_talk'];
|
||||
speaker.innerHTML = '';
|
||||
infoline.innerHTML = '';
|
||||
}
|
||||
|
||||
if (current_talk && current_talk['track']) {
|
||||
$('#broadcast_tools_lower_thirds_box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
|
||||
box.style.borderBottom = '10px solid ' + current_talk['track']['color'];
|
||||
} else {
|
||||
$('#broadcast_tools_lower_thirds_box').css('border-bottom', 'none');
|
||||
box.style.borderBottom = 'none';
|
||||
}
|
||||
}
|
||||
window.setInterval(update_lower_third, 1000);
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
$(function() {
|
||||
$('#broadcast_tools_room_info_title').text('Content will appear soon.');
|
||||
$('#broadcast_tools_room_info_speaker').text('');
|
||||
$('#broadcast_tools_room_info_qr').text('');
|
||||
});
|
||||
|
||||
function update_room_info() {
|
||||
room_name = get_room_name();
|
||||
|
||||
|
@ -12,31 +6,37 @@ function update_room_info() {
|
|||
return
|
||||
}
|
||||
|
||||
box = document.getElementById('broadcast_tools_room_info');
|
||||
roomname = document.getElementById('broadcast_tools_room_info_roomname');
|
||||
title = document.getElementById('broadcast_tools_room_info_title');
|
||||
speaker = document.getElementById('broadcast_tools_room_info_speaker');
|
||||
qr = document.getElementById('broadcast_tools_room_info_qr');
|
||||
|
||||
if (!room_name) {
|
||||
$('#broadcast_tools_room_info_roomname').text(event_info['name']);
|
||||
$('#broadcast_tools_room_info_title').text('Backstage');
|
||||
$('#broadcast_tools_room_info_speaker').text('');
|
||||
$('#broadcast_tools_room_info_qr').text('');
|
||||
$('#broadcast_tools_room_info').css('background-color', event_info['color']);
|
||||
roomname.innerHTML = event_info['name'];
|
||||
title.innerHTML = 'Backstage';
|
||||
speaker.innerHTML = '';
|
||||
qr.innerHTML = '';
|
||||
box.style.backgroundColor = event_info['color'];
|
||||
return
|
||||
}
|
||||
|
||||
if (!schedule) {
|
||||
$('#broadcast_tools_room_info_speaker').text('Waiting for schedule ...')
|
||||
speaker.innerHTML = 'Waiting for schedule ...';
|
||||
return
|
||||
}
|
||||
|
||||
if ('error' in schedule) {
|
||||
$('#broadcast_tools_room_info_title').text('Error')
|
||||
$('#broadcast_tools_room_info_speaker').html(schedule['error'].join('<br>'));
|
||||
$('#broadcast_tools_room_info_qr').text('');
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = schedule['error'].join('<br>');
|
||||
qr.innerHTML = '';
|
||||
return
|
||||
}
|
||||
|
||||
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||
$('#broadcast_tools_room_info_title').text('Error')
|
||||
$('#broadcast_tools_room_info_speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
|
||||
$('#broadcast_tools_room_info_qr').text('');
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||
qr.innerHTML = '';
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -54,26 +54,28 @@ function update_room_info() {
|
|||
qr_info = '';
|
||||
}
|
||||
|
||||
$('#broadcast_tools_room_info_roomname').text(room_name);
|
||||
$('#broadcast_tools_room_info_title').text(current_talk['title']);
|
||||
$('#broadcast_tools_room_info_speaker').text(current_talk['persons'].join(', '));
|
||||
$('#broadcast_tools_room_info_qr').html(qr_info);
|
||||
roomname.innerHTML = room_name;
|
||||
title.innerHTML = current_talk['title'];
|
||||
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||
qr.innerHTML = qr_info;
|
||||
} else {
|
||||
$('#broadcast_tools_room_info_roomname').text(event_info['name']);
|
||||
$('#broadcast_tools_room_info_title').text(room_name);
|
||||
$('#broadcast_tools_room_info_qr').text('');
|
||||
roomname.innerHTML = event_info['name'];
|
||||
title.innerHTML = room_name;
|
||||
qr.innerHTML = '';
|
||||
|
||||
if (next_talk && event_info['room-info']['show_next_talk']) {
|
||||
$('#broadcast_tools_room_info_speaker').text(format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title']);
|
||||
speaker.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
|
||||
} else {
|
||||
$('#broadcast_tools_room_info_speaker').text('');
|
||||
speaker.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (current_talk && current_talk['track']) {
|
||||
$('#broadcast_tools_room_info').css('background-color', current_talk['track']['color']);
|
||||
box.style.backgroundColor = current_talk['track']['color'];
|
||||
} else if (next_talk && next_talk['track'] && event_info['room-info']['show_next_talk']) {
|
||||
box.style.backgroundColor = next_talk['track']['color'];
|
||||
} else {
|
||||
$('#broadcast_tools_room_info').css('background-color', event_info['color']);
|
||||
box.style.backgroundColor = event_info['color'];
|
||||
}
|
||||
}
|
||||
window.setInterval(update_room_info, 1000);
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
function _left_zero_pad(i) {
|
||||
if (i < 10) {
|
||||
i = "0" + i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function update_room_info() {
|
||||
room_name = get_room_name();
|
||||
|
||||
if (!event_info) {
|
||||
console.warn("Waiting for event info ...");
|
||||
return
|
||||
}
|
||||
|
||||
box = document.getElementById('broadcast_tools_room_timer');
|
||||
header = document.getElementById('broadcast_tools_room_timer_header');
|
||||
title = document.getElementById('broadcast_tools_room_timer_title');
|
||||
speaker = document.getElementById('broadcast_tools_room_timer_speaker');
|
||||
scheduledata = document.getElementById('broadcast_tools_room_timer_scheduledata');
|
||||
timeleft = document.getElementById('broadcast_tools_room_timer_timeleft_timer');
|
||||
timehint = document.getElementById('broadcast_tools_room_timer_timeleft_hint');
|
||||
progressbar= document.getElementById('broadcast_tools_room_timer_progressbar');
|
||||
progressbar_bar = document.getElementById('broadcast_tools_room_timer_progressbar_bar');
|
||||
|
||||
box.style.backgroundColor = event_info['color'];
|
||||
|
||||
if (!schedule) {
|
||||
speaker.innerHTML = 'Waiting for schedule ...';
|
||||
return
|
||||
}
|
||||
|
||||
if ('error' in schedule) {
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = schedule['error'].join('<br>');
|
||||
return
|
||||
}
|
||||
|
||||
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
|
||||
title.innerHTML = 'Error';
|
||||
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
|
||||
return
|
||||
}
|
||||
|
||||
current_talk = get_current_talk(60);
|
||||
next_talk = get_next_talk();
|
||||
now = new Date();
|
||||
|
||||
if (current_talk) {
|
||||
title.innerHTML = current_talk['title'];
|
||||
speaker.innerHTML = current_talk['persons'].join(', ');
|
||||
scheduledata.innerHTML = format_time_from_pretalx(current_talk['start']);
|
||||
scheduledata.innerHTML += ' - ';
|
||||
scheduledata.innerHTML += format_time_from_pretalx(current_talk['end']);
|
||||
|
||||
scheduled_start = new Date(current_talk['start']);
|
||||
scheduled_end = new Date(current_talk['end']);
|
||||
|
||||
if (scheduled_start > now) {
|
||||
timeleft.innerHTML = '';
|
||||
progressbar_bar.style.width = '0';
|
||||
timehint.innerHTML = '';
|
||||
} else if (scheduled_end < now) {
|
||||
timeleft.innerHTML = '0:00';
|
||||
progressbar_bar.style.width = '100vw';
|
||||
timehint.innerHTML = 'talk has ended';
|
||||
} else {
|
||||
diff = scheduled_end - now;
|
||||
let diff_s = Math.floor(diff / 1000) % 60;
|
||||
let diff_m = Math.floor(diff / 1000 / 60) % 60;
|
||||
let diff_h = Math.floor(diff / 1000 / 60 / 60);
|
||||
|
||||
if (diff_h > 0) {
|
||||
timeleft.innerHTML = diff_h + ":" + _left_zero_pad(diff_m) + ":" + _left_zero_pad(diff_s);
|
||||
} else {
|
||||
timeleft.innerHTML = diff_m + ":" + _left_zero_pad(diff_s);
|
||||
}
|
||||
|
||||
total_time = scheduled_end - scheduled_start;
|
||||
progressbar_bar.style.width = (((diff/total_time)*100)-100)*-1 + 'vw';
|
||||
timehint.innerHTML = 'left in this talk';
|
||||
}
|
||||
|
||||
if (current_talk['track']) {
|
||||
header.style.backgroundColor = current_talk['track']['color'];
|
||||
progressbar.style.borderTop = '2px solid ' + current_talk['track']['color'];
|
||||
progressbar_bar.style.backgroundColor = current_talk['track']['color'];
|
||||
} else {
|
||||
header.style.backgroundColor = null;
|
||||
progressbar.style.borderTop = '2px solid white';
|
||||
progressbar_bar.style.backgroundColor = 'white';
|
||||
}
|
||||
} else {
|
||||
progressbar.style.borderTop = 'none';
|
||||
progressbar_bar.style.width = '0';
|
||||
speaker.innerHTML = 'Break';
|
||||
timehint.innerHTML = '';
|
||||
title.innerHTML = room_name;
|
||||
|
||||
timeleft.innerHTML = _left_zero_pad(now.getHours()) + ":" + _left_zero_pad(now.getMinutes()) + ":" + _left_zero_pad(now.getSeconds());
|
||||
|
||||
if (next_talk) {
|
||||
scheduledata.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
|
||||
|
||||
if (next_talk['track']) {
|
||||
header.style.backgroundColor = next_talk['track']['color'];
|
||||
} else {
|
||||
header.style.backgroundColor = null;
|
||||
}
|
||||
} else {
|
||||
scheduledata.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
window.setInterval(update_room_info, 1000);
|
78
pretalx_broadcast_tools/tasks.py
Normal file
78
pretalx_broadcast_tools/tasks.py
Normal 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
|
||||
)
|
|
@ -6,9 +6,6 @@
|
|||
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>{{ request.event.name }} lower thirds</title>
|
||||
{% compress js %}
|
||||
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
|
||||
{% endcompress %}
|
||||
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||
<script src="{% static "pretalx_broadcast_tools/lower_thirds.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||
|
|
|
@ -1,82 +1,78 @@
|
|||
{% extends "orga/base.html" %}
|
||||
{% load bootstrap4 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if localized_rooms %}
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{% trans "room list" %}</th>
|
||||
<th scope="col" colspan="2">Feature</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for room in localized_rooms %}
|
||||
<tr>
|
||||
<th scope="row">{{ room }}</th>
|
||||
<td><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room }}">Lower Thirds</a></td>
|
||||
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_info' request.event.slug %}#{{ room }}">Room Info</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<h2>{% translate "broadcasting tools" %}</h2>
|
||||
|
||||
<p>
|
||||
pretalx will automatically replace some placeholders in your custom
|
||||
content:
|
||||
</p>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{% translate "room" %}</th>
|
||||
<th scope="col" colspan="3">{% translate "Feature" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for room in request.event.rooms.all %}
|
||||
<tr>
|
||||
<th scope="row">{{ room.name }}</th>
|
||||
<td><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.uuid }}">{% translate "Lower Thirds" %}</a></td>
|
||||
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_info' request.event.slug %}#{{ room.uuid }}">{% translate "Room Info" %}</a></td>
|
||||
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_timer' request.event.slug %}#{{ room.uuid }}">{% translate "Room Timer" %}</a></td>
|
||||
</tr>
|
||||
{% 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>
|
||||
<dl>
|
||||
<dt><code>{CODE}</code></dt>
|
||||
<dd>talk code (<code>MUX9U3</code> for example) - most useful in combination with pretalx-proposal-redirects or something like that</dd>
|
||||
<dd>{% translate "talk code (<code>MUX9U3</code> for example) - most useful in combination with pretalx-proposal-redirects or something like that" %}</dd>
|
||||
|
||||
<dt><code>{EVENT_SLUG}</code></dt>
|
||||
<dd>The event slug (<code>{{ request.event.slug }}</code>)</dd>
|
||||
<dd>{% translate "The event slug" %} (<code>{{ request.event.slug }}</code>)</dd>
|
||||
|
||||
<dt><code>{FEEDBACK_URL}</code></dt>
|
||||
<dd>URL to the talk feedback page.</dd>
|
||||
<dd>{% translate "URL to the talk feedback page." %}</dd>
|
||||
|
||||
<dt><code>{TALK_SLUG}</code></dt>
|
||||
<dd>The talk slug (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
|
||||
<dd>{% translate "The talk slug" %} (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
|
||||
|
||||
<dt><code>{TALK_URL}</code></dt>
|
||||
<dd>URL to the talk detail page.</dd>
|
||||
<dd>{% translate "URL to the talk detail page." %}</dd>
|
||||
|
||||
<dt><code>{TRACK_NAME}</code> or <code>{TRACK_NAME_COLOURED}</code></dt>
|
||||
<dd>Track name in plain text or coloured using the track colour.</dd>
|
||||
<dt><code>{TRACK_NAME}</code> {% translate "or" %} <code>{TRACK_NAME_COLOURED}</code></dt>
|
||||
<dd>{% translate "Track name in plain text or coloured using the track colour." %}</dd>
|
||||
</dl>
|
||||
|
||||
<h2>{% translate "Settings" %}</h2>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{% translate "Lower thirds" %}
|
||||
{% translate "Lower Thirds" %}
|
||||
</legend>
|
||||
{% bootstrap_field form.broadcast_tools_lower_thirds_no_talk_info layout='event' %}
|
||||
{% bootstrap_field form.broadcast_tools_lower_thirds_info_string layout='event' %}
|
||||
<p>
|
||||
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_no_talk_info.as_field_group }}
|
||||
{{ form.broadcast_tools_lower_thirds_info_string.as_field_group }}
|
||||
{{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{% translate "Room info" %}
|
||||
{% translate "Room Info" %}
|
||||
</legend>
|
||||
{% bootstrap_field form.broadcast_tools_room_info_lower_content layout='event' %}
|
||||
{% bootstrap_field form.broadcast_tools_room_info_show_next_talk layout='event' %}
|
||||
{{ form.broadcast_tools_room_info_lower_content.as_field_group }}
|
||||
{{ form.broadcast_tools_room_info_show_next_talk.as_field_group }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{% translate "PDF export" %}
|
||||
{% translate "PDF Export" %}
|
||||
</legend>
|
||||
{% bootstrap_field form.broadcast_tools_pdf_show_internal_notes layout='event' %}
|
||||
{% bootstrap_field form.broadcast_tools_pdf_ignore_do_not_record layout='event' %}
|
||||
{% bootstrap_field form.broadcast_tools_pdf_questions_to_include layout='event' %}
|
||||
{% bootstrap_field form.broadcast_tools_pdf_additional_content layout='event' %}
|
||||
{{ form.broadcast_tools_pdf_show_internal_notes.as_field_group }}
|
||||
{{ form.broadcast_tools_pdf_ignore_do_not_record.as_field_group }}
|
||||
{{ form.broadcast_tools_pdf_questions_to_include.as_field_group }}
|
||||
{{ form.broadcast_tools_pdf_additional_content.as_field_group }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div class="submit-group panel">
|
||||
|
@ -87,7 +83,7 @@
|
|||
name="action" value="save"
|
||||
>
|
||||
<i class="fa fa-check"></i>
|
||||
{% trans "Save" %}
|
||||
{% translate "Save" %}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>{{ request.event.name }} room info</title>
|
||||
{% compress js %}
|
||||
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
|
||||
{% endcompress %}
|
||||
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||
<script src="{% static "pretalx_broadcast_tools/room_info.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{% load static %}
|
||||
{% load compress %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>{{ request.event.name }} room timer</title>
|
||||
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
|
||||
<script src="{% static "pretalx_broadcast_tools/room_timer.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
|
||||
{% if request.event and request.event.custom_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.event.custom_css.url }}"/>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body id="broadcast_tools_room_timer">
|
||||
<div id="broadcast_tools_room_timer_header">
|
||||
<h2 id="broadcast_tools_room_timer_title">Loading ...</h2>
|
||||
<h3 id="broadcast_tools_room_timer_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</h3>
|
||||
<p id="broadcast_tools_room_timer_scheduledata"></p>
|
||||
</div>
|
||||
<div id="broadcast_tools_room_timer_timeleft">
|
||||
<p id="broadcast_tools_room_timer_timeleft_timer"></p>
|
||||
<p id="broadcast_tools_room_timer_timeleft_hint"></p>
|
||||
</div>
|
||||
<div id="broadcast_tools_room_timer_progressbar">
|
||||
<p id="broadcast_tools_room_timer_progressbar_bar"> </p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,45 +1,66 @@
|
|||
from django.urls import re_path
|
||||
from pretalx.event.models.event import SLUG_REGEX
|
||||
from django.urls import include, path
|
||||
|
||||
from .views.event_info import BroadcastToolsEventInfoView
|
||||
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.static_html import (
|
||||
BroadcastToolsLowerThirdsView,
|
||||
BroadcastToolsRoomInfoView,
|
||||
BroadcastToolsRoomTimerView,
|
||||
)
|
||||
from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
rf"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/event.json$",
|
||||
BroadcastToolsEventInfoView.as_view(),
|
||||
name="event_info",
|
||||
path(
|
||||
"<slug:event>/p/broadcast-tools/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"event.json",
|
||||
BroadcastToolsEventInfoView.as_view(),
|
||||
name="event_info",
|
||||
),
|
||||
path(
|
||||
"schedule.json",
|
||||
BroadcastToolsScheduleView.as_view(),
|
||||
name="schedule",
|
||||
),
|
||||
path(
|
||||
"lower-thirds/",
|
||||
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(),
|
||||
name="feedback_qr_id",
|
||||
),
|
||||
path(
|
||||
"public-qr/<talk>.svg",
|
||||
BroadcastToolsPublicQrCodeSvg.as_view(),
|
||||
name="public_qr_id",
|
||||
),
|
||||
path(
|
||||
"room-info/",
|
||||
BroadcastToolsRoomInfoView.as_view(),
|
||||
name="room_info",
|
||||
),
|
||||
path(
|
||||
"room-timer/",
|
||||
BroadcastToolsRoomTimerView.as_view(),
|
||||
name="room_timer",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/schedule.json$",
|
||||
BroadcastToolsScheduleView.as_view(),
|
||||
name="schedule",
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/lower-thirds/$",
|
||||
BroadcastToolsLowerThirdsView.as_view(),
|
||||
name="lowerthirds",
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/feedback-qr/(?P<talk>[0-9]+).svg$",
|
||||
BroadcastToolsFeedbackQrCodeSvg.as_view(),
|
||||
name="feedback_qr_id",
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/public-qr/(?P<talk>[0-9]+).svg$",
|
||||
BroadcastToolsPublicQrCodeSvg.as_view(),
|
||||
name="public_qr_id",
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/room-info/$",
|
||||
BroadcastToolsRoomInfoView.as_view(),
|
||||
name="room_info",
|
||||
),
|
||||
re_path(
|
||||
f"^orga/event/(?P<event>{SLUG_REGEX})/settings/p/broadcast-tools/$",
|
||||
path(
|
||||
"orga/event/<slug:event>/settings/p/broadcast-tools/",
|
||||
BroadcastToolsOrgaView.as_view(),
|
||||
name="orga",
|
||||
),
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
def placeholders(schedule, talk, supports_html_colour=False):
|
||||
def placeholders(event, talk, supports_html_colour=False):
|
||||
track_name = str(talk.submission.track.name) if talk.submission.track else ""
|
||||
|
||||
result = {
|
||||
"CODE": talk.submission.code,
|
||||
"EVENT_SLUG": str(schedule.event.slug),
|
||||
"EVENT_SLUG": str(event.slug),
|
||||
"FEEDBACK_URL": "{}{}".format(
|
||||
schedule.event.custom_domain or settings.SITE_URL,
|
||||
event.custom_domain or settings.SITE_URL,
|
||||
talk.submission.urls.feedback,
|
||||
),
|
||||
"TALK_SLUG": talk.frab_slug,
|
||||
"TALK_URL": "{}{}".format(
|
||||
schedule.event.custom_domain or settings.SITE_URL,
|
||||
event.custom_domain or settings.SITE_URL,
|
||||
talk.submission.urls.public,
|
||||
),
|
||||
"TRACK_NAME": track_name,
|
||||
|
|
|
@ -21,6 +21,10 @@ class BroadcastToolsEventInfoView(View):
|
|||
else False
|
||||
),
|
||||
},
|
||||
"rooms": {
|
||||
str(room.uuid): room.name.localize(self.request.event.locale)
|
||||
for room in self.request.event.rooms.all()
|
||||
},
|
||||
"slug": self.request.event.slug,
|
||||
"start": self.request.event.date_from.isoformat(),
|
||||
"end": self.request.event.date_to.isoformat(),
|
||||
|
|
|
@ -12,14 +12,6 @@ class BroadcastToolsOrgaView(PermissionRequired, FormView):
|
|||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["localized_rooms"] = [
|
||||
room.name.localize(self.request.event.locale)
|
||||
for room in self.request.event.rooms.all()
|
||||
]
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
return super().form_valid(form)
|
||||
|
|
|
@ -64,7 +64,7 @@ class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, View):
|
|||
"room": room["name"].localize(schedule.event.locale),
|
||||
"infoline": infoline.format(
|
||||
**placeholders(
|
||||
schedule, talk, supports_html_colour=True
|
||||
schedule.event, talk, supports_html_colour=True
|
||||
)
|
||||
),
|
||||
"image_url": talk.submission.image_url,
|
||||
|
|
|
@ -7,3 +7,7 @@ class BroadcastToolsLowerThirdsView(TemplateView):
|
|||
|
||||
class BroadcastToolsRoomInfoView(TemplateView):
|
||||
template_name = "pretalx_broadcast_tools/room_info.html"
|
||||
|
||||
|
||||
class BroadcastToolsRoomTimerView(TemplateView):
|
||||
template_name = "pretalx_broadcast_tools/room_timer.html"
|
||||
|
|
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal file
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal 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
|
Loading…
Add table
Reference in a new issue