SReview-0.8.0/ 0000755 0001750 0001750 00000000000 14116343665 012550 5 ustar wouter wouter SReview-0.8.0/COPYING 0000644 0001750 0001750 00000103330 13401522031 013561 0 ustar wouter wouter GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
SReview-0.8.0/META.yml 0000644 0001750 0001750 00000001631 14116343665 014022 0 ustar wouter wouter ---
abstract: 'a video review and transcoding system'
author:
- 'Wouter Verhelst '
build_requires:
ExtUtils::MakeMaker: '0'
Test::More: '0'
YAML::XS: '0'
configure_requires:
ExtUtils::Depends: '0'
dynamic_config: 1
generated_by: 'ExtUtils::MakeMaker version 7.44, CPAN::Meta::Converter version 2.150010'
license: unknown
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: '1.4'
name: SReview
no_index:
directory:
- t
- inc
requires:
Class::Type::Enum: '0'
DateTime: '0'
DateTime::Format::ISO8601: '0'
DateTime::Format::Pg: '0'
Mojo::JSON: '0'
Mojo::Pg: '0'
Mojolicious::Plugin::OpenAPI: '4.03'
Moose: '0'
Net::Amazon::S3: '0'
Net::SSH::AuthorizedKeysFile: '0'
resources:
homepage: https://yoe.github.io/SReview
repository: https://github.com/yoe/SReview.git
version: v0.8.0
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
SReview-0.8.0/MYMETA.json 0000644 0001750 0001750 00000003155 14116343575 014443 0 ustar wouter wouter {
"abstract" : "a video review and transcoding system",
"author" : [
"Wouter Verhelst "
],
"dynamic_config" : 0,
"generated_by" : "ExtUtils::MakeMaker version 7.44, CPAN::Meta::Converter version 2.150010",
"license" : [
"unknown"
],
"meta-spec" : {
"url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
"version" : 2
},
"name" : "SReview",
"no_index" : {
"directory" : [
"t",
"inc"
]
},
"prereqs" : {
"build" : {
"requires" : {
"ExtUtils::MakeMaker" : "0"
}
},
"configure" : {
"requires" : {
"ExtUtils::Depends" : "0"
}
},
"runtime" : {
"requires" : {
"Class::Type::Enum" : "0",
"DateTime" : "0",
"DateTime::Format::ISO8601" : "0",
"DateTime::Format::Pg" : "0",
"Mojo::JSON" : "0",
"Mojo::Pg" : "0",
"Mojolicious::Plugin::OpenAPI" : "4.03",
"Moose" : "0",
"Net::Amazon::S3" : "0",
"Net::SSH::AuthorizedKeysFile" : "0"
}
},
"test" : {
"requires" : {
"Test::More" : "0",
"YAML::XS" : "0"
}
}
},
"release_status" : "stable",
"resources" : {
"homepage" : "https://yoe.github.io/SReview",
"repository" : {
"type" : "git",
"url" : "https://github.com/yoe/SReview.git",
"web" : "https://github.com/yoe/SReview"
}
},
"version" : "v0.8.0",
"x_serialization_backend" : "JSON::PP version 4.04"
}
SReview-0.8.0/MANIFEST.SKIP 0000644 0001750 0001750 00000000146 14054727355 014452 0 ustar wouter wouter .ci/*
.git/*
.travis.yml
debian/*
docs/*
helm/*
dockerfiles/*
javascript-test/*
MANIFEST.bak
Makefile
SReview-0.8.0/lib/ 0000755 0001750 0001750 00000000000 14116343665 013316 5 ustar wouter wouter SReview-0.8.0/lib/SReview/ 0000755 0001750 0001750 00000000000 14116343665 014702 5 ustar wouter wouter SReview-0.8.0/lib/SReview/CodecMap.pm 0000644 0001750 0001750 00000001106 13401522031 016667 0 ustar wouter wouter package SReview::CodecMap;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT_OK=qw/detect_to_write/;
my %writemap = (
'vorbis' => 'libvorbis',
'vp8' => 'libvpx',
'vp9' => 'libvpx-vp9',
'h264' => 'libx264',
'hevc' => 'libx265',
'opus' => 'libopus',
);
open CHECK_FDK, "ffmpeg -hide_banner -h encoder=libfdk_aac|";
if( !~ /is not recognized/) {
$writemap{aac} = 'libfdk_aac';
}
close CHECK_FDK;
sub detect_to_write($) {
my $detected = shift;
if(exists($writemap{$detected})) {
return $writemap{$detected};
} else {
return $detected;
}
}
1;
SReview-0.8.0/lib/SReview/Video/ 0000755 0001750 0001750 00000000000 14116343665 015750 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Video/PNGGen.pm 0000644 0001750 0001750 00000000744 13401522031 017347 0 ustar wouter wouter package SReview::Video::PNGGen;
use Moose;
use Carp;
extends 'SReview::Video';
sub readopts {
my $self = shift;
my $output = shift;
if(defined($output->video_size) && ($self->video_size ne $output->video_size)) {
carp "Video resolution does not match image resolution. Will scale, but the result may be suboptimal...";
}
return ('-loop', '1', '-framerate', $output->video_framerate, '-i', $self->url, '-f', 'lavfi', '-i', 'anullsrc=channel_layout=mono');
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Video/ProfileFactory.pm 0000644 0001750 0001750 00000016170 13610061736 021235 0 ustar wouter wouter =head1 NAME
SReview::Video::ProfileFactory - Create an output profile from an input video.
=head1 SYNOPSIS
use SReview::Video;
use SReview::Videopipe;
use SReview::Video::ProfileFactory;
package SReview::Video::Profile::myprofile;
use Moose;
extends SReview::Video::Profile::webm;
has '+audio_samplerate' => (
builder => '_probe_my_audiorate',
);
has '+audio_codec' => (
default => 'vorbis',
);
sub _probe_exten {
return 'my.webm',
}
sub _probe_my_audiorate {
my $self = shift;
return $self->reference->audio_samplerate / 2;
}
no Moose;
package main;
my $input = SReview::Video->new(url => "foo.mp4");
my $profile = SReview::Video::ProfileFactory->create("myprofile", $input);
my $output = SReview::Video->new(url => "foo." . $profile->exten, reference => $profile);
SReview::Videopipe->new(inputs => [$input], output => $output)->run();
=head1 DESCRIPTION
C is a subclass of SReview::Video, but with
a number of the probing methods overridden so that they return values
that are not in line with the reference of the given video.
The C's C method is a simple
helper to:
=over
=item *
ensure that the relevant C> module
has been loaded
=item *
create an C subclass of the right type, with reference
set to the passed input C object.
=back
=head1 CREATING NEW PROFILES
It is possible to create a new profile by extending an existing one. The
C profile in the above example shows how to do so. Any
property that is known by L can be overridden in the
manner given.
To create a new profile, one can use the C configuration
setting; however, profiles created in this manner can only hardcode
values, and cannot vary any parameters based on the input file. To
create a profile that can do so, when the new profile just changes a
minor detail of an existing profile, extend that profile and change the
detail which you want to change. To create a new profile from scratch,
extend the C profile (see below).
=head1 PRE-EXISTING PROFILES
The following profiles are defined by C:
=cut
package SReview::Video::Profile::Base;
=head2 Base
This profile serves as a base class for the other profiles. It should
not be used directly.
It adds the extension, and defaults the pixel format to yuv420p.
=cut
use Moose;
extends 'SReview::Video';
has '+reference' => (
required => 1,
);
has 'exten' => (
lazy => 1,
is => 'ro',
builder => '_probe_exten',
);
has '+pix_fmt' => (
builder => '_build_pixfmt',
);
sub _build_pixfmt {
return 'yuv420p';
}
sub _probe_exten {
return 'IEK - extension not defined';
}
package SReview::Video::Profile::vp9;
=head2 vp9
Produces a video in WebM/VP9 format, using the quality/bitrate settings
recommended by Google on L,
and with OPUS audio. Produces files with the C extension.
Audio settings are hardcoded to 48KHz sampling rate, 128k bits per
second.
=cut
use Moose;
extends 'SReview::Video::Profile::Base';
sub _probe_exten {
return 'vp9.webm'
}
my %rates_30 = (
240 => 150,
360 => 276,
480 => 750,
720 => 1024,
1080 => 1800,
1440 => 6000,
2160 => 12000
);
my %rates_50 = (
240 => 150,
360 => 276,
480 => 750,
720 => 1800,
1080 => 3000,
1440 => 9000,
2160 => 18000
);
my %quals = (
240 => 37,
360 => 36,
480 => 33,
720 => 32,
1080 => 31,
1440 => 24,
2160 => 15,
);
sub _probe_videobitrate {
my $self = shift;
if(eval($self->video_framerate) > 30) {
return $rates_50{$self->video_height};
} else {
return $rates_30{$self->video_height};
}
}
sub _probe_audiorate {
return "48000";
}
sub _probe_audiobitrate {
return "128k";
}
sub _probe_quality {
my $self = shift;
return $quals{$self->video_height};
}
sub speed {
my $self = shift;
if($self->reference->has_pass) {
if($self->reference->pass == 1 || $self->video_height < 720) {
return 4;
}
return 2;
}
return 4;
}
sub _probe_videocodec {
return "vp9";
}
sub _probe_audiocodec {
return "opus";
}
no Moose;
package SReview::Video::Profile::vp8;
=head2 vp8
Produces a video in WebM/VP8 format. Since no similar recommendations
for VP8 exist as do for VP9, no explicit quality or bitrate settings are
configured in this profile. The libvpx video codec is selected, and the
libvorbis one for audio.
The audio bitrate is explicitly left to ffmpeg defaults; the extension
is set to C
=cut
use Moose;
extends 'SReview::Video::Profile::Base';
sub _probe_exten {
return 'vp8.webm';
}
sub _probe_videocodec {
return "vp8";
}
sub _probe_audiocodec {
return "vorbis";
}
sub _probe_audiobitrate {
return undef;
}
no Moose;
package SReview::Video::Profile::webm;
=head2 webm
This profile subclasses from the C profile, and only changes the
extension to plain C instead of C.
Additionally, if a future version of WebM is ever defined, then when
SReview gains support for that version of WebM, this class will become a
subclass of that class instead.
=cut
use Moose;
extends 'SReview::Video::Profile::vp9';
sub _probe_exten {
return 'webm',
}
no Moose;
package SReview::Video::Profile::vp8_lq;
=head2 vp8_lq
This profile subclasses from the C profile. The extension is set to
C. In addition to the changes made by the C profile, this
profile also rescales the video to a fraction of the original; that is,
the height and width of the video are both divided by 8.
=cut
use Moose;
extends 'SReview::Video::Profile::vp8';
sub _probe_exten {
return 'lq.webm',
}
sub _probe_height {
my $self = shift;
return undef unless defined ($self->reference->video_height);
return int($self->reference->video_height / 4);
}
sub _probe_width {
my $self = shift;
return undef unless defined ($self->reference->video_width);
return int($self->reference->video_width / 4);
}
sub _probe_videosize {
my $self = shift;
my $width = $self->video_width;
my $height = $self->video_height;
return undef unless defined($width) && defined($height);
return undef unless $width && $height;
return $self->video_width . "x" . $self->video_height;
}
no Moose;
package SReview::Video::ProfileFactory;
use SReview::Config::Common;
sub create {
my $class = shift;
my $profile = shift;
my $ref = shift;
my $config = shift;
my $profiles = SReview::Config::Common::setup()->get('extra_profiles');
if(!exists($profiles->{$profile})) {
eval "require SReview::Video::Profile::$profile;";
return "SReview::Video::Profile::$profile"->new(url => '', reference => $ref);
} else {
my $parent = $profiles->{$profile}{parent};
eval "require SReview::Video::Profile::$parent;";
my $rv = "SReview::Video::Profile::$parent"->new(url => '', reference => $ref);
foreach my $param(keys %{$profiles->{$profile}{settings}}) {
next if($param eq 'parent');
$rv->meta->find_attribute_by_name($param)->set_value($rv, $profiles->{$profile}{settings}{$param});
}
return $rv;
}
die "Unknown profile $profile requested!";
}
1;
=head1 SEE ALSO
L
SReview-0.8.0/lib/SReview/Video/Concat.pm 0000644 0001750 0001750 00000002105 13401522031 017471 0 ustar wouter wouter package SReview::Video::Concat;
use Moose;
extends 'SReview::Video';
has 'components' => (
traits => ['Array'],
isa => 'ArrayRef[SReview::Video]',
required => 1,
is => 'rw',
handles => {
add_component => 'push',
},
);
has '+duration' => (
builder => '_build_duration',
);
sub readopts {
my $self = shift;
if(($self->has_pass && $self->pass < 2) || !$self->has_pass) {
die "refusing to overwrite file " . $self->url . "!\n" if (-f $self->url);
my $content = "ffconcat version 1.0\n\n";
foreach my $component(@{$self->components}) {
my $input = $component->url;
$content .= "file '$input'\n";
}
print "Writing " . $self->url . " with content:\n$content\n";
open CONCAT, ">" . $self->url;
print CONCAT $content;
close CONCAT;
}
return ('-f', 'concat', '-safe', '0', $self->SReview::Video::readopts());
}
sub _build_duration {
my $self = shift;
my $rv = 0;
foreach my $component(@{$self->components}) {
$rv += $component->duration;
}
return $rv;
}
sub _probe {
my $self = shift;
return $self->components->[0]->_get_probedata;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Video/Profile/ 0000755 0001750 0001750 00000000000 14116343665 017350 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Video/Profile/mp4.pm 0000644 0001750 0001750 00000000376 13516623643 020414 0 ustar wouter wouter use SReview::Video::ProfileFactory;
package SReview::Video::Profile::mp4;
use Moose;
extends 'SReview::Video::Profile::Base';
sub _probe_exten {
return 'mp4';
}
sub _probe_videocodec {
return "h264";
}
sub _probe_audiocodec {
return "aac";
}
1;
SReview-0.8.0/lib/SReview/Video/Profile/FOSDEM.pm 0000644 0001750 0001750 00000000504 13401522031 020640 0 ustar wouter wouter use SReview::Video::ProfileFactory;
package SReview::Video::Profile::FOSDEM;
use Moose;
extends 'SReview::Video::Profile::mp4';
sub _probe_extra_params {
return { "g" => "45",
"profile" => "main",
"preset" => "veryfast" };
}
sub _probe_videobitrate {
return "512";
}
sub speed {
return undef;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Video/Profile/copy.pm 0000644 0001750 0001750 00000001160 13401522031 020634 0 ustar wouter wouter package SReview::Video::Profile::copy;
use SReview::Video::ProfileFactory;
use SReview::CodecMap qw/detect_to_write/;
use Moose;
extends 'SReview::Video::Profile::Base';
sub _probe_exten {
my $self = shift;
my $ref = $self->reference;
my $vid = $ref->video_codec;
my $aud = $ref->audio_codec;
if (($vid eq 'vp9' && $aud eq 'opus')
or ($vid eq 'vp8' && $aud eq 'vorbis')) {
return 'webm';
}
if ($vid eq 'h264' && $aud eq 'aac') {
return 'mp4';
}
die "unknown video format; can't do copy profile";
}
sub _probe_videocodec {
my $self = shift;
return "copy";
}
sub _probe_audiocodec {
return "copy";
}
SReview-0.8.0/lib/SReview/Video/Profile/DebConf.pm 0000644 0001750 0001750 00000000333 13401522031 021163 0 ustar wouter wouter use SReview::Video::ProfileFactory;
package SReview::Video::Profile::DebConf;
use Moose;
extends 'SReview::Video::Profile::mpeg2';
sub _probe_videobitrate {
return "1800";
}
sub speed {
return 4;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Video/Profile/mpeg2.pm 0000644 0001750 0001750 00000000406 13401522031 020676 0 ustar wouter wouter use SReview::Video::ProfileFactory;
package SReview::Video::Profile::mpeg2;
use Moose;
extends 'SReview::Video::Profile::Base';
sub _probe_exten {
return 'mpg';
}
sub _probe_videocodec {
return "mpeg2video";
}
sub _probe_audiocodec {
return "mp2";
}
1;
SReview-0.8.0/lib/SReview/Video/NGinX.pm 0000644 0001750 0001750 00000001705 13401522031 017252 0 ustar wouter wouter package SReview::Video::NGinX;
use Moose;
use WWW::Curl::Easy;
extends 'SReview::Video';
has 'workfile' => (
required => 1,
is => 'rw',
isa => 'Str',
);
has 'origurl' => (
is => 'rw',
);
sub readopts {
my $self = shift;
my $output = shift;
my $curl = WWW::Curl::Easy->new;
my @opts;
open OUTPUT, ">" . $self->workfile;
$curl->setop(CURLOPT_WRITEDATA, \*OUTPUT);
my $url = $self->url;
my $start = 0;
if ($self->has_fragment_start) {
push @opts, 'start=' . $self->fragment_start;
$start = $self->fragment_start;
}
if ($self->has_duration) {
push @opts, 'end=' . ($start + $self->duration);
}
$url .= '?' . join('&', @opts);
$curl->setopt(CURLOPT_URL, $url);
my $res = $curl->perform;
if($res != 0) {
die "Received HTTP error code $res: " . $curl->strderror($res) . " " . $curl->errbuf;
}
close OUTPUT;
$self->origurl($self->url);
$self->url = $workfile;
return $self->SReview::Video::readopts($self, $output);
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Schedule/ 0000755 0001750 0001750 00000000000 14116343665 016436 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Schedule/Wafer.pm 0000644 0001750 0001750 00000003304 14111430524 020022 0 ustar wouter wouter package SReview::Schedule::Wafer::Talk;
use Moose;
use Mojo::Util 'slugify';
use DateTime::Format::ISO8601;
extends 'SReview::Schedule::Penta::Talk';
sub _load_upstreamid {
return shift->schedref->attribute('guid');
}
sub _load_slug {
return shift->xml_helper('slug');
}
sub _load_speakers {
my $self = shift;
return [] if (grep(/^persons$/, $self->schedref->children_names) == 0);
return $self->SUPER::_load_speakers;
}
sub _load_filtered {
return 0 if defined(shift->xml_helper('type'));
return 1;
}
sub _load_starttime {
return DateTime::Format::ISO8601->parse_datetime(shift->xml_helper('date'));
}
no Moose;
package SReview::Schedule::Wafer;
use Moose;
use SReview::Schedule::Penta;
extends 'SReview::Schedule::Penta';
=head1 NAME
SReview::Schedule::Wafer - sreview-import schedule parser for the Pentabarf XML format as created by the Wafer conference management system.
=head1 DESCRIPTION
The Wafer conference management system has the ability to create an XML
version of its schedule that is compatible with the Pentabarf XML
format. However, it is mildly different in the way it creates it, most
significantly in the way it creates unique IDs. As such, importing such
a schedule with the L parser will fail to
create a stable schedule in SReview.
This parser uses the L parser with the minimal
required changes to make it work with the Wafer schedule parser.
=head1 OPTIONS
C only supports one option:
=head2 url
The URL where the schedule can be found.
=head1 SEE ALSO
L, L
=cut
sub _load_talktype {
return 'SReview::Schedule::Wafer::Talk';
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Schedule/Multi.pm 0000644 0001750 0001750 00000007360 14054727355 020077 0 ustar wouter wouter package SReview::Schedule::Multi::ShadowTalk;
use Moose;
use SReview::Schedule::WithShadow;
extends 'SReview::Schedule::WithShadow::ShadowedTalk';
has 'prefix' => (
is => 'ro',
isa => 'Str',
default => '',
);
has 'suffix' => (
is => 'ro',
isa => 'Str',
default => '',
);
sub _load_title {
my $self = shift;
return $self->prefix . $self->shadow->title . $self->suffix;
}
no Moose;
package SReview::Schedule::Multi::ShadowEvent;
use Moose;
use SReview::Schedule::WithShadow;
extends 'SReview::Schedule::WithShadow::ShadowedEvent';
has 'talk_prefix' => (
is => 'ro',
isa => 'Str',
);
has 'talk_suffix' => (
is => 'ro',
isa => 'Str',
);
has 'event_prefix' => (
is => 'ro',
isa => 'Str',
default => '',
);
has 'event_suffix' => (
is => 'ro',
isa => 'Str',
default => '',
);
has 'talk_opts' => (
is => 'ro',
isa => 'HashRef',
default => sub { {} },
);
sub _load_talks {
my $self = shift;
my $rv = [];
my $opts = $self->talk_opts;
foreach my $talk(@{$self->shadow->talks}) {
push @$rv, SReview::Schedule::Multi::ShadowTalk->new(shadow => $talk, prefix => $self->talk_prefix, suffix => $self->talk_suffix, %$opts);
}
return $rv;
}
sub _load_name {
my $self = shift;
return $self->event_prefix . $self->shadow->name . $self->event_suffix;
}
package SReview::Schedule::Multi;
=head1 NAME
SReview::Schedule::Multi - system to duplicate event parsing into a main and a shadow one.
=head1 SYNOPSIS
$schedule_format = "multi";
$schedule_options = { url => "http://...", base_type => "penta", base_options => {},
shadows => [{ talk_prefix => "Video for talk '", talk_suffix => "'",
event_prefix => "Videos for event '", event_suffix => "'", talk_opts => {} }]};
=head1 DESCRIPTION
SReview::Schedule::Multi is a schedule parser for L that
creates "shadow" events based on a base event. This can be used in case
multiple events are required in SReview for an upstream event (e.g., one
for preprocessing, and one for postprocessing).
=head1 OPTIONS
SReview::Schedule::Multi takes the following options:
=head2 base_type
The type of the parser of the base event. Must be another
C parser. Required.
=head2 base_options
Any options, other than the C option, to be passed to the base
parser to configure it. Optional.
=head2 url
The URL of the schedule. Passed on, unmodified, to the base parser.
=head2 shadows
An array of hashes, one for each shadow event that is to be created.
Each hash can have the following options:
=head3 event_prefix
A string that will be prepended to the event's name.
=head3 event_suffix
A string that will be appended to the event's name.
=head3 talk_prefix
A string that will be prepended to each and every talk's title.
=head3 talk_suffix
A string that will be appended to each and every talk's title.
=head3 talk_opts
Extra properties to be sent to the SReview::Schedule::::Talk
object at creation time. This can be used to override certain properties
of the talk, e.g., the flags.
=head1 SEE ALSO
L, L
=cut
use Moose;
use SReview::Schedule::WithShadow;
extends 'SReview::Schedule::WithShadow';
has 'shadows' => (
is => 'ro',
isa => 'ArrayRef[HashRef[Any]]',
required => 1,
);
sub _load_events {
my $self = shift;
my $rv_type = "SReview::Schedule::" . ucfirst($self->base_type);
eval "require $rv_type;" or die $!;
my $opts = $self->base_options;
$opts = {} unless defined($opts);
$opts->{url} = $self->url;
my $base_parser = "$rv_type"->new(%$opts);
my $rv = [];
foreach my $event(@{$base_parser->events}) {
push @$rv, $event;
foreach my $shadow(@{$self->shadows}) {
push @$rv, SReview::Schedule::Multi::ShadowEvent->new(shadow => $event, %$shadow);
}
}
return $rv;
}
1;
SReview-0.8.0/lib/SReview/Schedule/WithShadow.pm 0000644 0001750 0001750 00000011425 14054727355 021063 0 ustar wouter wouter package SReview::Schedule::WithShadow::ShadowedSpeaker;
use Moose;
use SReview::Schedule::Base;
extends 'SReview::Schedule::Base::Speaker';
has 'shadow' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Speaker',
required => 1,
);
sub _load_name {
return shift->shadow->name;
}
sub _load_email {
return shift->shadow->email;
}
sub _load_upstreamid {
return shift->shadow->upstreamid;
}
no Moose;
package SReview::Schedule::WithShadow::ShadowedRoom;
use Moose;
extends 'SReview::Schedule::Base::Room';
has 'shadow' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Room',
required => 1,
);
sub _load_name {
return shift->shadow->name;
}
sub _load_altname {
return shift->shadow->altname;
}
sub _load_outputname {
return shift->shadow->outputname;
}
no Moose;
package SReview::Schedule::WithShadow::ShadowedTrack;
use Moose;
extends 'SReview::Schedule::Base::Track';
has 'shadow' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Track',
required => 1,
);
sub _load_name {
return shift->shadow->name;
}
sub _load_email {
return shift->shadow->email;
}
sub _load_upstreamid {
return shift->shadow->upstreamid;
}
no Moose;
package SReview::Schedule::WithShadow::ShadowedTalk;
use Moose;
use SReview::Schedule::Base;
extends 'SReview::Schedule::Base::Talk';
has 'shadow' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Talk',
required => 1,
);
has 'speaker_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_speaker_type',
);
sub _load_speaker_type {
return 'SReview::Schedule::WithShadow::ShadowedSpeaker';
}
has 'speaker_opts' => (
is => 'ro',
isa => 'HashRef[Any]',
default => sub { {} },
);
has 'track_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_track_type',
);
sub _load_track_type {
return 'SReview::Schedule::WithShadow::ShadowedTrack';
}
has 'track_opts' => (
is => 'ro',
isa => 'HashRef[Any]',
default => sub { {} },
);
has 'room_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_room_type',
);
sub _load_room_type {
return 'SReview::Schedule::WithShadow::ShadowedRoom';
}
has 'room_opts' => (
is => 'ro',
isa => 'HashRef[Any]',
default => sub { {} },
);
sub _load_room {
my $self = shift;
my $type = $self->room_type;
return $type->new(shadow => $self->shadow->room, %{$self->room_opts});
}
sub _load_slug {
return shift->shadow->slug;
}
sub _load_starttime {
return shift->shadow->starttime;
}
sub _load_endtime {
return shift->shadow->endtime;
}
sub _load_length {
return shift->shadow->length;
}
sub _load_title {
return shift->shadow->title;
}
sub _load_upstreamid {
return shift->shadow->upstreamid;
}
sub _load_subtitle {
return shift->shadow->subtitle;
}
sub _load_track {
my $self = shift;
my $type = $self->track_type;
return $type->new(shadow => $self->shadow->track, %{$self->track_opts});
}
sub _load_description {
return shift->shadow->description
}
sub _load_flags {
return shift->shadow->flags;
}
sub _load_speakers {
my $self = shift;
my $rv = [];
my $type = $self->speaker_type;
foreach my $speaker(@{$self->shadow->speakers}) {
push @$rv, "$type"->new(shadow => $speaker, %{$self->speaker_opts});
}
return $rv;
}
sub _load_filtered {
return shift->shadow->filtered;
}
no Moose;
package SReview::Schedule::WithShadow::ShadowedEvent;
use Moose;
use SReview::Schedule::Base;
extends 'SReview::Schedule::Base::Event';
has 'shadow' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Event',
required => 1,
);
has 'talk_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_talk_type',
);
has 'talk_opts' => (
is => 'ro',
isa => 'HashRef[Any]',
default => sub { {} },
);
sub _load_talk_type {
return "SReview::Schedule::WithShadow::ShadowedTalk";
}
sub _load_talks {
my $self = shift;
my $rv = [];
my $type = $self->talk_type;
my $opts = $self->talk_opts;
foreach my $talk(@{$self->shadow->talks}) {
push @$rv, $type->new(shadow => $talk, %$opts);
}
return $rv;
}
sub _load_name {
return shift->shadow->name;
}
no Moose;
package SReview::Schedule::WithShadow;
use Moose;
extends 'SReview::Schedule::Base';
has 'event_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_event_type',
);
has 'event_opts' => (
is => 'ro',
isa => 'HashRef[Any]',
default => sub { {} },
);
has 'base_type' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_base_type',
);
has 'base_options' => (
is => 'ro',
isa => 'HashRef[Any]',
);
sub _load_events {
my $self = shift;
my $event_type = $self->event_type;
my $base_type = "SReview::Schedule::" . ucfirst($self->base_type);
eval "require $base_type" or die $!;
my $event_opts = $self->event_opts;
my $rv = [];
foreach my $event(@{$base_type->new(url => $self->url)->events}) {
push @$rv, $event_type->new(shadow => $event, %$event_opts);
}
return $rv;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Schedule/Penta.pm 0000644 0001750 0001750 00000010532 14054727355 020047 0 ustar wouter wouter package SReview::Schedule::Penta::Talk;
use Moose;
use DateTime;
use DateTime::Format::ISO8601;
use DateTime::Duration;
extends 'SReview::Schedule::Base::Talk';
has 'schedref' => (
is => 'ro',
isa => 'Ref',
required => 1,
);
has 'day' => (
is => 'ro',
isa => 'DateTime',
required => 1,
);
sub _load_slug {
return shift->schedref->child('slug')->value();
}
sub _load_starttime {
my $self = shift;
my $rv = DateTime::Format::ISO8601->parse_datetime($self->day);
my $time = $self->schedref->child('start')->value();
my $dur;
if($time =~ /^([0-9]{2}):([0-9]{2})$/) {
$dur = DateTime::Duration->new(hours => $1, minutes => $2);
} else {
die "Could not parse starttime: attribute of talk \"" . $self->title . "\" does not parse as time";
}
$rv->add_duration($dur);
return $rv;
}
sub _load_length {
my $self = shift;
my $time = $self->schedref->child('duration')->value();
if($time =~ /^([0-9]{2}):([0-9]{2})$/) {
return DateTime::Duration->new(hours => $1, minutes => $2);
}
die "Could not parse duration: attribute of talk \"" . $self->title . "\" does not parse as time";
}
sub xml_helper($$) {
my $self = shift;
my $name = shift;
my $rv = $self->schedref->child($name);
return $rv->value if defined($rv);
return undef;
}
sub _load_title {
return shift->xml_helper('title');
}
sub _load_upstreamid {
return shift->schedref->attribute('id');
}
sub _load_subtitle {
return shift->xml_helper('subtitle');
}
sub _load_track {
my $track = shift->xml_helper('track');
return SReview::Schedule::Base::Track->new(name => $track) if defined($track);
return undef;
}
sub _load_description {
return shift->xml_helper('description');
}
sub _load_speakers {
my $self = shift;
my $rv = [];
foreach my $person($self->schedref->child('persons')->children('person')) {
next if $person eq '';
push @$rv, SReview::Schedule::Base::Speaker->new(name => $person->value(), upstreamid => $person->attribute('id'));
}
return $rv;
}
no Moose;
package SReview::Schedule::Penta::Event;
use Moose;
use DateTime::Format::ISO8601;
extends 'SReview::Schedule::Base::Event';
has 'schedref' => (
is => 'ro',
isa => 'Ref',
required => 1,
);
has 'talktype' => (
is => 'ro',
isa => 'Str',
default => 'SReview::Schedule::Penta::Talk',
);
sub _load_name {
return shift->schedref->child('conference')->child('title')->value();
}
sub _load_talks {
my $self = shift;
my $rv = [];
my %rooms;
my $talktype = $self->talktype;
return $rv unless(grep(/^day$/, $self->schedref->children_names));
foreach my $day($self->schedref->children('day')) {
my $dt = DateTime::Format::ISO8601->parse_datetime($day->attribute('date'));
next unless(grep(/^room$/, $day->children_names));
foreach my $room($day->children('room')) {
my $roomname = $room->attribute('name');
if(!exists($rooms{$roomname})) {
$rooms{$roomname} = SReview::Schedule::Base::Room->new(name => $roomname);
}
next unless (grep(/^event$/, $room->children_names) == 1);
foreach my $talk($room->children('event')) {
push @$rv, "$talktype"->new(room => $rooms{$roomname}, schedref => $talk, day => $dt);
}
}
}
return $rv;
}
package SReview::Schedule::Penta;
=head1 NAME
SReview::Schedule::Penta - sreview-import schedule parser for the Pentabarf XML format
=head1 SYNOPSIS
$schedule_format = "multi";
$schedule_options = { url => "http://..." };
=head1 DESCRIPTION
This module is a schedule parser for L that converts a
Pentabarf XML schedule format into the objects expected by
sreview-import.
Note that the Pentabarf XML files as created by the Wafer conference
management tool is subtly different in ways that matter for SReview. To
parse a Wafer file, see SReview::Schedule::Wafer.
=head1 OPTIONS
C only supports one option:
=head2 url
The URL where the schedule can be found.
=head1 SEE ALSO
L, L
=cut
use Moose;
use XML::SimpleObject;
use SReview::Schedule::Base;
extends 'SReview::Schedule::Base';
has 'talktype' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_talktype',
);
sub _load_events {
my $self = shift;
my $xml = XML::SimpleObject->new(XML => $self->_get_raw);
return [SReview::Schedule::Penta::Event->new(schedref => $xml->child('schedule'), talktype => $self->talktype)];
}
sub _load_talktype {
return 'SReview::Schedule::Penta::Talk';
}
1;
SReview-0.8.0/lib/SReview/Schedule/Filtered.pm 0000644 0001750 0001750 00000002267 14054727355 020544 0 ustar wouter wouter package SReview::Schedule::Filtered::FilteredTalk;
use Moose;
use SReview::Schedule::WithShadow;
extends 'SReview::Schedule::WithShadow::ShadowedTalk';
has 'require_match' => (
is => 'ro',
isa => 'HashRef[Str]',
default => sub { {} },
);
has 'forbid_match' => (
is => 'ro',
isa => 'HashRef[Str]',
default => sub { {} },
);
sub _load_filtered {
my $self = shift;
foreach my $filter(keys %{$self->require_match}) {
if($self->shadow->meta->find_attribute_by_name($filter)->get_value($self->shadow) !~ $self->require_match->{$filter}) {
return 1;
}
}
foreach my $filter(keys %{$self->forbid_match}) {
if($self->shadow->meta->find_attribute_by_name($filter)->get_value($self->shadow) =~ $self->forbid_match->{$filter}) {
return 1;
}
}
return 0;
}
no Moose;
package SReview::Schedule::Filtered::FilteredEvent;
use Moose;
extends 'SReview::Schedule::WithShadow::ShadowedEvent';
sub _load_talk_type {
return 'SReview::Schedule::Filtered::FilteredTalk';
}
package SReview::Schedule::Filtered;
use Moose;
use SReview::Schedule::WithShadow;
extends 'SReview::Schedule::WithShadow';
sub _load_event_type {
return 'SReview::Schedule::Filtered::FilteredEvent';
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Schedule/Base.pm 0000644 0001750 0001750 00000011034 14051436474 017644 0 ustar wouter wouter package SReview::Schedule::Base::Speaker;
use Moose;
has 'name' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_name',
);
has 'email' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_email',
);
sub _load_email {
return undef;
}
has 'upstreamid' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_upstreamid',
);
sub _load_upstreamid {
return undef;
}
no Moose;
package SReview::Schedule::Base::Room;
use Moose;
has 'name' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_name',
);
sub _load_name {
return undef;
}
has 'altname' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_altname',
);
sub _load_altname {
return undef;
}
has 'outputname' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_outputname',
);
sub _load_outputname {
return undef;
}
package SReview::Schedule::Base::Track;
use Moose;
use Mojo::Util 'slugify';
has 'name' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_name',
);
sub _load_name {
return undef;
}
has 'email' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_email',
);
sub _load_email {
return undef;
}
has 'upstreamid' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_upstreamid',
);
sub _load_upstreamid {
return slugify(shift->name);
}
package SReview::Schedule::Base::Talk;
use Moose;
use Mojo::Util 'slugify';
use DateTime;
use DateTime::Duration;
has 'room' => (
is => 'ro',
isa => 'SReview::Schedule::Base::Room',
lazy => 1,
builder => '_load_room',
);
sub _load_room {
return SReview::Schedule::Base::Room->new;
}
has 'slug' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_slug',
);
sub _load_slug {
my $self = shift;
return substr(slugify($self->title), 0, 40);
}
has 'starttime' => (
is => 'ro',
isa => 'DateTime',
lazy => 1,
builder => '_load_starttime',
);
sub _load_starttime {
return DateTime->now;
}
has 'endtime' => (
is => 'ro',
isa => 'DateTime',
lazy => 1,
builder => '_load_endtime',
);
sub _load_endtime {
my $self = shift;
my $start = $self->starttime;
my $tz = $start->time_zone;
$start->set_time_zone('UTC');
my $end = $self->starttime + $self->length;
$start->set_time_zone($tz);
return $end;
}
has 'length' => (
is => 'ro',
isa => 'DateTime::Duration',
lazy => 1,
builder => '_load_length',
);
sub _load_length {
return DateTime::Duration->new(hours => 1);
}
has 'title' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_title',
);
sub _load_title {
return "";
}
has 'upstreamid' => (
is => 'ro',
isa => 'Str',
lazy => 1,
builder => '_load_upstreamid',
);
sub _load_upstreamid {
return shift->slug;
}
has 'subtitle' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_subtitle',
);
sub _load_subtitle {
return undef;
}
has 'track' => (
is => 'ro',
isa => 'Maybe[SReview::Schedule::Base::Track]',
lazy => 1,
builder => '_load_track',
);
sub _load_track {
return undef;
}
has 'description' => (
is => 'ro',
isa => 'Maybe[Str]',
lazy => 1,
builder => '_load_description',
);
sub _load_description {
return undef;
}
has 'flags' => (
is => 'ro',
isa => 'Maybe[HashRef[Bool]]',
lazy => 1,
builder => '_load_flags',
);
sub _load_flags {
return undef;
}
has 'speakers' => (
is => 'ro',
isa => 'Maybe[ArrayRef[SReview::Schedule::Base::Speaker]]',
lazy => 1,
builder => '_load_speakers',
);
sub _load_speakers {
return undef;
}
has 'filtered' => (
is => 'ro',
isa => 'Bool',
lazy => 1,
builder => '_load_filtered',
);
sub _load_filtered {
return 0;
}
no Moose;
package SReview::Schedule::Base::Event;
use Moose;
has 'talks' => (
is => 'ro',
lazy => 1,
isa => 'ArrayRef[SReview::Schedule::Base::Talk]',
builder => '_load_talks',
);
sub _load_talks {
return [];
}
has 'name' => (
is => 'ro',
lazy => 1,
isa => 'Str',
builder => '_load_name',
);
sub _load_name {
return "";
}
package SReview::Schedule::Base;
use Moose;
use Mojo::UserAgent;
has 'url' => (
required => 1,
is => 'ro',
);
has '_raw' => (
lazy => 1,
builder => '_load_raw',
is => 'bare',
reader => '_get_raw',
);
sub _load_raw {
my $self = shift;
my $ua = Mojo::UserAgent->new;
$ua->proxy->detect;
my $res = $ua->get($self->url)->result;
die "Could not access " . $self->url . ": " . $res->code . " " . $res->message unless $res->is_success;
return $res->body;
}
has 'events' => (
is => 'ro',
isa => 'ArrayRef[SReview::Schedule::Base::Event]',
lazy => 1,
builder => '_load_events',
);
sub _load_events {
return [];
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Normalizer/ 0000755 0001750 0001750 00000000000 14116343665 017024 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Normalizer/Ffmpeg.pm 0000644 0001750 0001750 00000003527 14103023227 020556 0 ustar wouter wouter package SReview::Normalizer::Ffmpeg;
use Moose;
extends 'SReview::Normalizer';
use Mojo::JSON qw/decode_json/;
use Symbol 'gensym';
use IPC::Open3;
use SReview::CodecMap qw/detect_to_write/;
=head1 NAME
SReview::Normalizer::Ffmpeg - normalize the audio of a video asset using the ffmpeg 'loudnorm' filter
=head1 SYNOPSIS
SReview::Normalizer::Ffmpeg->new(input => SReview::Video->new(...), output => SReview::Video->new(...))->run();
=head1 DESCRIPTION
C is a class to normalize the audio of a given
SReview::Video asset. This class is an implementation of the API using
the ffmpeg "loudnorm" filter.
=head1 ATTRIBUTES
C supports all the attributes of
L
=head1 METHODS
=head2 run
Performs the normalization
=cut
sub run {
my $self = shift;
my $input = $self->input;
my @command = ("ffmpeg", "-y", "-i", $input->url, "-af", "loudnorm=i=-23.0:print_format=json", "-f", "null", "-");
print "Running: '" . join("' '", @command) . "'\n";
open3 (my $in, my $out, my $ffmpeg = gensym, @command);
my $json = "";
my $reading_json = 0;
while(<$ffmpeg>) {
if($reading_json) {
$json .= $_;
next;
}
if(/Parsed_loudnorm/) {
$reading_json = 1;
}
}
$json = decode_json($json);
# TODO: abstract filters so they can be applied to an
# SReview::Videopipe. Not now.
my $codec = $self->output->audio_codec;
if(!defined($codec)) {
$codec = detect_to_write($input->audio_codec);
}
@command = ("ffmpeg", "-loglevel", "warning", "-y", "-i", $input->url, "-af", "loudnorm=i=-23.0:dual_mono=true:measured_i=" . $json->{input_i} . ":measured_tp=" . $json->{input_tp} . ":measured_lra=" . $json->{input_lra} . ":measured_thresh=" . $json->{input_thresh}, "-c:v", "copy", "-c:a", $codec, $self->output->url);
print "Running: '" . join("' '", @command) . "'\n";
system(@command);
}
SReview-0.8.0/lib/SReview/Normalizer/None.pm 0000644 0001750 0001750 00000000372 14054731105 020252 0 ustar wouter wouter package SReview::Normalizer::None;
use SReview::Normalizer;
use Moose;
use File::Copy;
extends 'SReview::Normalizer';
sub run {
my $self = shift;
if ($self->input->url ne $self->output->url) {
copy($self->input->url, $self->output->url);
}
}
SReview-0.8.0/lib/SReview/Normalizer/Bs1770gain.pm 0000644 0001750 0001750 00000004740 14054743763 021115 0 ustar wouter wouter package SReview::Normalizer::Bs1770gain;
use Moose;
use File::Basename;
use File::Temp qw/tempdir/;
use SReview::CodecMap qw/detect_to_write/;
use SReview::Config::Common;
use SReview::Map;
use SReview::Video;
use SReview::Videopipe;
extends 'SReview::Normalizer';
=head1 NAME
SReview::Normalizer::Bs1770gain - normalize the audio of a video asset using bs1770gain
=head1 SYNOPSIS
SReview::Normalizer::Bs1770gain->new(input => SReview::Video->new(...), output => SReview::Video->new(...))->run();
=head1 DESCRIPTION
C is a class to normalize the audio of
a given SReview::Video asset, using bs1770gain at its default settings.
It looks at the C configuration parameter to decide
whether to pass the C<--suffix> option to bs1770gain: if the installed
version of C is at 0.5 or below, set the C key
of C to 0.5 to remove the C<--suffix> parameter from the
command line (which is required for 0.6 or above, but not supported by
0.5 or below).
=head1 ATTRIBUTES
C supports all the attributes of
L.
=cut
has '_tempdir' => (
is => 'rw',
isa => 'Str',
lazy => 1,
builder => '_probe_tempdir',
);
sub _probe_tempdir {
my $self = shift;
return tempdir("normXXXXXX", DIR => SReview::Config::Common::setup()->get('workdir'), CLEANUP => 1);
}
=head1 METHODS
=head2 run
Performs the normalization.
=cut
sub run {
my $self = shift;
my $exten;
$self->input->url =~ /(.*)\.[^.]+$/;
my $base = $1;
if(!defined($self->input->video_codec)) {
$exten = "flac";
} else {
$exten = "mkv";
}
my @command = ("bs1770gain", "-a", "-o", $self->_tempdir);
my $tune = SReview::Config::Common::setup()->get("command_tune");
if(exists($tune->{bs1770gain}) && $tune->{bs1770gain} ne "0.5") {
$exten = "mkv";
push @command, "--suffix=mkv";
}
push @command, $self->input->url;
print "Running: '" . join("' '", @command) . "'\n";
system(@command);
my $intermediate = $self->_tempdir . "/" . basename($base) . ".$exten";
my $check = SReview::Video->new(url => $intermediate);
if($check->audio_codec eq $self->input->audio_codec) {
SReview::Videopipe->new(inputs => [SReview::Video->new(url => $intermediate)], output => $self->output, vcopy => 1, acopy => 1)->run();
} else {
$self->output->audio_codec($self->input->audio_codec);
SReview::Videopipe->new(inputs => [SReview::Video->new(url => $intermediate)], output => $self->output, vcopy => 1, acopy => 0)->run();
}
}
1;
SReview-0.8.0/lib/SReview/Video.pm 0000644 0001750 0001750 00000044514 14054727355 016321 0 ustar wouter wouter package SReview::Video;
our $VERSION;
use SReview;
=head1 NAME
SReview::Video - SReview internal representation of an asset
=head1 SYNOPSIS
use SReview::Video;
use SReview::Video::ProfileFactory;
use SReview::Videopipe;
# convert any input file to VP9 at recommended settings for vertical resolution and frame rate
my $input = SReview::Video->new(url => $input_filename);
my $profile = SReview::Video::ProfileFactory->new("vp9", $input);
my $output = SReview::Video->new(url => $output_filename, reference => $profile);
SReview::Videopipe->new(inputs => [$input], output => $output)->run();
# do that again; but this time, force vorbis audio:
$output = SReview::Video->new(url => $other_filename, reference => $profile);
$output->audio_codec("libvorbis");
SReview::Videopipe->new(inputs => [$input], output => $output)->run();
=head1 DESCRIPTION
The SReview::Video package is used to represent media assets inside
SReview. It is a C-based base class for much of the other Video*
classes in SReview.
There is one required attribute, C, which represents the filename
of the video (however, for SReview::Video::NGinX, it should be an HTTP
URL instead).
If the C attribute points to an existing file and an attempt is
made to read any of the codec, framerate, bit rate, or similar
attributes (without explicitly writing to them first), then
C will call C on the file in question, and use
that to populate the requested attributes. If it does not, or C
is incapable of detecting the requested attribute (which may be the case
for things like audio or video bitrate), then the attribute in question
will resolve to C.
If the C attribute does not point to an existing file and an
attempt is made to read any of the codec, framerate, bit rate, or
similar attributes (without explicitly writing to them first), then they
will resolve to C. However, if the C attribute is
populated with another C object, then reading any of the
codec, framerate, bit rate, or similar attributes (without explicitly
writing to them first) will resolve to the value of the requested
attribute that is set or detected on the C object.
The return value of Ccreate()> is
also an SReview::Video object, but with different implementations of
some of the probing methods; this allows it to choose the correct values
for things like bitrate and encoder speed based on properties set in the
input object provided to the
Ccreate()> method.
For more information on how to use the files referred to in the
C object in an ffmpeg command line, please see
C.
=head1 ATTRIBUTES
The following attributes are supported by SReview::Video. All attributes
will be probed from ffprobe output unless noted otherwise.
=cut
use Mojo::JSON qw(decode_json);
use SReview::CodecMap qw/detect_to_write/;
use Moose;
=head2 url
The filename of the asset this object should deal with. Required at
construction time. Will not be probed.
=cut
has 'url' => (
is => 'rw',
required => 1,
);
=head2 mtime
The mtime of the file backing this asset. Only defined if the file
exists at the time the attribute is first read, and is not updated later
on.
=cut
has 'mtime' => (
is => 'ro',
lazy => 1,
builder => '_probe_mtime',
);
sub _probe_mtime {
my $self = shift;
if($self->has_reference) {
return $self->reference->mtime;
}
my @statdata = stat($self->url);
if(scalar(@statdata) == 0) {
return undef;
}
return $statdata[9];
}
=head2 duration
The duration of this asset.
=cut
has 'duration' => (
is => 'rw',
builder => '_probe_duration',
lazy => 1,
);
sub _probe_duration {
my $self = shift;
if($self->has_reference) {
return $self->reference->duration;
}
return $self->_get_probedata->{format}{duration};
}
=head2 duration_style
The time unit is used for the C attribute. One of 'seconds'
(default) or 'frames'. Will not be probed.
=cut
has 'duration_style' => (
is => 'rw',
default => 'seconds',
);
=head2 video_codec
The codec in use for the video stream. Note that C will
sometimes use a string (e.g., "vp8") that is not the best choice when
instructing C to transcode video to the said codec (for vp8, the
use of "libvpx" is recommended). C is used to map
detected codecs to output codecs and resolve this issue.
=cut
has 'video_codec' => (
is => 'rw',
builder => '_probe_videocodec',
lazy => 1,
);
sub _probe_videocodec {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_codec;
}
return $self->_get_videodata->{codec_name};
}
=head2 audio_codec
The codec in use for the audio stream. Note that C will
sometimes use a string (e.g., "vorbis") that is not the best choice when
instructing C to transcode audio to the said codec (for vorbis,
the use of "libvorbis" is recommended). C is used to
map detected codecs to output codecs and resolve this issue.
=cut
has 'audio_codec' => (
is => 'rw',
builder => '_probe_audiocodec',
lazy => 1,
);
sub _probe_audiocodec {
my $self = shift;
if($self->has_reference) {
return $self->reference->audio_codec;
}
return $self->_get_audiodata->{codec_name};
}
=head2 video_size
A string representing the resolution of the video in C format,
where W is the width and H is the height.
This attribute is special in that in contrast to all the other
attributes, it is not provided directly by C; instead, when
this parameter is read, the C and C
attributes are read and combined.
That does mean that you should not read this attribute, and based on
that possibly set the height and/or width attributes of a video (or vice
versa). Instead, you should read I the C and
C attribute, I this one.
Failure to follow this rule will result in undefined behaviour.
=cut
has 'video_size' => (
is => 'rw',
builder => '_probe_videosize',
lazy => 1,
predicate => 'has_video_size',
);
sub _probe_videosize {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_size;
}
my $width = $self->video_width;
my $height = $self->video_height;
return undef unless defined($width) && defined($height);
return $self->video_width . "x" . $self->video_height;
}
=head2 video_width
The width of the video, in pixels.
=cut
has 'video_width' => (
is => 'rw',
builder => '_probe_width',
lazy => 1,
);
sub _probe_width {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_width;
}
if($self->has_video_size) {
return (split /x/, $self->video_size)[0];
} else {
return $self->_get_videodata->{width};
}
}
=head2 video_height
The height of the video, in pixels.
=cut
has 'video_height' => (
is => 'rw',
builder => '_probe_height',
lazy => 1,
);
sub _probe_height {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_height;
}
if($self->has_video_size) {
return (split /x/, $self->video_size)[1];
} else {
return $self->_get_videodata->{height};
}
}
=head2 video_bitrate
The bit rate of this video, in bits per second.
Note that not all container formats support probing the bitrate of the
encoded video or audio; when read on input objects with those that do
not, this will resolve to C.
=cut
has 'video_bitrate' => (
is => 'rw',
builder => '_probe_videobitrate',
lazy => 1,
);
sub _probe_videobitrate {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_bitrate;
}
return $self->_get_videodata->{bit_rate};
}
=head2 video_minrate
The minimum bit rate for this video, in bits per second.
Defaults to 0.5 * video_bitrate
=cut
has 'video_minrate' => (
is => 'rw',
builder => '_probe_videominrate',
lazy => 1,
);
sub _probe_videominrate {
my $self = shift;
my $rate;
if($self->has_reference) {
$rate = $self->reference->video_minrate;
if(defined($rate)) {
return $rate;
}
}
$rate = $self->video_bitrate;
if(defined($rate)) {
return $rate * 0.5;
}
return undef;
}
=head2 video_maxrate
The maximum bit rate for this video, in bits per second.
Defaults to 1.45 * video_bitrate
=cut
has 'video_maxrate' => (
is => 'rw',
builder => '_probe_videomaxrate',
lazy => 1,
);
sub _probe_videomaxrate {
my $self = shift;
my $rate;
if($self->has_reference) {
$rate = $self->reference->video_maxrate;
if(defined($rate)) {
return $rate;
}
}
$rate = $self->video_bitrate;
if(defined($rate)) {
return $rate * 1.45;
}
return undef;
}
=head2 aspect_ratio
The Display Aspect Ratio of a video. Note that with non-square pixels,
this is not guaranteed to be what one would expect when reading the
C attribute.
=cut
has 'aspect_ratio' => (
is => 'rw',
builder => '_probe_aspect_ratio',
lazy => 1,
);
sub _probe_aspect_ratio {
my $self = shift;
if($self->has_reference) {
return $self->reference->aspect_ratio;
}
return $self->_get_videodata->{display_aspect_ratio};
}
=head2 audio_bitrate
The bit rate of the audio stream on this video, in bits per second
=cut
has 'audio_bitrate' => (
is => 'rw',
builder => '_probe_audiobitrate',
lazy => 1,
);
sub _probe_audiobitrate {
my $self = shift;
if($self->has_reference) {
return $self->reference->audio_bitrate;
}
return $self->_get_audiodata->{bit_rate};
}
=head2 audio_samplerate
The sample rate of the audio, in samples per second
=cut
has 'audio_samplerate' => (
is => 'rw',
builder => '_probe_audiorate',
lazy => 1,
);
sub _probe_audiorate {
my $self = shift;
if($self->has_reference) {
return $self->reference->audio_samplerate;
}
return $self->_get_audiodata->{sample_rate};
}
=head2 video_framerate
The frame rate of the video, as a fraction.
Note that in the weird US frame rate, this could be 30000/1001.
=cut
has 'video_framerate' => (
is => 'rw',
builder => '_probe_framerate',
lazy => 1,
);
sub _probe_framerate {
my $self = shift;
if($self->has_reference) {
return $self->reference->video_framerate;
}
my $framerate = $self->_get_videodata->{r_frame_rate};
return $framerate;
}
=head2 fragment_start
If set, this instructs SReview on read to only read a particular part of the
video from this file. Should be specified in seconds; will not be probed.
=cut
has 'fragment_start' => (
is => 'rw',
predicate => 'has_fragment_start',
);
=head2 quality
The quality used for the video encoding, i.e., the value passed to the C<-crf>
parameter. Mostly for use by a profile. Will not be probed.
=cut
has 'quality' => (
is => 'rw',
builder => '_probe_quality',
lazy => 1,
);
sub _probe_quality {
my $self = shift;
if($self->has_reference) {
return $self->reference->quality;
}
return undef;
}
=head2 metadata
Can be used to set video metadata (as per C's C<-metadata>
parameter). Functions C and C can be used
to add or remove individual metedata values. Will not be probed.
=cut
has 'metadata' => (
traits => ['Hash'],
isa => 'HashRef[Str]',
is => 'ro',
handles => {
add_metadata => 'set',
drop_metadata => 'delete',
},
predicate => 'has_metadata',
);
=head2 reference
If set to any C object, then when any value is being
probed, rather than trying to run C on the file pointed to by
our C attribute, we will use the value reported by the referenced
object.
Can be used in "build a file almost like this one, but with these things
different" kind of scenarios.
Will not be probed (obviously).
=cut
has 'reference' => (
isa => 'SReview::Video',
is => 'ro',
predicate => 'has_reference',
);
=head2 pix_fmt
The pixel format (e.g., C or the likes) of the video.
=cut
has 'pix_fmt' => (
is => 'rw',
builder => '_probe_pix_fmt',
lazy => 1,
);
sub _probe_pix_fmt {
my $self = shift;
if($self->has_reference) {
return $self->reference->pix_fmt;
}
return $self->_get_videodata->{pix_fmt};
}
=head2 astream_id
Returns the numeric ID for the first audio stream in this file. Useful for the
implementation of stream mappings etc; see C
=cut
has 'astream_id' => (
is => 'rw',
builder => '_probe_astream_id',
lazy => 1,
);
sub _probe_astream_id {
my $self = shift;
return $self->_get_audiodata->{index};
}
=head2 blackspots
Returns an array of hashes. Each hash contains a member C,
C, and C, containing the start, end, and duration,
respectively, of locations in the video file that are (almost) entirely
black.
Could be used by a script for automatic review.
Note that the ffmpeg run required to detect blackness is CPU intensive
and may require a very long time to finish.
=cut
has blackspots => (
is => 'ro',
isa => 'ArrayRef[HashRef[Num]]',
builder => '_probe_blackspots',
lazy => 1,
);
sub _probe_blackspots {
my $self = shift;
my $blacks = [];
pipe R, W;
if(fork == 0) {
open STDERR, ">&W";
open STDOUT, ">&W";
my @cmd = ("ffmpeg", "-threads", "1", "-nostats", "-i", $self->url, "-vf", "blackdetect=d=0:pix_th=.01", "-f", "null", "/dev/null");
exec @cmd;
die "exec failed";
}
close W;
while() {
if(/blackdetect.*black_start:(?[\d\.]+)\sblack_end:(?[\d\.]+)\sblack_duration:(?[\d\.]+)/) {
push @$blacks, { %+ };
}
}
close(R);
return $blacks;
}
=head2 astream_ids
Returns an array with the IDs for the audio streams in this file.
=head2 astream_count
Returns the number of audio streams in this file.
=cut
has 'astream_ids' => (
is => 'rw',
traits => ['Array'],
isa => 'ArrayRef[Int]',
builder => '_probe_astream_ids',
lazy => 1,
handles => {
astream_count => "count",
},
);
sub _probe_astream_ids {
my $self = shift;
my @rv;
foreach my $stream(@{$self->_get_probedata->{streams}}) {
if($stream->{codec_type} eq "audio") {
push @rv, $stream->{index};
}
}
return \@rv;
}
=head2 vstream_id
Returns the numeric ID for the first video stream in this file. Useful
for the implementation of stream mappings etc; see C
=cut
has 'vstream_id' => (
is => 'rw',
builder => '_probe_vstream_id',
lazy => 1,
);
sub _probe_vstream_id {
my $self = shift;
return $self->_get_videodata->{index};
}
=head2 extra_params
Add extra parameters. This should be used sparingly, rather add some
abstraction.
=cut
has 'extra_params' => (
traits => ['Hash'],
isa => 'HashRef[Str]',
is => 'ro',
handles => {
add_param => 'set',
drop_param => 'delete',
},
builder => "_probe_extra_params",
lazy => 1,
);
sub _probe_extra_params {
my $self = shift;
if($self->has_reference) {
return $self->reference->extra_params;
}
return {};
}
=head2 time_offset
Apply an input time offset to this video (only valid when used as an
input video in L). Can be used to apply A/V sync
correction values.
=cut
has 'time_offset' => (
isa => 'Num',
is => 'ro',
predicate => 'has_time_offset',
);
# Only to be used by the Videopipe class when doing multiple passes
has 'pass' => (
is => 'rw',
predicate => 'has_pass',
clearer => 'clear_pass',
);
## The below exist to help autodetect sizes, and are not meant for the end user
has 'videodata' => (
is => 'bare',
reader => '_get_videodata',
builder => '_probe_videodata',
lazy => 1,
);
has 'audiodata' => (
is => 'bare',
reader => '_get_audiodata',
builder => '_probe_audiodata',
lazy => 1,
);
has 'probedata' => (
is => 'bare',
reader => '_get_probedata',
builder => '_probe',
clearer => 'clear_probedata',
lazy => 1,
);
sub readopts {
my $self = shift;
my @opts = ();
if($self->has_time_offset) {
push @opts, ("-itsoffset", $self->time_offset);
}
push @opts, ("-i", $self->url);
return @opts;
}
sub writeopts {
my $self = shift;
my $pipe = shift;
my @opts = ();
if(!$pipe->vcopy && !$pipe->vskip) {
push @opts, ('-threads', '1');
if(defined($self->video_codec)) {
push @opts, ('-c:v', detect_to_write($self->video_codec));
}
if(defined($self->video_bitrate)) {
push @opts, ('-b:v', $self->video_bitrate . "k", '-minrate', $self->video_minrate . "k", '-maxrate', $self->video_maxrate . "k");
}
if(defined($self->video_framerate)) {
push @opts, ('-r:v', $self->video_framerate);
}
if(defined($self->quality)) {
push @opts, ('-crf', $self->quality);
}
if(defined($self->speed)) {
push @opts, ('-speed', $self->speed);
}
if($self->has_pass) {
push @opts, ('-pass', $self->pass, '-passlogfile', $self->url . '-multipass');
}
}
if(!$pipe->acopy && !$pipe->askip) {
if(defined($self->audio_codec)) {
push @opts, ('-c:a', detect_to_write($self->audio_codec));
}
if(defined($self->audio_bitrate)) {
push @opts, ('-b:a', $self->audio_bitrate);
}
if(defined($self->audio_samplerate)) {
push @opts, ('-ar', $self->audio_samplerate);
}
}
if($self->has_fragment_start) {
push @opts, ('-ss', $self->fragment_start);
}
if(defined($self->duration)) {
if($self->duration_style eq 'seconds') {
push @opts, ('-t', $self->duration);
} else {
push @opts, ('-frames:v', $self->duration);
}
}
if(defined($self->pix_fmt)) {
push @opts, ('-pix_fmt', $self->pix_fmt);
}
if($self->has_metadata) {
foreach my $meta(keys %{$self->metadata}) {
push @opts, ('-metadata', $meta . '=' . $self->metadata->{$meta});
}
}
if(!defined($self->duration) && $#{$pipe->inputs}>0) {
push @opts, '-shortest';
}
if(scalar(keys(%{$self->extra_params}))>0) {
foreach my $param(keys %{$self->extra_params}) {
push @opts, ("-$param", $self->extra_params->{$param});
}
}
if(exists($ENV{SREVIEW_NONSTRICT})) {
push @opts, ("-strict", "-2");
}
push @opts, $self->url;
return @opts;
}
sub _probe {
my $self = shift;
if($self->has_reference) {
return $self->reference->_get_probedata;
}
open JSON, "-|:encoding(UTF-8)", "ffprobe", "-loglevel", "quiet", "-print_format", "json", "-show_format", "-show_streams", $self->url;
my $json = "";
while() {
$json .= $_;
}
close JSON;
return decode_json($json);
}
sub _probe_audiodata {
my $self = shift;
if(!exists($self->_get_probedata->{streams})) {
return {};
}
foreach my $stream(@{$self->_get_probedata->{streams}}) {
if($stream->{codec_type} eq "audio") {
return $stream;
}
}
return {};
}
sub _probe_videodata {
my $self = shift;
if(!exists($self->_get_probedata->{streams})) {
return {};
}
foreach my $stream(@{$self->_get_probedata->{streams}}) {
if($stream->{codec_type} eq "video") {
return $stream;
}
}
return {};
}
sub speed {
my $self = shift;
if($self->has_reference) {
return $self->reference->speed;
}
return 4;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/AvSync.pm 0000644 0001750 0001750 00000002607 14111772702 016441 0 ustar wouter wouter package SReview::AvSync;
use Moose;
use SReview::Video;
use SReview::Config::Common;
use SReview::Videopipe;
use SReview::Map;
use File::Temp qw/tempdir/;
has "input" => (
is => 'rw',
required => 1,
isa => 'SReview::Video',
);
has 'output' => (
is => 'rw',
required => 1,
isa => 'SReview::Video',
);
has "value" => (
is => 'rw',
required => 1,
);
my $config = SReview::Config::Common::setup();
sub run() {
my $self = shift;
my $tempdir = tempdir("avsXXXXXX", DIR => $config->get("workdir"), CLEANUP => 1);
if($self->value == 0) {
# Why are we here??
SReview::Videopipe->new(inputs => [$self->input], output => $self->output)->run();
return;
}
SReview::Videopipe->new(inputs => [$self->input], output => SReview::Video->new(url => "$tempdir/pre.mkv"))->run();
my $input_audio = SReview::Video->new(url => "$tempdir/pre.mkv", time_offset => $self->value);
my $input_video = SReview::Video->new(url => "$tempdir/pre.mkv");
my $sync_video = SReview::Video->new(url => "$tempdir/synced.mkv");
SReview::Videopipe->new(inputs => [$input_audio, $input_video], map => [SReview::Map->new(input => $input_audio, type => "stream", choice => "audio"), SReview::Map->new(input => $input_video, type => "stream", choice => "video")], output => $sync_video)->run();
$self->output->fragment_start(abs($self->value));
SReview::Videopipe->new(inputs => [$sync_video], output => $self->output)->run();
}
SReview-0.8.0/lib/SReview/Videopipe.pm 0000644 0001750 0001750 00000010364 14112200113 017140 0 ustar wouter wouter package SReview::Videopipe;
use Mojo::JSON qw(decode_json);
use Moose;
use Carp;
has 'inputs' => (
traits => ['Array'],
is => 'ro',
isa => 'ArrayRef[SReview::Video]',
default => sub { [] },
clearer => 'clear_inputs',
handles => {
add_input => 'push',
},
);
has 'output' => (
is => 'rw',
isa => 'SReview::Video',
required => 1,
);
has 'map' => (
traits => ['Array'],
is => 'ro',
isa => 'ArrayRef[SReview::Map]',
default => sub {[]},
clearer => 'clear_map',
handles => {
add_map => 'push',
},
);
has 'vcopy' => (
isa => 'Bool',
is => 'rw',
default => 1,
);
has 'acopy' => (
isa => 'Bool',
is => 'rw',
default => 1,
);
has 'vskip' => (
isa => 'Bool',
is => 'rw',
default => 0,
);
has 'askip' => (
isa => 'Bool',
is => 'rw',
default => 0,
);
has 'multipass' => (
isa => 'Bool',
is => 'rw',
default => 0,
);
has 'progress' => (
isa => 'CodeRef',
is => 'ro',
predicate => 'has_progress',
);
has 'has_run' => (
isa => 'Bool',
is => 'rw',
default => 0,
traits => ['Bool'],
handles => {
run_complete => 'set',
}
);
sub run_progress {
my $self = shift;
my $command = shift;
my $pass = shift;
my $multipass = shift;
my ($in, $out, $err);
my $running;
my @lines;
my $old_perc = -1;
my %vals;
my $length = $self->inputs->[0]->duration * 1000000;
shift @$command;
unshift @$command, ('ffmpeg', '-progress', '/dev/stdout');
open my $ffmpeg, "-|", @{$command};
while(<$ffmpeg>) {
/^(\w+)=(.*)$/;
$vals{$1} = $2;
if($1 eq 'progress') {
my $perc = int($vals{out_time_ms} / $length * 100);
if($vals{progress} eq 'end') {
$perc = 100;
}
if($multipass) {
$perc = int($perc / 2);
}
if($pass == 2) {
$perc += 50;
}
if($perc != $old_perc) {
$old_perc = $perc;
&{$self->progress}($perc);
}
}
}
$self->run_complete;
}
sub run {
my $self = shift;
my $pass;
my @attrs = (
'video_codec' => 'vcopy',
'video_size' => 'vcopy',
'video_width' => 'vcopy',
'video_height' => 'vcopy',
'video_bitrate' => 'vcopy',
'video_framerate' => 'vcopy',
'pix_fmt' => 'vcopy',
'audio_codec' => 'acopy',
'audio_bitrate' => 'acopy',
'audio_samplerate' => 'acopy',
);
my @video_attrs = ('video_codec', 'video_size', 'video_width', 'video_height', 'video_bitrate', 'video_framerate', 'pix_fmt');
my @audio_attrs = ('audio_codec', 'audio_bitrate', 'audio_samplerate');
for($pass = 1; $pass <= ($self->multipass ? 2 : 1); $pass++) {
my @command = ("ffmpeg", "-loglevel", "warning", "-y");
foreach my $input(@{$self->inputs}) {
if($self->multipass) {
$input->pass($pass);
$self->output->pass($pass);
}
while(scalar(@attrs) > 0) {
my $attr = shift @attrs;
my $target = shift @attrs;
next unless $self->meta->get_attribute($target)->get_value($self);
my $oval = $self->output->meta->find_attribute_by_name($attr)->get_value($self->output);
my $ival = $input->meta->find_attribute_by_name($attr)->get_value($input);
if(defined($oval) && $ival ne $oval) {
$self->meta->get_attribute($target)->set_value($self, 0);
}
}
push @command, $input->readopts($self->output);
}
if(!$self->vcopy() && !$self->vskip()) {
my $isize = $self->inputs->[0]->video_size;
my $osize = $self->output->video_size;
if(defined($isize) && defined($osize) && $isize ne $osize) {
push @command, ("-vf", "scale=" . $osize);
}
}
foreach my $map(@{$self->map}) {
my $in_map = $map->input;
my $index;
for(my $i=0; $i<=$#{$self->inputs}; $i++) {
if($in_map == ${$self->inputs}[$i]) {
$index = $i;
}
}
push @command, $map->arguments($index);
}
if($self->vcopy) {
push @command, ('-c:v', 'copy');
}
if($self->acopy) {
push @command, ('-c:a', 'copy');
}
if($self->vskip) {
push @command, ('-vn');
}
if($self->askip) {
push @command, ('-an');
}
push @command, $self->output->writeopts($self);
print "Running: '" . join ("' '", @command) . "'\n";
if($self->has_progress) {
$self->run_progress(\@command, $pass, $self->multipass);
} else {
system(@command);
}
}
foreach my $input(@{$self->inputs}) {
$input->clear_pass;
}
$self->output->clear_pass;
$self->run_complete;
}
sub DESTROY {
if(!(shift->has_run)) {
confess "object destructor for videopipe entered without having seen a run!";
}
}
no Moose;
1;
SReview-0.8.0/lib/SReview/API.pm 0000644 0001750 0001750 00000136207 14111420654 015647 0 ustar wouter wouter package SReview::API;
use SReview::Config::Common;
sub init {
my $app = shift;
my $config = SReview::Config::Common::setup();
$app->plugin("OpenAPI" => {
url => "data:///api.yml",
schema => "v3",
security => {
api_key => sub {
my ($c, $definition, $scopes, $cb) = @_;
return $c->$cb('API key not configured') unless defined($config->get('api_key'));
return $c->$cb('API key not present') unless defined($c->req->headers->header("X-SReview-Key"));
return $c->$cb('API key invalid') unless $c->req->headers->header("X-SReview-Key") eq $config->get('api_key');
return $c->$cb();
},
sreview_auth => sub {
my ($c, $definition, $scopes, $cb) = @_;
return $c->$cb('OAuth2 not yet implemented');
},
}
});
}
1;
__DATA__
@@ api.yml
openapi: 3.0.1
info:
title: SReview API
description: SReview is an AGPLv3 video review tool.
contact:
email: w@uter.be
license:
name: AGPLv3
url: https://www.gnu.org/licenses/agpl-3.0.en.html
version: 1.0.0
externalDocs:
description: Find out more about SReview
url: https://yoe.github.io/sreview
servers:
- url: https://sreview.example.com/api/v1
tags:
- name: event
description: Managing events
- name: rawfile
description: Managing raw files
- name: room
description: Managing rooms
- name: speaker
description: Managing speakers
- name: system
description: System information
- name: talk
description: Managing talks
- name: track
description: Managing tracks
- name: user
description: Managing users
paths:
/config:
get:
tags:
- system
x-mojo-to:
controller: config
action: get_config
summary: Get configuration values
operationId: get_config
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigData'
/collection/list:
get:
tags:
- system
summary: Get a list of the known file collections
operationId: get_collections
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Collection'
x-mojo-to:
controller: rawfile
action: collections
/collection/{collectionName}/rawfile/list:
get:
tags:
- rawfile
summary: Get a list of the known raw files in a given collection
description: This operation requests a list of known raw files. It does *not* search the file system or the S3 bucket for files; it only queries the database for files that it knows about that would match the collection, and returns a list of raw files that way.
operationId: get_raw_files
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/RawFile'
x-mojo-to:
controller: rawfile
action: list
/collection/{collectionName}/rawfile/{rawId}:
patch:
tags:
- rawfile
summary: Update a raw file's metadata
operationId: update_raw_file
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
- name: rawId
in: path
required: true
schema:
$ref: '#/components/schemas/RawFile/properties/id'
requestBody:
description: RawFile object that needs to be updated
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
security:
- api_key: []
x-mojo-to:
controller: rawfile
action: update
get:
tags:
- rawfile
summary: Retrieve a raw file from the database
operationId: get_raw_file
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
- name: rawId
in: path
required: true
schema:
$ref: '#/components/schemas/RawFile/properties/id'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
x-mojo-to:
controller: rawfile
action: get
post:
tags:
- rawfile
summary: Add a raw file to the database
operationId: add_raw_file
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
- name: rawId
in: path
required: true
schema:
$ref: '#/components/schemas/RawFile/properties/id'
requestBody:
description: RawFile object to add
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
security:
- api_key: []
x-mojo-to:
controller: rawfile
action: add
delete:
tags:
- rawfile
summary: Remove a raw file from the database
operationId: delete_raw_file
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
- name: rawId
in: path
required: true
schema:
$ref: '#/components/schemas/RawFile/properties/id'
responses:
200:
description: OK
content: {}
security:
- api_key: []
x-mojo-to:
controller: rawfile
action: delete
/collection/{collectionName}/rawfile/{rawId}/server:
patch:
tags:
- rawfile
summary: Update a raw file's metadata
description: "Update a raw file's metadata in the database. The difference between this operation and the `update_raw_file` one is that the former only adds the given metadata to the database, whereas this one will create a new `SReview::Video` object for the file's name, and compute the `endtime` property based on the `starttime` one and the file's length. This requires that the file be accessible from the server."
operationId: update_raw_file_server
parameters:
- name: collectionName
in: path
required: true
schema:
$ref: '#/components/schemas/Collection/properties/name'
- name: rawId
in: path
required: true
schema:
$ref: '#/components/schemas/RawFile/properties/id'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/RawFile'
security:
- api_key: []
x-mojo-to:
controller: rawfile
action: update_with_probe
/event:
post:
tags:
- event
summary: Add a new event
operationId: add_event
requestBody:
description: Event object that needs to be added to the store
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
required: true
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Event"
security:
- api_key: []
x-mojo-to:
controller: event
action: add
/event/{eventId}:
patch:
tags:
- event
summary: Update an existing event
operationId: update_event
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
description: Event object that needs to be modified in the store
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
required: true
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Event"
404:
description: Event not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: event
action: update
get:
tags:
- event
summary: Find event by ID
description: Returns a single event
operationId: get_event
parameters:
- name: eventId
in: path
description: ID of event to return
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
x-mojo-to:
controller: event
action: getById
delete:
tags:
- event
summary: Delete an event
operationId: delete_event
parameters:
- name: eventId
in: path
description: Event id to delete
required: true
schema:
type: integer
format: int64
responses:
200:
description: Successful operation
content:
application/json:
schema:
type: integer
format: int64
security:
- api_key: []
x-mojo-to:
controller: event
action: delete
/event/list:
get:
tags:
- event
summary: Return a list of events
operationId: event_list
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Event'
x-mojo-to:
controller: event
action: list
/event/{eventId}/overview:
get:
tags:
- event
summary: Return data for the overview page for this event
operationId: event_overview
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/TalkReadable'
x-mojo-to:
controller: event
action: overview
/event/{eventId}/bystate/{state}:
get:
tags:
- event
summary: Return JSON data for talks in this event that are in the given state
operationId: event_released
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: state
in: path
required: true
schema:
$ref:
'#/components/schemas/Talk/properties/state'
responses:
200:
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/ReleasedData'
security:
- api_key: []
x-mojo-to:
controller: event
action: talksByState
/event/{eventId}/speaker/byupstream/{upstreamId}:
get:
tags:
- speaker
summary: Find speakers by their upstream ID
operationId: event_speaker_by_upstreamid
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: upstreamId
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
x-mojo-to:
controller: speaker
action: getByUpstream
security:
- api_key: []
/event/{eventId}/talk:
post:
tags:
- talk
summary: Add a new talk
operationId: add_talk
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Talk'
required: false
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Talk'
404:
description: Event not found
content: {}
405:
description: Invalid input
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: add
/event/{eventId}/talk/{talkId}:
patch:
tags:
- talk
summary: Update an existing talk
operationId: update_talk
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Talk'
required: false
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Talk"
400:
description: Invalid input
content: {}
404:
description: Event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: update
delete:
tags:
- talk
summary: Delete a talk
operationId: delete_talk
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: Successful operation
content: {}
404:
description: Event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: delete
get:
tags:
- talk
summary: Get a talk by ID
operationId: get_talk
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Talk'
404:
description: Event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: getById
/event/{eventId}/talk/{talkId}/corrections:
get:
tags:
- talk
summary: Get the corrections for the talk
operationId: talk_corrections
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/TalkCorrections'
404:
description: Event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: getCorrections
patch:
tags:
- talk
summary: Set the corrections for the talk
operationId: set_corrections
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TalkCorrections'
required: false
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/TalkCorrections'
404:
description: Event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: setCorrections
/event/{eventId}/talk/{talkId}/relative_name:
get:
tags:
- talk
summary: Retrieve the relative name for the assets for this talk
operationId: talk_relative_name
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
type: string
security:
- api_key: []
x-mojo-to:
controller: talk
action: getRelativeName
/event/{eventId}/talk/{slug}/preroll:
get:
tags:
- talk
summary: Retrieve the preroll image for the talk
operationId: talk_preroll
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: slug
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: successful operation
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "pre"
/event/{eventId}/talk/{slug}/postroll:
get:
tags:
- talk
summary: Retrieve the postroll image for the talk
operationId: talk_postroll
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: slug
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: successful operation
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "post"
/event/{eventId}/talk/{slug}/sorry:
get:
tags:
- talk
summary: Retrieve the apology image for the talk
operationId: talk_sorry
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: slug
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: successful operation
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "sorry"
/event/{eventId}/talk/list:
get:
tags:
- talk
summary: Get a list of talks for the event
operationId: talk_list
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Talk'
404:
description: Event not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: listByEvent
/event/{eventId}/talk/{talkId}/speakers:
get:
tags:
- speaker
summary: Get the list of speakers for a talk
operationId: get_talk_speakers
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Speaker'
404:
description: event or talk not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: speaker
action: listByTalk
put:
tags:
- speaker
summary: Replace the list of speakers for a given talk
operationId: replace_speakers
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
type: array
items:
type: integer
format: int64
required: false
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
type: integer
format: int64
404:
description: event, talk, or speakers not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: setSpeakers
post:
tags:
- speaker
summary: Add speakers to the given talk
operationId: add_speakers
parameters:
- name: eventId
in: path
required: true
schema:
type: integer
format: int64
- name: talkId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
type: array
items:
type: integer
format: int64
required: false
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
type: integer
format: int64
404:
description: event, talk, or speakers not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: talk
action: addSpeakers
/nonce/{nonce}/preroll:
get:
tags:
- talk
summary: Retrieve the preroll image for a talk, by nonce
operationId: talk_nonce_preroll
parameters:
- name: nonce
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: OK
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "pre"
/nonce/{nonce}/postroll:
get:
tags:
- talk
summary: Retrieve the postroll image for a talk, by nonce
operationId: talk_nonce_postroll
parameters:
- name: nonce
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: OK
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "post"
/nonce/{nonce}/sorry:
get:
tags:
- talk
summary: Retrieve the apology image for a talk, by nonce
operationId: talk_nonce_sorry
parameters:
- name: nonce
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: string
responses:
200:
description: OK
content:
image/png: {}
x-mojo-to:
controller: CreditPreviews
action: serve_png
suffix: "sorry"
/nonce/{nonce}/data:
get:
tags:
- talk
summary: Retrieve talk data by nonce
operationId: get_nonce_data
parameters:
- name: nonce
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/TalkData'
404:
description: talk not found
content: {}
x-mojo-to:
controller: review
action: data
/nonce/{nonce}/talk:
get:
tags:
- talk
summary: Retrieve talk object by nonce
operationId: get_nonce_talk
parameters:
- name: nonce
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Talk'
404:
description: talk not found
content: {}
x-mojo-to:
controller: talk
action: getByNonce
/nonce/{nonce}/talk/corrections:
get:
tags:
- talk
summary: Retrieve talk corrections by nonce
operationId: get_nonce_corrections
parameters:
- name: nonce
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/TalkCorrections'
404:
description: talk not found
content: {}
x-mojo-to:
controller: talk
action: getCorrections
patch:
tags:
- talk
summary: Set talk corrections by nonce
operationId: set_nonce_corrections
parameters:
- name: nonce
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/TalkCorrections'
404:
description: talk not found
content: {}
x-mojo-to:
controller: talk
action: getCorrections;
/speaker/search/{searchString}:
get:
tags:
- speaker
summary: Find speakers based on a substring of their name or email address
operationId: find_speaker
parameters:
- name: searchString
in: path
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Speaker'
404:
description: speaker not found
content: {}
x-mojo-to:
controller: speaker
action: search
security:
- api_key: []
/speaker:
post:
tags:
- speaker
summary: Add a new speaker to the system
operationId: add_speaker
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
security:
- api_key: []
x-mojo-to:
controller: speaker
action: add
/speaker/{speakerId}:
patch:
tags:
- speaker
summary: Update an existing speaker
operationId: update_speaker
parameters:
- name: speakerId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
404:
description: speaker not found
content: {}
security:
- api_key: []
x-mojo-to:
controller: speaker
action: update
get:
tags:
- speaker
summary: Get a speaker
operationId: get_speaker
parameters:
- name: speakerId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Speaker'
404:
description: Speaker not found
content: {}
x-mojo-to:
controller: speaker
action: getById
security:
- api_key: []
delete:
tags:
- speaker
summary: Remove a speaker from the system
operationId: delete_speaker
parameters:
- name: speakerId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: successful operation
content: {}
404:
description: speaker not found
content: {}
security:
- api_key: []
/room:
post:
tags:
- room
summary: Add a room
operationId: add_room
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Room'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Room'
security:
- api_key: []
x-mojo-to:
controller: room
action: add
/room/{roomId}:
patch:
tags:
- room
summary: Update a room
operationId: update_room
parameters:
- name: roomId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Room'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Room'
security:
- api_key: []
x-mojo-to:
controller: room
action: update
get:
tags:
- room
summary: Retrieve a room's details
operationId: get_room
parameters:
- name: roomId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Room'
x-mojo-to:
controller: room
action: getById
delete:
tags:
- room
summary: Remove a room
operationId: delete_room
parameters:
- name: roomId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
type: integer
format: int64
security:
- api_key: []
x-mojo-to:
controller: room
action: delete
/room/list:
get:
tags:
- room
summary: Retrieve the list of rooms
operationId: room_list
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Room'
x-mojo-to:
controller: room
action: list
/track:
post:
tags:
- track
summary: Add a track
operationId: add_track
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Track'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Track'
security:
- api_key: []
x-mojo-to:
controller: track
action: add
/track/list:
get:
tags:
- track
summary: Retrieve the list of tracks
operationId: track_list
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Track'
x-mojo-to:
controller: track
action: list
/track/{trackId}:
patch:
tags:
- track
summary: Update a track
operationId: update_track
parameters:
- name: trackId
in: path
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Track'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Track'
security:
- api_key: []
x-mojo-to:
controller: track
action: update
get:
tags:
- track
summary: Retrieve a track by ID
operationId: get_track
parameters:
- name: trackId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Track'
x-mojo-to:
controller: track
action: getById
delete:
tags:
- track
summary: Delete a track
operationId: delete_track
parameters:
- name: trackId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content: {}
security:
- api_key: []
x-mojo-to:
controller: track
action: delete
/user:
post:
tags:
- user
summary: Add a user
operationId: add_user
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
security:
- api_key: []
x-mojo-to:
controller: user
action: add
/user/{userId}:
patch:
tags:
- user
summary: Update a user
operationId: update_user
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
parameters:
- name: userId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
security:
- api_key: []
x-mojo-to:
controller: user
action: update
get:
tags:
- user
summary: Retrieve a user's details
operationId: get_user
parameters:
- name: userId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
security:
- api_key: []
x-mojo-to:
controller: user
action: getById
delete:
tags:
- user
summary: Delete a user
operationId: delete_user
parameters:
- name: userId
in: path
required: true
schema:
type: integer
format: int64
responses:
200:
description: OK
content: {}
security:
- api_key: []
x-mojo-to:
controller: user
action: delete
/user/list:
get:
tags:
- user
summary: Retrieve the list of all users
operationId: user_list
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
security:
- api_key: []
x-mojo-to:
controller: user
action: list
components:
schemas:
ConfigData:
type: object
properties:
event:
$ref: '#/components/schemas/Event/properties/id'
Talk:
type: object
properties:
id:
type: integer
format: int64
description: ID of the talk
room:
type: integer
format: int64
event:
type: integer
format: int64
nonce:
type: string
description: nonce (URL part to be handed out to unauthenticated reviewers) of this talk
slug:
type: string
description: unique part of the string, will be used for (part of) the output name
starttime:
type: string
format: date-time
description: time this talk will (have) start(ed)
endtime:
type: string
format: date-time
description: time this talk will (have) end(ed)
title:
type: string
description: title of this talk
subtitle:
type: string
nullable: true
description: subtitle of this talk, if any
state:
type: string
default: waiting_for_files
description: current state of the talk
enum:
- waiting_for_files
- cutting
- generating_previews
- notification
- preview
- transcoding
- uploading
- publishing
- notify_final
- finalreview
- announcing
- done
- injecting
- remove
- removing
- broken
- needs_work
- lost
- ignored
progress:
type: string
default: waiting
description: how far along the talk is in the current state
enum:
- waiting
- scheduled
- running
- done
- failed
prelen:
type: string
format: time
description: length of the 'pre' video of this talk, if any
nullable: true
postlen:
type: string
format: time
description: length of the 'post' video of this talk, if any
nullable: true
track:
type: integer
format: int64
nullable: true
description: Track this talk is being held in, if any
reviewer:
type: integer
format: int64
nullable: true
description: Reviewer who last touched this talk, if known
perc:
type: integer
format: int32
nullable: true
description: percentage completion of the current state
apologynote:
type: string
nullable: true
description: apology note for technical issues, if any
description:
type: string
nullable: true
description: long-form description of this talk
active_stream:
type: string
default: ""
description: stream of this talk that is currently active
upstreamid:
type: string
flags:
type: object
description: JSON object of flags on this talk, if any
example: {"is_injected":false}
nullable: true
Event:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
inputdir:
type: string
nullable: true
outputdir:
type: string
nullable: true
Room:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
altname:
type: string
nullable: true
outputname:
type: string
nullable: true
Track:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
email:
type: string
format: email
upstreamid:
type: string
User:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
email:
type: string
format: email
isAdmin:
type: boolean
isVolunteer:
type: boolean
limitToRoom:
type: integer
format: int64
Speaker:
type: object
properties:
id:
type: integer
format: int64
email:
type: string
format: email
nullable: true
name:
type: string
upstreamid:
type: string
nullable: true
TalkData:
type: object
properties:
start:
type: string
format: date-time
end:
type: string
format: date-time
start_iso:
type: string
format: date-time
end_iso:
type: string
format: date-time
TalkReadable:
type: object
properties:
title:
$ref: '#/components/schemas/Talk/properties/title'
reviewurl:
type: string
example: '/r/685982011ffda40c772395355db9dc9b699afb4957eb3478d3c3f9e144f995c9'
nullable: true
nonce:
$ref: '#/components/schemas/Talk/properties/nonce'
speakers:
type: string
example: 'Wouter Verhelst, Tammy Verhelst and Roel Verhelst'
room:
$ref: '#/components/schemas/Room/properties/name'
starttime:
$ref: '#/components/schemas/Talk/properties/starttime'
endtime:
$ref: '#/components/schemas/Talk/properties/endtime'
state:
$ref: '#/components/schemas/Talk/properties/state'
progress:
$ref: '#/components/schemas/Talk/properties/progress'
TalkCorrections:
type: object
properties:
offset_audio:
type: string
audio_channel:
type: string
length_adj:
type: string
offset_start:
type: string
serial:
type: string
Collection:
type: object
properties:
class:
type: string
enum:
- direct
- S3
baseurl:
type: string
example: "/local/path"
name:
type: string
RawFile:
type: object
properties:
id:
type: integer
format: int64
filename:
type: string
example: "/full/path/to/local/file"
room:
$ref: '#/components/schemas/Room/properties/id'
starttime:
type: string
format: date-time
endtime:
type: string
format: date-time
stream:
type: string
example: ''
mtime:
type: string
format: date-time
ReleasedData:
type: object
properties:
conference:
type: object
properties:
date:
type: array
items:
type: string
format: date
title:
type: string
video_formats:
type: object
additionalProperties:
type: object
properties:
acodec:
type: string
bitrate:
type: string
resolution:
type: string
vcodec:
type: string
container:
type: string
video_base:
type: string
schedule:
type: string
videos:
type: array
items:
type: object
properties:
description:
type: string
details_url:
type: string
start:
type: string
format: date-time
end:
type: string
format: date-time
eventid:
type: string
room:
type: string
speakers:
type: array
items:
type: string
title:
type: string
video:
type: string
alt_formats:
type: object
additionalProperties:
type: string
securitySchemes:
api_key:
type: apiKey
name: X-SReview-Key
in: header
SReview-0.8.0/lib/SReview/Talk.pm 0000644 0001750 0001750 00000053537 14111744013 016133 0 ustar wouter wouter package SReview::Talk;
use Moose;
use Mojo::Pg;
use Mojo::Template;
use Mojo::JSON qw/encode_json decode_json/;
use SReview::Config::Common;
use SReview::Talk::State;
use SReview::Video;
use SReview::Video::ProfileFactory;
use DateTime::Format::Pg;
my $config = SReview::Config::Common::setup;
my $pg = Mojo::Pg->new->dsn($config->get('dbistring')) or die "Cannot connect to database!";
=head1 NAME
SReview::Talk - Database abstraction for talks in the SReview database
=head1 SYNOPSIS
use SReview::Talk;
my $talk = SReview::Talk->new(talkid => 1);
print $talk->nonce;
my $nonce = $talk->nonce;
my $talk_alt = SReview::Talk->by_nonce($nonce);
print $talk_alt->talkid; # 1
$talk->add_correction(length_adj => 1);
$talk->done_correcting;
=head1 DESCRIPTION
SReview::Talk provides a (Moose-based) object-oriented interface to the
data related to a talk that is stored in the SReview database. Although
it is not yet used everywhere, the intention is for it to eventually
replace all the direct PostgreSQL calls.
=head1 PROPERTIES
=head2 talkid
The unique ID of the talk. Required attribute at construction time (but
see the C method, below). Is used to look up the relevant data
in the database.
=cut
has 'talkid' => (
required => 1,
is => 'ro',
trigger => sub {
my $self = shift;
my $val = shift;
my $st = $pg->db->dbh->prepare("SELECT count(*) FROM talks WHERE id = ?");
$st->execute($val);
die "Talk does not exist.\n" unless $st->rows == 1;
},
);
=head2 pathinfo
Helper property to look up information from the database. Should not be
used directly.
=cut
has 'pathinfo' => (
lazy => 1,
is => 'bare',
builder => '_load_pathinfo',
reader => '_get_pathinfo',
);
sub _load_pathinfo {
my $self = shift;
my $pathinfo = {};
my $eventdata = $pg->db->dbh->prepare("SELECT events.id AS eventid, events.name AS event, events.outputdir AS event_output, rooms.name AS room, rooms.outputname AS room_output, rooms.id AS room_id, talks.starttime, talks.starttime::date AS date, to_char(starttime, 'DD Month yyyy at HH:MI') AS readable_date, to_char(talks.starttime, 'yyyy') AS year, talks.endtime, talks.slug, talks.title, talks.subtitle, talks.state, talks.nonce, talks.apologynote FROM talks JOIN events ON talks.event = events.id JOIN rooms ON rooms.id = talks.room WHERE talks.id = ?");
$eventdata->execute($self->talkid);
my $row = $eventdata->fetchrow_hashref();
$pathinfo->{"workdir"} = join('/', $row->{eventid}, $row->{date}, substr($row->{room}, 0, 1));
my @elements = ($config->get('outputdir'));
foreach my $element(@{$config->get('output_subdirs')}) {
push @elements, $row->{$element};
}
$pathinfo->{"finaldir"} = join('/', @elements);
$pathinfo->{"slug"} = $row->{"slug"};
$pathinfo->{"raw"} = $row;
return $pathinfo;
}
=head2 flags
Flags set on this talk. Setter: C; getter: C. Flags can be deleted with C.
=cut
has 'flags' => (
is => 'rw',
traits => [ 'Hash' ],
isa => 'HashRef[Bool]',
builder => '_probe_flags',
lazy => 1,
predicate => '_has_flags',
handles => {
set_flag => 'set',
get_flag => 'get',
delete_flag => 'delete',
},
);
sub _probe_flags {
my $self = shift;
my $st = $pg->db->dbh->prepare("SELECT flags FROM talks WHERE id = ?");
$st->execute($self->talkid);
my $row = $st->fetchrow_arrayref;
if(defined($row->[0])) {
return decode_json($row->[0]);
}
return {};
}
has 'active_stream' => (
is => 'rw',
builder => '_probe_stream',
lazy => 1,
predicate => 'has_stream',
);
sub _probe_stream {
my $self = shift;
my $st = $pg->db->dbh->prepare("SELECT active_stream FROM talks WHERE id = ?");
$st->execute($self->talkid);
my $row = $st->fetchrow_arrayref;
return $row->[0];
}
=head2 apology
The apology note, if any. Predicate: C.
=cut
has 'apology' => (
lazy => 1,
is => 'rw',
builder => '_load_apology',
clearer => 'clear_apology',
predicate => 'has_apology',
);
sub _load_apology {
return shift->_get_pathinfo->{raw}{apologynote};
}
=head2 comment
The comments that the user entered in the "other brokenness" field.
Predicate: C; clearer: C.
=cut
has 'comment' => (
lazy => 1,
is => 'rw',
builder => '_load_comment',
clearer => 'clear_comment',
predicate => 'has_comment',
);
sub _load_comment {
my $self = shift;
my $st = $pg->db->dbh->prepare("WITH orderedlog(talk, comment, logdate) AS (SELECT talk, comment, logdate FROM commentlog ORDER BY logdate DESC) SELECT talk, string_agg(logdate || E'\n' || comment, E'\n\n') AS comments FROM orderedlog WHERE talk = ? GROUP BY talk");
$st->execute($self->talkid);
my $row = $st->fetchrow_hashref;
return $row->{comments};
}
=head2 corrected_times
The start- and endtime of the talk, with corrections (if any) applied.
=cut
has 'corrected_times' => (
lazy => 1,
is => 'ro',
builder => '_load_corrected_times',
);
sub _load_corrected_times {
my $self = shift;
my $times = {};
my $st = $pg->db->dbh->prepare("SELECT starttime, to_char(starttime, 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS isostart, endtime, to_char(endtime, 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS isoend from talks WHERE id = ?");
$st->execute($self->talkid);
die "talk lost" unless $st->rows > 0;
my $row = $st->fetchrow_hashref();
$times->{start} = $row->{starttime};
$times->{end} = $row->{endtime};
$times->{start_iso} = $row->{isostart};
$times->{end_iso} = $row->{isoend};
$st = $pg->db->dbh->prepare("SELECT coalesce(talks.starttime + (corrections.property_value || ' seconds')::interval, talks.starttime) AS corrected_time, to_char(coalesce(talks.starttime + (corrections.property_value || ' seconds')::interval, talks.starttime), 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS isotime FROM talks LEFT JOIN corrections ON talks.id = corrections.talk LEFT JOIN properties ON properties.id = corrections.property WHERE talks.id = ? AND properties.name = 'offset_start'");
$st->execute($self->talkid);
if($st->rows > 0) {
$row = $st->fetchrow_hashref();
$times->{start} = $row->{corrected_time};
$times->{start_iso} = $row->{isotime};
}
$st = $pg->db->dbh->prepare("SELECT corrected_time, to_char(corrected_time, 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS isotime FROM (select ?::timestamptz + (talks.endtime - talks.starttime) + (coalesce(corrections.property_value, '0') || ' seconds')::interval AS corrected_time FROM talks LEFT JOIN corrections ON talks.id = corrections.talk LEFT JOIN properties ON properties.id = corrections.property WHERE talks.id = ? AND properties.name = 'length_adj') AS sq");
$st->execute($times->{start}, $self->talkid);
if($st->rows > 0) {
$row = $st->fetchrow_hashref();
$times->{end} = $row->{corrected_time};
$times->{end_iso} = $row->{isotime};
}
return $times;
}
=head2 nonce
The talk's unique hex string, used to look it up for review.
=cut
has 'nonce' => (
is => 'rw',
builder => '_load_nonce',
lazy => 1,
);
sub _load_nonce {
return shift->_get_pathinfo->{raw}{nonce};
}
=head2 date
The date on which the talk happened
=cut
has 'date' => (
lazy => 1,
is => 'rw',
builder => '_load_date',
);
sub _load_date {
return shift->_get_pathinfo->{raw}{date};
}
=head2 readable_date
The date on which the talk happened, in a (somewhat) more human-readable
format than the C property.
=cut
has 'readable_date' => (
lazy => 1,
is => 'rw',
builder => '_load_readable_date',
);
sub _load_readable_date {
return shift->_get_pathinfo->{raw}{readable_date};
}
=head2 eventname
The name of the event of which this talk is part
=cut
has 'eventname' => (
lazy => 1,
is => 'ro',
builder => '_load_eventname',
);
sub _load_eventname {
my $self = shift;
return $self->_get_pathinfo->{raw}{event};
}
=head2 event_output
The name of the event as used in output directories, if any.
=cut
has 'event_output' => (
lazy => 1,
is => 'ro',
builder => '_load_event_output',
);
sub _load_event_output {
return shift->_get_pathinfo->{raw}{event_output};
}
=head2 state
The current state of the talk, as an L
=cut
has 'state' => (
lazy => 1,
is => 'rw',
isa => 'SReview::Talk::State',
builder => '_load_state',
);
sub _load_state {
my $self = shift;
return SReview::Talk::State->new($self->_get_pathinfo->{raw}{state});
}
=head2 title
The title of the talk
=cut
has 'title' => (
lazy => 1,
is => 'rw',
builder => '_load_title',
);
sub _load_title {
my $self = shift;
return $self->_get_pathinfo->{raw}{title};
}
=head2 subtitle
The subtitle of the talk
=cut
has 'subtitle' => (
lazy => 1,
is => 'rw',
builder => '_load_subtitle',
);
sub _load_subtitle {
my $self = shift;
return $self->_get_pathinfo->{raw}{subtitle};
}
=head2 workdir
The working directory where the files for this talk should be stored
=cut
has 'workdir' => (
lazy => 1,
is => 'rw',
builder => '_load_workdir',
);
sub _load_workdir {
my $self = shift;
return join('/', $config->get("pubdir"), $self->_get_pathinfo->{"workdir"});
}
=head2 relative_name
The relative path- and file name under the output directory for this
talk.
=cut
has 'relative_name' => (
lazy => 1,
is => 'rw',
builder => '_load_relative_name',
);
sub _load_relative_name {
my $self = shift;
return join('/', $self->_get_pathinfo->{"workdir"}, $self->_get_pathinfo->{'slug'});
}
=head2 outname
The output name for this talk
=cut
has 'outname' => (
lazy => 1,
is => 'rw',
builder => '_load_outname',
);
sub _load_outname {
my $self = shift;
return join('/', $self->workdir, $self->_get_pathinfo->{"slug"});
}
=head2 finaldir
The directory in which things are stored
=cut
has 'finaldir' => (
lazy => 1,
is => 'rw',
builder => '_load_finaldir',
);
sub _load_finaldir {
my $self = shift;
return $self->_get_pathinfo->{"finaldir"};
}
=head2 slug
A short, safe representation of the talk; used for filenames.
=cut
has 'slug' => (
lazy => 1,
is => 'rw',
builder => '_load_slug',
);
sub _load_slug {
my $self = shift;
return $self->_get_pathinfo->{"slug"};
}
=head2 corrections
The corrections that are set on this talk.
Supports:
=over
=item has_correction
check whether a correction exists (by name)
=item set_correction
Overwrite a correction with a new value
=item clear_correction
Remove a correction from the set of corrections
=item correction_pairs
Get a key/value list of corrections
=back
=cut
has 'corrections' => (
traits => ['Hash'],
isa => 'HashRef[Str]',
lazy => 1,
is => 'rw',
builder => '_load_corrections',
clearer => '_clear_corrections',
handles => {
has_correction => 'exists',
set_correction => 'set',
clear_correction => 'delete',
correction_pairs => 'kv',
},
);
sub _load_corrections {
my $self = shift;
my $corrections_data = $pg->db->dbh->prepare("SELECT corrections.talk, properties.name AS property, corrections.property_value FROM corrections LEFT JOIN properties ON corrections.property = properties.id WHERE talk = ?");
$corrections_data->execute($self->talkid);
my %corrections;
while(my $row = $corrections_data->fetchrow_hashref()) {
my $name = $row->{property};
my $val = $row->{property_value};
$corrections{$name} = $val;
}
foreach my $prop ("offset_start", "length_adj", "offset_audio", "audio_channel") {
if(!exists($corrections{$prop})) {
$corrections{$prop} = 0;
}
}
return \%corrections;
}
=head2 video_fragments
Gets a list of hashes with data on the fragments of video files that are
necessary to build the talk, given the schedule and the current
corrections.
Each hash contains:
=over
=item talkid
The talk ID for fragments that are part of the main video; -1 for
fragments that are part of the pre video; and -2 for fragments that are
part of the post video.
=item rawid
The unique ID of the raw file
=item raw_filename
The filename of the raw file
=item fragment_start
The offset into the raw file where the interesting content begins.
=item raw_length
The length of the entire video (should be the same for each fragment)
=item raw_length_corrected
The length of the interesting content in I raw file
=back
=cut
has 'video_fragments' => (
lazy => 1,
is => 'rw',
builder => '_load_video_fragments',
);
sub _load_video_fragments {
my $self = shift;
my $corrections = $self->corrections;
my $talk_data = $pg->db->dbh->prepare("SELECT talkid, rawid, raw_filename, extract(epoch from fragment_start) AS fragment_start, extract(epoch from raw_length) as raw_length, extract(epoch from raw_length_corrected) as raw_length_corrected FROM adjusted_raw_talks(?, make_interval(secs :=?::numeric), make_interval(secs := ?::numeric)) ORDER BY talk_start, raw_start");
$talk_data->execute($self->talkid, $corrections->{"offset_start"}, $corrections->{"length_adj"});
my $rows;
while(my $row = $talk_data->fetchrow_hashref()) {
push @$rows, $row;
}
return $rows;
}
=head2 avs_video_fragments
The same values as the video_fragments attribute, but with every length
extended as needed for A/V sync operations.
=cut
has 'avs_video_fragments' => (
lazy => 1,
is => 'rw',
builder => '_load_avs_video_fragments',
);
sub _load_avs_video_fragments {
my $self = shift;
my $corrections = $self->corrections;
if($corrections->{offset_audio} == 0) {
return $self->video_fragments;
}
my $talk_data = $pg->db->dbh->prepare("SELECT talkid, rawid, raw_filename, extract(epoch from fragment_start) as fragment_start, extract(epoch from raw_length) as raw_length, extract(epoch from raw_length_corrected) as raw_length_corrected from adjusted_raw_talks(?, make_interval(secs :=?::numeric), make_interval(secs := ?::numeric), make_interval(secs :=abs(?::numeric))) order by talk_start, raw_start");
$talk_data->execute($self->talkid, $corrections->{"offset_start"}, $corrections->{"length_adj"}, $corrections->{"offset_audio"});
my $rows;
while(my $row = $talk_data->fetchrow_hashref()) {
push @$rows, $row;
}
return $rows;
}
=head2 speakers
The names of the speakers as a single string, in the format 'Firstname
Lastname, Firstname Lastname, ..., Firstname Lastname and Firstname
Lastname'
=cut
has 'speakers' => (
lazy => 1,
is => 'ro',
builder => '_load_speakers',
);
sub _load_speakers {
my $self = shift;
my $spk = $pg->db->dbh->prepare("SELECT speakerlist(?)");
$spk->execute($self->talkid);
my $row = $spk->fetchrow_arrayref;
return $row->[0];
}
=head2 speakerlist
An array of speaker names
=cut
has 'speakerlist' => (
lazy => 1,
is => 'ro',
isa => 'ArrayRef[Str]',
builder => '_load_speakerlist',
);
sub _load_speakerlist {
my $self = shift;
my $query = $pg->db->dbh->prepare("SELECT speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = ?");
$query->execute($self->talkid);
my $rv = [];
while(my $talk = $query->fetchrow_arrayref) {
push @$rv, $talk->[0];
}
return $rv;
}
=head2 room
The room in which the talk happened/will happen
=cut
has 'room' => (
lazy => 1,
is => 'rw',
builder => '_load_room',
);
sub _load_room {
return shift->_get_pathinfo->{raw}{room};
}
=head2 roomid
The unique ID of the room
=cut
has 'roomid' => (
lazy => 1,
is => 'rw',
builder => '_load_roomid',
);
sub _load_roomid {
return shift->_get_pathinfo->{raw}{room_id}
}
=head2 eventurl
The URL for the talk on the event's website. Only contains data if
C<$eventurl_format> is set in the config file; if it doesn't, returns
the empty string.
=cut
has 'eventurl' => (
lazy => 1,
is => 'rw',
builder => '_load_eventurl',
);
sub _load_eventurl {
my $self = shift;
my $mt = Mojo::Template->new;
if(defined($config->get('eventurl_format'))) {
my $rv = $mt->vars(1)->render($config->get('eventurl_format'), {
slug => $self->slug,
room => $self->room,
date => $self->date,
event => $self->eventname,
event_output => $self->event_output,
talk => $self,
year => $self->_get_pathinfo->{raw}{year}});
chomp $rv;
return $rv;
}
return "";
}
=head2 output_video_urls
An array of URLs for the output videos, as they will be published. Used by final review.
=cut
has 'output_video_urls' => (
lazy => 1,
is => 'ro',
isa => 'ArrayRef[HashRef[Str]]',
builder => '_load_output_urls',
);
sub _load_output_urls {
my $self = shift;
my $mt = Mojo::Template->new;
my $form = $config->get("output_video_url_format");
my $rv = [];
if(defined($form)) {
my $vid = SReview::Video->new(url => "");
foreach my $prof(@{$config->get("output_profiles")}) {
my $item = {prof => $prof};
if($prof eq "copy") {
$prof = $config->get("input_profile");
}
my $exten = SReview::Video::ProfileFactory->create($prof, $vid)->exten;
my $url = $mt->vars(1)->render($form, {
talk => $self,
year => $self->_get_pathinfo->{raw}{year},
exten => $exten
});
chomp $url;
$item->{url} = $url;
push @$rv, $item;
}
}
return $rv;
}
=head2 preview_exten
The file extension of the preview file (.webm or .mp4)
=cut
has 'preview_exten' => (
lazy => 1,
is => 'ro',
builder => '_load_preview_exten',
);
# TODO: autodetect this, rather than hardcoding it
sub _load_preview_exten {
return $config->get('preview_exten');
}
=head2 scheduled_length
The length of the talk, as scheduled
=cut
has 'scheduled_length' => (
is => "ro",
lazy => 1,
builder => "_load_scheduled_length",
);
sub _load_scheduled_length {
my $self = shift;
my $start = DateTime::Format::Pg->parse_datetime($self->_get_pathinfo->{raw}{starttime});
my $end = DateTime::Format::Pg->parse_datetime($self->_get_pathinfo->{raw}{endtime});
return $end->epoch - $start->epoch;
}
=head1 METHODS
=head2 by_nonce
Looks up (and returns) the talk by nonce, rather than by talk ID
=cut
sub by_nonce {
my $klass = shift;
my $nonce = shift;
my $st = $pg->db->dbh->prepare("SELECT * FROM talks WHERE nonce = ?");
$st->execute($nonce);
die "Talk does not exist.\n" unless $st->rows == 1;
my $row = $st->fetchrow_arrayref;
my $rv = SReview::Talk->new(talkid => $row->[0], nonce => $nonce);
return $rv;
}
=head2 by_slug
Looks up (and returns) the talk by slug, rather than by talk ID
=cut
sub by_slug {
my $klass = shift;
my $slug = shift;
my $event = shift;
my $st;
if(defined($event)) {
$st = $pg->db->dbh->prepare("SELECT * FROM talks WHERE slug = ? AND event = ?");
$st->execute($slug, $event);
} else {
$st = $pg->db->dbh->prepare("SELECT * FROM talks WHERE slug = ?");
$st->execute($slug);
}
die "Talk does not exist (or the slug is not unique in the database).\n" unless $st->rows == 1;
my $row = $st->fetchrow_arrayref;
my $rv = SReview::Talk->new(talkid => $row->[0], slug => $slug);
return $rv;
}
=head2 add_correction
Interpret a correction as a number, and add the passed parameter to it.
The new value of the correction will be the sum of the parameter and the
old correction.
=cut
sub add_correction {
my $self = shift;
my $corrname = shift;
my $value = shift;
if($self->has_correction($corrname)) {
$value = $self->corrections->{$corrname} + $value;
}
$self->set_correction($corrname, $value);
}
=head2 done_correcting
Commit the created corrections to the database. Also commits other
things, like the comment and the flags.
=cut
sub done_correcting {
my $self = shift;
my $db = $pg->db->dbh;
my $st = $db->prepare("INSERT INTO corrections(talk, property, property_value) VALUES (?, (SELECT id FROM properties WHERE name = ?), ?)");
$self->add_correction(serial => 1);
my $corrs = $self->corrections;
my $start = $corrs->{offset_start};
my $end = $corrs->{offset_end};
$start = 0 unless defined $start;
$end = 0 unless defined $end;
$self->set_correction(length_adj => $end - $start);
foreach my $pair($self->correction_pairs) {
$st->execute($self->talkid, $pair->[0], $pair->[1]);
}
if($self->has_comment) {
$db->prepare("INSERT INTO commentlog(comment, talk, state) VALUES (?, ?, ?)")->execute($self->comment, $self->talkid, $self->state);
}
if($self->has_apology) {
$db->prepare("UPDATE talks SET apologynote=? WHERE id = ?")->execute($self->apology, $self->talkid);
}
if($self->_has_flags) {
$db->prepare("UPDATE talks SET flags=? WHERE id = ?")->execute(encode_json($self->flags), $self->talkid);
}
if($self->has_stream) {
$db->prepare("UPDATE talks SET active_stream=? WHERE id = ?")->execute($self->active_stream, $self->talkid);
}
}
=head2 set_state
Override the state of the talk to a new state, ignoring the state
transitions. Note, does not update the object, so this should be done
just before destroying it.
=cut
sub set_state {
my $self = shift;
my $newstate = shift;
my $progress = shift;
$progress = 'waiting' unless defined($progress);
my $st = $pg->db->dbh->prepare("UPDATE talks SET state=?, progress=? WHERE id=?");
$st->execute($newstate, $progress, $self->talkid);
}
=head2 state_done
Set the progress to "done" in the given state. Does nothing if the talk
has since moved to another state.
=cut
sub state_done {
my $self = shift;
my $state = shift;
my $st = $pg->db->dbh->prepare("UPDATE talks SET progress='done' WHERE state = ? AND id = ?");
$st->execute($state, $self->talkid);
}
=head2 reset_corrections
Clear all corrections, except the serial one. Used when a user requests
that the talk be reset to default.
=cut
sub reset_corrections {
my $self = shift;
$self->add_correction(serial => 1);
$pg->db->dbh->prepare("DELETE FROM corrections WHERE talk = ? AND property NOT IN (SELECT id FROM properties WHERE name = 'serial')")->execute($self->talkid) or die $!;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Access.pm 0000644 0001750 0001750 00000000654 13426272044 016441 0 ustar wouter wouter package SReview::Access;
use Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw(admin_for);
sub admin_for($$) {
my $c = shift;
my $talk = shift;
if(exists($c->session->{admin})) {
return 1;
}
if(exists($c->session->{id})) {
my $st = $c->dbh->prepare("SELECT room FROM users WHERE id = ?");
$st->execute($c->session->{id});
my $row = $st->fetchrow_hashref;
if($talk->roomid = $row->{room}) {
return 1;
}
}
}
SReview-0.8.0/lib/SReview/Config/ 0000755 0001750 0001750 00000000000 14116343665 016107 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Config/Common.pm 0000644 0001750 0001750 00000034335 14111641426 017674 0 ustar wouter wouter package SReview::Config::Common;
use SReview::Config;
use strict;
use warnings;
use feature 'state';
use Mojo::JSON qw/decode_json/;
sub get_default_cfile {
my $dir = $ENV{SREVIEW_WDIR};
my $write = shift;
$dir = "." unless defined($dir);
my $cfile = join('/', $dir, 'config.pm');
if(!-f $cfile && !exists($ENV{SREVIEW_WDIR})) {
$cfile = join('/', '', 'etc', 'sreview', 'config.pm');
}
return $cfile;
}
sub compute_dbistring {
if(!exists($ENV{SREVIEW_DBICOMPONENTS})) {
return undef;
}
my @comps = ();
foreach my $comp(split /\s/, $ENV{SREVIEW_DBICOMPONENTS}) {
my $COMP = uc $comp;
push @comps, "$comp=" . $ENV{"SREVIEW_DBI_" . $COMP};
}
return "dbi:Pg:" . join(";", @comps);
}
sub compute_accessconfig {
if(!exists($ENV{SREVIEW_S3_DEFAULT_ACCESSKEY}) || !exists($ENV{SREVIEW_S3_DEFAULT_SECRETKEY})) {
return undef;
}
my $rv = { default => {aws_access_key_id => $ENV{SREVIEW_S3_DEFAULT_ACCESSKEY}, aws_secret_access_key => $ENV{SREVIEW_S3_DEFAULT_SECRETKEY} } };
if(exists($ENV{SREVIEW_S3_DEFAULT_SECURE})) {
$rv->{secure} = $ENV{SREVIEW_S3_DEFAULT_SECURE};
}
if(exists($ENV{SREVIEW_S3_DEFAULT_HOST})) {
$rv->{host} = $ENV{SREVIEW_S3_DEFAULT_HOST};
}
if(exists($ENV{SREVIEW_S3_EXTRA_CONFIGS})) {
my $extras = decode_json($ENV{SREVIEW_S3_EXTRA_CONFIGS});
foreach my $extra(keys %$extras) {
$rv->{$extra} = $extras->{$extra};
}
}
return $rv;
}
sub setup {
my $cfile = shift;
if(!defined($cfile)) {
$cfile = get_default_cfile();
}
state $config;
return $config if(defined $config);
$config = SReview::Config->new($cfile);
# common values
$config->define('dbistring', 'The DBI connection string used to connect to the database', 'dbi:Pg:dbname=sreview');
$config->define_computed('dbistring', \&compute_dbistring);
$config->define('accessmethods', 'The way to access files for each collection. Can be \'direct\' or \'S3\'. For the latter, the \'$s3_access_config\' configuration needs to be set, too', {input => 'direct', output => 'direct', intermediate => 'direct'});
$config->define('s3_access_config', 'Configuration for accessing S3-compatible buckets. Any option that can be passed to the "new" method of the Net::Amazon::S3 Perl module can be passed to any of the child hashes of the toplevel hash. Uses the same toplevel keys as the "$accessmethods" configuration item, but falls back to "default"', {default => {}});
$config->define_computed('s3_access_config', \&compute_accessconfig);
$config->define('api_key', 'The API key, to allow access to the API', undef);
# Values for sreview-web
$config->define('event', 'The event to handle by this instance of SReview.');
$config->define('secret', 'A random secret key, used to encrypt the cookies.', '_INSECURE_DEFAULT_REPLACE_ME_');
$config->define("vid_prefix", "The URL prefix to be used for video data files", "/video");
$config->define("anonreviews", "Set to truthy if anonymous reviews should be allowed, or to falsy if not", 0);
$config->define("preview_exten", "The extension used by previews (webm or mp4). Should be autodetected in the future, but...", "webm");
$config->define("eventurl_format", "A Mojo::Template that generates an event URL. Used by the /released metadata URL", undef);
$config->define("adminuser", 'email address for the initial admin user created. Note: if this user is removed and this configuration value continues to exist, then the user will be recreated upon the next database initialization (which might be rather quick).', undef);
$config->define('adminpw', 'password for the admin user. See under "adminuser" for details.', undef);
$config->define('review_template', 'The template name to be used for the review page. Can be one of "full" (full editing capabilities) or "confirm" (confirmation only)', 'full');
$config->define('inject_fatal_checks', 'Checks to be run on an uploaded video. When a check fails, the upload is rejected. Same syntax as for inject_transcode_skip_checks.', {});
# Values for encoder scripts
$config->define('pubdir', 'The directory on the file system where files served by the webinterface should be stored', '/srv/sreview/web/public');
$config->define('workdir', 'A directory where encoder jobs can create a subdirectory for temporary files', '/tmp');
$config->define('outputdir', 'The base directory under which SReview should place the final released files', '/srv/sreview/output');
$config->define('output_subdirs', 'An array of fields to be used to create subdirectories under the output directory.', ['event', 'room', 'date']);
$config->define('script_output', 'The directory to which the output of scripts should be redirected', '/srv/sreview/script-output');
$config->define('preroll_template', 'An SVG template to be used as opening credits. Should have the same nominal dimensions (in pixels) as the video assets. May be a file or an http(s) URL.', undef);
$config->define('postroll_template', 'An SVG template to be used as closing credits. Should have the same nominal dimensions (in pixels) as the video assets. May be a file or an http(s) URL.', undef);
$config->define('postroll', 'A PNG file to be used as closing credits. Will only be used if no postroll_template was defined. Should have the same dimensions as the video assets. Must be a direct file.', undef);
$config->define('apology_template', 'An SVG template to be used as apology template (shown just after the opening credits when technical issues occurred. Should have the same nominal dimensions (in pixels) as the video assets. May be a file or an http(s) URL.', undef);
$config->define('output_profiles', 'An array of profiles, one for each encoding, to be used for output encodings', ['webm']);
$config->define('input_profile', 'The profile that is used for input videos.', undef);
$config->define('audio_multiplex_mode', 'The way in which the primary and backup audio are multiplexed in the input stream. One of \'stereo\' for the primary in the left channel of the first audio stream and the backup in the right channel, or \'astream\' for the primary in the first audio stream, and the backup in the second audio stream', 'stereo');
$config->define('normalizer', 'The implementation used to normalize audio. Currently only bs1770gain is supported', 'bs1770gain');
$config->define('web_pid_file', 'The PID file for the webinterface, when running under hypnotoad.','/var/run/sreview/sreview-web.pid');
$config->define('autoreview_detect', 'The script to run when using sreview-autoreview', undef);
# Values for detection script
$config->define('inputglob', 'A filename pattern (glob) that tells SReview where to find new files', '/srv/sreview/incoming/*/*/*');
$config->define('parse_re', 'A regular expression to parse a filename into year, month, day, hour, minute, second, room, and stream', '.*\/(?[^\/]+)(?(-[^\/-]+)?)\/(?\d{4})-(?\d{2})-(?\d{2})\/(?\d{2}):(?\d{2}):(?\d{2})');
$config->define('url_re', 'If set, used with parse_re in an s///g command to produce an input URL', undef);
# Values for dispatch script
$config->define('state_actions', 'A hash that tells SReview what to do with a talk when it is in a given state. Mojo::Template is used to transform these.', {
cutting => 'sreview-cut <%== $talkid %> > <%== $output_dir %>/cut.<%== $talkid %>.out 2> <%== $output_dir %>/cut.<%== $talkid %>.err',
generating_previews => 'sreview-previews <%== $talkid %> > <%== $output_dir %>/preview.<%== $talkid %>.out 2> <%== $output_dir %>/preview.<%== $talkid %>.err',
transcoding => 'sreview-transcode <%== $talkid %> > <%== $output_dir %>/trans.<%== $talkid %>.out 2> <%== $output_dir%>/trans.<%== $talkid %>.err',
uploading => 'sreview-skip <%== $talkid %>',
notification => 'sreview-skip <%== $talkid %>',
announcing => 'sreview-skip <%== $talkid %>',
injecting => 'sreview-inject -t <%== $talkid %>',
});
$config->define('query_limit', 'A maximum number of jobs that should be submitted in a single loop in sreview-dispatch. 0 means no limit.', 1);
$config->define('published_headers', 'The HTTP headers that indicate that the video is available now. Use _code for the HTTP status code.', undef);
$config->define('inject_actions', 'A command that tells SReview what to do with a talk that needs to be injected', 'sreview-inject <%== $talkid %> <%== $output_dir %>/inject.<%== $talkid %>.out 2> <%== $output_dir %>/cut.<%== $talkid %>.err');
# Values for notification script
$config->define('notify_actions', 'An array of things to do when notifying the readyness of a preview video. Can contain one or more of: email, command.', []);
$config->define('announce_actions', 'An array of things to do when announcing the completion of a transcode. Can contain one or more of: email, command.', []);
$config->define('notify_final_actions', 'An array of things to do when notifying the readiness of a final review. Can contain one or more of: email, command', []);
$config->define('email_template', 'A filename of a Mojo::Template template to process, returning the email body used in notifications or announcements. Can be overridden by announce_email_template or notify_email_template.', undef);
$config->define('notify_email_template', 'A filename of a Mojo::Template template to process, returning the email body used in notifications. Required, but defaults to the value of email_template', undef);
$config->define('announce_email_template', 'A filename of a Mojo::Template template to process, returning the email body used in announcements. Required, but defaults to the value of email_template', undef);
$config->define('notify_final_email_template', 'A filename of a Mojo::Template template to process, returning the email body used in final review notifications. Required, but defaults to the value of email_template', undef);
$config->define('email_from', 'The data for the From: header in any email. Required if notify_actions, notify_final_actions, or announce_actions includes email.', undef);
$config->define('notify_email_subject', 'The data for the Subject: header in the email. Required if notify_actions includes email.', undef);
$config->define('announce_email_subject', 'The data for the Subject: header in the email. Required if announc_actions includes email.', undef);
$config->define('notify_final_email_subject', 'The data for the Subject: header in the email. Required if notify_final_actions includes email.', undef);
$config->define('urlbase', 'The URL on which SReview runs. Note that this is used by sreview-notify to generate URLs, not by sreview-web.', '');
$config->define('notify_commands', 'An array of commands to run to perform notifications. Each component is passed through Mojo::Template before processing. To avoid quoting issues, it is a two-dimensional array, so that no shell will be called to run this.', [['echo', '<%== $title %>', 'is', 'available', 'at', '<%== $url %>']]);
$config->define('announce_commands', 'An array of commands to run to perform announcements. Each component is passed through Mojo::Template before processing. To avoid quoting issues, it is a two-dimensional array, so that no shell will be called to run this.', [['echo', '<%== $title %>', 'is', 'available', 'at', '<%== $url %>']]);
$config->define('notify_final_commands', 'An array of commands to run to perform final review notification. Each component is passed through Mojo::Template before processing. To avoid quoting issues, it is a two-dimensional array, so that no shell will be called to run this.', [['echo', '<%== $title %>', 'is', 'available', 'for', 'final', 'review', 'at', '<%== $url %>']]);
# Values for upload script
$config->define('upload_actions', 'An array of commands to run on each file to be uploaded. Each component is passed through Mojo::Template before processing. To avoid quoting issues, it is a two-dimensional array, so that no shell will be called to run this.', [['echo', '<%== $file %>', 'ready for upload']]);
$config->define('remove_actions', 'An array of commands to run on each file to be removed, when final review determines that the file needs to be reprocessed. Same format as upload_actions', [['echo', '<%== $file %>', 'ready for removal']]);
$config->define('cleanup', 'Whether to remove files after they have been published. Possible values: "all" (removes all files), "previews" (removes the output of sreview-cut, but not that of sreview-transcode), and "output" (removes the output of sreview-transcode, but not the output of sreview-cut). Other values will not remove files', 'none');
# for sreview-copy
$config->define('extra_collections', 'A hash of extra collection basenames. Can be used by sreview-copy.', undef);
# for sreview-keys
$config->define('authkeyfile', 'The authorized_keys file that sreview-keys should manage. If set to undef, the default authorized_keys file will be used.');
# for extending profiles
$config->define('extra_profiles', 'Any extra custom profiles you want to use. This hash should have two keys: the "parent" should be a name of a profile to subclass from, and the "settings" should contain a hash reference with attributes for the new profile to set', {});
# for sreview-import
$config->define('schedule_format', 'The format in which the schedule is set. Must be implemented as a child class of SReview::Schedule::Base', 'penta');
$config->define('schedule_options', 'The options to pass to the schedule parser as specified through schedule_format. See the documentation of your chosen parser for details.', {});
# for sreview-inject
$config->define('inject_transcode_skip_checks', "Minimums and maximums, or exact values, of video assets that cause sreview-inject to skip the transcode if they match the video asset", {});
$config->define('inject_collection', "The collection into which uploads are stored. One of: input, pub, or any of the keys of the 'extra_collections' hash", "input");
# for tuning command stuff
$config->define('command_tune', 'Some commands change incompatibly from one version to the next. This option exists to deal with such incompatibilities', {});
# for final review
$config->define('finalhosts', 'A list of hosts that may host videos for final review, to be added to Content-Security-Policy "media-src" directive.', undef);
$config->define('output_video_url_format', 'A Mojo::Template that will produce the URLs for the produced videos. Can use the $talk variable for the SReview::Talk, and the $exten variable for the extension of the current video profile');
return $config;
}
1;
SReview-0.8.0/lib/SReview/Talk/ 0000755 0001750 0001750 00000000000 14116343665 015575 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Talk/State.pm 0000644 0001750 0001750 00000000773 14054727355 017225 0 ustar wouter wouter package SReview::Talk::State;
use Class::Type::Enum values => [qw(
waiting_for_files
cutting
generating_previews
notification
preview
transcoding
uploading
publishing
notify_final
finalreview
announcing
done
injecting
remove
removing
broken
needs_work
lost
ignored
)];
use overload '<=>' => 'cmp', '++' => "incr", '--' => "decr";
sub incr {
if($_[0] eq "injecting") {
${$_[0]} = $_[0]->sym_to_ord->{generating_previews};
} else {
++${$_[0]};
}
}
sub decr {
--${$_[0]};
}
1;
SReview-0.8.0/lib/SReview/Template.pm 0000644 0001750 0001750 00000005707 14054727355 017027 0 ustar wouter wouter package SReview::Template;
use Moose;
use Mojo::Template;
use SReview::Config::Common;
=head1 NAME
SReview::Template - process a string or a file and apply changes to it
=head1 SYNOPSIS
use SReview::Template;
use SReview::Talk;
my $talk = SReview::Talk->new(...);
my $template = SReview::Template->new(talk => $talk, vars => { foo => "bar" }, regexvars => {"@FOO@" => "foo"});
my $processed = $template->string("The @FOO@ is <%== $foo %>, and the talk is titled <%== $talk->title %>");
# $processed now contains "The foo is bar, and the talk is titled ..."
# (with the actual talk title there)
$template->file("inputfile.txt", "outputfile.txt"
=head1 DESCRIPTION
C is a simple wrapper around L. All
the variables that are passed in to the "vars" parameter are passed as
named variables to L.
In addition, some bits of SReview previously did some simple sed-like
search-and-replace templating. For backwards compatibility, this module
also supports such search-and-replace templating (e.g.,
L has a few of those). These, however, are now
deprecated; the C<$talk> variable and L-style templates
should be used instead.
=head1 ATTRIBUTES
C objects support the following attributes:
=head2 talk
An L object for the talk that this template is for.
Required. Will be passed on to the template as the C<$talk> variable.
=cut
has 'talk' => (
is => 'ro',
isa => 'SReview::Talk',
required => 1,
);
has '_mt' => (
is => 'ro',
isa => 'Mojo::Template',
default => sub { my $mt = Mojo::Template->new(); $mt->vars(1); },
);
=head2 vars
Additional L variables to be made available to the
template.
=cut
has 'vars' => (
is => 'ro',
isa => 'HashRef',
);
=head2 regexvars
Variables to be replaced by search-and-replace.
=cut
has 'regexvars' => (
is => 'ro',
isa => 'HashRef[Str]',
predicate => '_has_regexes',
);
=head1 METHODS
=head2 file
A method to process an input file through the templating engine into an
output file.
Takes to arguments: the name of the input file, followed by the name of
the output file.
Is implemented in terms of the C method.
=cut
sub file {
my $self = shift;
my $inputname = shift;
my $outputname = shift;
local $_;
open my $input, '<:encoding(UTF-8)', $inputname;
open my $output, '>:encoding(UTF-8)', $outputname;
while(<$input>) {
$_ = $self->string($_);
print $output $_;
}
close $input;
close $output;
}
=head2 string
A function to process a string, passed as the only argument. Returns the
result of the template function.
=cut
sub string {
my $self = shift;
my $string = shift;
my $vars = $self->vars;
$vars->{talk} = $self->talk;
my $rendered = $self->_mt->render($string, $vars);
if($self->_has_regexes) {
my $revals = $self->regexvars;
foreach my $key(keys %{$revals}) {
$rendered =~ s/$key/$revals->{$key}/g;
}
}
return $rendered;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Map.pm 0000644 0001750 0001750 00000002655 14054727443 015766 0 ustar wouter wouter package SReview::Map;
use Moose;
use Carp;
has 'input' => (
required => 1,
is => 'rw',
isa => 'SReview::Video',
);
has 'type' => (
isa => 'Str',
is => 'rw',
default => 'channel',
);
has 'choice' => (
isa => 'Str',
is => 'rw',
default => 'left',
);
sub arguments($$) {
my $self = shift;
my $index = shift;
my $stream_id;
if($self->type eq "channel") {
if($self->choice eq "both") {
return ('-ac', '1');
}
$stream_id = $self->input->astream_id;
if($self->choice eq "left") {
return ('-map_channel', "$index.$stream_id.0");
} elsif($self->choice eq "right") {
return ('-map_channel', "$index.$stream_id.1");
} else {
# other choices exist?!?
...
}
} elsif($self->type eq "stream") {
if($self->choice eq 'audio') {
return ('-map', "$index:a");
} elsif($self->choice eq 'video') {
return ('-map', "$index:v");
} else {
...
}
} elsif($self->type eq "astream") {
my $choice = $self->choice;
if($choice > $self->input->astream_count) {
croak("Invalid audio stream, not supported by input video");
}
if($choice == -1) {
my $ids = $self->input->astream_ids;
my $id1 = $ids->[0];
my $id2 = $ids->[1];
return ('-filter_complex', "[$index:$id1][$index:$id2]amix=inputs=2:duration=first");
}
return ('-map', "$index:a:$choice");
} elsif($self->type eq "allcopy") {
return ('-map', '0');
} elsif($self->type eq "none") {
return ();
} else {
...
}
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Files/ 0000755 0001750 0001750 00000000000 14116343665 015744 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Files/Factory.pm 0000644 0001750 0001750 00000015763 14051446221 017713 0 ustar wouter wouter package SReview::Files::Base;
use Moose;
has 'is_collection' => (
isa => 'Bool',
is => 'ro',
);
has 'url' => (
isa => 'Str',
is => 'ro',
lazy => 1,
builder => '_probe_url',
);
no Moose;
package SReview::Files::Access::Base;
use Moose;
use DateTime;
use Carp;
extends 'SReview::Files::Base';
has '+is_collection' => (
default => 0,
);
has 'relname' => (
is => 'rw',
isa => 'Str',
required => 1,
);
has 'filename' => (
isa => 'Str',
is => 'ro',
lazy => 1,
builder => '_get_file',
);
has 'mtime' => (
isa => 'DateTime',
is => 'ro',
lazy => 1,
builder => '_probe_mtime',
);
has 'baseurl' => (
isa => 'Str',
is => 'ro',
required => 1,
);
has 'create' => (
is => 'rw',
traits => ['Bool'],
isa => 'Bool',
default => 0,
required => 1,
handles => {
has_data => 'not',
},
);
has 'is_stored' => (
is => 'ro',
isa => 'Bool',
traits => ['Bool'],
default => 0,
handles => {
auto_save => 'unset',
no_auto_save => 'set',
stored => 'set',
},
);
sub _probe_url {
my $self = shift;
return join('/', $self->baseurl, $self->relname);
}
sub DESTROY {
my $self = shift;
if($self->create) {
if(!$self->is_stored) {
carp "object destructor for '" . $self->url . "' entered without an explicit store, storing now...";
$self->store_file;
}
}
}
no Moose;
package SReview::Files::Access::direct;
use Moose;
use DateTime;
use File::Path qw/make_path/;
use File::Basename qw/dirname/;
extends 'SReview::Files::Access::Base';
sub _get_file {
my $self = shift;
if($self->create) {
make_path(dirname($self->url));
unlink($self->url);
}
return $self->url;
}
sub store_file {
my $self = shift;
$self->stored;
return 1;
}
sub _probe_mtime {
my $self = shift;
my @stat = stat($self->filename);
return DateTime->from_epoch(epoch => $stat[9]);
}
sub delete {
my $self = shift;
unlink($self->url);
}
sub valid_path_filename {
my $self = shift;
return $self->url;
}
no Moose;
package SReview::Files::Collection::Base;
use Moose;
use Carp;
extends 'SReview::Files::Base';
has '+is_collection' => (
default => 1,
);
has 'children' => (
isa => 'ArrayRef[SReview::Files::Base]',
traits => ['Array'],
is => "ro",
lazy => 1,
handles => {
sorted_files => 'sort',
},
builder => '_probe_children',
);
has 'baseurl' => (
isa => 'Str',
is => 'ro',
predicate => 'has_baseurl',
writer => '_set_baseurl',
lazy => 1,
builder => '_probe_baseurl',
);
has 'globpattern' => (
isa => 'Str',
is => 'ro',
predicate => 'has_globpattern',
lazy => 1,
builder => '_probe_globpattern',
);
has 'fileclass' => (
isa => 'Str',
is => 'ro',
required => 1,
);
sub _probe_baseurl {
my $self = shift;
if(!$self->has_globpattern) {
croak("either a globpattern or a baseurl are required!\n");
}
@_ = split(/\*/, $self->globpattern);
my $rv = $_[0];
while(substr($rv, -1) eq '/') {
substr($rv, -1) = '';
}
return $rv;
}
sub _probe_url {
return shift->baseurl;
}
sub _probe_globpattern {
my $self = shift;
if(!$self->has_baseurl) {
croak("either a globpattern or a baseurl are required!\n");
}
return join('/', $self->baseurl, '*');
}
sub _create {
my $self = shift;
my %options = @_;
if(exists($options{fullname})) {
if(substr($options{fullname}, 0, length($self->baseurl)) ne $self->baseurl) {
croak($options{fullname} . " is not accessible through this collection");
}
$options{relname} = substr($options{fullname}, length($self->baseurl));
while(substr($options{relname}, 0, 1) eq '/') {
$options{relname} = substr($options{relname}, 1);
}
delete $options{fullname};
}
$options{baseurl} = $self->baseurl;
my $fileclass = $self->fileclass;
return "$fileclass"->new(%options);
}
sub get_file {
my $self = shift;
my %options = @_;
$options{create} = 0;
return $self->_create(%options);
}
sub add_file {
my $self = shift;
my %options = @_;
$options{create} = 1;
return $self->_create(%options);
}
sub has_file {
my $self = shift;
my $target = shift;
return scalar(grep({(!$_->is_collection) && ($_->relname eq $target)} @{$self->children}));
}
sub delete_files {
my $self = shift;
my %options = @_;
my @names;
if(exists($options{files})) {
@names = sort(@{$options{files}});
} elsif(exists($options{relnames})) {
@names = map({join('/', $self->baseurl, $_)} sort(@{$options{relnames}}));
} else {
croak("need list of files, or list of relative names");
}
my @ownfiles = sort({$a->url cmp $b->url} @{$self->children});
my @to_delete = ();
do {
if($ownfiles[0]->is_collection) {
if($names[0] eq $ownfiles[0]->baseurl) {
push @to_delete, shift @ownfiles;
} elsif(substr($names[0], 0, length($ownfiles[0]->baseurl)) eq $ownfiles[0]->baseurl) {
$ownfiles[0]->delete_files(files => [$names[0]]);
}
shift @names;
shift @ownfiles;
} elsif($names[0] eq $ownfiles[0]->url) {
shift @names;
push @to_delete, shift @ownfiles;
} elsif($names[0] eq substr($ownfiles[0]->url, 0, length($names[0]))) {
push @to_delete, shift @ownfiles;
if((!scalar(@ownfiles)) || $names[0] lt $ownfiles[0]->url) {
shift @names;
}
} elsif ($names[0] gt $ownfiles[0]->url) {
shift @ownfiles;
} else {
carp "${names[0]} is not a member of this collection, ignored";
shift @names;
}
} while(scalar(@names) && scalar(@ownfiles));
if(scalar(@names)) {
carp "${names[0]} is not a member of this collection, ignored";
}
foreach my $file(@to_delete) {
$file->delete;
}
}
sub delete {
my $self = shift;
foreach my $child($self->children) {
$child->delete;
}
}
no Moose;
package SReview::Files::Collection::direct;
use Moose;
use File::Basename;
use Carp;
extends 'SReview::Files::Collection::Base';
has '+fileclass' => (
default => 'SReview::Files::Access::direct',
);
sub _probe_children {
my $self = shift;
my @return;
foreach my $file(glob($self->globpattern)) {
my $child;
if(-d $file) {
$child = SReview::Files::Collection::direct->new(baseurl => join("/", $self->baseurl, basename($file)));
} else {
my $basename = substr($file, length($self->baseurl));
while(substr($basename, 0, 1) eq '/') {
$basename = substr($basename, 1);
}
$child = SReview::Files::Access::direct->new(baseurl => $self->baseurl, relname => $basename);
}
push @return, $child;
}
return \@return;
}
sub has_file {
my ($self, $target) = @_;
if(-f join('/', $self->baseurl, $target)) {
return 1;
}
return 0;
}
sub delete {
my $self = shift;
$self->SUPER::delete;
rmdir($self->baseurl);
}
no Moose;
package SReview::Files::Factory;
use SReview::Config::Common;
sub create {
my $class = shift;
my $target = shift;
my $relname = shift;
my $config = SReview::Config::Common::setup();
my $methods = $config->get("accessmethods");
my $method;
if(!exists($methods->{$target})) {
die "missing method for $target\n";
}
$method = $methods->{$target};
eval "require SReview::Files::Collection::$method;";
if($@) {
die "$@: $!";
}
if($target eq "input") {
return "SReview::Files::Collection::$method"->new(globpattern => $relname);
} else {
return "SReview::Files::Collection::$method"->new(baseurl => $relname);
}
}
1;
SReview-0.8.0/lib/SReview/Files/Collection/ 0000755 0001750 0001750 00000000000 14116343665 020037 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Files/Collection/S3.pm 0000644 0001750 0001750 00000006212 14051436474 020662 0 ustar wouter wouter package SReview::Files::Access::S3;
use Moose;
use File::Temp qw/tempfile tempdir mktemp/;
use File::Path qw/make_path/;
use File::Basename;
use DateTime::Format::ISO8601;
use Carp;
extends 'SReview::Files::Access::Base';
has 's3object' => (
is => 'ro',
required => 1,
isa => 'Net::Amazon::S3::Bucket',
);
has '+filename' => (
predicate => 'has_download',
);
has 'workdir' => (
is => 'ro',
lazy => 1,
builder => '_get_workdir',
);
sub _get_workdir {
return tempdir(CLEANUP => 1);
}
sub _get_file {
my $self = shift;
my @parts = split('\.', $self->relname);
my $ext = pop(@parts);
my $dir = $self->workdir;
if($self->has_data) {
my ($fh, $file) = tempfile("s3-XXXXXX", dir => $dir, SUFFIX => ".$ext");
$self->s3object->get_key_filename($self->relname, "GET", $file);
return $file;
} else {
my $file = join("/", $self->workdir, basename($self->relname));
return $file;
}
}
sub _probe_mtime {
my $self = shift;
my $meta = $self->s3object->head_key($self->relname);
return DateTime::Format::ISO8601->parse_datetime($meta->{last_modified});
}
sub store_file {
my $self = shift;
return if(!$self->has_download);
$self->s3object->add_key_filename($self->relname, $self->filename, {}) or croak($self->s3object->errstr);
$self->stored;
}
sub delete {
my $self = shift;
$self->s3object->delete_key($self->relname)
}
sub valid_path_filename {
my $self = shift;
my $path = join('/', $self->workdir, $self->relname);
make_path(dirname($path));
symlink($self->filename, $path);
return $path;
}
sub DESTROY {
my $self = shift;
$self->SUPER::DESTROY();
if($self->has_download) {
unlink($self->filename);
}
}
no Moose;
package SReview::Files::Collection::S3;
use Moose;
use Net::Amazon::S3;
use DateTime::Format::ISO8601;
use SReview::Config::Common;
extends "SReview::Files::Collection::Base";
has 's3object' => (
is => 'ro',
isa => 'Net::Amazon::S3::Bucket',
lazy => 1,
builder => '_probe_s3obj',
);
has '+fileclass' => (
default => 'SReview::Files::Access::S3',
);
sub _probe_s3obj {
my $self = shift;
my $config = SReview::Config::Common::setup();
my $bucket;
if($self->has_baseurl) {
$bucket = $self->baseurl;
} else {
my @elements = split('\/', $self->globpattern);
do {
$bucket = shift(@elements)
} while(!length($bucket));
$self->_set_baseurl($bucket);
}
my $aconf = $config->get('s3_access_config');
if(exists($aconf->{$bucket})) {
$aconf = $aconf->{$bucket};
} else {
if(!exists($aconf->{default})) {
croak("S3 access configuration does not exist for $bucket, and nor does a default exist");
}
$aconf = $aconf->{default};
}
return Net::Amazon::S3->new($aconf)->bucket($bucket);
}
sub _probe_children {
my $self = shift;
my $return = [];
my $baseurl;
foreach my $key(@{$self->s3object->list_all->{keys}}) {
push @$return, SReview::Files::Access::S3->new(
s3object => $self->s3object,
baseurl => $self->baseurl,
mtime => DateTime::Format::ISO8601->parse_datetime($key->{last_modified}),
relname => $key->{key},
);
}
return $return;
}
sub _create {
my $self = shift;
my %options = @_;
$options{s3object} = $self->s3object;
return $self->SUPER::_create(%options);
}
no Moose;
1;
SReview-0.8.0/lib/SReview/API/ 0000755 0001750 0001750 00000000000 14116343665 015313 5 ustar wouter wouter SReview-0.8.0/lib/SReview/API/Helpers.pm 0000644 0001750 0001750 00000011067 14051436474 017257 0 ustar wouter wouter package SReview::API::Helpers;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw/db_query update_with_json add_with_json delete_with_query/;
our @EXPORT_OK = qw/db_query_log/;
use SReview::Config::Common;
use Mojo::JSON qw/decode_json encode_json/;
use DateTime::Format::Pg;
sub db_query_log {
my ($app, $dbh, $query, @args) = @_;
my $st = $dbh->prepare($query);
$st->execute(@args);
my $results = [];
while(my @row = $st->fetchrow_array) {
if(scalar(@row) > 1) {
my $h = {};
foreach my $col(@{$st->{NAME_lc}}) {
$h->{$col} = shift @row;
}
push @$results, $h;
} else {
push @$results, @row;
}
}
return $results;
}
sub db_query {
my ($dbh, $query, @args) = @_;
return db_query_log(undef, $dbh, $query, @args);
}
sub delete_with_query {
my ($c, $query, @args) = @_;
my $st = $c->dbh->prepare($query);
eval {
$st->execute(@args);
};
if($st->err) {
$c->render(openapi => {errors => [{message => 'could not delete:', $st->errmsg}]}, status => 400);
return;
}
if($st->rows < 1) {
$c->render(openapi => {errors => [{message => 'not found'}]}, status => 404);
return;
}
my $row = $st->fetchrow_arrayref;
$c->render(openapi => $row->[0]);
}
sub update_with_json {
my ($c, $json, $tablename, $fields) = @_;
my @args;
if(!exists($json->{id})) {
$c->render(openapi => {errors => [{message => 'id required'}]}, status => 400);
return;
}
$c->app->log->debug("updating $tablename with " . encode_json($json));
my @updates;
while(my @tuple = each %$json) {
if($tuple[0] eq "id") {
$c->app->log->debug("skipping id");
next;
}
unless(exists($fields->{$tuple[0]})) {
$c->app->log->debug("skipping unknown field " . $tuple[0]);
next;
}
my $update = $tuple[0] . " = ?";
push @updates, $update;
push @args, $tuple[1];
}
my $updates = join(', ', @updates);
my $dbh = $c->dbh;
my $res;
my $query = "UPDATE $tablename SET $updates WHERE id = ? RETURNING $tablename.*";
eval {
$res = db_query($dbh, $query ,@args, $json->{id});
};
if($@) {
$c->app->log->warn("error running $query: " . $dbh->errstr);
$c->render(openapi => {errors => [{message => "error communicating with database"}]}, status => 500);
return;
}
if(scalar(@$res) < 1) {
$c->render(openapi => {errors => [{message => "not found"}]}, status => 404);
return;
}
my $result = $res->[0];
foreach my $field(keys %$fields) {
next unless exists($fields->{$field}{format});
if($fields->{$field}{format} eq "date-time") {
# PostgreSQL never uses the T in date-time
# fields unless we're encoding JSON. Doing that
# makes Mojo::JSON unhappy. Not doing that makes
# OpenAPI unhappy about the lack of the T.
#
# JSON makes me unhappy.
$c->app->log->debug("changing date; before: ");
$c->app->log->debug($result->{$field});
$result->{$field} = DateTime::Format::Pg->parse_datetime($result->{$field})->iso8601() or die;
$c->app->log->debug("after: " . $result->{$field});
}
}
$c->render(openapi => $res->[0]);
}
sub add_with_json {
my ($c, $json, $tablename, $fields) = @_;
my @args;
my @inserts;
if(exists($json->{id})) {
delete $json->{id};
}
while(my @tuple = each %$json) {
next if($tuple[0] eq "id");
next unless(exists($fields->{$tuple[0]}));
push @inserts, $tuple[0];
push @args, $tuple[1];
}
my $inserts = join(', ', @inserts);
my $fieldlist;
if(scalar(@inserts) > 0) {
$fieldlist = "?, " x (scalar(@inserts) - 1) . "?";
} else {
$fieldlist = "";
}
my $dbh = $c->dbh;
my $res;
eval {
$res = db_query($dbh, "INSERT INTO $tablename($inserts) VALUES($fieldlist) RETURNING $tablename.*", @args);
};
if(!defined($res) || scalar(@$res) < 1) {
$c->render(openapi => {errors => [{message => "failed to add data: " . $dbh->errstr}]}, status => 400);
return;
}
my $result = $res->[0];
foreach my $field(keys %$fields) {
next unless exists($fields->{$field}{format});
if($fields->{$field}{format} eq "date-time") {
# PostgreSQL never uses the T in date-time
# fields unless we're encoding JSON. Doing that
# makes Mojo::JSON unhappy. Not doing that makes
# OpenAPI unhappy about the lack of the T.
#
# JSON makes me unhappy.
$c->app->log->debug("changing date; before: ");
$c->app->log->debug($result->{$field});
$result->{$field} = DateTime::Format::Pg->parse_datetime($result->{$field})->iso8601() or die;
$c->app->log->debug("after: " . $result->{$field});
}
}
$c->render(openapi => $res->[0]);
}
SReview-0.8.0/lib/SReview/Web.pm 0000644 0001750 0001750 00000036624 14111412444 015753 0 ustar wouter wouter package SReview::Web;
use Mojo::Base 'Mojolicious';
use Mojo::Collection 'c';
use Mojo::JSON qw(encode_json);
use Mojo::Pg;
use Mojo::URL;
use SReview;
use SReview::Config;
use SReview::Config::Common;
use SReview::Db;
use SReview::Video;
use SReview::Video::ProfileFactory;
use SReview::API;
sub startup {
my $self = shift;
my $dir = $ENV{SREVIEW_WDIR};
SReview::API::init($self);
my $config = SReview::Config::Common::setup;
$self->max_request_size(2*1024*1024*1024);
if(defined($config->get("web_pid_file"))) {
$self->config(hypnotoad => { pid_file => $config->get("web_pid_file") });
}
die "Need to configure secrets!" if $config->get("secret") eq "_INSECURE_DEFAULT_REPLACE_ME_";
$self->secrets([$config->get("secret")]);
SReview::Db::init($config);
if(-d "/usr/share/sreview/templates") {
push @{$self->renderer->paths}, "/usr/share/sreview/templates";
push @{$self->static->paths}, "/usr/share/sreview/public";
push @{$self->static->paths}, "/usr/share/javascript";
}
if(-d "public") {
push @{$self->static->paths}, "./public";
push @{$self->renderer->paths}, "./templates";
}
if(defined($config->get("pubdir"))) {
push @{$self->static->paths}, $config->get("pubdir");
}
$self->hook(before_dispatch => sub {
my $c = shift;
my $vpr = $config->get('vid_prefix');
state $media = undef;
if(!defined($media)) {
$media = "media-src 'self'";
my $url = Mojo::URL->new($vpr);
if(defined($url->host)) {
$vpr = $url->host;
$media = "media-src $vpr";
}
if(defined($config->get("finalhosts"))) {
$media .= " " . $config->get("finalhosts");
}
$media .= ";";
}
$c->res->headers->content_security_policy("default-src 'none'; connect-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self'; style-src 'self'; img-src 'self'; frame-ancestors 'none'; $media");
});
$self->helper(dbh => sub {
state $pg = Mojo::Pg->new->dsn($config->get('dbistring'));
return $pg->db->dbh;
});
$self->helper(srconfig => sub {
return $config;
});
$self->helper(auth_scope => sub {
my $c = shift;
my $scope = shift;
$self->log->debug("checking if authorized for $scope");
if($c->session->{admin}) {
return 1;
} elsif($scope eq "api") {
return 1;
} elsif($scope eq "api/event") {
return 1;
} elsif($scope eq "api/talks") {
if(exists($c->session->{id})) {
return 1;
}
} elsif($scope eq "api/talks/detailed") {
if(exists($c->session->{id})) {
return 1;
}
}
$self->log->debug("not authorized for $scope");
return 0;
});
$self->helper(talk_update => sub {
my $c = shift;
my $talk = shift;
my $choice = $c->param('choice');
if(!defined($choice)) {
die "choice empty";
} elsif($choice eq "reset") {
my $sth = $c->dbh->prepare("UPDATE talks SET state='preview', progress='waiting' WHERE id = ?");
$sth->execute($talk) or die;
} elsif($choice eq "ok") {
my $sth = $c->dbh->prepare("UPDATE talks SET state='preview', progress='done' WHERE id = ?");
$sth->execute($talk) or die;
} elsif($choice eq 'standard') {
my $sth = $c->dbh->prepare("SELECT id, name FROM properties");
$sth->execute();
while(my $row = $sth->fetchrow_hashref("NAME_lc")) {
my $name = $row->{name};
my $parm = $c->param("correction_${name}");
next unless defined($parm);
next if (length($parm) == 0);
my $s = $c->dbh->prepare("INSERT INTO corrections(property_value, talk, property) VALUES (?, ?, ?)");
$s->execute($parm, $talk, $row->{id}) or die;
}
$sth = $c->dbh->prepare("UPDATE talks SET state='waiting_for_files', progress='done' WHERE id = ?");
$sth->execute($talk) or die;
} elsif($choice eq "comments") {
my $sth = $c->dbh->prepare("UPDATE talks SET state='broken', progress='failed', comments = ? WHERE id = ?");
my $comments = $c->param("comment_text");
$sth->execute($comments, $talk) or die;
} else {
$c->stash(message => "Unknown action.");
$c->render("error");
return undef;
}
$c->stash(message => 'Update successful.');
});
my $eventid = undef;
$self->helper(eventid => sub {
if(!defined($eventid)) {
if(defined($config->get("event"))) {
my $st = $self->dbh->prepare("SELECT id FROM events WHERE name = ?");
$st->execute($config->get("event")) or die "Could not find event!\n";
while(my $row = $st->fetchrow_hashref("NAME_lc")) {
die if defined($eventid);
$eventid = $row->{id};
}
} else {
my $st = $self->dbh->prepare("SELECT max(id) FROM events");
$st->execute() or die "Could not query for events";
my $row = $st->fetchrow_hashref("NAME_lc");
$eventid = $row->{id};
}
}
return $eventid;
});
$self->helper(version => sub {
state $rv;
if(defined $rv) {
return $rv;
}
open GIT, "git describe --tags --dirty 2>/dev/null|";
$rv = ;
close GIT;
if(!defined $rv) {
if(exists($ENV{GIT_DESCRIBE})) {
$rv = $ENV{GIT_DESCRIBE};
} else {
$rv = $SReview::VERSION;
}
}
chomp $rv;
return $rv;
});
my $r = $self->routes;
$r->get('/' => sub {
my $c = shift;
$c->render;
} => 'index');
$r->get('/login');
$r->post('/login_post' => sub {
my $c = shift;
my $email = $c->param('email');
my $pass = $c->param('pass');
my $st = $c->dbh->prepare("SELECT id, isadmin, isvolunteer, name, room FROM users WHERE email=? AND password=crypt(?, password)");
my $rv;
if(!($rv = $st->execute($email, $pass))) {
die "Could not check password: " . $st->errstr;
}
if($rv == 0) {
$c->stash(message => "Incorrect username or password.");
$c->render('error');
return undef;
}
my $row = $st->fetchrow_arrayref or die "eep?! username query returned nothing\n";
$c->session->{id} = $row->[0];
$c->session->{email} = $email;
$c->session->{admin} = $row->[1];
$c->session->{volunteer} = $row->[2];
$c->session->{name} = $row->[3];
$c->session->{room} = $row->[4];
if($c->session->{volunteer}) {
return $c->redirect_to('/volunteer/list');
} else {
return $c->redirect_to('/admin');
}
});
$r->get('/i/:nonce')->to(controller => 'inject', action => 'view', layout => 'default');
$r->post('/i/:nonce/update')->to(controller => 'inject', action => 'update', layout => 'default');
$r->get('/r/:nonce')->to(controller => 'review', action => 'view', layout => 'default');
$r->post('/r/:nonce/update')->to(controller => 'review', layout => 'default', action => 'update');
$r->get('/r/:nonce/data')->to(controller => 'review', action => 'data');
$r->get('/f/:nonce')->to(controller => 'finalreview', action => 'view', layout => 'default');
$r->post('/f/:nonce/update')->to(controller => 'finalreview', action => 'update', layout => 'default');
$r->get('/released' => sub {
my $c = shift;
my $st;
my $conference = {};
my $videos = [];
my %json;
my %formats;
my $have_default = 0;
$st = $c->dbh->prepare("SELECT MIN(starttime::date), MAX(endtime::date) FROM talks WHERE event = ?");
$st->execute($c->eventid);
$conference->{title} = $config->get("event");
my $row = $st->fetchrow_hashref();
$conference->{date} = [ $row->{min}, $row->{max} ];
$conference->{video_formats} = [];
$st = $c->dbh->prepare("SELECT filename FROM raw_files JOIN talks ON raw_files.room = talks.room WHERE talks.event = ? LIMIT 1");
$st->execute($c->eventid);
if($st->rows < 1) {
$c->render(json => {});
return;
}
$row = $st->fetchrow_hashref;
my $vid = SReview::Video->new(url => $row->{filename});
foreach my $format(@{$config->get("output_profiles")}) {
my $nf;
$self->log->debug("profile $format");
my $prof = SReview::Video::ProfileFactory->create($format, $vid);
if(!$have_default) {
$nf = "default";
$have_default = 1;
} else {
$nf = $format;
}
push @{$conference->{video_formats}}, { $nf => { vcodec => $prof->video_codec, acodec => $prof->audio_codec, resolution => $prof->video_size, bitrate => $prof->video_bitrate } };
$formats{$nf} = $prof;
}
$json{conference} = $conference;
$st = $c->dbh->prepare("SELECT title, subtitle, speakerlist(talks.id), description, starttime, starttime::date AS date, to_char(starttime, 'yyyy') AS year, endtime, rooms.name AS room, rooms.outputname AS room_output, upstreamid, events.name AS event, slug FROM talks JOIN rooms ON talks.room = rooms.id JOIN events ON talks.event = events.id WHERE state='done' AND event = ?");
$st->execute($c->eventid);
if($st->rows < 1) {
$c->render(json => {});
return;
}
my $mt = Mojo::Template->new;
$mt->vars(1);
while (my $row = $st->fetchrow_hashref()) {
my $video = {};
my $subtitle = defined($row->{subtitle}) ? " " . $row->{subtitle} : "";
$video->{title} = $row->{title} . $subtitle;
$video->{speakers} = [ $row->{speakerlist} ];
$video->{description} = $row->{description};
$video->{start} = $row->{starttime};
$video->{end} = $row->{endtime};
$video->{room} = $row->{room};
$video->{eventid} = $row->{upstreamid};
my @outputdirs;
foreach my $subdir(@{$config->get('output_subdirs')}) {
push @outputdirs, $row->{$subdir};
}
my $outputdir = join('/', @outputdirs);
if(defined($config->get('eventurl_format'))) {
$video->{details_url} = $mt->render($config->get('eventurl_format'), {
slug => $row->{slug},
room => $row->{room},
date => $row->{date},
event => $row->{event},
upstreamid => $row->{upstreamid},
year => $row->{year} });
chomp $video->{details_url};
}
$video->{video} = join('/',$outputdir, $row->{slug}) . "." . $formats{default}->exten;
push @$videos, $video;
}
$json{videos} = $videos;
$c->render(json => \%json);
});
$r->get('/overview' => sub {
shift->render;
});
$r->get("/credits" => sub {
shift->render;
});
$r->post('/talk_update' => sub {
my $c = shift;
my $nonce = $c->param("nonce");
if(!defined($nonce)) {
$c->stash(message=>"Unauthorized.");
$c->res->code(403);
$c->render('error');
return undef;
}
my $sth = $c->dbh->prepare("SELECT id FROM talks WHERE nonce = ? AND state IN ('preview', 'broken')");
$sth->execute($nonce);
my $row = $sth->fetchrow_arrayref;
if(scalar($row) == 0) {
$c->stash(message=>"Change not allowed. If this talk exists, it was probably reviewed by someone else while you were doing so too. Please try again later, or check the overview page.");
$c->res->code(403);
$c->render('error');
return undef;
}
$c->stash(layout => 'default');
$c->stash(template => 'talk');
$c->flash(completion_message => 'Your change has been accepted. Thanks for your help!');
$c->talk_update($row->[0]);
$c->redirect_to("/review/$nonce");
} => 'talk_update');
my $vol = $r->under('/volunteer' => sub {
my $c = shift;
if(!exists($c->session->{id})) {
$c->redirect_to('/login');
return 0;
}
$c->stash(id => $c->session->{id});
return 1;
});
$vol->get('/list')->to('volunteer#list');
my $admin = $r->under('/admin' => sub {
my $c = shift;
if(!exists($c->session->{id})) {
$c->res->code(403);
$c->redirect_to('/login');
return 0;
}
if($c->session->{volunteer}) {
$c->redirect_to('/volunteer/list');
return 0;
}
$c->stash(layout => "admin");
$c->stash(admin => $c->session->{admin});
return 1;
});
$admin->any('/schedule/list')->to(controller => 'schedule', action => 'talks');
$admin->any('/schedule/talk/')->to(controller => 'schedule', action => 'mod_talk');
$admin->any('/schedule/')->to(controller => 'schedule', action => 'index');
$admin->get('/')->to('admin#main')->name("admin_talk");
$admin->get('/logout' => sub {
my $c = shift;
delete $c->session->{id};
delete $c->session->{room};
$c->redirect_to('/');
});
$admin->get('/talk')->to('review#view');
$admin->get('/brokens' => sub {
my $c = shift;
my $st = $c->dbh->prepare("SELECT talks.id, title, speakeremail(talks.id), tracks.email, comments, state, nonce FROM talks JOIN tracks ON talks.track = tracks.id WHERE state>='broken' ORDER BY state,id");
my $tst = $c->dbh->prepare("SELECT rooms.altname, count(talks.id) FROM talks JOIN rooms ON talks.room = rooms.id WHERE talks.state='broken' GROUP BY rooms.altname");
my $rows = [];
$st->execute;
$tst->execute;
$c->stash(title => 'Broken talks');
$c->stash(titlerow => [ 'id', 'Title', 'Speakers', 'Track email', 'Comments', 'State', 'Link' ]);
$c->stash(tottitrow => [ 'Room', 'Count' ]);
my $pgrows = $st->fetchall_arrayref;
foreach my $row(@{$pgrows}) {
my $nonce = pop @$row;
push @$row, "review";
push @$rows, $row;
}
$c->stash(rows => $rows);
$c->stash(totals => $tst->fetchall_arrayref);
$c->stash(header => 'Broken talks');
$c->stash(layout => 'admin');
$c->stash(totals => undef);
$c->stash(autorefresh => 0);
$c->render(template => 'table');
} => 'broken_table');
my $sysadmin = $admin->under('/system' => sub {
my $c = shift;
if(!exists($c->session->{id})) {
$c->res->code(403);
$c->render(text => 'Unauthorized (not logged on)');
return 0;
}
if(!$c->session->{admin}) {
$c->res->code(403);
$c->render(text => 'Unauthorized (not admin)');
return 0;
}
$c->stash(layout => "admin");
$c->stash(admin => $c->session->{admin});
return 1;
});
$sysadmin->get('/' => sub {
my $c = shift;
$c->stash(email => $c->session->{email});
my $st = $c->dbh->prepare("SELECT DISTINCT rooms.id, rooms.name FROM rooms LEFT JOIN talks ON rooms.id = talks.room WHERE talks.event = ?");
$st->execute($c->eventid);
my $rooms = [['All rooms' => '', selected => 'selected']];
while(my $row = $st->fetchrow_arrayref) {
push @$rooms, [$row->[1] => $row->[0]];
}
$c->stash(rooms => $rooms);
} => 'admin/dashboard');
$sysadmin->get('/adduser' => sub {
my $c = shift;
open PASSWORD, "pwgen -s 10 -n 1|";
my $password = ;
close(PASSWORD);
chomp $password;
my $st = $c->dbh->prepare("INSERT INTO users(email, name, isadmin, isvolunteer, password, room) VALUES(?, ?, ?, ?, crypt(?, gen_salt('bf', 8)), ?)");
my $room = $c->param('rooms');
if($room eq "") {
$room = undef;
}
$st->execute($c->param('email'), $c->param('name'), $c->param('isadmin'), $c->param('isvolunteer'), $password, $room) or die;
$c->dbh->prepare("UPDATE users SET isadmin = false WHERE isadmin is null")->execute;
$c->flash(msg => "User with email " . $c->param('email') . " created, with password '$password'");
$c->redirect_to('/admin/system');
});
$sysadmin->get('/chpw' => sub {
my $c = shift;
my $st = $c->dbh->prepare("SELECT * FROM users WHERE email=?");
my $email = $c->param("email");
$st->execute($email);
if($st->rows != 1) {
$c->flash(msg => "There is no user with email address " . $c->param('email') . ". Try creating it?");
$c->redirect_to('/admin/system');
return;
}
open PASSWORD, "pwgen -s 10 -n 1|";
my $password = ;
close(PASSWORD);
chomp $password;
$st = $c->dbh->prepare("UPDATE users SET password = crypt(?, gen_salt('bf', 8)) WHERE email = ?");
$st->execute($password, $email);
$c->flash(msg => "Password for user $email set to '$password'");
$c->redirect_to('/admin/system');
});
$sysadmin->get('/setpw' => sub {
my $c = shift;
my $pw1 = $c->param('password1');
my $pw2 = $c->param('password2');
if ($pw1 ne $pw2) {
$c->flash(msg => "Passwords did not match!");
$c->redirect_to('/admin/system');
return;
}
my $st = $c->dbh->prepare("UPDATE users SET password = crypt(?, gen_salt('bf', 8)) WHERE email = ?");
$st->execute($pw1, $c->session->{email});
$c->flash(msg => "Password changed.");
$c->redirect_to('/admin/system');
});
$r->get('*any' => sub {
my $c = shift;
$c->redirect_to('/overview');
});
}
1;
SReview-0.8.0/lib/SReview/Web/ 0000755 0001750 0001750 00000000000 14116343665 015417 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Web/Controller/ 0000755 0001750 0001750 00000000000 14116343665 017542 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Web/Controller/Finalreview.pm 0000644 0001750 0001750 00000004515 14054727355 022363 0 ustar wouter wouter package SReview::Web::Controller::Finalreview;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Collection 'c';
use SReview::Talk;
use SReview::Access qw/admin_for/;
sub view {
my $c = shift;
my $id = $c->stash("id");
my $talk;
$c->stash(adminspecial => 0);
eval {
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
$talk = SReview::Talk->by_nonce($c->stash("nonce"));
}
};
if($@) {
$c->stash(error => $@);
$c->render(variant => "error");
return;
}
my $variant;
my $nonce = $talk->nonce;
if($talk->state eq "finalreview") {
$variant = undef;
} elsif(admin_for($c, $talk)) {
$variant = undef;
$c->stash(adminspecial => 1);
} elsif($talk->state gt "finalreview" && $talk->state lt "done") {
$variant = 'working';
} elsif($talk->state ge 'remove' && $talk->state le 'removing') {
$variant = 'working';
} else {
$variant = 'done';
}
$c->stash(talk => $talk);
$c->stash(stylesheets => ['/review.css']);
$c->render(variant => $variant);
}
sub update {
my $c = shift;
my $id = $c->stash("id");
my $talk;
$c->stash(stylesheets => ['/review.css']);
eval {
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
$talk = SReview::Talk->by_nonce($c->stash("nonce"));
}
};
if($@) {
$c->stash(error => $@);
$c->render(variant => "error");
return;
}
$c->stash(talk => $talk);
if(!admin_for($c, $talk) && $talk->state ne 'finalreview') {
$c->stash(error => 'This talk is not currently available for final review. Please try again later!');
$c->render(variant => 'error');
return;
}
$talk->add_correction(serial => 0);
if($c->param("serial") != $talk->corrections->{serial}) {
$c->stash(error => 'This talk was updated (probably by someone else) since you last loaded it. Please reload the page, and try again.');
$c->render(variant => 'error');
return;
}
if(!defined($c->param("video_state"))) {
$c->stash(error => 'Invalid submission data; missing parameter video_state.');
$c->render(variant => "error");
return;
}
if(defined($c->param("comment_text")) && length($c->param("comment_text")) > 0) {
$talk->comment($c->param("comment_text"));
}
if($c->param("video_state") eq "ok") {
$talk->state_done("finalreview");
$c->render(variant => "done");
return;
}
$talk->set_state("remove");
$c->render(variant => "unpublish");
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Volunteer.pm 0000644 0001750 0001750 00000001672 13401522031 022047 0 ustar wouter wouter package SReview::Web::Controller::Volunteer;
use Mojo::Base 'Mojolicious::Controller';
sub list {
my $c = shift;
my @talks;
$c->dbh->begin_work;
my $already = $c->dbh->prepare("SELECT nonce, title, id, state FROM talks WHERE reviewer = ? AND state <= 'preview'");
my $new = $c->dbh->prepare("SELECT nonce, title, id, state FROM talks WHERE reviewer IS NULL AND state = 'preview'::talkstate LIMIT ? FOR UPDATE");
my $claim = $c->dbh->prepare("UPDATE talks SET reviewer = ? WHERE id = ?");
$already->execute($c->session->{id});
my $count = $already->rows;
if($count < 5) {
$new->execute(5 - $count);
}
for(my $i = 0; $i < $count; $i++) {
my $row = [ $already->fetchrow_array ];
push @talks, $row;
}
for(my $i = 0; $i < $new->rows; $i++) {
my $row = [ $new->fetchrow_array ];
$claim->execute($c->session->{id}, $row->[2]);
push @talks, $row;
}
$c->stash(talks => \@talks);
$c->stash(layout => 'admin');
$c->dbh->commit;
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/User.pm 0000644 0001750 0001750 00000002246 14051436474 021021 0 ustar wouter wouter package SReview::Web::Controller::User;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers;
sub add {
my $c = shift->openapi->valid_input or return;
my $user = $c->req->json;
return add_with_json($c, $user, "users", $c->openapi->spec('/components/schemas/User/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $userId = $c->param("userId");
my $user = $c->req->json;
$user->{id} = $userId;
return update_with_json($c, $user, "users", $c->openapi->spec('/components/schemas/User/properties'));
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $userId = $c->param('userId');
my $user = db_query($c->dbh, "SELECT users.* FROM users WHERE id = ?", $userId);
if(scalar(@$user) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
$c->render(openapi => $user->[0]);
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $userId = $c->param('userId');
return delete_with_query($c, "DELETE FROM users WHERE id = ?", $userId);
}
sub list {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => db_query($c->dbh, "SELECT users.* FROM users"));
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Speaker.pm 0000644 0001750 0001750 00000005062 14051436474 021474 0 ustar wouter wouter package SReview::Web::Controller::Speaker;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers;
sub listByTalk {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $talkId = $c->param("talkId");
my $talk = db_query($c->dbh, "SELECT id FROM talks WHERE event = ? AND id = ?", $eventId, $talkId);
if(scalar(@$talk) < 1) {
$c->res->code(404);
$c->render(text => 'not found');
return;
}
my $speakers = db_query($c->dbh, "SELECT speakers.* FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = ?", $talkId);
$c->render(openapi => $speakers);
}
sub search {
my $c = shift->openapi->valid_input or return;
my $searchString = "%" . $c->param("searchString") . "%";
$c->render(openapi => db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE name ILIKE ? OR email ILIKE ?", $searchString, $searchString));
}
sub getByUpstream {
my $c = shift->openapi->valid_input or return;
my $speaker = db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE upstreamid = ?", $c->param("upstreamId"));
if(scalar(@$speaker) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
$c->render(openapi => $speaker->[0]);
}
sub add {
my $c = shift->openapi->valid_input or return;
my $speaker = $c->req->json;
$c->app->log->debug(join(',', keys %$speaker));
return add_with_json($c, $speaker, "speakers", $c->openapi->spec('/components/schemas/Speaker/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $speakerId = $c->param("speakerId");
my $speaker = $c->req->json;
$speaker->{id} = $speakerId;
return update_with_json($c, $speaker, "speakers", $c->openapi->spec('/components/schemas/Speaker/properties'));
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $speakerId = $c->param("speakerId");
my $speaker = db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE id = ?", $speakerId);
if(scalar(@$speaker) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
$c->render(openapi => $speaker->[0]);
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $speakerId = $c->param('speakerId');
return delete_with_query($c, "DELETE FROM speakers WHERE id = ?", $speakerId);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Talk.pm 0000644 0001750 0001750 00000015301 14111204223 020750 0 ustar wouter wouter package SReview::Web::Controller::Talk;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers qw/db_query update_with_json add_with_json/;
use Mojo::Util;
use Mojo::JSON qw/encode_json decode_json/;
use DateTime::Format::Pg;
use SReview::Talk;
sub format_talks {
my $talks = shift;
foreach my $talk(@$talks) {
$talk->{starttime} = DateTime::Format::Pg->parse_datetime($talk->{starttime})->iso8601();
$talk->{endtime} = DateTime::Format::Pg->parse_datetime($talk->{endtime})->iso8601();
if($talk->{flags}) {
$talk->{flags} = decode_json($talk->{flags});
}
}
return $talks;
}
sub listByEvent {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $event = db_query($c->dbh, "SELECT id FROM events WHERE id = ?", $eventId);
if(scalar(@$event) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
my $res = db_query($c->dbh, "SELECT talks.* FROM talks WHERE event = ?", $eventId);
$res = format_talks($res);
$c->render(openapi => $res);
}
sub add {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $event = db_query($c->dbh, "SELECT id FROM events WHERE id = ?", $eventId);
if(scalar(@$event) < 1) {
$c->res->code(404);
$c->render(text => "Event not found");
return;
}
my $talk = $c->req->json;
$talk->{event} = $event->[0];
return add_with_json($c, $talk, "talks", $c->openapi->spec('/components/schemas/Talk/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param('eventId');
my $talkId = $c->param('talkId');
my $talk_check = db_query($c->dbh, "SELECT id FROM talks WHERE id = ? AND event = ?", $talkId, $eventId);
if(scalar(@$talk_check) < 1) {
$c->res->code(404);
$c->render(text => 'Talk not found in given event');
return;
}
my $talk = $c->req->json;
$talk->{id} = $talkId;
if(exists($talk->{flags})) {
$talk->{flags} = encode_json($talk->{flags});
}
return update_with_json($c, $talk, "talks", $c->openapi->spec('/components/schemas/Talk/properties'));
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param('eventId');
my $talkId = $c->param('talkId');
my $event = db_query($c->dbh, "SELECT id FROM events WHERE id = ?", $eventId);
if(scalar(@$event) < 1) {
$c->res->code(404);
$c->render(text => 'Event not found');
return;
}
return delete_with_query($c, 'DELETE FROM talks WHERE id = ? AND event = ?', $talkId, $eventId);
}
sub setSpeakers {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param('eventId');
my $talkId = $c->param('talkId');
my $event = db_query($c->dbh, "SELECT id FROM talks WHERE id = ? AND event = ?", $talkId, $eventId);
if(scalar(@$event) < 1) {
$c->res->code(404);
$c->render(text => 'Event or talk not found');
return;
}
my $speakers = $c->req->json;
my $dbh = $c->dbh;
$dbh->begin_work();
db_query($dbh, 'DELETE FROM speakers_talks WHERE talk = ? RETURNING talk', $talkId);
if(scalar(@$speakers) < 1) {
$c->render(openapi => []);
$dbh->commit;
return;
}
foreach my $speakerId(@$speakers) {
my $speaker = db_query($dbh, 'SELECT id FROM speakers WHERE id = ?', $speakerId);
if(scalar(@$speaker) < 1) {
$c->res->code(404);
$c->render(text => 'Speaker not found');
$dbh->rollback;
return;
}
db_query($dbh, 'INSERT INTO speakers_talks(speaker, talk) VALUES(?, ?) RETURNING speaker', $speakerId, $talkId);
if($dbh->err) {
$c->res->code(400);
$c->render(text => 'Could not add speaker:' . $dbh->errmsg);
$dbh->rollback;
return;
}
}
$dbh->commit;
$speakers = db_query($dbh, "SELECT speaker FROM speakers_talks WHERE talk = ?", $talkId);
return $c->render(openapi => $speakers);
}
sub addSpeakers {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param('eventId');
my $talkId = $c->param('talkId');
my $event = db_query($c->dbh, "SELECT id FROM talks WHERE event = ? AND id = ?", $eventId, $talkId);
if(scalar(@$event) < 1) {
$c->res->code(404);
$c->render(text => 'Event or talk not found');
return;
}
my $speakers = $c->req->json;
if(scalar(@$speakers) < 1) {
$c->res->code(400);
$c->render(text => 'at least one speaker is required');
return;
}
my $dbh = $c->dbh;
$dbh->begin_work;
foreach my $speakerId(@$speakers) {
my $speaker = db_query($dbh, 'SELECT id FROM speakers WHERE id = ?', $speakerId);
if(scalar(@$speaker) < 1) {
$c->res->code(404);
$c->render(text => 'Speaker not found');
$dbh->rollback;
return;
}
eval {
db_query($dbh, 'INSERT INTO speakers_talks(speaker, talk) VALUES(?, ?) RETURNING speaker', $speakerId, $talkId);
};
if($@ && $dbh->err) {
$c->res->code(400);
$c->render(text => 'Could not add speaker:' . $dbh->errstr);
$dbh->rollback;
return;
}
}
$dbh->commit;
$speakers = db_query($dbh, "SELECT speaker FROM speakers_talks WHERE talk = ?", $talkId);
return $c->render(openapi => $speakers);
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $talkId = $c->param("talkId");
my $talk = db_query($c->dbh, "SELECT talks.* FROM talks WHERE event = ? AND id = ?", $eventId, $talkId);
if(scalar(@$talk) < 1) {
$c->res->code(404);
$c->render(text => "Event or talk not found");
return;
}
$c->render(openapi => format_talks($talk)->[0]);
}
sub getByNonce {
my $c = shift->openapi->valid_input or return;
my $nonce = $c->param("nonce");
my $talk = db_query($c->dbh, "SELECT talks.* FROM talks WHERE nonce = ?", $nonce);
if(scalar(@$talk) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
foreach my $r(@$talk) {
$r->{starttime} = DateTime::Format::Pg->parse_datetime($r->{starttime})->iso8601();
$r->{endtime} = DateTime::Format::Pg->parse_datetime($r->{endtime})->iso8601();
}
$c->render(openapi => $talk->[0]);
}
sub getCorrections {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $talkId = $c->param("talkId");
my $talk = db_query($c->dbh, "SELECT id FROM talks WHERE event = ? AND id = ?", $eventId, $talkId);
if(scalar(@$talk) < 1) {
$c->res->code(404);
$c->render(text => "event or talk not found");
return;
}
$talk = SReview::Talk->new(talkid => $talkId);
$c->render(openapi => $talk->corrections);
}
sub getRelativeName {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $talkId = $c->param("talkId");
my $talk = db_query($c->dbh, "SELECT id FROM talks WHERE event = ? AND id = ?", $eventId, $talkId);
if(scalar(@$talk) < 1) {
$c->res->code(404);
$c->render(text => "event or talk not found");
return;
}
$talk = SReview::Talk->new(talkid => $talkId);
$c->render(openapi => $talk->relative_name);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Track.pm 0000644 0001750 0001750 00000002570 14051436474 021147 0 ustar wouter wouter package SReview::Web::Controller::Track;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers;
sub add {
my $c = shift->openapi->valid_input or return;
my $track = $c->req->json;
return add_with_json($c, $track, "tracks", $c->openapi->spec('/components/schemas/Track/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $track = $c->req->json;
my $trackId = $c->param('trackId');
$track->{id} = $trackId;
return update_with_json($c, $track, "tracks", $c->openapi->spec('/components/schemas/Track/properties'));
}
sub list {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => db_query($c->dbh, "SELECT tracks.* FROM tracks"));
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $trackId = $c->param('trackId');
my $track = db_query($c->dbh, "SELECT tracks.* FROM tracks WHERE id = ?", $trackId);
if(scalar(@$track) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
$c->render(openapi => $track->[0]);
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $trackId = $c->param("trackId");
return delete_with_query($c, "DELETE FROM tracks WHERE id = ?", $trackId);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Room.pm 0000644 0001750 0001750 00000002577 14051436474 021026 0 ustar wouter wouter package SReview::Web::Controller::Room;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers;
sub add {
my $c = shift->openapi->valid_input or return;
my $room = $c->req->json;
return add_with_json($c, $room, "rooms", $c->openapi->spec('/components/schemas/Room/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $roomId = $c->param('roomId');
my $room = $c->req->json;
$room->{id} = $roomId;
return update_with_json($c, $room, "rooms", $c->openapi->spec('/components/schemas/Room/properties'));
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $roomId = $c->param('roomId');
my $room = db_query($c->dbh, "SELECT rooms.* FROM rooms WHERE id = ?", $roomId);
if(scalar(@$room) < 1) {
$c->res->code(404);
$c->render(text => "not found");
return;
}
$c->render(openapi => $room->[0]);
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $roomId = $c->param("roomId");
return delete_with_query($c, "DELETE FROM rooms WHERE id = ? RETURNING id", $roomId);
}
sub list {
my $c = shift->openapi->valid_input or return;
my $rooms = db_query($c->dbh, "SELECT rooms.* FROM rooms");
$c->render(openapi => $rooms);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Inject.pm 0000644 0001750 0001750 00000013430 14107470155 021310 0 ustar wouter wouter package SReview::Web::Controller::Inject;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Collection 'c';
use Data::Dumper;
use SReview::Access qw/admin_for/;
use SReview::Files::Factory;
use SReview::Talk;
use SReview::Video;
sub view {
my $c = shift;
my $talk;
my $id = $c->stash("id");
eval {
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
$talk = SReview::Talk->by_nonce($c->stash("nonce"));
}
};
if($@) {
$c->stash(error => $@);
$c->stash(short_error => "Exception occurred");
$c->render(variant => 'error');
return;
}
my $nonce = $talk->nonce;
my $variant;
$c->stash(adminspecial => 0);
if ($talk->state <= 'preview') {
$variant = undef;
} elsif(admin_for($c, $talk)) {
$variant = undef;
$c->stash(adminspecial => 1);
} elsif(!$talk->get_flag("can_inject")) {
$variant = 'error';
$c->stash(short_error => "Injection not allowed for this talk");
$c->stash(error => "Talks can only be injected when an administrator enables the option for that talk. Please talk to the administrators of the review system and ask them to enable this option for this talk.");
} else {
$variant = 'done';
}
my $vid_prefix = $c->srconfig->get('vid_prefix');
$vid_prefix = '' unless defined($vid_prefix);
$c->stash(vid_prefix => $vid_prefix);
$c->stash(talk => $talk);
$c->stash(stylesheets => ['/review.css']);
$c->stash(variant => $variant);
}
sub update {
my $c = shift;
my $id = $c->stash("id");
my $talk;
$c->stash(stylesheets => ['/review.css']);
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
eval {
$talk = SReview::Talk->by_nonce($c->stash('nonce'));
};
if($@) {
$c->stash(error => $@);
$c->stash(short_error => 'Exception occurred');
$c->render(variant => 'error');
return;
}
}
$c->stash(talk => $talk);
if(!admin_for($c, $talk) && $talk->state > 'preview' && $talk->state != 'injecting') {
$c->stash(short_error => 'Not available');
$c->stash(error => 'This talk is not currently available for data injection. Please try again later!');
$c->render(variant => 'error');
return;
}
my $collname = $c->srconfig->get("inject_collection");
foreach my $upload(@{$c->req->uploads}) {
if($upload->name eq "video_asset") {
next unless defined($upload->filename) && length($upload->filename) > 0;
$c->app->log->debug("copying video asset " . $upload->filename);
my @parts = split /\./, $upload->filename;
my $ext = pop @parts;
my $fn = join('.', $talk->slug, $ext);
my $coll;
if($collname eq "input") {
$coll = SReview::Files::Factory->create("input", $c->srconfig->get("inputglob"), $c->srconfig);
} elsif($collname eq "pub") {
$coll = SReview::Files::Factory->create("intermediate", $c->srconfig->get("pubdir"), $c->srconfig);
} else {
$coll = SReview::Files::Factory->create($collname, $c->srconfig->get("extra_collections")->{$collname});
}
my $file = $coll->add_file(relname => join("/", "injected", $fn));
$c->dbh->prepare("DELETE FROM raw_files WHERE filename LIKE ? AND stream = 'injected' AND room = ?")->execute($coll->url . "/injected/" . $talk->slug . ".%", $talk->roomid);
my $st = $c->dbh->prepare("INSERT INTO raw_files(filename, room, starttime, stream) VALUES(?,?,?,'injected') ON CONFLICT DO NOTHING");
$st->execute($file->url, $talk->roomid, $talk->corrected_times->{start});
$upload->move_to($file->filename);
$c->app->log->debug("checking video asset " . $upload->filename);
my $input = SReview::Video->new(url => $file->filename);
my $checks = $c->srconfig->get("inject_fatal_checks");
foreach my $prop(keys %$checks) {
my $attr = $input->meta->find_attribute_by_name($prop);
my $val = $attr->get_value($input);
if(!defined($val)) {
$c->stash(short_error => "Invalid upload");
$c->stash(error => "Could not find the attribute $prop of the uploaded file. Cannot process this file.");
$c->render(variant => "error");
return;
} elsif(exists($checks->{$prop}{min}) && exists($checks->{$prop}{max})) {
if(($val > $checks->{$prop}{max}) || ($val < $checks->{$prop}{min})) {
$c->stash(short_error => "Invalid upload");
$c->stash(error => "Value of property $prop out of bounds for the uploaded file. Cannot process this file.");
$c->render(variant => "error");
return;
}
} elsif(exists($checks->{$prop}{val})) {
if($val ne $checks->{$prop}{val}) {
$c->stash(short_error => "Invalid upload");
$c->stash(error => "Value of property $prop does not string-equal expected value. Cannot process this file.");
$c->render(variant => "error");
return;
}
} elsif(exists($checks->{$prop}{talkattr_max})) {
my $talkattr = $talk->meta->find_attribute_by_name($checks->{$prop}{talkattr_max});
if($val >= $talkattr->get_value($talk)) {
$c->stash(short_error => "Invalid upload");
$c->stash(error => "Value of property $prop is too high for this talk. Cannot process this file.");
$c->render(variant => "error");
return;
}
} else {
die "invalid configuration: $prop requires either minimum and maximum, or an exact value.";
}
}
$file->store_file;
$talk->active_stream("injected");
$talk->set_state("injecting");
$talk->done_correcting;
} elsif($upload->name eq "other_asset") {
next unless defined($upload->filename) && length($upload->filename) > 0;
$c->app->log->debug("copying other asset " . $upload->filename);
my $coll = SReview::Files::Factory->create($collname, $c->srconfig->get("extra_collections")->{$collname}, $c->srconfig);
my $file = $coll->add_file(relname => join("/", "assets", $talk->slug, $upload->filename));
$upload->move_to($file->filename);
$file->store_file;
}
$c->app->log->debug($upload->filename . " done");
}
$c->render;
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Event.pm 0000644 0001750 0001750 00000013435 14111421632 021151 0 ustar wouter wouter package SReview::Web::Controller::Event;
use Mojo::Base 'Mojolicious::Controller';
use SReview::API::Helpers;
use Data::Dumper;
sub add {
my $c = shift->openapi->valid_input or return;
return add_with_json($c, $c->req->json, "events", $c->openapi->spec('/components/schemas/Event/properties'));
}
sub update {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $event = $c->req->json;
$event->{id} = $eventId;
return update_with_json($c, $event, "events", $c->openapi->spec('/components/schemas/Event/properties'));
}
sub delete {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param('eventId');
my $query = "DELETE FROM events WHERE id = ? RETURNING id";
return delete_with_query($c, $query, $eventId);
}
sub getById {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $event = db_query($c->dbh, "SELECT events.* FROM events WHERE id = ?", $eventId);
if(scalar(@$event) < 1) {
return $c->render(openapi => {errors => [{message => "not found"}]}, status => 404);
}
$c->render(openapi => $event->[0]);
}
sub list {
my $c = shift->openapi->valid_input or return;
my $events = db_query($c->dbh, "SELECT events.* FROM events");
$c->render(openapi => $events);
}
sub overview {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $query;
my $st = $c->dbh->prepare("SELECT id FROM events WHERE id = ?");
$st->execute($eventId);
if($st->rows < 1) {
return $c->render(openapi => {errors => [{message => "not found"}]}, status => 404);
}
if($c->srconfig->get("anonreviews")) {
$query = "SELECT CASE WHEN state IN ('preview', 'broken') THEN '/r/' || nonce WHEN state='finalreview' THEN '/f/' || nonce ELSE null END AS reviewurl, nonce, name, speakers, room, starttime::timestamp, endtime::timestamp, state, progress FROM talk_list WHERE eventid = ? AND state IS NOT NULL ORDER BY state, progress, room, starttime";
} else {
$query = "SELECT name, speakers, room, starttime::timestamp, endtime::timestamp, state, progress FROM talk_list WHERE eventid = ? AND state IS NOT NULL ORDER BY state, progress, room, starttime";
}
my $res = db_query($c->dbh, $query, $eventId);
$c->render(openapi => $res);
}
sub talksByState {
my $c = shift->openapi->valid_input or return;
my $eventId = $c->param("eventId");
my $state = SReview::Talk::State->new($c->param("state"));
my $st = $c->dbh->prepare("SELECT MIN(starttime::date) AS start, MAX(endtime::date) AS end, name AS title FROM events JOIN talks ON events.id = talks.event WHERE events.id = ? GROUP BY events.name");
$st->execute($eventId);
if($st->rows < 1) {
return $c->render(openapi => {errors => [{message => "not found"}]},status => 404);
}
my $row = $st->fetchrow_hashref;
my $rv = {};
my $have_default = 0;
my %formats;
$rv->{conference}{title} = $row->{title};
$rv->{conference}{date} = [ $row->{start}, $row->{end} ];
$st = $c->dbh->prepare("SELECT filename FROM raw_files JOIN talks ON raw_files.room = talks.room WHERE talks.event = ? LIMIT 1");
$st->execute($eventId);
if($st->rows < 1) {
$c->render(openapi => {errors => [{message => "can't detect video files yet"}]},status => 400);
return;
}
$row = $st->fetchrow_hashref;
my $vid = SReview::Video->new(url => $row->{filename});
foreach my $format(@{$c->srconfig->get("output_profiles")}) {
my $nf;
$c->app->log->debug("profile $format");
my $prof = SReview::Video::rofileFactory->create($format, $vid);
if(!$have_default) {
$nf = 'default';
$have_default = 1;
} else {
$nf = $format;
}
$rv->{conference}{video_formats}{$nf} = { vcodec => $prof->video_codec, acodec => $prof->audio_codec, resolution => $prof->video_size, bitrate => $prof->video_bitrate . "k" };
$formats{$nf} = $prof;
}
$rv->videos = [];
$st = $c->dbh->prepare("SELECT id, title, subtitle, description, starttime, starttime::date AS date, to_char(starttime, 'yyyy') AS year, endtime, rooms.name as room, rooms.outputname as rooms_output, upstreamid, events.name as event, slug FROM talks JOIN rooms on talks.room = rooms.id JOIN events on talks.event = events.id WHERE state=? AND event=?");
$st->execute($state, $eventId);
my $speakers = $c->dbh->prepare("SELECT name FROM speakers JOIN speakerslist ON speakers.id = speakerlist.speaker WHERE speakerlist.talk = ?");
if($st->rows < 1) {
$c->render(openapi => $rv);
}
my $mt = Mojo::Template->new;
$mt->vars(1);
while(my $row = $st->fetchrow_hashref()) {
my $video = {};
$speakers->execute($row->{id});
my $subtitle = defined($row->{subtitle}) ? ": " . $row->{subtitle} : "";
$video->{title} = $row->{title} . $subtitle;
$video->{speakers} = [];
while(my $srow = $speakers->fetchrow_hashref()) {
push @{$video->{speakers}}, $srow->{name};
}
$video->{description} = $row->{description};
$video->{start} = $row->{starttime};
$video->{end} = $row->{endtime};
$video->{room} = $row->{room};
$video->{eventid} = $row->{upstreamid};
my @outputdirs;
foreach my $subdir(@{$c->srconfig->get("output_subdirs")}) {
push @outputdirs, $row->{$subdir};
}
my $outputdir = join('/', @outputdirs);
if($state > 'transcoding' && $state <= 'done') {
if(defined($c->srconfig->get('eventurl_format'))) {
$video->{details_url} = $mt->render($c->srconfig->get('eventurl_format'), {
slug => $row->{slug},
room => $row->{room},
date => $row->{date},
event => $row->{event},
upstreamid => $row->{upstreamid},
year => $row->{year} });
chomp $video->{details_url};
}
$video->{video} = join('/',$outputdir, $row->{slug}) . "." . $formats{default}->exten;
} elsif($state > 'cutting' && $state < 'preview') {
$video->{video} = join('/',$eventId, $row->{date}, substr($row->{room}, 0, 1), $row->{slug} . ".mkv");
}
push @{$rv->{videos}}, $video;
}
$c->render(openapi => $rv);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Schedule.pm 0000644 0001750 0001750 00000002011 13401522031 021604 0 ustar wouter wouter package SReview::Web::Controller::Schedule;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Log;
sub talks {
my $self = shift;
my $db = $self->dbh;
my $eventdata = $db->prepare("SELECT * FROM talk_list WHERE eventid = ? ORDER BY id");
$eventdata->execute($self->eventid());
$self->app->log->debug("finding talks for event " . $self->eventid());
my $rv = ();
while(my $row = $eventdata->fetchrow_hashref()) {
$self->app->log->debug("found talk with id: " . $row->{id});
push @$rv, $row;
}
$self->render(json => $rv);
}
sub index { }
1;
__DATA__
@@ schedule/index.html.ep
% layout 'admin'
Schedule management
Possitble actions:
GET /admin/schedule/list
Creates a JSON list of all talks in the current event
DELETE /admin/schedule/talk/:id
Delete the talk with the given ID
PUT /admin/schedule/talk/
Create a new talk (requires JSON object)
PUT /admin/schedule/talk/:id
Update the data of the talk with the given ID
SReview-0.8.0/lib/SReview/Web/Controller/Review.pm 0000644 0001750 0001750 00000015636 14054727355 021357 0 ustar wouter wouter package SReview::Web::Controller::Review;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Collection 'c';
use feature "switch";
use SReview::Talk;
use SReview::Access qw/admin_for/;
sub view {
my $c = shift;
my $id = $c->stash("id");
my $talk;
$c->stash(adminspecial => 0);
eval {
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
$talk = SReview::Talk->by_nonce($c->stash('nonce'));
}
};
if($@) {
$c->stash(error => $@);
$c->render(variant => 'error');
return;
}
my $nonce = $talk->nonce;
my $variant;
if ($talk->state eq 'preview' || $talk->state eq 'broken') {
$variant = undef;
} elsif(admin_for($c, $talk)) {
$variant = undef;
$c->stash(adminspecial => 1);
} elsif($talk->state < 'preview') {
$variant = 'preparing';
} elsif($talk->state < 'done') {
$variant = 'transcode';
} elsif($talk->state == 'injecting') {
$variant = 'injecting';
} else {
$variant = 'done';
}
my $vid_prefix = $c->srconfig->get('vid_prefix');
$vid_prefix = '' unless defined($vid_prefix);
$c->stash(vid_prefix => $vid_prefix);
$c->stash(talk => $talk);
$c->stash(stylesheets => ['/review.css']);
$c->render(template => "review/" . $c->srconfig->get("review_template"), variant => $variant);
}
sub update {
my $c = shift;
my $id = $c->stash("id");
my $talk;
$c->stash(stylesheets => ['/review.css']);
if(defined($id)) {
$talk = SReview::Talk->new(talkid => $id);
} else {
eval {
$talk = SReview::Talk->by_nonce($c->stash('nonce'));
};
if($@) {
$c->stash(error => $@);
$c->render(variant => 'error');
return;
}
}
$c->stash(talk => $talk);
if(!admin_for($c, $talk) && $talk->state ne 'preview' && $talk->state ne 'broken') {
$c->stash(error => 'This talk is not currently available for review. Please try again later!');
$c->render(variant => 'error');
return;
}
$talk->add_correction(serial => 0);
if($c->param('serial') != $talk->corrections->{serial}) {
$c->stash(error => 'This talk was updated (probably by someone else) since you last loaded it. Please reload the page, and try again.');
$c->render(variant => 'error');
return;
}
if(defined($c->param("comment_text")) && length($c->param("comment_text")) > 0) {
$talk->comment($c->param("comment_text"));
}
if(defined($c->param("complete_reset")) && $c->param("complete_reset") eq "1") {
$talk->reset_corrections();
$talk->set_state("cutting");
$c->render(variant => 'reset');
return;
}
if(!defined($c->param("video_state"))) {
$c->stash(error => 'Invalid submission data; missing parameter video_state.');
$c->render(variant => "error");
return;
}
if($c->param("video_state") eq "ok") {
if($talk->corrections->{serial} == 0) {
$c->stash(error => 'No corrections have yet been applied to this talk. Unless (at least) start and end times are applied through this webinterface, the likelihood that the video starts and ends at the correct time is very low. Please go back and set the correct start and end times; if by extreme coincidence this video does start and end at the correct time, then please select the "there are problems" option in the previous screen, and submit the form without any changes.');
$c->render(variant => "error");
return;
}
$talk->done_correcting;
$talk->state_done("preview");
$c->render(variant => 'done');
return;
}
my $corrections = {};
if(!defined($c->param("audio_channel"))) {
$c->stash(error => 'Invalid submission data; missing parameter audio_channel.');
$c->render(variant => 'error');
return;
}
if($c->param("audio_channel") ne "3") {
$talk->set_correction("audio_channel", $c->param("audio_channel"));
$corrections->{audio_channel} = $c->param("audio_channel");
} else {
if($c->param("no_audio_options") eq "no_publish") {
$talk->set_state("broken");
$talk->comment("The audio is broken; the talk should not be released.");
$talk->done_correcting;
$c->render(variant => 'other');
return;
}
}
if(!defined($c->param("start_time"))) {
$c->stash(error => 'Invalid submission data; missing parameter start_time.');
$c->render(variant => 'error');
return;
}
if($c->param("start_time") ne "start_time_ok") {
$talk->add_correction("offset_start", $c->param("start_time_corrval"));
$corrections->{start} = $c->param("start_time_corrval");
}
if(!defined($c->param("end_time"))) {
$c->stash(error => 'Invalid submission data; missing parameter end_time.');
$c->render(variant => 'error');
return;
}
if($c->param("end_time") ne "end_time_ok") {
$talk->add_correction("offset_end", $c->param("end_time_corrval"));
$corrections->{end} = $c->param("end_time_corrval");
}
if(!defined($c->param("av_sync"))) {
$c->stash(error => 'Invalid submission data; missing parameter av_sync.');
$c->render(variant => 'error');
return;
}
if($c->param("av_sync") eq "av_not_ok_audio") {
$talk->add_correction("offset_audio", $c->param("av_seconds"));
$corrections->{audio_offset} = $c->param("av_seconds");
} elsif($c->param("av_sync") eq "av_not_ok_video") {
$talk->add_correction("offset_audio", "-" . $c->param("av_seconds"));
$corrections->{audio_offset} = "-" . $c->param("av_seconds");
}
if(defined($c->param("broken")) && $c->param("broken") eq "yes") {
$talk->set_state("broken");
$c->stash(other_msg => $c->param("comment_text"));
$talk->done_correcting;
$c->render(variant => "other");
return;
}
$talk->done_correcting;
$talk->set_state("waiting_for_files");
$talk->state_done("waiting_for_files");
$c->stash(corrections => $corrections);
$c->render(variant => 'newreview');
}
sub data {
my $c = shift;
my $talk = SReview::Talk->by_nonce($c->stash('nonce'));
my $data = $talk->corrected_times;
$c->app->log->debug($talk->video_fragments);
$data->{filename} = $talk->relative_name . "." . $c->srconfig->get("preview_exten");
$data->{room} = $talk->room;
$c->render(json => $data);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Admin.pm 0000644 0001750 0001750 00000002210 14054727355 021126 0 ustar wouter wouter package SReview::Web::Controller::Admin;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Collection 'c';
sub main {
my $c = shift;
my $st;
my $talks = ();
my $room;
my $lastroom = '';
if(defined($c->session->{room})) {
$st = $c->dbh->prepare('SELECT nonce, room, name, starttime, speakers, state FROM talk_list WHERE eventid = ? AND roomid = ? ORDER BY starttime');
$st->execute($c->eventid, $c->session->{room});
} else {
$st = $c->dbh->prepare('SELECT nonce, room, name, starttime, speakers, state FROM talk_list WHERE eventid = ? ORDER BY room, starttime');
$st->execute($c->eventid);
}
while(my $row = $st->fetchrow_hashref("NAME_lc")) {
if ($row->{'room'} ne $lastroom) {
if(defined($room)) {
push @$talks, c($lastroom => $room);
}
$room = [];
}
$lastroom = $row->{'room'};
next unless defined($row->{nonce});
push @$room, [$row->{'starttime'} . ': ' . $row->{'name'} . ' by ' . $row->{'speakers'} . ' (' . $row->{'state'} . ')' => $row->{'nonce'}];
}
if(defined($room)) {
push @$talks, c($lastroom => $room);
}
$c->stash(email => $c->session->{email});
$c->stash(talks => $talks);
$c->render;
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/Config.pm 0000644 0001750 0001750 00000000502 14051436474 021301 0 ustar wouter wouter package SReview::Web::Controller::Config;
use Mojo::Base 'Mojolicious::Controller';
sub get_config {
my $c = shift->openapi->valid_input;
my $eventid = $c->eventid;
my $config;
if(defined($eventid)) {
$config = { event => $c->eventid };
} else {
$config = {};
}
return $c->render(openapi => $config);
}
1;
SReview-0.8.0/lib/SReview/Web/Controller/CreditPreviews.pm 0000644 0001750 0001750 00000004165 14063331116 023032 0 ustar wouter wouter package SReview::Web::Controller::CreditPreviews;
use Mojo::Base 'Mojolicious::Controller';
use SReview::Talk;
use SReview::Template::SVG qw/process_template/;
use SReview::Files::Factory;
my %valid_suffix = (pre => ["preroll_template"], post => ["postroll_template", "postroll"], sorry => , ["apology_template"]);
sub serve_png {
my $c = shift->openapi->valid_input or return;;
my $slug = $c->param("slug");
my $suffix = $c->stash("suffix");
my $nonce = $c->param("nonce");
my $talk;
if(defined($slug)) {
$talk = SReview::Talk->by_slug($slug);
} elsif(defined($nonce)) {
$talk = SReview::Talk->by_nonce($nonce);
} else {
$c->app->log->debug("no slug or nonce, can't generate a preview");
return $c->reply->not_found;
}
if(!defined($talk)) {
$c->app->log->debug("talk not found");
return $c->reply->not_found;
}
my $input_coll = SReview::Files::Factory->create("intermediate", $c->srconfig->get("pubdir"));
my $template;
if(!exists($valid_suffix{$suffix})) {
$c->app->log->debug("invalid suffix, ignored");
return $c->reply->not_found;
}
if(scalar(@{$valid_suffix{$suffix}}) > 1) {
$c->app->log->debug("checking if static file exists");
my $png = $c->srconfig->get($valid_suffix{$suffix}[1]);
if(defined $png && -f $png) {
$c->app->log->debug("using prerendered file");
return $c->reply->file($png);
}
}
$template = $c->srconfig->get($valid_suffix{$suffix}[0]);
if(!defined $template) {
$c->app->log->debug("template not configured, ignored");
return $c->reply->not_found;
}
$c->app->log->debug("looking for render of template $template");
my $relname = $talk->relative_name . "-" . $suffix . ".png";
my $force = $c->param("force");
if((defined($force) && $force ne "false") || !($input_coll->has_file($relname))) {
$c->app->log->debug("file does not exist or force specified, rerendering");
my $preroll_file = $input_coll->add_file(relname => $relname);
process_template($template, $preroll_file->filename, $talk, $c->srconfig);
$preroll_file->store_file;
}
$c->app->log->debug("serving rendered file...");
return $c->reply->file($input_coll->get_file(relname => $relname)->filename);
}
1;
SReview-0.8.0/lib/SReview/Db.pm 0000644 0001750 0001750 00000224327 14111751734 015572 0 ustar wouter wouter package SReview::Db;
use strict;
use warnings;
use Mojo::Pg;
use Mojo::Pg::Migrations;
use SReview::Config;
my $code;
my $init;
my $db;
sub selfdestruct {
my %where = @_;
for my $key('code', 'init') {
if(!exists($where{$key})) {
$where{$key} = 0;
}
}
$code->migrate($where{code});
$init->migrate($where{init});
}
sub init {
my $config = shift;
$db = Mojo::Pg->new->dsn($config->get('dbistring'));
$code = Mojo::Pg::Migrations->new(pg => $db);
$code->name('code');
$code->from_data();
$init = Mojo::Pg::Migrations->new(pg => $db);
$init->name('init');
$init->from_data();
$code->migrate(0);
$init->migrate() or return 0;
$code->migrate() or return 0;
if(defined($config->get("adminuser")) && defined($config->get("adminpw"))) {
$db->db->dbh->prepare("INSERT INTO users(email, password, isadmin) VALUES(?, crypt(?, gen_salt('bf', 8)), true) ON CONFLICT ON CONSTRAINT users_email_unique DO NOTHING")->execute($config->get("adminuser"), $config->get("adminpw"));
}
return 1;
}
1;
__DATA__
@@ init
-- 1 up
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
-- 1 down
DROP EXTENSION IF EXISTS plpgsql;
DROP EXTENSION IF EXISTS pgcrypto;
-- 2 up
CREATE TYPE talkstate AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'needs_work',
'lost'
);
CREATE TYPE jobstate AS ENUM (
'waiting',
'scheduled',
'running',
'done',
'failed'
);
-- 2 down
DROP TYPE talkstate;
DROP TYPE jobstate;
-- 3 up
CREATE TABLE events (
id SERIAL PRIMARY KEY,
name character varying NOT NULL,
time_offset integer DEFAULT 0 NOT NULL
);
CREATE TABLE rooms (
id SERIAL PRIMARY KEY,
name character varying,
altname character varying
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email character varying,
password text,
isadmin boolean DEFAULT false,
room integer REFERENCES rooms(id),
name character varying,
isvolunteer boolean DEFAULT false
);
CREATE TABLE raw_files (
id SERIAL PRIMARY KEY,
filename character varying NOT NULL,
room integer NOT NULL REFERENCES rooms(id),
starttime timestamp with time zone,
endtime timestamp with time zone
);
CREATE TABLE tracks (
id SERIAL PRIMARY KEY,
name character varying,
email character varying,
upstreamid character varying
);
CREATE TABLE talks (
id SERIAL PRIMARY KEY,
room integer NOT NULL REFERENCES rooms(id),
slug character varying NOT NULL,
nonce character varying DEFAULT encode(gen_random_bytes(32), 'hex'::text) NOT NULL UNIQUE,
starttime timestamp with time zone NOT NULL,
endtime timestamp with time zone NOT NULL,
title character varying NOT NULL,
event integer NOT NULL REFERENCES events(id),
state talkstate DEFAULT 'waiting_for_files'::talkstate NOT NULL,
progress jobstate DEFAULT 'waiting'::jobstate NOT NULL,
comments text,
upstreamid character varying NOT NULL,
subtitle character varying,
prelen interval,
postlen interval,
track integer REFERENCES tracks(id),
reviewer integer REFERENCES users(id),
perc integer,
apologynote text,
UNIQUE(event, slug)
);
CREATE TABLE speakers (
id SERIAL PRIMARY KEY,
email character varying,
name character varying NOT NULL
);
CREATE TABLE speakers_talks (
speaker integer REFERENCES speakers(id),
talk integer REFERENCES talks(id),
PRIMARY KEY (speaker, talk)
);
CREATE TABLE properties (
id SERIAL PRIMARY KEY,
name character varying,
description character varying,
helptext character varying
);
CREATE TABLE corrections (
talk integer NOT NULL REFERENCES talks(id),
property integer NOT NULL REFERENCES properties(id),
property_value character varying,
PRIMARY KEY(talk, property)
);
CREATE TABLE speakers_events (
speaker integer NOT NULL REFERENCES speakers(id),
event integer NOT NULL REFERENCES events(id),
upstreamid character varying
);
-- 3 down
DROP TABLE speakers_events;
DROP TABLE corrections;
DROP TABLE properties;
DROP TABLE speakers_talks;
DROP TABLE speakers;
DROP TABLE talks;
DROP TABLE tracks;
DROP TABLE raw_files;
DROP TABLE users;
DROP TABLE rooms;
DROP TABLE events;
-- 4 up
CREATE VIEW raw_talks AS
SELECT talks.id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime AS talk_start,
talks.endtime AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime - talks.starttime) AS talks_length,
(raw_files.endtime - raw_files.starttime) AS raw_length,
(LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime)) AS raw_length_corrected,
sum((LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime))) OVER (PARTITION BY talks.id) AS raw_total,
CASE
WHEN (raw_files.starttime < talks.starttime) THEN (talks.starttime - raw_files.starttime)
ELSE '00:00:00'::interval
END AS fragment_start
FROM talks,
raw_files
WHERE (((((talks.starttime >= raw_files.starttime) AND (talks.starttime <= raw_files.endtime)) OR ((talks.endtime >= raw_files.starttime) AND (talks.endtime <= raw_files.endtime))) OR ((talks.starttime <= raw_files.starttime) AND (talks.endtime >= raw_files.endtime))) AND (talks.room = raw_files.room));
CREATE VIEW last_room_files AS
SELECT raw_files.filename,
raw_files.starttime,
raw_files.endtime,
(date_part('epoch'::text, raw_files.endtime) - date_part('epoch'::text, raw_files.starttime)) AS length,
rooms.name AS room
FROM (raw_files
JOIN rooms ON ((raw_files.room = rooms.id)))
WHERE ((raw_files.room, raw_files.starttime) IN ( SELECT raw_files_1.room,
max(raw_files_1.starttime) AS max
FROM raw_files raw_files_1
GROUP BY raw_files_1.room));
CREATE FUNCTION speakerlist(integer) RETURNS character varying
LANGUAGE plpgsql STABLE
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
curname speakers.name%TYPE;
prevname varchar;
retval varchar;
BEGIN
retval=NULL;
prevname=NULL;
curname=NULL;
FOR row IN crsr LOOP
prevname = curname;
curname = row.name;
IF prevname IS NOT NULL THEN
retval = concat_ws(', ', retval, prevname);
END IF;
END LOOP;
retval = concat_ws(' and ', retval, curname);
RETURN retval;
END;
$_$;
CREATE VIEW mailers AS
SELECT speakers.email,
talks.nonce,
talks.title
FROM ((speakers_talks
JOIN speakers ON ((speakers_talks.speaker = speakers.id)))
JOIN talks ON ((speakers_talks.talk = talks.id)))
WHERE (speakers.email IS NOT NULL);
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
CREATE FUNCTION adjusted_raw_talks(integer, interval, interval) RETURNS SETOF raw_talks
LANGUAGE plpgsql
AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start,
talks.endtime + start_off + end_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off THEN talks.starttime + start_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval THEN (talks.starttime + start_off - '00:20:00'::interval) - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - '00:20:00'::interval) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off THEN talks.endtime + start_off + end_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
CREATE FUNCTION corrections_redirect() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
corrs RECORD;
BEGIN
FOR corrs IN SELECT * FROM corrections WHERE talk = NEW.talk AND property = NEW.property LOOP
UPDATE corrections SET property_value = NEW.property_value WHERE talk = NEW.talk AND property = NEW.property;
RETURN NULL;
END LOOP;
RETURN NEW;
END $$;
CREATE FUNCTION speakeremail(integer) RETURNS character varying
LANGUAGE plpgsql
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.email, speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
retval VARCHAR;
BEGIN
retval = NULL;
FOR row IN crsr LOOP
retval = concat_ws(', ', retval, row.name || ' <' || row.email || '>');
END LOOP;
RETURN retval;
END; $_$;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
CREATE TRIGGER corr_redirect_conflict BEFORE INSERT ON corrections FOR EACH ROW EXECUTE PROCEDURE corrections_redirect();
-- 4 down
DROP TRIGGER corr_redirect_conflict ON corrections;
DROP VIEW talk_list;
DROP VIEW mailers;
DROP VIEW last_room_files;
DROP FUNCTION state_next(talkstate);
DROP FUNCTION corrections_redirect();
DROP FUNCTION adjusted_raw_talks(integer, interval, interval);
DROP FUNCTION speakeremail(integer);
DROP FUNCTION speakerlist(integer);
DROP VIEW raw_talks;
-- 5 up
INSERT INTO properties(name, description, helptext) VALUES('length_adj', 'Length adjustment', 'Set a relative adjustment value for the talk here, specified in seconds. To shorten the talk length, enter a negative value; to increase the talk length, enter a positive value');
INSERT INTO properties(name, description, helptext) VALUES('offset_audio', 'Audio offset', 'Use for fixing A/V sync issues. Positive delays audio, negative delays video. Seconds; may be fractional.');
INSERT INTO properties(name, description, helptext) VALUES('audio_channel', 'Audio channel', 'Use 0 for the main channel, 1 for the alternate channel, or 2 for both channels mixed together');
INSERT INTO properties(name, description, helptext) VALUES('offset_start', 'Time offset', 'Use to adjust the time position of this talk. Negative values move the start to earlier in time, positive to later. Note that both start and end position are updated; if the end should not be updated, make sure to also set the "Length adjustment" value. Seconds; may be fractional.');
-- 5 down
DELETE FROM corrections WHERE property IN (SELECT id FROM properties WHERE name IN ('length_adj', 'offset_audio', 'audio_channel', 'offset_start'));
DELETE FROM properties WHERE name IN ('length_adj', 'offset_audio', 'audio_channel', 'offset_start');
-- 6 up
ALTER TABLE speakers ADD upstreamid VARCHAR;
-- 6 down
ALTER TABLE speakers DROP upstreamid;
-- 7 up
ALTER TABLE talks ADD description TEXT;
-- 7 down
ALTER TABLE talks DROP description;
-- 8 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'ignored',
'needs_work',
'lost'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 8 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'needs_work',
'lost'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
UPDATE talks SET state='broken' WHERE state='ignored';
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 9 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 9 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'ignored',
'needs_work',
'lost'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 10 up
ALTER TABLE rooms ADD outputname VARCHAR;
DROP VIEW talk_list;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
-- 10 down
DROP VIEW talk_list;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
ALTER TABLE rooms DROP outputname;
-- 11 up
ALTER TABLE talks
ADD CONSTRAINT check_positive_length
CHECK (starttime < endtime);
ALTER TABLE events ADD inputdir VARCHAR, ADD outputdir VARCHAR;
-- 11 down
ALTER TABLE talks
DROP CONSTRAINT check_positive_length;
ALTER TABLE events DROP inputdir, DROP outputdir;
-- 12 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'announcing',
'done',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 12 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'done',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
DROP VIEW talk_list;
UPDATE talks SET state='done' WHERE state='announcing';
ALTER TABLE talks ALTER state TYPE talkstate_new USING (state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM (((rooms
LEFT JOIN talks ON ((rooms.id = talks.room)))
LEFT JOIN events ON ((talks.event = events.id)))
LEFT JOIN tracks ON ((talks.track = tracks.id)));
DROP FUNCTION state_next(talkstate);
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 13 up
DROP TRIGGER corr_redirect_conflict ON corrections;
DROP FUNCTION adjusted_raw_talks(integer, interval, interval);
DROP FUNCTION corrections_redirect();
DROP VIEW talk_list;
DROP FUNCTION speakerlist(integer);
DROP FUNCTION speakeremail(integer);
DROP FUNCTION state_next(talkstate);
DROP VIEW last_room_files;
DROP VIEW mailers;
DROP VIEW raw_talks;
-- 13 down
CREATE VIEW last_room_files AS
SELECT raw_files.filename,
raw_files.starttime,
raw_files.endtime,
date_part('epoch'::text, raw_files.endtime) - date_part('epoch'::text, raw_files.starttime) AS length,
rooms.name AS room
FROM raw_files
JOIN rooms ON raw_files.room = rooms.id
WHERE ((raw_files.room, raw_files.starttime) IN ( SELECT raw_files_1.room,
max(raw_files_1.starttime) AS max
FROM raw_files raw_files_1
GROUP BY raw_files_1.room));
CREATE VIEW mailers AS
SELECT speakers.email,
talks.nonce,
talks.title
FROM speakers_talks
JOIN speakers ON speakers_talks.speaker = speakers.id
JOIN talks ON speakers_talks.talk = talks.id
WHERE speakers.email IS NOT NULL;
CREATE VIEW raw_talks AS
SELECT talks.id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime AS talk_start,
talks.endtime AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
talks.endtime - talks.starttime AS talks_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime) AS raw_length_corrected,
sum(LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime)) OVER (PARTITION BY talks.id) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime THEN talks.starttime - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM talks,
raw_files
WHERE (talks.starttime >= raw_files.starttime AND talks.starttime <= raw_files.endtime OR talks.endtime >= raw_files.starttime AND talks.endtime <= raw_files.endtime OR talks.starttime <= raw_files.starttime AND talks.endtime >= raw_files.endtime) AND talks.room = raw_files.room;
CREATE FUNCTION speakerlist(integer) RETURNS varchar
LANGUAGE plpgsql STABLE
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
curname speakers.name%TYPE;
prevname varchar;
retval varchar;
BEGIN
retval=NULL;
prevname=NULL;
curname=NULL;
FOR row IN crsr LOOP
prevname = curname;
curname = row.name;
IF prevname IS NOT NULL THEN
retval = concat_ws(', ', retval, prevname);
END IF;
END LOOP;
retval = concat_ws(' and ', retval, curname);
RETURN retval;
END;
$_$;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM rooms
LEFT JOIN talks ON rooms.id = talks.room
LEFT JOIN events ON talks.event = events.id
LEFT JOIN tracks ON talks.track = tracks.id;
CREATE FUNCTION adjusted_raw_talks(integer, interval, interval) RETURNS SETOF raw_talks LANGUAGE plpgsql AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start,
talks.endtime + start_off + end_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off THEN talks.starttime + start_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval THEN (talks.starttime + start_off - '00:20:00'::interval) - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - '00:20:00'::interval) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off THEN talks.endtime + start_off + end_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
CREATE FUNCTION corrections_redirect() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
corrs RECORD;
BEGIN
FOR corrs IN SELECT * FROM corrections WHERE talk = NEW.talk AND property = NEW.property LOOP
UPDATE corrections SET property_value = NEW.property_value WHERE talk = NEW.talk AND property = NEW.property;
RETURN NULL;
END LOOP;
RETURN NEW;
END $$;
CREATE FUNCTION speakeremail(integer) RETURNS varchar
LANGUAGE plpgsql
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.email, speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
retval VARCHAR;
BEGIN
retval = NULL;
FOR row IN crsr LOOP
retval = concat_ws(', ', retval, row.name || ' <' || row.email || '>');
END LOOP;
RETURN retval;
END; $_$;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
CREATE TRIGGER corr_redirect_conflict BEFORE INSERT ON corrections FOR EACH ROW EXECUTE PROCEDURE corrections_redirect();
-- 14 up
ALTER TABLE raw_files ADD stream VARCHAR DEFAULT '' NOT NULL;
ALTER TABLE talks ADD active_stream VARCHAR DEFAULT '' NOT NULL;
-- 14 down
ALTER TABLE raw_files DROP stream;
ALTER TABLE talks DROP active_stream;
-- 15 up
INSERT INTO properties(name) VALUES('serial');
-- 15 down
LOCK TABLE corrections IN SHARE MODE;
DELETE FROM corrections USING properties WHERE corrections.property = properties.id AND properties.name = 'serial';
DELETE FROM properties WHERE name = 'serial';
-- 16 up
INSERT INTO properties(name) VALUES('offset_end');
-- 16 down
LOCK TABLE corrections IN SHARE MODE;
DELETE FROM corrections USING properties WHERE corrections.property = properties.id AND properties.name = 'offset_end';
DELETE FROM properties WHERE name = 'offset_end';
-- 17 up
ALTER TABLE raw_files ADD mtime INTEGER;
-- 17 down
ALTER TABLE raw_files DROP mtime;
-- 18 up
CREATE TABLE config_overrides (
id SERIAL PRIMARY KEY,
event integer REFERENCES events(id),
nodename character varying,
value character varying NOT NULL
);
-- 18 down
DROP TABLE config_overrides;
-- 19 up
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
-- 19 down
ALTER TABLE users DROP CONSTRAINT users_email_unique;
-- 20 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'announcing',
'done',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 20 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'announcing',
'done',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
UPDATE talks SET state='announcing' WHERE state='publishing';
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 21 up
ALTER TABLE talks ADD flags json;
-- 21 down
ALTER TABLE talks DROP flags;
-- 22 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'announcing',
'done',
'injecting',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 22 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'announcing',
'done'
'broken'
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
UPDATE talks SET state='broken' WHERE state='injecting';
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 23 up
ALTER TABLE raw_files ADD CONSTRAINT unique_filename UNIQUE(filename);
ALTER TABLE talks ALTER flags TYPE jsonb;
-- 23 down
ALTER TABLE raw_files DROP CONSTRAINT unique_filename;
ALTER TABLE talks ALTER flags TYPE json;
-- 24 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'finalreview',
'announcing',
'done',
'injecting',
'removing',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 24 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'announcing',
'done',
'injecting',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
UPDATE talks SET state='publishing' WHERE state IN ('finalreview','removing');
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 25 up
CREATE TABLE commentlog (
id SERIAL PRIMARY KEY,
talk integer REFERENCES talks(id),
comment TEXT,
state varchar,
logdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
INSERT INTO commentlog(talk, comment) SELECT id, comments FROM talks WHERE comments IS NOT NULL;
UPDATE talks SET comments = NULL;
-- 25 down
WITH logtexts(talk, comments) AS
(WITH orderedlog(talk, comment, logdate) AS
(SELECT talk, comment, logdate FROM commentlog ORDER BY logdate)
SELECT talk, string_agg(logdate || E'\n' || comment, E'\n\n') AS comments
FROM orderedlog
GROUP BY talk)
UPDATE talks SET comments = logtexts.comments
FROM logtexts
WHERE talks.id = logtexts.talk;
DROP TABLE commentlog;
-- 26 up
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'notify_final',
'finalreview',
'announcing',
'done',
'injecting',
'remove',
'removing',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
-- 26 down
CREATE TYPE talkstate_new AS ENUM (
'waiting_for_files',
'cutting',
'generating_previews',
'notification',
'preview',
'transcoding',
'uploading',
'publishing',
'finalreview',
'announcing',
'done',
'injecting',
'removing',
'broken',
'needs_work',
'lost',
'ignored'
);
ALTER TABLE talks ALTER state DROP DEFAULT;
UPDATE talks SET state='finalreview' WHERE state='notify_final';
ALTER TABLE talks ALTER state TYPE talkstate_new USING(state::varchar)::talkstate_new;
ALTER TABLE talks ALTER state SET DEFAULT 'waiting_for_files';
DROP TYPE talkstate;
ALTER TYPE talkstate_new RENAME TO talkstate;
@@ code
-- 1 up
CREATE VIEW last_room_files AS
SELECT raw_files.filename,
raw_files.starttime,
raw_files.endtime,
date_part('epoch'::text, raw_files.endtime) - date_part('epoch'::text, raw_files.starttime) AS length,
rooms.name AS room
FROM raw_files
JOIN rooms ON raw_files.room = rooms.id
WHERE ((raw_files.room, raw_files.starttime) IN ( SELECT raw_files_1.room,
max(raw_files_1.starttime) AS max
FROM raw_files raw_files_1
GROUP BY raw_files_1.room));
CREATE VIEW mailers AS
SELECT speakers.email,
talks.nonce,
talks.title
FROM speakers_talks
JOIN speakers ON speakers_talks.speaker = speakers.id
JOIN talks ON speakers_talks.talk = talks.id
WHERE speakers.email IS NOT NULL;
CREATE VIEW raw_talks AS
SELECT talks.id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime AS talk_start,
talks.endtime AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
talks.endtime - talks.starttime AS talks_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime) AS raw_length_corrected,
sum(LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime)) OVER (PARTITION BY talks.id) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime THEN talks.starttime - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM talks,
raw_files
WHERE (talks.starttime >= raw_files.starttime AND talks.starttime <= raw_files.endtime OR talks.endtime >= raw_files.starttime AND talks.endtime <= raw_files.endtime OR talks.starttime <= raw_files.starttime AND talks.endtime >= raw_files.endtime) AND talks.room = raw_files.room;
CREATE FUNCTION speakerlist(integer) RETURNS varchar
LANGUAGE plpgsql STABLE
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
curname speakers.name%TYPE;
prevname varchar;
retval varchar;
BEGIN
retval=NULL;
prevname=NULL;
curname=NULL;
FOR row IN crsr LOOP
prevname = curname;
curname = row.name;
IF prevname IS NOT NULL THEN
retval = concat_ws(', ', retval, prevname);
END IF;
END LOOP;
retval = concat_ws(' and ', retval, curname);
RETURN retval;
END;
$_$;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM rooms
LEFT JOIN talks ON rooms.id = talks.room
LEFT JOIN events ON talks.event = events.id
LEFT JOIN tracks ON talks.track = tracks.id;
CREATE FUNCTION adjusted_raw_talks(integer, interval, interval) RETURNS SETOF raw_talks LANGUAGE plpgsql AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start,
talks.endtime + start_off + end_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off THEN talks.starttime + start_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval THEN (talks.starttime + start_off - '00:20:00'::interval) - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - '00:20:00'::interval) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off THEN talks.endtime + start_off + end_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
CREATE FUNCTION corrections_redirect() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
corrs RECORD;
BEGIN
FOR corrs IN SELECT * FROM corrections WHERE talk = NEW.talk AND property = NEW.property LOOP
UPDATE corrections SET property_value = NEW.property_value WHERE talk = NEW.talk AND property = NEW.property;
RETURN NULL;
END LOOP;
RETURN NEW;
END $$;
CREATE FUNCTION speakeremail(integer) RETURNS varchar
LANGUAGE plpgsql
AS $_$
DECLARE
crsr CURSOR FOR SELECT speakers.email, speakers.name FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = $1;
row RECORD;
retval VARCHAR;
BEGIN
retval = NULL;
FOR row IN crsr LOOP
retval = concat_ws(', ', retval, row.name || ' <' || row.email || '>');
END LOOP;
RETURN retval;
END; $_$;
CREATE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
CREATE TRIGGER corr_redirect_conflict BEFORE INSERT ON corrections FOR EACH ROW EXECUTE PROCEDURE corrections_redirect();
-- 1 down
DROP TRIGGER corr_redirect_conflict ON corrections;
DROP FUNCTION adjusted_raw_talks(integer, interval, interval);
DROP FUNCTION corrections_redirect();
DROP VIEW talk_list;
DROP FUNCTION speakerlist(integer);
DROP FUNCTION speakeremail(integer);
DROP FUNCTION state_next(talkstate);
DROP VIEW last_room_files;
DROP VIEW mailers;
DROP VIEW raw_talks;
-- 2 up
CREATE OR REPLACE VIEW raw_talks AS
SELECT talks.id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime AS talk_start,
talks.endtime AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
talks.endtime - talks.starttime AS talks_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime) AS raw_length_corrected,
sum(LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime)) OVER (PARTITION BY talks.id) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime THEN talks.starttime - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM talks,
raw_files
WHERE (talks.starttime >= raw_files.starttime AND talks.starttime <= raw_files.endtime OR talks.endtime >= raw_files.starttime AND talks.endtime <= raw_files.endtime OR talks.starttime <= raw_files.starttime AND talks.endtime >= raw_files.endtime) AND talks.room = raw_files.room AND talks.active_stream = raw_files.stream;
CREATE OR REPLACE FUNCTION adjusted_raw_talks(integer, interval, interval) RETURNS SETOF raw_talks LANGUAGE plpgsql AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start,
talks.endtime + start_off + end_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off THEN talks.starttime + start_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND talks.active_stream = raw_files.stream
AND ((talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval THEN (talks.starttime + start_off - '00:20:00'::interval) - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND talks.active_stream = raw_files.stream
AND ((talks.starttime + start_off - '00:20:00'::interval) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off THEN talks.endtime + start_off + end_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND talks.active_stream = raw_files.stream
AND ((talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
-- 2 down
CREATE OR REPLACE VIEW raw_talks AS
SELECT talks.id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime AS talk_start,
talks.endtime AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
talks.endtime - talks.starttime AS talks_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime) AS raw_length_corrected,
sum(LEAST(raw_files.endtime, talks.endtime) - GREATEST(raw_files.starttime, talks.starttime)) OVER (PARTITION BY talks.id) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime THEN talks.starttime - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM talks,
raw_files
WHERE (talks.starttime >= raw_files.starttime AND talks.starttime <= raw_files.endtime OR talks.endtime >= raw_files.starttime AND talks.endtime <= raw_files.endtime OR talks.starttime <= raw_files.starttime AND talks.endtime >= raw_files.endtime) AND talks.room = raw_files.room;
CREATE OR REPLACE FUNCTION adjusted_raw_talks(integer, interval, interval) RETURNS SETOF raw_talks LANGUAGE plpgsql AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start,
talks.endtime + start_off + end_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off THEN talks.starttime + start_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval THEN (talks.starttime + start_off - '00:20:00'::interval) - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - '00:20:00'::interval) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off)) OVER (range unbounded preceding),
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off THEN talks.endtime + start_off + end_off - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
-- 3 up
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
DECLARE
enumvals talkstate[];
startval ALIAS FOR $1;
BEGIN
IF startval = 'injecting' THEN
return 'generating_previews'::talkstate;
ELSE
IF startval >= 'done' THEN
return startval;
ELSE
enumvals := enum_range(startval, NULL);
return enumvals[2];
END IF;
END IF;
END $_$;
-- 3 down
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
declare
enumvals talkstate[];
startval alias for $1;
begin
enumvals := enum_range(startval, NULL);
return enumvals[2];
end $_$;
-- 4 up
DROP VIEW talk_list;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
events.outputdir AS event_output,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM rooms
LEFT JOIN talks ON rooms.id = talks.room
LEFT JOIN events ON talks.event = events.id
LEFT JOIN tracks ON talks.track = tracks.id;
-- 4 down
DROP VIEW talk_list;
CREATE VIEW talk_list AS
SELECT talks.id,
talks.event AS eventid,
events.name AS event,
rooms.name AS room,
rooms.outputname AS room_output,
speakerlist(talks.id) AS speakers,
talks.title AS name,
talks.nonce,
talks.slug,
talks.starttime,
talks.endtime,
talks.state,
talks.progress,
talks.comments,
rooms.id AS roomid,
talks.prelen,
talks.postlen,
talks.subtitle,
talks.apologynote,
tracks.name AS track
FROM rooms
LEFT JOIN talks ON rooms.id = talks.room
LEFT JOIN events ON talks.event = events.id
LEFT JOIN tracks ON talks.track = tracks.id;
-- 5 up
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
DECLARE
enumvals talkstate[];
startval ALIAS FOR $1;
BEGIN
IF startval = 'injecting' THEN
return 'generating_previews'::talkstate;
ELSE
IF startval = 'removing' THEN
return 'waiting_for_files'::talkstate;
ELSE
IF startval >= 'done' THEN
return startval;
ELSE
enumvals := enum_range(startval, NULL);
return enumvals[2];
END IF;
END IF;
END IF;
END $_$;
-- 5 down
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
DECLARE
enumvals talkstate[];
startval ALIAS FOR $1;
BEGIN
IF startval = 'injecting' THEN
return 'generating_previews'::talkstate;
ELSE
IF startval >= 'done' THEN
return startval;
ELSE
enumvals := enum_range(startval, NULL);
return enumvals[2];
END IF;
END IF;
END $_$;
-- 6 up
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE Plpgsql
AS $_$
DECLARE
enumvals talkstate[];
startval ALIAS FOR $1;
BEGIN
IF startval = 'injecting' THEN
return 'generating_previews'::talkstate;
ELSE
IF startval = 'remove' THEN
return 'removing'::talkstate;
ELSE
IF startval = 'removing' THEN
return 'waiting_for_files'::talkstate;
ELSE
IF startval >= 'done' THEN
return startval;
ELSE
enumvals := enum_range(startval, NULL);
return enumvals[2];
END IF;
END IF;
END IF;
END IF;
END $_$;
-- 6 down
CREATE OR REPLACE FUNCTION state_next(talkstate) RETURNS talkstate
LANGUAGE plpgsql
AS $_$
DECLARE
enumvals talkstate[];
startval ALIAS FOR $1;
BEGIN
IF startval = 'injecting' THEN
return 'generating_previews'::talkstate;
ELSE
IF startval = 'removing' THEN
return 'waiting_for_files'::talkstate;
ELSE
IF startval >= 'done' THEN
return startval;
ELSE
enumvals := enum_range(startval, NULL);
return enumvals[2];
END IF;
END IF;
END IF;
END $_$;
-- 7 up
CREATE FUNCTION adjusted_raw_talks(integer, interval, interval, interval) RETURNS SETOF raw_talks LANGUAGE plpgsql AS $_$
DECLARE
talk_id ALIAS FOR $1;
start_off ALIAS FOR $2;
end_off ALIAS FOR $3;
audio_margin ALIAS FOR $4;
BEGIN
RETURN QUERY
SELECT talk_id AS talkid,
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off AS talk_start, -- the time where the talk starts, after adjustments
talks.endtime + start_off + end_off AS talk_end, -- the time where the talk ends, after adjustments
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
(talks.endtime + start_off + end_off) - (talks.starttime + start_off) AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - audio_margin) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - audio_margin)) OVER (range unbounded preceding) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime + start_off - audio_margin THEN talks.starttime + start_off - audio_margin - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - audio_margin) >= raw_files.starttime AND (talks.starttime + start_off - audio_margin) <= raw_files.endtime
OR (talks.endtime + start_off + end_off) >= raw_files.starttime AND (talks.endtime + start_off + end_off) <= raw_files.endtime
OR (talks.starttime + start_off - audio_margin) <= raw_files.starttime AND (talks.endtime + start_off + end_off) >= raw_files.endtime)
UNION
SELECT
-1 AS talkid, -- use -1 to mark that this is the pre video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.starttime + start_off - '00:20:00'::interval AS talk_start,
talks.starttime + start_off AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval - audio_margin) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.starttime + start_off) - GREATEST(raw_files.starttime, talks.starttime + start_off - '00:20:00'::interval - audio_margin)) OVER (range unbounded preceding) AS raw_total,
CASE
WHEN raw_files.starttime < talks.starttime + start_off - '00:20:00'::interval - audio_margin THEN talks.starttime + start_off - '00:20:00'::interval - audio_margin - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.starttime + start_off - '00:20:00'::interval - audio_margin) >= raw_files.starttime AND (talks.starttime + start_off - '00:20:00'::interval - audio_margin) <= raw_files.endtime
OR (talks.starttime + start_off) >= raw_files.starttime AND (talks.starttime + start_off) <= raw_files.endtime
OR (talks.starttime + start_off - '00:20:00'::interval - audio_margin) <= raw_files.starttime AND (talks.endtime + start_off) >= raw_files.endtime)
UNION
SELECT
-2 AS talkid, -- use -2 to mark that this is the post video
talks.slug,
raw_files.id AS rawid,
raw_files.filename AS raw_filename,
talks.endtime + start_off + end_off AS talk_start,
talks.endtime + start_off + end_off + '00:20:00'::interval AS talk_end,
raw_files.starttime AS raw_start,
raw_files.endtime AS raw_end,
'00:20:00'::interval AS talk_length,
raw_files.endtime - raw_files.starttime AS raw_length,
LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off - audio_margin) AS raw_length_corrected,
SUM(LEAST(raw_files.endtime, talks.endtime + start_off + end_off + '00:20:00'::interval) - GREATEST(raw_files.starttime, talks.endtime + start_off + end_off - audio_margin)) OVER (range unbounded preceding) AS raw_total,
CASE
WHEN raw_files.starttime < talks.endtime + start_off + end_off - audio_margin THEN talks.endtime + start_off + end_off - audio_margin - raw_files.starttime
ELSE '00:00:00'::interval
END AS fragment_start
FROM raw_files JOIN rooms ON raw_files.room = rooms.id JOIN talks ON rooms.id = talks.room
WHERE talks.id = talk_id
AND ((talks.endtime + start_off + end_off - audio_margin) >= raw_files.starttime AND (talks.endtime + start_off + end_off - audio_margin) <= raw_files.endtime
OR (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) <= raw_files.endtime
OR (talks.endtime + start_off + end_off - audio_margin) <= raw_files.starttime AND (talks.endtime + start_off + end_off + '00:20:00'::interval) >= raw_files.endtime);
END $_$;
-- 7 down
DROP FUNCTION adjusted_raw_talks(integer, interval, interval, interval);
SReview-0.8.0/lib/SReview/Template/ 0000755 0001750 0001750 00000000000 14116343665 016455 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Template/SVG.pm 0000644 0001750 0001750 00000007125 14054727355 017462 0 ustar wouter wouter package SReview::Template::SVG;
use SReview::Template;
use Mojo::UserAgent;
use Mojo::Util qw/xml_escape/;
use File::Temp qw/tempdir/;
use Exporter 'import';
our @EXPORT_OK = qw/process_template/;
=head1 NAME
SReview::Template::SVG - module to process an SVG template into a PNG
file
=head1 SYNOPSIS
use SReview::Template::SVG qw/process_template/;
use SReview::Talk;
use SReview::Config::Common;
my $talk = SReview::Talk->new(talkid => ...);
my $config = SReview::Config::Common::setup();
process_template($input_svg_template, $output_png_filename, $talk, $config);
# now a PNG file is written to $output_png_filename
=head1 DESCRIPTION
C uses L to process an input
file into a templated SVG file, and then runs inkscape over it to
convert the templated SVG file to a PNG file at the given location.
The input file can either be a file on the local file system, or it can
be an HTTP or HTTPS URL (in which case the template at that location
will first be downloaded, transparently).
=head1 TEMPLATE TAGS
In addition to the L syntax on C<$talk> that
L provides, C also passes the
these regexvars to L (for more information, see
SReview::Template):
=over
=item @SPEAKERS@
The value of C<$talk-Espeakers>
=item @ROOM@
The value of C<$talk-Eroom>
=item @TITLE@
The value of C<$talkEtitle>
=item @SUBTITLE@
The value of C<$talkEsubtitle>
=item @DATE@
The value of C<$talkEdate>
=item @APOLOGY@
The value of C<$talkEapology>
=back
Note that all these values are XML-escaped first.
=head1 CONFIGURATION
This module checks the following configuration values:
=head2 command_tune
If the value C in this hash is set to "C<0.9>", then the C
command is invoked with command-line parameters for Inkscape version 0.9
or below. In all other cases, command-line parameters for Inkscape
version 1.0 or above are used instead.
=head2 workdir
The location for temporary files that the module needs.
=cut
sub process_template($$$$) {
my $input = shift;
my $output = shift;
my $talk = shift;
my $config = shift;
my $tempdir = tempdir('svgXXXXXX', DIR => $config->get("workdir"), CLEANUP => 1);
my $outputsvg = "$tempdir/tmp.svg";
my $speakers = xml_escape($talk->speakers);
my $room = xml_escape($talk->room);
my $title = xml_escape($talk->title);
my $subtitle = xml_escape($talk->subtitle);
my $startdate = xml_escape($talk->date);
my $apology = xml_escape($talk->apology);
my $regexvars = {
'@SPEAKERS@' => $speakers,
'@ROOM@' => $room,
'@TITLE@' => $title,
'@SUBTITLE@' => $subtitle,
'@DATE@' => $startdate,
'@APOLOGY@' => $apology,
};
my $content = "";
my $template_engine = SReview::Template->new(talk => $talk, regexvars => $regexvars);
if($input =~ /^http(s)?:\/\//) {
my $ua = Mojo::UserAgent->new->connect_timeout(60)->max_redirects(10);
my $res = $ua->get($input)->result;
if(!$res->is_success) {
die "could not download: " . $res->message;
}
$content = $res->body;
} else {
open INPUT, '<:encoding(UTF-8)', $input;
while() {
$content .= $_;
}
close INPUT;
}
open my $fh, ">:encoding(UTF-8)", $outputsvg;
print "creating $output from $input\n";
print $fh $template_engine->string($content);
close $fh;
my $inkscape_options = "--batch-process -o $output";
if($config->get("command_tune")->{inkscape} eq "0.9") {
$inkscape_options = "--export-png=$output";
}
system("inkscape $inkscape_options $outputsvg");
}
1;
SReview-0.8.0/lib/SReview/Config.pm 0000644 0001750 0001750 00000020320 14063131375 016434 0 ustar wouter wouter use strict;
use warnings;
package SReview::Config;
use Data::Dumper;
use Carp;
use Mojo::JSON qw/decode_json encode_json/;
=head1 NAME
SReview::Config - Self-reproducing and self-documenting configuration file system
=head1 SYNOPSIS
use SReview::Config;
my $config = SReview::Config->new('/etc/sreview/config.pm');
$config->define('name', 'The name of this element', 'default');
...
print "You configured " . $config->get('name') . " as the name\n";
print "Full configuration: \n" . $config->dump;
=head1 DESCRIPTION
SReview::Config is a class to easily manage self-reproducing and
self-documenting configuration. You create an SReview::Config object,
populate it with possible configuration values, and then retrieve them.
=head1 METHODS
=head2 SReview::Config->new('path/to/filename');
Create a new SReview::Config object.
=cut
sub new {
my $self = {defs => {}};
my $class = shift;
bless $self, $class;
my $cfile = shift;
if (! -f $cfile) {
unless (grep /^SREVIEW_/, keys(%ENV)) {
carp "Warning: could not find configuration file $cfile, falling back to defaults";
}
} else {
package SReview::Config::_private;
use Carp;
my $rc = do($cfile);
if($@) {
croak "could not compile config file $cfile: $@";
} elsif(!defined($rc)) {
carp "could not read config file $cfile. Falling back to defaults.";
} elsif(!$rc) {
croak "could not process config file $cfile";
}
}
return $self;
};
=head2 $config->define(name, doc, default)
Define a new configuration value. Not legal after C has already
been called.
Name should be the name of the configuration value. Apart from the fact
that it should not have a sigil, it should be a valid name for a perl
scalar variable.
=cut
sub define {
my $self = shift;
my $name = shift;
my $doc = shift;
my $default = shift;
if(exists($self->{fixed})) {
croak "Tried to define a new value after a value has already been requested. This is not allowed!";
}
$self->{defs}{$name}{doc} = $doc;
$self->{defs}{$name}{default} = $default;
my $NAME = uc $name;
if(exists($ENV{"SREVIEW_${NAME}"})) {
$self->set($name => decode_json($ENV{"SREVIEW_${NAME}"}));
}
};
=head2 $config->define_deprecated(oldname, newname, conversion_sub)
Define a name as a deprecated way of configuring things. When this value
is set, SReview::Config will issue a warning that this option is now
deprecated, and that the user should use some other option instead.
The conversion subroutine is an optional argument that should mangle the
value given to "oldname" into the value expected by "newname". If it
returns nonzero, then SReview::Config will croak. It will receive a
reference to the "config" object, the value that is trying to be set, and the
name of the new parameter.
The default conversion sub just sets the value of the newname
configuration without any conversion.
=cut
sub define_deprecated {
my $self = shift;
my $oldname = shift;
my $newname = shift;
my $convert = shift;
if(exists($self->{fixed})) {
croak "Tried to define a new value after a value has already been requested. This is not allowed!";
}
$self->{defs}{$oldname}{deprecated} = 1;
$self->{defs}{$oldname}{instead} = $newname;
if(defined($convert)) {
$self->{defs}{$oldname}{convert} = $convert;
} else {
$self->{defs}{$oldname}{convert} = sub { my $self = shift; my $old = shift; $self->set($newname => $old); return 0; };
}
if(exists($SReview::Config::_private::{$oldname})) {
carp "Found a value for \"$oldname\" when it was being defined as a deprecated name. Please convert this value to a value of $newname!\n";
if ((&$self->{defs}{$oldname}{convert}($self, $SReview::Config::_private::{$oldname}, $newname)) != 0) {
croak "Could not convert deprecated value to new name: $!";
}
}
my $NAME = uc $oldname;
if(exists($ENV{"SREVIEW_${NAME}"})) {
$self->set($newname => decode_json($ENV{"SREVIEW_${NAME}"}));
}
}
=head2 $config->define_computed('name')
Defines a default value for a particular configuration parameter through
a subroutine.
If the subroutine returns C, that value will be ignored (and the
normal logic for defining a default will be used).
Should be used on a parameter that has already been defined through
$config->define
=cut
sub define_computed {
my $self = shift;
my $name = shift;
my $sub = shift;
$self->{defs}{$name}{sub} = $sub;
}
=head2 $config->get('name')
Return the value of the given configuration item. Also finalizes the
definitions of this configuration file; that is, once this method has
been called, the C method above will croak.
The returned value will either be the default value configured at
C time, the value configured in the configuration file, or the
value set (in JSON format) in the environment variable
C >, where I is the upper-case version of the name
of the configuration item.
=cut
sub get {
my $self = shift;
my $name = shift;
my $talk = shift;
if(!exists($self->{defs}{$name})) {
croak "e: definition for config file item $name does not exist!";
}
$self->{fixed} = 1;
if(exists($self->{defs}{$name}{sub})) {
my $rv = &{$self->{defs}{$name}{sub}}($self, $talk);
if(defined($rv)) {
return $rv;
}
}
if(exists($SReview::Config::_private::{$name})) {
return ${$SReview::Config::_private::{$name}};
}
if(defined($ENV{'SREVIEW_VERBOSE'}) && $ENV{'SREVIEW_VERBOSE'} gt 0) {
print "No configuration value found for $name, using defaults\n";
}
return $self->{defs}{$name}{default};
};
=head2 $config->set('name', value);
Change the current value of the given configuration item.
Note, this does not change the defaults, only the configured value.
=cut
sub set {
my $self = shift;
my %vals = @_;
foreach my $name(keys %vals) {
if(! exists($self->{defs}{$name})) {
croak "Configuration value $name is not defined yet";
}
{
my $val = $vals{$name};
if(exists($self->{defs}{$name}{deprecated})) {
my $newname = $self->{defs}{$name}{instead};
carp "A value for \"$name\" is being set, which is a deprecated name for $newname. Please update things for the new value\n";
if ((&{$self->{defs}{$name}{convert}}($self, $vals{$name}, $self->{defs}{$name}{instead})) != 0) {
croak "Could not convert deprecated value for $name to $newname format\n";
}
return;
}
$SReview::Config::_private::{$name} = \$val;
}
}
}
=head2 $config->describe('name');
Return the documentation string for the given name
=cut
sub describe {
my $self = shift;
my $conf = shift;
return $self->{defs}{$conf}{doc};
}
=head2 $config->dump
Return a string describing the whole configuration.
Each configuration item will produced in one of the following two
formats:
=over
=item *
For an item that only has a default set:
# Documentation value given to define
#$name = "default value";
=item *
For an item that has a different value configured (either through the
configuration file, or through C):
# Documentation value given to define
$name = "current value";
=cut
sub dump {
my $self = shift;
my $rv = "";
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
foreach my $conf(sort(keys %{$self->{defs}})) {
$rv .= "# " . $self->{defs}{$conf}{doc} . "\n";
if(exists($SReview::Config::_private::{$conf}) && (!defined($self->{defs}{$conf}{default}) || ${$SReview::Config::_private::{$conf}} ne $self->{defs}{$conf}{default})) {
$Data::Dumper::Pad = "";
$rv .= Data::Dumper->Dump([${$SReview::Config::_private::{$conf}}], [$conf]) . "\n";
} else {
$Data::Dumper::Pad = "#";
$rv .= Data::Dumper->Dump([$self->{defs}{$conf}{default}], [$conf]) . "\n";
}
}
$rv .= "# Do not remove this, perl needs it\n1;\n";
return $rv;
};
=back
=head2 $config->dump_item("item")
Print a JSON value for the given configuration item. Prints the default
item if this item hasn't been set a value.
=cut
sub dump_item {
my ($self, $item) = @_;
print encode_json($self->get($item));
}
=head2 $config->is_default("item")
Return a truthy value if the given configuration item is still at its
default value.
=cut
sub is_default {
my ($self, $item) = @_;
return (exists($SReview::Config::_private::{$item})) ? 0 : 1;
}
=head1 BUGS
It is currently not possible to load more than one configuration file in
the same process space. This will be fixed at some point in the future.
=cut
1;
SReview-0.8.0/lib/SReview/Model/ 0000755 0001750 0001750 00000000000 14116343665 015742 5 ustar wouter wouter SReview-0.8.0/lib/SReview/Model/DbElement.pm 0000644 0001750 0001750 00000000740 14000557463 020133 0 ustar wouter wouter package SReview::Model::DbElement;
use Moose;
use SReview::Config::Common;
use SReview::Db;
has 'config' => (
is => 'rw',
isa => 'SReview::Config',
builder => '_get_config',
lazy => 1,
);
sub _get_config {
my $self = shift;
return SReview::Config::Common::setup;
}
has 'dbh' => (
is => 'rw',
isa => 'Mojo::Pg',
builder => '_get_dbh',
lazy => 1,
);
sub _get_dbh {
my $self = shift;
my $pg = Mojo::Pg->new()->dsn($self->config->get('dbistring'));
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Model/Event.pm 0000644 0001750 0001750 00000001130 14000557466 017352 0 ustar wouter wouter package SReview::Model::Event;
use Moose;
extends 'SReview::Model::DbElement';
has 'name' => (
is => 'ro',
builder => '_get_name',
);
sub _get_name {
return shift->config->get('event');
}
has 'inputdir' => (
is => 'ro',
builder => '_get_inputdir',
lazy => 1,
);
sub _get_inputdir {
my $self = shift;
my $st = $self->dbh->db->dbh->prepare("SELECT inputdir FROM events WHERE name = ?");
$st->execute($self->name);
if($st->rows == 0) {
return $self->name;
}
my $dir = $st->fetchrow_hashref()->{inputdir};
if(!defined($dir)) {
return $self->name;
}
return $dir;
}
no Moose;
1;
SReview-0.8.0/lib/SReview/Job.pm 0000644 0001750 0001750 00000002407 13767615127 015763 0 ustar wouter wouter package SReview::Job;
use SReview::Config;
use Scalar::Util qw(weaken);
use Carp;
my $singleton;
sub new {
my $class = shift;
if(defined $singleton) {
croak "$class object created twice!";
}
my $self = {};
$self->{jobname} = shift;
$self->{talkid} = shift;
$self->{dbh} = shift;
die "Missing arguments!" unless defined($dbh);
$self->{jobs} = [];
$dbh->begin;
my $set = $dbh->prepare("UPDATE talks SET progress='running' WHERE id = ?");
$set->execute($talkid);
$dbh->commit;
bless $self, $class;
$singleton = $self;
weaken($singleton);
return $self;
};
$SIG{'__DIE__'} = sub {
my $msg = shift;
my $ref = $singleton;
if(!defined($ref)) {
die $msg;
}
my $insert = $ref->{dbh}->prepare('INSERT INTO logs(talkid, job, message) VALUES(?, ?, ?)');
$ref->{dbh}->begin_work();
$insert->execute($ref->{jobname}, $ref->{talkid}, $msg);
$ref->{dbh}->commit();
};
sub add_job {
my $self = shift;
push @{$self->{jobs}}, shift;
};
sub DESTROY {
my $self = shift;
my $progress = $dbh->prepare('UPDATE talks SET perc = (?/?)*100 WHERE id = ?');
my $total = scalar(@{$self->{jobs}});
for(my $i=0; $i<$total; $i++) {
$dbh->begin;
$progress->execute($i, $total, $talkid);
my $job = ${$self->{jobs}[$i]};
print $job;
system($job);
$dbh->commit;
}
};
1;
SReview-0.8.0/lib/SReview/Normalizer.pm 0000644 0001750 0001750 00000003165 14051446247 017365 0 ustar wouter wouter package SReview::Normalizer;
use Moose;
use SReview::Config::Common;
=head1 NAME
SReview::Normalizer - normalize the audio of a video asset.
=head1 SYNOPSIS
SReview::Normalizer->new(input => SReview::Video->new(...), output => SReview::Video->new(...))->run();
=head1 DESCRIPTION
C is a class to normalize the audio of
a given SReview::Video asset, using bs1770gain at its default settings.
It looks at the C configuration parameter to decide
whether to pass the C<--suffix> option to bs1770gain: if the installed
version of C is at 0.5 or below, set the C key
of C to 0.5 to remove the C<--suffix> parameter from the
command line (which is required for 0.6 or above, but not supported by
0.5 or below).
=head1 ATTRIBUTES
The following attributes are supported by
SReview::Normalizer.
=head2 input
An L object for which the audio should be normalized.
Required.
=cut
has 'input' => (
is => 'rw',
isa => 'SReview::Video',
required => 1,
);
=head2 output
An L object that the normalized audio should be written
to, together with the video from the input file.
Required. Must point to a .mkv file.
=cut
has 'output' => (
is => 'rw',
isa => 'SReview::Video',
required => 1,
);
=head1 METHODS
=head2 run
Performs the normalization.
=cut
sub run {
my $self = shift;
my $config = SReview::Config::Common::setup();
my $pkg = "SReview::Normalizer::" . ucfirst($config->get("normalizer"));
eval "require $pkg;";
if($@) {
die "$@: $!";
}
return $pkg->new(input => $self->input, output => $self->output)->run();
}
1;
SReview-0.8.0/lib/SReview.pm 0000644 0001750 0001750 00000001254 14116343541 015233 0 ustar wouter wouter package SReview;
use strict;
use warnings;
our $VERSION;
$VERSION = "0.8.0";
=head1 NAME
SReview - a video review and transcoding system
=head1 DESCRIPTION
SReview is a system to review and transcode conference videos. You feed
it a bunch of timestamped videos and a schedule, and it creates initial
cuts based on that schedule. Next, you review (or ask reviewers) to
decide on the actual start- and end times of the talks, through a
webinterface. Once those start- and endtimes have been decided upon,
SReview prepends opening and closing credits, transcodes the videos to
archive quality, and publishes them.
For more information, see L
=cut
SReview-0.8.0/web/ 0000755 0001750 0001750 00000000000 14116343665 013325 5 ustar wouter wouter SReview-0.8.0/web/sreview-web 0000755 0001750 0001750 00000001621 13401522031 015470 0 ustar wouter wouter #!/usr/bin/perl -w
# SReview, a web-based video review and transcoding system
# Copyright (c) 2016-2017, Wouter Verhelst
#
# SReview is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see
# .
use strict;
use warnings;
use lib '../lib';
use Mojolicious::Commands;
Mojolicious::Commands->start_app('SReview::Web');
SReview-0.8.0/web/public/ 0000755 0001750 0001750 00000000000 14116343665 014603 5 ustar wouter wouter SReview-0.8.0/web/public/style.css 0000644 0001750 0001750 00000000062 13401522031 016431 0 ustar wouter wouter #version {
font-size: 12px;
color: #999999;
}
SReview-0.8.0/web/public/overview.js 0000644 0001750 0001750 00000002066 14051436474 017012 0 ustar wouter wouter function updated(vm) {
if(vm.event !== vm.last_event) {
fetch("/api/v1/event/" + vm.event + "/overview")
.then(response => response.json())
.then((data) => {vm.rows = data; vm.last_event = vm.event})
.catch(error => console.error(error));
}
};
var vm = new Vue({
el: '#overview',
data: {
title: "",
rows: [],
events: [],
event: undefined,
last_event: undefined,
},
methods: {
reloadEvent: function() {
fetch("/api/v1/event/" + vm.event + "/overview")
.then(response => response.json())
.then((data) => {vm.rows = data; vm.last_event = vm.event})
.catch(error => console.error(error));
}
},
created: function() {
fetch("/api/v1/config")
.then(response => response.json())
.then(data => {this.event = data.event; updated(this);})
.catch(error => console.error(error));
fetch("/api/v1/event/list")
.then(response => response.json())
.then(data => {this.events = data;})
.catch(error => console.error(error));
},
updated: function() {
updated(this);
}
})
SReview-0.8.0/web/public/review.css 0000644 0001750 0001750 00000003044 13426272044 016612 0 ustar wouter wouter body {
font-size: 16px;
}
.eventname {
display: block;
margin-bottom: 10px;
}
img {
max-width: 100%;
height: auto;
display: block;
}
.container {
margin-bottom: 40px;
}
h1 > small {
display: block;
margin-bottom:10px;
}
#main_video, #video_starts_too_late, #av_delay, #talk_info, .help-text, #video_ends_too_early, #instructions, #video_state_information {
margin-top: 20px;
}
#talk_info .dl-horizontal {
margin-bottom: 5px;
}
.dl-horizontal dt {
width: auto;
padding-right: 5px;
}
.dl-horizontal dd {
margin-left: 0;
}
#video_starts_too_early, #video_ends_too_late {
margin-top: 20px;
margin-bottom: 20px;
}
.btn-block + .alert {
margin-top: 15px;
}
.hidden {
display: none;
}
.alert_with_control {
padding:5px 15px;
}
#video_state_information .alert + .alert-info {
padding: 20px;
}
.audio_player {
width: 90%;
padding: 10px 0 20px 0;
}
.jumbotron {
padding-top: 30px;
padding-bottom: 30px;
}
.jumbotron h2 {
font-size: 35px;
margin-bottom: 40px;
}
.jumbotron ul {
margin-bottom: 20px;
}
.jumbotron ul > li {
font-size: 21px;
font-weight: 200;
}
.restore_original .btn {
margin-top: 20px;
}
.restore_original, .error_div, .video_has_problems, .other_brokennes, .info_row {
margin-top: 20px;
}
.video_has_problems legend {
margin-bottom: 10px;
}
#av_delay .form-inline {
width: 40%;
}
#main_action {
margin: 30px 0;
}
#no_audio h5 {
margin-top: 15px;
}
.glyphicon-question-sign {
color: #999999;
margin-left: 3px;
}
a .glyphicon-question-sign:hover {
color: #000000;
}
SReview-0.8.0/web/public/credits.js 0000644 0001750 0001750 00000003251 14111157671 016572 0 ustar wouter wouter Vue.component("talk-preview", {
template: `
`,
props: ["talk", "which"],
methods: {
setForce: function() {
this.force = Date.now();
}
},
data: function() {
return {
force: false
}
},
})
function updated(app) {
if(app.event !== app.last_event) {
fetch("/api/v1/event/" + app.event + "/overview")
.then(response => response.json())
.then((data) => {
app.rows = [];
for(row of data) {
if(row.state !== "ignored") {
app.rows.push(row);
}
}
app.last_event = app.event
})
.catch(error => console.error(error));
}
}
var app = new Vue({
el: '#preview',
data: {
title: "",
rows: [],
events: [],
event: undefined,
last_event: undefined,
},
methods: {
reloadEvent: function() {
fetch("/api/v1/event/" + this.event + "/overview")
.then(response => response.json())
.then((data) => {this.rows = data; this.last_event = this.event})
.catch(error => console.error(error));
}
},
created: function() {
fetch("/api/v1/config")
.then(response => response.json())
.then(data => {this.event = data.event; updated(this);})
.catch(error => console.error(error));
fetch("/api/v1/event/list")
.then(response => response.json())
.then(data => {this.events = data})
.catch(error => console.error(error));
},
updated: function() {
updated(this);
}
});
SReview-0.8.0/web/public/mangler.js 0000644 0001750 0001750 00000005262 13401522031 016551 0 ustar wouter wouter /* @licstart The following is the entire license notice for this
* project, including all its JavaScript.
*
* SReview, a web-based video review and transcoding system.
* Copyright (c) 2016-2017 Wouter Verhelst
*
* SReview is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more detilas.
*
* You should have received a copy of the GNU Affero General Public
* License along with SReview. If not, see
* .
*
* @licend The above is the entire license notice for this project,
* including all its JavaScript.
*/
sreview_viddata.init = function() {
this.current_length_adj = this.corrvals.length_adj;
this.current_offset = this.corrvals.offset_start;
this.lengths = {
"pre": this.prelen,
"main_initial": this.mainlen,
"post": this.postlen
};
this.startpoints = {
"pre": 0,
"main": this.lengths.pre + this.corrvals.offset_start,
"post": this.lengths.pre + this.corrvals.offset_start + this.lengths.main_initial + this.current_length_adj
};
this.newpoints = {
"start": this.startpoints.main,
"end": this.startpoints.post
};
};
sreview_viddata.point_to_abs = function(which, where) {
// which = which video (pre/main/post)
// where = where the time value of the video should be (fractional seconds)
return where + this.startpoints[which];
};
sreview_viddata.abs_to_offset = function(abs) {
return abs - this.startpoints.main + this.current_offset;
};
sreview_viddata.abs_to_adj = function(abs) {
let newlen = abs - this.newpoints.start;
return newlen - this.lengths.main_initial;
};
sreview_viddata.set_point = function(which, what, where) {
// which = which video (pre/main/post)
// what = what point to set (start/end)
// where = where the time value of the video should be (fractional seconds)
this.newpoints[what] = this.point_to_abs(which, where);
};
sreview_viddata.get_start_offset = function() {
return this.abs_to_offset(this.newpoints.start);
};
sreview_viddata.get_length_adjust = function() {
return this.abs_to_adj(this.newpoints.end);
};
sreview_viddata.set_start_offset = function(off) {
this.newpoints.start = this.startpoints.main + off;
};
sreview_viddata.set_length_adj = function(adj) {
this.newpoints.end = this.newpoints.start + this.lengths.main_initial + adj;
};
sreview_viddata.init();
SReview-0.8.0/web/templates/ 0000755 0001750 0001750 00000000000 14116343665 015323 5 ustar wouter wouter SReview-0.8.0/web/templates/review/ 0000755 0001750 0001750 00000000000 14116343665 016624 5 ustar wouter wouter SReview-0.8.0/web/templates/review/update.html+reset.ep 0000644 0001750 0001750 00000001061 13426272044 022506 0 ustar wouter wouter
Note: this talk is currently in the state <%= $talk->state %>, not in one of the preview or broken states. You can only see this review page because you are admin!
Note: this talk is currently in the state <%= $talk->state %>, not in one of the preview or broken states. You can only see this review page because you are admin!
Note: this talk is currently in the state <%= $talk->state %>, not in a state where injecting is normally possible. You can only see this page because you are admin!