├── .ci └── setup-minio.pl ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── COPYING ├── MANIFEST ├── MANIFEST.SKIP ├── Makefile.PL ├── README.md ├── debian ├── changelog ├── control ├── copyright ├── not-installed ├── rules ├── sreview-common.dirs ├── sreview-common.install ├── sreview-common.postinst ├── sreview-common.postrm ├── sreview-detect.cron.d ├── sreview-detect.install ├── sreview-encoder.install ├── sreview-master.config ├── sreview-master.dbc ├── sreview-master.install ├── sreview-master.postinst ├── sreview-master.postrm ├── sreview-master.prerm ├── sreview-master.sreview-dispatch.service ├── sreview-web.apache2 ├── sreview-web.conf ├── sreview-web.install ├── sreview-web.postinst └── sreview-web.service ├── docker-compose.yml ├── dockerfiles ├── common │ └── Dockerfile ├── detect │ └── Dockerfile ├── encoder │ └── Dockerfile ├── master-kube │ ├── Dockerfile │ ├── config.pm │ ├── example-job-template.ep │ ├── example-kubequeue-configs.yaml │ ├── example-kubequeue-template.ep │ └── run-kube ├── master │ └── Dockerfile ├── tests │ └── test.pl └── web │ ├── Dockerfile │ └── local.Dockerfile ├── docs ├── components.md ├── index.md ├── installation.md └── production.md ├── helm ├── sreview │ ├── .helmignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── requirements.lock │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmaps.yaml │ │ ├── cronjobs.yaml │ │ ├── deployments.yaml │ │ ├── ingress.yaml │ │ ├── rbac.yaml │ │ └── services.yaml │ └── values.yaml └── testvals.yaml ├── javascript-test └── test.html ├── lib ├── SReview.pm └── SReview │ ├── API.pm │ ├── API │ └── Helpers.pm │ ├── Access.pm │ ├── CodecMap.pm │ ├── Config.pm │ ├── Config │ └── Common.pm │ ├── Db.pm │ ├── Files │ ├── Collection │ │ ├── HTTP.pm │ │ ├── HTTP │ │ │ └── nginx │ │ │ │ └── JSON.pm │ │ ├── Net.pm │ │ ├── S3.pm │ │ └── SFTP.pm │ └── Factory.pm │ ├── Job.pm │ ├── Model │ ├── DbElement.pm │ └── Event.pm │ ├── Schedule │ ├── Base.pm │ ├── Filtered.pm │ ├── Ics.pm │ ├── Multi.pm │ ├── Penta.pm │ ├── Wafer.pm │ ├── WithShadow.pm │ └── Yaml.pm │ ├── Talk.pm │ ├── Talk │ ├── Progress.pm │ └── State.pm │ ├── Template.pm │ ├── Template │ ├── SVG.pm │ └── Synfig.pm │ ├── Web.pm │ └── Web │ └── Controller │ ├── Admin.pm │ ├── Config.pm │ ├── CreditPreviews.pm │ ├── Event.pm │ ├── Finalreview.pm │ ├── Inject.pm │ ├── Review.pm │ ├── Room.pm │ ├── Schedule.pm │ ├── Speaker.pm │ ├── Talk.pm │ ├── Track.pm │ ├── User.pm │ └── Volunteer.pm ├── openshift ├── README.md └── sreview.yaml ├── scripts ├── sreview-autoreview ├── sreview-config ├── sreview-copy ├── sreview-cut ├── sreview-detect ├── sreview-dispatch ├── sreview-import ├── sreview-inject ├── sreview-inject-job ├── sreview-keys ├── sreview-notify ├── sreview-notranscode ├── sreview-previews ├── sreview-reply ├── sreview-skip ├── sreview-titles ├── sreview-transcode ├── sreview-transcribe ├── sreview-upload ├── sreview-user └── util │ ├── export_states_prometheus │ ├── poke_track_managers │ ├── schedule_parsers │ ├── debconf │ └── fosdem │ └── sync-states ├── t ├── 010-config.t ├── 040-db.t ├── 080-runthrough.t ├── 085-schedule-export.t ├── 090-talk.t ├── 095-files.t ├── 095-synfig.t ├── 100-mojo.t ├── 110-api.t ├── 120-types.t ├── 130-schedule.t ├── test.cf └── testvids │ ├── README.md │ ├── animated.sif │ ├── bbb.mp4 │ ├── just-title.svg │ └── schedule.yaml └── web ├── public ├── credits.js ├── mangler.js ├── overview.js ├── review.css └── style.css ├── sreview-web └── templates ├── admin ├── dashboard.html.ep └── main.html.ep ├── credits.html.ep ├── error.html.ep ├── finalreview ├── update.html+done.ep ├── update.html+error.ep ├── update.html+unpublish.ep └── view.html.ep ├── index.html.ep ├── inject ├── update.html+error.ep ├── update.html.ep ├── view.html+error.ep └── view.html.ep ├── layouts ├── admin.html.ep └── default.html.ep ├── login.html.ep ├── msg.html.ep ├── overview.html.ep ├── review ├── confirm.html+done.ep ├── confirm.html+error.ep ├── confirm.html+injecting.ep ├── confirm.html+preparing.ep ├── confirm.html+transcode.ep ├── confirm.html.ep ├── full.html+done.ep ├── full.html+error.ep ├── full.html+injecting.ep ├── full.html+preparing.ep ├── full.html+transcode.ep ├── full.html.ep ├── update.html+done.ep ├── update.html+error.ep ├── update.html+newreview.ep ├── update.html+other.ep └── update.html+reset.ep ├── schedule └── index.html.ep ├── table.html.ep └── volunteer └── list.html.ep /.ci/setup-minio.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Net::Amazon::S3; 7 | use Mojo::JSON qw/decode_json/; 8 | 9 | exit 0 unless exists($ENV{SREVIEWTEST_S3_CONFIG}); 10 | exit 0 unless exists($ENV{SREVIEWTEST_BUCKET}); 11 | 12 | my $config = decode_json($ENV{SREVIEWTEST_S3_CONFIG}); 13 | 14 | my $s3 = Net::Amazon::S3->new($config->{default}); 15 | 16 | $s3->add_bucket({bucket => $ENV{SREVIEWTEST_BUCKET}}); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MYMETA.json 2 | MYMETA.json.lock 3 | MYMETA.yml 4 | Makefile 5 | blib 6 | pm_to_blib 7 | dev-incoming/ 8 | dev-script-output/ 9 | dev-output/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: perl 3 | perl: 4 | - "5.24" 5 | - "5.28" 6 | services: 7 | - postgresql 8 | addons: 9 | postgresql: "9.6" 10 | apt: 11 | packages: 12 | - ffmpeg 13 | - inkscape 14 | - bs1770gain 15 | env: 16 | - SREVIEW_TESTDB=sreview SREVIEW_NONSTRICT=1 17 | before_script: 18 | - psql -c "create database $SREVIEW_TESTDB;" -U postgres 19 | script: 20 | - perl Makefile.PL && make TEST_VERBOSE=1 test 21 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | #install_default 2 | .ci/.* 3 | .git.* 4 | .travis.yml 5 | debian/.* 6 | docs/.* 7 | helm/.* 8 | dockerfiles/.* 9 | javascript-test/.* 10 | MANIFEST.bak 11 | Makefile$ 12 | SReview-[\d.]+ 13 | docker-compose.yml 14 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use ExtUtils::MakeMaker; 7 | use ExtUtils::Depends; 8 | 9 | my $eud = ExtUtils::Depends->new(qw/Alien::ffmpeg/); 10 | 11 | WriteMakefile( 12 | NAME => "SReview", 13 | ABSTRACT_FROM => "lib/SReview.pm", 14 | VERSION_FROM => "lib/SReview.pm", 15 | AUTHOR => [ 16 | 'Wouter Verhelst ', 17 | ], 18 | PREREQ_PM => { 19 | 'Media::Convert' => "1.0.1", 20 | 'Mojo::JSON' => 0, 21 | 'Mojo::Pg' => 0, 22 | 'Mojolicious::Plugin::OpenAPI' => '>= 4.03', 23 | 'Moose' => 0, 24 | 'Class::Type::Enum' => 0, 25 | 'Crypt::PRNG' => 0, 26 | 'DateTime' => 0, 27 | 'DateTime::Format::ISO8601' => 0, 28 | 'DateTime::Format::Pg' => 0, 29 | 'Net::Amazon::S3' => 0, 30 | 'Net::SSH::AuthorizedKeysFile' => 0, 31 | 'Net::SSH2' => 0, 32 | 'Text::Format' => 0, 33 | }, 34 | TEST_REQUIRES => { 35 | 'Test::More' => 0, 36 | 'YAML::XS' => 0, 37 | }, 38 | CONFIGURE_REQUIRES => { 39 | 'ExtUtils::Depends' => 0, 40 | }, 41 | CONFIGURE_REQUIRES => { 42 | 'ExtUtils::Depends' => 0, 43 | }, 44 | EXE_FILES => [ 45 | 'scripts/sreview-autoreview', 46 | 'scripts/sreview-cut', 47 | 'scripts/sreview-config', 48 | 'scripts/sreview-copy', 49 | 'scripts/sreview-detect', 50 | 'scripts/sreview-dispatch', 51 | 'scripts/sreview-skip', 52 | 'scripts/sreview-user', 53 | 'scripts/sreview-notify', 54 | 'scripts/sreview-reply', 55 | 'scripts/sreview-upload', 56 | 'scripts/sreview-transcode', 57 | 'scripts/sreview-transcribe', 58 | 'scripts/sreview-notranscode', 59 | 'scripts/sreview-titles', 60 | 'scripts/sreview-previews', 61 | 'scripts/sreview-inject', 62 | 'scripts/sreview-inject-job', 63 | 'scripts/sreview-import', 64 | 'web/sreview-web', 65 | ], 66 | META_MERGE => { 67 | "meta-spec" => { version => 2 }, 68 | resources => { 69 | repository => { 70 | type => "git", 71 | url => "https://github.com/yoe/SReview.git", 72 | web => "https://github.com/yoe/SReview", 73 | }, 74 | homepage => "https://yoe.github.io/SReview", 75 | } 76 | }, 77 | $eud->get_makefile_vars 78 | ); 79 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: sreview 2 | Section: video 3 | Priority: optional 4 | Rules-Requires-Root: no 5 | Standards-Version: 4.1.1 6 | Maintainer: Wouter Verhelst 7 | Vcs-Browser: https://salsa.debian.org/wouter/sreview 8 | Vcs-Git: https://salsa.debian.org/wouter/sreview 9 | Build-Depends: debhelper-compat (= 13), 10 | dh-apache2, 11 | dh-exec, 12 | ffmpeg, 13 | libclass-type-enum-perl, 14 | libcryptx-perl, 15 | libdatetime-format-pg-perl, 16 | libdatetime-perl, 17 | libextutils-depends-perl, 18 | libfile-which-perl, 19 | libmedia-convert-perl (>= 1.1.0-2~), 20 | libmojo-pg-perl, 21 | libmoose-perl, 22 | libtest-deep-perl, 23 | libtext-format-perl, 24 | libyaml-libyaml-perl, 25 | 26 | Package: sreview-master 27 | Architecture: all 28 | Depends: sreview-common, 29 | ${misc:Depends}, 30 | ${perl:Depends}, 31 | Recommends: postgresql, 32 | postgresql-contrib, 33 | rsync, 34 | sreview-detect, 35 | Description: SReview components for master host 36 | SReview is a video review and transcoding system. It allows users to 37 | review videos, and will then (after the review has finished) transcode 38 | them into archive-quality video files. 39 | . 40 | This package contains the components that should run on just one 41 | server. It is not useful without one or more machines also running the 42 | webinterface (in the sreview-web package) or the encoder (in the 43 | sreview-encoder package). 44 | 45 | Package: sreview-web 46 | Architecture: all 47 | Depends: fonts-font-awesome, 48 | libcryptx-perl, 49 | libjs-bootstrap4, 50 | libjs-vue, 51 | libmojolicious-plugin-openapi-perl, 52 | pwgen, 53 | sreview-common, 54 | ${misc:Depends}, 55 | Recommends: httpd, 56 | Description: SReview webinterface 57 | SReview is a video review and transcoding system. It allows users to 58 | review videos, and will then (after the review has finished) transcode 59 | them into archive-quality video files. 60 | . 61 | This package contains the sreview webinterface, used by administrators 62 | and reviewers. 63 | 64 | Package: sreview-encoder 65 | Architecture: all 66 | Depends: libemail-address-perl, 67 | libemail-sender-perl, 68 | libemail-stuffer-perl, 69 | sreview-common, 70 | ${misc:Depends}, 71 | ${perl:Depends}, 72 | Recommends: gridengine-exec, 73 | Description: SReview encoder code 74 | SReview is a video review and transcoding system. It allows users to 75 | review videos, and will then (after the review has finished) transcode 76 | them into archive-quality video files. 77 | . 78 | This package contains the encoder scripts that do all the hard work. 79 | It should be installed on the machines which will do the actual 80 | transcoding. 81 | 82 | Package: sreview-common 83 | Architecture: all 84 | Depends: ffmpeg, 85 | inkscape, 86 | libclass-type-enum-perl, 87 | libdatetime-format-iso8601-perl, 88 | libdatetime-format-pg-perl, 89 | libdatetime-perl, 90 | libmedia-convert-perl (>= 1.1.0-2~), 91 | libmojo-pg-perl, 92 | libmojolicious-perl, 93 | libmoose-perl, 94 | libnet-amazon-s3-perl, 95 | libtext-format-perl, 96 | pwgen, 97 | ${misc:Depends}, 98 | Suggests: libwww-curl-perl, 99 | Description: SReview -- common code 100 | SReview is a video review and transcoding system. It allows users to 101 | review videos, and will then (after the review has finished) transcode 102 | them into archive-quality video files. 103 | . 104 | This package contains the common code used by all the other SReview 105 | packages. 106 | 107 | Package: sreview-detect 108 | Architecture: all 109 | Depends: libdatetime-format-pg-perl, 110 | libical-parser-perl, 111 | libjson-perl, 112 | libxml-simpleobject-perl, 113 | sreview-common, 114 | ${misc:Depends}, 115 | Description: SReview input detection script 116 | SReview is a video review and transcoding system. It allows users to 117 | review videos, and will then (after the review has finished) transcode 118 | them into archive-quality video files. 119 | . 120 | This package contains the sreview-detect script, which probes files in 121 | the input directory and either adds them to the database if they're 122 | new, or just updates their length if they're already known. 123 | . 124 | It also contains the sreview-import script, which is used to import 125 | schedules from conference management systems. 126 | -------------------------------------------------------------------------------- /debian/not-installed: -------------------------------------------------------------------------------- 1 | usr/share/man3/SReview::Normalizer* 2 | usr/share/perl5/JSON/Validator/cache/* 3 | usr/share/perl5/SReview/Job.pm 4 | usr/share/perl5/SReview/Normalizer/ 5 | usr/share/perl5/SReview/Model/ 6 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with apache2 5 | 6 | execute_before_dh_install: 7 | chmod +x debian/sreview-master.install debian/sreview-master.dbc 8 | chmod -R u+rwX debian/tmp 9 | 10 | override_dh_installsystemd: 11 | dh_installsystemd -psreview-master --name=sreview-dispatch 12 | dh_installsystemd --remaining-packages 13 | -------------------------------------------------------------------------------- /debian/sreview-common.dirs: -------------------------------------------------------------------------------- 1 | etc/sreview 2 | -------------------------------------------------------------------------------- /debian/sreview-common.install: -------------------------------------------------------------------------------- 1 | usr/bin/sreview-config 2 | usr/bin/sreview-user 3 | usr/share/man/man1/sreview-config* 4 | usr/share/man/man1/sreview-user* 5 | usr/share/man/man3/SReview* 6 | usr/share/man/man3/SReview::Config* 7 | usr/share/man/man3/SReview::Talk* 8 | usr/share/perl5/SReview.pm 9 | usr/share/perl5/SReview/CodecMap.pm 10 | usr/share/perl5/SReview/Config.pm 11 | usr/share/perl5/SReview/Config/ 12 | usr/share/perl5/SReview/Db.pm 13 | usr/share/perl5/SReview/Files/ 14 | usr/share/perl5/SReview/Talk.pm 15 | usr/share/perl5/SReview/Talk/*.pm 16 | usr/share/perl5/SReview/Template.pm 17 | usr/share/perl5/SReview/Template/ 18 | -------------------------------------------------------------------------------- /debian/sreview-common.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #DEBHELPER# 4 | 5 | if ! id sreview > /dev/null 2>&1; then 6 | adduser --system --group --home /var/lib/sreview sreview 7 | fi 8 | -------------------------------------------------------------------------------- /debian/sreview-common.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ $1 = "purge" ] 6 | then 7 | rm -f /etc/sreview/config.pm 8 | fi 9 | 10 | #DEBHELPER# 11 | -------------------------------------------------------------------------------- /debian/sreview-detect.cron.d: -------------------------------------------------------------------------------- 1 | */30 * * * * sreview [ -x /usr/bin/sreview-detect ] && /usr/bin/sreview-detect 2 | -------------------------------------------------------------------------------- /debian/sreview-detect.install: -------------------------------------------------------------------------------- 1 | usr/bin/sreview-detect 2 | usr/bin/sreview-import 3 | usr/share/man/man1/sreview-detect* 4 | usr/share/man/man1/sreview-import* 5 | usr/share/perl5/SReview/Schedule/ 6 | -------------------------------------------------------------------------------- /debian/sreview-encoder.install: -------------------------------------------------------------------------------- 1 | usr/bin/sreview-autoreview 2 | usr/bin/sreview-copy 3 | usr/bin/sreview-cut 4 | usr/bin/sreview-inject* 5 | usr/bin/sreview-notify 6 | usr/bin/sreview-notranscode 7 | usr/bin/sreview-previews 8 | usr/bin/sreview-skip 9 | usr/bin/sreview-titles 10 | usr/bin/sreview-transcode 11 | usr/bin/sreview-transcribe 12 | usr/bin/sreview-upload 13 | usr/share/man/man1/sreview-cut* 14 | usr/share/man/man1/sreview-notranscode* 15 | usr/share/man/man1/sreview-previews* 16 | usr/share/man/man1/sreview-skip* 17 | usr/share/man/man1/sreview-transcode* 18 | usr/share/man/man1/sreview-transcribe* 19 | -------------------------------------------------------------------------------- /debian/sreview-master.config: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | dbc_dbuser=sreview 6 | dbc_dbname=sreview 7 | dbc_dbtypes=pgsql 8 | 9 | . /usr/share/debconf/confmodule 10 | if [ -f /usr/share/dbconfig-common/dpkg/config.pgsql ]; then 11 | . /usr/share/dbconfig-common/dpkg/config.pgsql 12 | dbc_go sreview-master "$@" 13 | fi 14 | 15 | #DEBHELPER# 16 | -------------------------------------------------------------------------------- /debian/sreview-master.dbc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | . /etc/dbconfig-common/sreview-master.conf 6 | 7 | if [ $dbc_dbtype = "none" ]; 8 | then 9 | exit # User wants do things manually 10 | fi 11 | 12 | [ $dbc_dbtype = "pgsql" ] # we can't work with anything but postgres 13 | 14 | dbistring="dbi:Pg:dbname='$dbc_dbname'" 15 | 16 | if [ ! -z "$dbc_dbuser" ] 17 | then 18 | dbistring="$dbistring;user='$dbc_dbuser'" 19 | fi 20 | 21 | if [ ! -z "$dbc_dbpass" ] 22 | then 23 | dbistring="$dbistring;password='$dbc_dbpass'" 24 | fi 25 | 26 | if [ ! -z "$dbc_dbserver" ] 27 | then 28 | dbistring="$dbistring;host='$dbc_dbserver'" 29 | fi 30 | 31 | if [ ! -z "$dbc_dbport" ] 32 | then 33 | dbistring="$dbistring;host='$dbc_dbport'" 34 | fi 35 | 36 | if [ "$dbc_ssl" = "true" ] 37 | then 38 | dbistring="$dbistring;sslmode=require" 39 | fi 40 | 41 | if [ "$dbc_authmethod_admin" = "ident" ] 42 | then 43 | su - $dbc_dbadmin -c "psql $dbc_dbname -c 'create extension if not exists pgcrypto; create extension if not exists plpgsql'" 44 | else 45 | psql -U $dbc_dbadmin $dbc_dbname -c 'create extension if not exists pgcrypto; create extension if not exists plpgsql' 46 | fi 47 | 48 | sreview-config --set=dbistring="$dbistring" --action=update 49 | sreview-config --action=initdb 50 | -------------------------------------------------------------------------------- /debian/sreview-master.install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/dh-exec 2 | usr/bin/sreview-dispatch 3 | usr/bin/sreview-reply 4 | usr/share/man/man1/sreview-dispatch* 5 | debian/sreview-master.dbc => usr/share/dbconfig-common/scripts/sreview-master/install/pgsql 6 | -------------------------------------------------------------------------------- /debian/sreview-master.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | . /usr/share/debconf/confmodule 6 | 7 | if [ -f /usr/share/dbconfig-common/dpkg/postinst.pgsql ]; then 8 | . /usr/share/dbconfig-common/dpkg/postinst.pgsql 9 | dbc_go sreview-master "$@" 10 | fi 11 | 12 | #DEBHELPER# 13 | -------------------------------------------------------------------------------- /debian/sreview-master.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | . /usr/share/debconf/confmodule 6 | 7 | if [ -f /usr/share/dbconfig-common/dpkg/postrm.pgsql ]; then 8 | . /usr/share/dbconfig-common/dpkg/postrm.pgsql 9 | dbc_go sreview-master "$@" 10 | fi 11 | 12 | #DEBHELPER# 13 | -------------------------------------------------------------------------------- /debian/sreview-master.prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | . /usr/share/debconf/confmodule 6 | 7 | if [ -f /usr/share/dbconfig-common/dpkg/prerm.pgsql ]; then 8 | . /usr/share/dbconfig-common/dpkg/prerm.pgsql 9 | dbc_go sreview-master "$@" 10 | fi 11 | 12 | #DEBHELPER# 13 | -------------------------------------------------------------------------------- /debian/sreview-master.sreview-dispatch.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SReview dispatch service 3 | Requires=postgresql.service 4 | Requires=gridengine-master.service 5 | [Service] 6 | User=sreview 7 | Group=sreview 8 | ExecStart=/usr/bin/sreview-dispatch 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /debian/sreview-web.apache2: -------------------------------------------------------------------------------- 1 | site debian/sreview-web.conf 2 | -------------------------------------------------------------------------------- /debian/sreview-web.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName sreview.example.com 3 | DocumentRoot /usr/share/sreview/public 4 | 5 | 6 | Require all granted 7 | AllowOverride None 8 | Options +Indexes 9 | Redirect permanent / https://sreview.example.com 10 | 11 | 12 | 13 | ServerName sreview.example.com 14 | 15 | 16 | UserDir disabled 17 | 18 | DocumentRoot /usr/share/sreview/public 19 | 20 | Require all granted 21 | Options +Indexes 22 | 23 | Alias /bootstrap4 /usr/share/javascript/bootstrap4 24 | Alias /jquery /usr/share/javascript/jquery 25 | Alias /vue /usr/share/javascript/vue 26 | Alias /font-awesome /usr/share/javascript/font-awesome 27 | Alias /popper.js /usr/share/javascript/popper.js 28 | 29 | Require all granted 30 | Options +FollowSymlinksIfOwnerMatch 31 | 32 | 33 | Require all granted 34 | 35 | SSLEngine on 36 | SSLProtocol -ALL +TLSv1.2 37 | # SSLCertificateFile 38 | # SSLCertificateKeyFile 39 | # SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH EDH+aRSA !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" 40 | Header set Strict-Transport-Security max-age=15768000 41 | Header set X-Frame-Options DENY 42 | Header set X-Content-Type-Options nosniff 43 | ProxyRequests off 44 | ProxyPreserveHost On 45 | ProxyPass /video ! 46 | ProxyPass /.well-known ! 47 | ProxyPass / http://localhost:8080/ keepalive=on 48 | ProxyPassReverse / http://localhost:8080/ 49 | RequestHeader set X-Forwarded-Proto "https" 50 | 51 | -------------------------------------------------------------------------------- /debian/sreview-web.install: -------------------------------------------------------------------------------- 1 | usr/bin/sreview-web 2 | usr/share/perl5/SReview/API 3 | usr/share/perl5/SReview/API.pm 4 | usr/share/perl5/SReview/Access.pm 5 | usr/share/perl5/SReview/Web.pm 6 | usr/share/perl5/SReview/Web/ 7 | web/public usr/share/sreview 8 | web/templates usr/share/sreview 9 | -------------------------------------------------------------------------------- /debian/sreview-web.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sreview-config --set=secret=$(pwgen -s 40 -n 1) --action=update 4 | 5 | #DEBHELPER# 6 | -------------------------------------------------------------------------------- /debian/sreview-web.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SReview web service 3 | [Service] 4 | ExecStart=/usr/bin/hypnotoad -f /usr/bin/sreview-web 5 | ExecStop=/usr/bin/hypnotoad -s /usr/bin/sreview-web 6 | RuntimeDirectory=sreview 7 | User=sreview 8 | Group=sreview 9 | PIDFile=/var/run/sreview/sreview-web.pid 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | web: 4 | build: 5 | context: . 6 | dockerfile: ./dockerfiles/web/local.Dockerfile 7 | volumes: 8 | - ./web/:/usr/share/sreview/ 9 | - ./lib/:/usr/local/lib/site_perl/ 10 | - ./scripts/:/usr/src/scripts/ 11 | - ./dev-incoming/:/srv/sreview/incoming/ 12 | - ./dev-script-output/:/srv/sreview/script-output/ 13 | - ./dev-output/:/srv/sreview/output/ 14 | ports: 15 | - 3000:3000 16 | depends_on: 17 | - db 18 | 19 | db: 20 | image: postgres:17.2 21 | volumes: 22 | - postgres_data:/var/lib/postgresql/data/ 23 | environment: 24 | - POSTGRES_USER=sreviewuser 25 | - POSTGRES_PASSWORD=sreviewpassword 26 | - POSTGRES_DB=sreviewdb 27 | 28 | volumes: 29 | postgres_data: 30 | -------------------------------------------------------------------------------- /dockerfiles/common/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-backports 2 | COPY *.deb /root/ 3 | RUN apt-get update && apt-get -y --no-install-recommends install libmedia-convert-perl && apt-get -y --no-install-recommends install -t stable-backports libmedia-convert-perl && apt-get -y --no-install-recommends install /root/sreview-common*deb && apt-get clean 4 | -------------------------------------------------------------------------------- /dockerfiles/detect/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ci_registry_image 2 | ARG ci_commit_ref_slug 3 | FROM $ci_registry_image/common:$ci_commit_ref_slug 4 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends /root/sreview-detect*deb 5 | ENTRYPOINT /usr/bin/sreview-detect 6 | -------------------------------------------------------------------------------- /dockerfiles/encoder/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ci_registry_image 2 | ARG ci_commit_ref_slug 3 | FROM $ci_registry_image/common:$ci_commit_ref_slug 4 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install /root/sreview-encoder*deb libdevel-trace-perl curl rsync ssh 5 | -------------------------------------------------------------------------------- /dockerfiles/master-kube/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ci_registry_image 2 | ARG ci_commit_ref_slug 3 | FROM $ci_registry_image/master:$ci_commit_ref_slug 4 | COPY run-kube /usr/local/bin/run-kube 5 | COPY config.pm /etc/sreview/config.pm 6 | RUN apt-get update && apt-get -y install curl libjson-perl libjson-xs-perl libyaml-libyaml-perl libdatetime-perl && curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl && apt-get --purge -y remove curl && apt-get --purge -y autoremove && chmod +x kubectl && mv kubectl /usr/local/bin/ 7 | -------------------------------------------------------------------------------- /dockerfiles/master-kube/config.pm: -------------------------------------------------------------------------------- 1 | $state_actions = { 2 | 'announcing' => 'run-kube announce <%== $talkid %>', 3 | 'notify_final' => 'run-kube notify_final <%== $talkid %>', 4 | 'cutting' => 'run-kube cut <%== $talkid %>', 5 | 'generating_previews' => 'run-kube previews <%== $talkid %>', 6 | 'notification' => 'run-kube notify <%== $talkid %>', 7 | 'transcoding' => 'run-kube transcode <%== $talkid %>', 8 | 'uploading' => 'run-kube upload <%== $talkid %>', 9 | 'injecting' => 'run-kube inject-job <%== $talkid %>', 10 | 'remove' => 'run-kube remove <%== $talkid %>', 11 | }; 12 | 13 | 1; 14 | -------------------------------------------------------------------------------- /dockerfiles/master-kube/example-job-template.ep: -------------------------------------------------------------------------------- 1 | % use YAML::XS; 2 | % push @$env, { name => "SREVIEW_DBISTRING", valueFrom => { secretKeyRef => { key => "SREVIEW_DBISTRING", name => $secretname } } }; 3 | % unshift @$args, $command; 4 | % my %data = ( apiVersion => "kubequeue.grep.be/v1", kind => "JobEntry", metadata => { name => "sreview-$jobsuffix" }, spec => { defName => "sreview-job-def", extraArgs => $args, extraEnv => $env } ); 5 | <%= Dump(\%data); %> 6 | -------------------------------------------------------------------------------- /dockerfiles/master-kube/example-kubequeue-template.ep: -------------------------------------------------------------------------------- 1 | % use YAML::XS; 2 | % push @$env, { name => "SREVIEW_DBISTRING", valueFrom => { secretKeyRef => { key => "SREVIEW_DBISTRING", name => $secretname } } }; 3 | % $jobsuffix =~ s/_/-/g; 4 | % my %data = ( apiVersion => "kubequeue.grep.be/v1", kind => "JobEntry", metadata => { name => "sreview-$jobsuffix" }, spec => { defName => "sreview-$task", extraArgs => $args, extraEnv => $env } ); 5 | <%= Dump(\%data); %> 6 | -------------------------------------------------------------------------------- /dockerfiles/master-kube/run-kube: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Config::Common; 7 | use SReview::Talk; 8 | use SReview::Template; 9 | use YAML::XS; 10 | use JSON; 11 | use DateTime; 12 | 13 | my $task = shift; 14 | my $id = shift; 15 | 16 | my $json = JSON->new->allow_nonref; 17 | 18 | my $config = SReview::Config::Common::setup; 19 | 20 | my $talk = SReview::Talk->new(talkid => $id); 21 | my $now = DateTime->now; 22 | my $start = DateTime->new( 23 | year => $now->year, 24 | month => $now->month, 25 | day => 1, 26 | hour => 0, 27 | minute => 0, 28 | second => 0, 29 | ); 30 | my $secs = $now->epoch - $start->epoch; 31 | my $jobsuffix = $talk->talkid . "-" . unpack("h*", pack("L", $secs)); 32 | 33 | my $image = "registry.salsa.debian.org/debconf-video-team/sreview/encoder"; 34 | if(exists($ENV{SREVIEW_ENCODER_IMAGE})) { 35 | $image = $ENV{SREVIEW_ENCODER_IMAGE}; 36 | } 37 | 38 | my $vars = { 39 | task => $task, 40 | image => $image, 41 | jobsuffix => $jobsuffix, 42 | command => "sreview-$task", 43 | args => [ $talk->talkid ], 44 | env => [], 45 | secretname => $ENV{SREVIEWSECRET_NAME}, 46 | }; 47 | 48 | if($task eq "announce" || $task eq "notify_final") { 49 | $vars->{command} = "sreview-notify"; 50 | push @{$vars->{args}}, $task; 51 | } 52 | 53 | foreach my $cfg ($config->keys) { 54 | next if $config->is_default($cfg); 55 | my $val = $json->encode($config->get($cfg)); 56 | push @{$vars->{env}}, {name => "SREVIEW_" . uc($cfg), value => $val}; 57 | } 58 | 59 | my $template = SReview::Template->new(talk => $talk, vars => $vars); 60 | my $data; 61 | 62 | { 63 | local $/ = undef; 64 | open my $input, "<:encoding(UTF-8)", "/opt/sreview/template.ep"; 65 | $data = <$input>; 66 | close $input; 67 | } 68 | 69 | my $yamldata = Load($template->string($data)); 70 | 71 | print "About to create this kubernetes object:\n"; 72 | print Dump($yamldata) . "\n"; 73 | open my $kubectl, "|-", "kubectl", "apply", "-f", "-"; 74 | print $kubectl Dump($yamldata) . "\n"; 75 | close $kubectl; 76 | -------------------------------------------------------------------------------- /dockerfiles/master/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ci_registry_image 2 | ARG ci_commit_ref_slug 3 | FROM $ci_registry_image/common:$ci_commit_ref_slug 4 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends /root/sreview-master*deb 5 | ENTRYPOINT /usr/bin/sreview-dispatch 6 | -------------------------------------------------------------------------------- /dockerfiles/tests/test.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Mojo::UserAgent; 7 | use Mojo::JSON qw/decode_json/; 8 | use Mojo::URL; 9 | use SReview::Talk; 10 | use SReview::Config::Common; 11 | use Data::Dumper; 12 | 13 | my $ua = Mojo::UserAgent->new; 14 | 15 | my $config = SReview::Config::Common::setup(); 16 | 17 | my $baseurl = Mojo::URL->new($config->get('urlbase')); 18 | 19 | my $res = $ua->post(Mojo::URL->new("/login_post")->base($baseurl)->to_abs => form => { email => $config->get('adminuser'), pass => $config->get('adminpw') })->result; 20 | 21 | $res->is_redirect or die "error " . $res->code . ": " . $res->message; 22 | 23 | my $talk = SReview::Talk->new(talkid => 1); 24 | 25 | $res = $ua->get(Mojo::URL->new("/r/" . $talk->nonce. "/data")->base($baseurl)->to_abs)->result; 26 | 27 | $res->is_success or die "error " . $res->code . ": " . $res->message; 28 | 29 | my $json = decode_json($res->body); 30 | 31 | print Dumper($json); 32 | -------------------------------------------------------------------------------- /dockerfiles/web/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ci_registry_image 2 | ARG ci_commit_ref_slug 3 | FROM $ci_registry_image/common:$ci_commit_ref_slug 4 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install /root/sreview-web*deb wait-for-it && mkdir -p /var/run/sreview && chown sreview:sreview /var/run/sreview 5 | ENV GIT_DESCRIBE @git_describe@ 6 | ENTRYPOINT ["/usr/bin/hypnotoad","-f","/usr/bin/sreview-web"] 7 | EXPOSE 8080 8 | -------------------------------------------------------------------------------- /dockerfiles/web/local.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-backports 2 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install perl postgresql-client ffmpeg inkscape bs1770gain libnet-amazon-s3-perl libmojolicious-perl libclass-type-enum-perl libcryptx-perl libdatetime-format-pg-perl libdatetime-perl libextutils-depends-perl libfile-which-perl libmojo-pg-perl libmoose-perl libtest-deep-perl libtext-format-perl libyaml-libyaml-perl fonts-font-awesome libcryptx-perl libjs-bootstrap4 libjs-vue libmojolicious-plugin-openapi-perl pwgen python3.11-venv && apt-get install -y --no-install-recommends -t bookworm-backports libmedia-convert-perl 3 | 4 | # torch without CUDA 5 | RUN python3 -m venv /venv && /venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu && /venv/bin/pip install openai-whisper 6 | 7 | RUN mkdir /etc/sreview 8 | 9 | WORKDIR /usr/share/sreview/ 10 | 11 | ADD /lib/ /usr/local/lib/site_perl/ 12 | ADD /scripts/sreview-config /usr/src/scripts/sreview-config 13 | 14 | RUN cd /usr/src/scripts/ && ./sreview-config --action=update --set=adminpw=dev --set=adminuser=dev@dev.dev --set=secret=INSECURE_DEV_SECRET --set=dbistring=dbi:Pg:dbname=sreviewdb\;host=db\;user=sreviewuser\;password=sreviewpassword --set=api_key=devkey --set=event=testevent 15 | 16 | CMD ./sreview-web daemon 17 | EXPOSE 8080 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # SReview documentation 2 | 3 | Welcome to SReview, a video review and transcoding system. 4 | 5 | Please note that any changes you make while reviewing your talk video 6 | will only be visible after you have submitted it for re-cutting. 7 | 8 | There are two main ways to run SReview. Anonymous mode keeps an 9 | overview list of talks to be reviewed, so anyone with access to the list 10 | can review. Notification mode directly informs people by email of the 11 | URL of a specific video to be reviewed. 12 | 13 | ## Reviewing 14 | 15 | If you received an email stating that your talk is ready for review, but 16 | the review page claims it is *not* ready, someone else may have already 17 | done some reviewing on it. Check the Cc field on the initial 18 | notification email for other recipients. Additionally, people with 19 | administration logon credentials may review any talk, even if they 20 | didn’t get the notification email. 21 | 22 | ### The web form 23 | 24 | The review form shows three video elements; one large video, and two 25 | small ones. The large video should contain *just* the talk that this 26 | video should contain, plus possibly an introduction at the start and the 27 | questions at the end, but no more and no less than that. 28 | 29 | The two smaller video elements above the large video element contain 30 | *context* video; the twenty minutes before and the twenty minutes after 31 | the large video element. These may be helpful should the main video 32 | start too late or stop too soon, and are re-cut whenever the main video 33 | is re-cut (so the end of the pre video should always be *just* before 34 | the start of the main video). 35 | 36 | Below the three video elements is a web form where you can decide what 37 | to do. Read on. 38 | 39 | If you review the recording, and all is well, just select the option 40 | labeled “This preview looks OK, please transcode it at high quality and 41 | release it” in the webform and submit it. Everything else will now 42 | happen automatically. You’re done! 43 | 44 | If the start and end points of the video are incorrect, locate the 45 | correct points in either the main video or the two smaller context 46 | videos. Using the pause button is helpful here, but not obligatory. 47 | Once you've found them, set them with "Take start point" and "Take end 48 | point". This will automatically set the correct values in the "Common 49 | fixes" section. Then, press "OK" to submit the video for re-cutting. 50 | 51 | If the video and audio are out of sync, and you feel confident adjusting 52 | it yourself, fill in an estimated value in the "audio offset" field 53 | under "Common fixes" and send the video for re-cutting. You will be able 54 | to check the adjustment after the video has been re-cut and you have a 55 | new preview. 56 | 57 | If the audio is of poor quality, try changing the audio channel under 58 | "Common fixes". 59 | 60 | If you don't feel confident adjusting the audio offset or channel 61 | yourself, select "This preview has the following issues not covered by 62 | the above" under "Other brokenness", and describe the problem in the 63 | text box there. 64 | 65 | Once you’ve made all the changes you think are needed, hit the “OK” 66 | button at the bottom of the page. Your talk will then be re-cut, after 67 | which it will be available for review again. If SReview is running in 68 | notification mode, you’ll get another notification email. You can check 69 | the new cut, make any more changes as needed, and re-submit the talk. 70 | Once your talk is just as you want it, select "This preview looks OK, 71 | please transcode it at high quality ad release it" and press "OK". 72 | 73 | If you see a problem not covered by "Common Fixes", please select the 74 | "Other brokenness" option, and explain the issue in the text area below 75 | that. Once you’ve submitted the form, your talk will now be placed in 76 | the “broken” state. If you change your mind, you can always choose the 77 | ‘Do not make changes, do not notify video team; set the state back to 78 | “preview”’ option. You can then go back to reviewing, as usual. 79 | 80 | Thanks for reviewing! 81 | 82 | ## Further documentation 83 | 84 | There is a document about the [components](components) of SReview, which 85 | explains how the system works. If you want to install, read the 86 | [installation](installation) document. 87 | -------------------------------------------------------------------------------- /docs/production.md: -------------------------------------------------------------------------------- 1 | # Production examples 2 | 3 | SReview has been used in production for the following conferences: 4 | 5 | * FOSDEM: Every edition since the 2017 edition 6 | * DebConf: Every edition since the 2017 edition 7 | * A few mini-debconfs: 2017 Cambridge, 2018 Hamburg, most mini-debconfs 8 | since. 9 | * OSW.za: 2019 edition 10 | 11 | What follows below is a short description of how SReview was installed 12 | and configured for some of those conferences. Note, however, that it is 13 | possible to run all components of SReview on a single host; for small 14 | conferences, doing so is recommended. 15 | 16 | ## FOSDEM 17 | 18 | ### 2017 19 | 20 | At FOSDEM 2017, SReview was installed on the following machines: 21 | 22 | - review.video.fosdem.org: web interface, postgresql, gridengine master, 23 | gridengine exec, dispatch script 24 | - encoder0.video.fosdem.org, encoder1.video.fosdem.org, ...: gridengine 25 | exec, encoder nodes 26 | - backend0.video.fosdem.org, backend1.video.fosdem.org, ...: 27 | `detect_files`, storage for raw recordings (access over HTTP only) 28 | - storage0.video.fosdem.org, storage1.video.fosdem.org, ...: NFS, 29 | storage for `cut_talk`, `previews`, and `transcode` output. 30 | 31 | All machines were "bare metal" machines at a cloud hoster. Data access 32 | to the database and the raw files was LAN-based and therefore quick. 33 | 34 | FOSDEM 2017 was the first ever conference where SReview was used. The 35 | code was *extremely* rough around the edges; in fact, much of the 36 | functionality wasn't there yet, and much of what was there has since 37 | been rewritten. 38 | 39 | ### 2018 40 | 41 | The setup for FOSDEM 2018 was very similar, but there were a few 42 | differences: 43 | 44 | - Everything ran from Debian packages, and we used the generic scripts 45 | (`sreview-transcode` rather than a custom `transcode` script, etc). 46 | - review.video.fosdem.org was renamed to review-master.video.fosdem.org, 47 | and a CNAME was created for the former, for more flexibility (which we 48 | turned out not to need in the end, sigh). 49 | - The NGinX-mp4 module was not used this time around to access the 50 | backend machines, instead we used NFS to access them. 51 | 52 | ## DebConf 53 | 54 | ### 2017 (Montéal, Canada) 55 | 56 | At DebConf17, SReview was installed on the following machines: 57 | 58 | - vittoria.debian.org: web interface, postgresql, gridengine master, 59 | gridengine exec, dispatch script with configuration for upload and 60 | notification *only* (upload script would pull from noc1st0 first and 61 | then publish). 62 | - noc1st0.debconf17.debconf.org: raw recordings, all intermediate files, 63 | nginx (for serving preview files to reviewers), gridengine master, 64 | `detect_files`, NFS server for raw and intermediate files, gridengine 65 | exec (for `cut_talks` scripts *only*). 66 | - encoder0.debconf17.debconf.org, encoder1.debconf17.debconf.org, ...: 67 | gridengine exec, encoder nodes. 68 | 69 | vittoria is a VM that runs on Debian infrastructure at GRNET (Greece). 70 | It is available to the DebConf video team all year. 71 | 72 | The other machines were local hardware, borrowed for the duration of 73 | DebConf17. They were configured to be granted access to *only* the 74 | postgresql on vittoria. 75 | 76 | During the conference, while the talks were in the `waiting_for_files` 77 | through `transcoding` states, no files were copied to vittoria. Only 78 | when the transcodes had finished were they copied (by the `uploads` 79 | script that ran on vittoria) to vittoria, and then pushed from vittoria 80 | to the DebConf [video archive](https://video.debian.net). 81 | 82 | After the conference, all raw recordings were rsync'd to vittoria. 83 | 84 | ### 2018 (Hsinchu, Taiwan) 85 | 86 | For DebConf18, the setup was very similar, but there was a major 87 | difference: rather than having multiple machines on-site, the DebConf18 88 | setup only had one 24-core VM with 10 TiB of storage on-site, 89 | `storage.dc18.debconf.org`. All cutting and transcoding was done on this 90 | machine; vittoria served the webinterface and the database, etc. 91 | 92 | ## Mini debconfs 93 | 94 | Everything was installed and run on vittoria.debian.org 95 | 96 | ## OSW.co.za 97 | 98 | The Open Source Week in Johannesburg, South Africa, used SReview for the 99 | review and transcode cycle. Everything was installed on a single 100 | machine (from Debian packages) that was only available from the NOC; 101 | review was done by a single user. After the first day of this 102 | conference, when less rooms were required, we also reused some of the 103 | machines that were originally used for recording as transcoding workers. 104 | -------------------------------------------------------------------------------- /helm/sreview/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/sreview/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: minio 3 | repository: https://charts.min.io/ 4 | version: 4.0.2 5 | - name: postgresql 6 | repository: https://charts.bitnami.com/bitnami 7 | version: 12.1.3 8 | digest: sha256:3822cc6854cfd5f62ceb809e5f37c4af30799b51459fb890a8e36ab13078f6c6 9 | generated: "2022-12-09T21:11:27.659012204+02:00" 10 | -------------------------------------------------------------------------------- /helm/sreview/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "latest" 3 | description: Helm chart for deploying SReview 4 | name: sreview 5 | version: 0.3.0 6 | dependencies: 7 | - name: minio 8 | version: 4.0.2 9 | repository: "https://charts.min.io/" 10 | condition: use_internal_minio 11 | - name: postgresql 12 | repository: https://charts.bitnami.com/bitnami 13 | version: 12.1.3 14 | condition: use_internal_pg 15 | -------------------------------------------------------------------------------- /helm/sreview/requirements.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: minio 3 | repository: https://helm.min.io/ 4 | version: 8.0.10 5 | - name: postgresql 6 | repository: https://charts.bitnami.com/bitnami 7 | version: 10.3.11 8 | digest: sha256:e21b78f9521827f63e0c76c680c10fb351faa292f6194281f167c3fb17a80122 9 | generated: "2021-03-09T13:00:07.911506686+02:00" 10 | -------------------------------------------------------------------------------- /helm/sreview/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "sreview.render_template" }} 2 | {{- if typeIs "string" .value }} 3 | {{- tpl .value .context }} 4 | {{- else }} 5 | {{- tpl (.value | toYaml) .context }} 6 | {{- end }} 7 | {{- end }} 8 | {{- define "sreview.envvals" }} 9 | envFrom: 10 | - configMapRef: 11 | name: {{ .Release.Name }}-config 12 | env: 13 | - name: SREVIEWSECRET_NAME 14 | value: {{ .Release.Name }}-secret 15 | {{- if .Values.use_internal_pg }} 16 | - name: SREVIEW_DBICOMPONENTS 17 | value: "host password dbname user" 18 | - name: SREVIEW_DBI_USER 19 | value: "postgres" 20 | - name: SREVIEW_DBI_HOST 21 | value: {{ .Release.Name }}-postgresql 22 | - name: SREVIEW_DBI_PASSWORD 23 | valueFrom: 24 | secretKeyRef: 25 | name: {{ .Release.Name }}-postgresql 26 | key: postgres-password 27 | - name: SREVIEW_DBI_DBNAME 28 | value: "postgres" 29 | {{- else }} 30 | - name: SREVIEW_DBISTRING 31 | valueFrom: 32 | secretKeyRef: 33 | name: {{ .Release.Name }}-secret 34 | key: SREVIEW_DBISTRING 35 | {{- end }} 36 | {{- if .Values.use_internal_minio }} 37 | - name: SREVIEW_S3_DEFAULT_ACCESSKEY 38 | valueFrom: 39 | secretKeyRef: 40 | name: {{ .Release.Name }}-minio 41 | key: {{ .Values.minioAccount | default "root" }}User 42 | - name: SREVIEW_S3_DEFAULT_SECRETKEY 43 | valueFrom: 44 | secretKeyRef: 45 | name: {{ .Release.Name }}-minio 46 | key: {{ .Values.minioAccount | default "root" }}Password 47 | - name: SREVIEW_S3_DEFAULT_HOST 48 | value: {{ .Release.Name }}-minio:9000 49 | - name: SREVIEW_S3_DEFAULT_SECURE 50 | value: "0" 51 | {{- else }} 52 | - name: SREVIEW_S3_ACCESS_CONFIG 53 | valueFrom: 54 | secretKeyRef: 55 | name: {{ .Release.Name }}-secret 56 | key: SREVIEW_S3_ACCESS_CONFIG 57 | {{- end }} 58 | - name: SREVIEW_ADMINPW 59 | valueFrom: 60 | secretKeyRef: 61 | name: {{ .Release.Name }}-secret 62 | key: SREVIEW_ADMINPW 63 | - name: SREVIEW_API_KEY 64 | valueFrom: 65 | secretKeyRef: 66 | name: {{ .Release.Name }}-secret 67 | key: SREVIEW_API_KEY 68 | {{- with .Values.extraEnv }} 69 | {{ include "sreview.render_template" (dict "value" . "context" $) }} 70 | {{- end }} 71 | {{- with .Values.secret }} 72 | {{- with .extraEnv }} 73 | {{ include "sreview.render_template" (dict "value" . "context" $) }} 74 | {{- end }} 75 | {{- end }} 76 | {{- end }} 77 | -------------------------------------------------------------------------------- /helm/sreview/templates/configmaps.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ .Release.Name }}-config 6 | data: 7 | {{- range $key, $val := .Values.config }} 8 | SREVIEW_{{ upper $key }}: '{{ $val }}' 9 | {{- end }} 10 | --- 11 | apiVersion: v1 12 | kind: Secret 13 | metadata: 14 | name: {{ .Release.Name }}-secret 15 | type: Opaque 16 | stringData: 17 | {{- $dbpass := randAlphaNum 40 }} 18 | SREVIEW_ADMINPW: '"{{ .Values.secret.adminpw | default ( randAlphaNum 40 ) }}"' 19 | SREVIEW_DBPASS: '"{{ $dbpass }}"' 20 | SREVIEW_DBISTRING: '"{{ .Values.secret.dbistring | default ( replace "release" .Release.Name ( replace "pw" $dbpass "dbi:Pg:dbname=sreview;host=release-db;user=sreview;password=pw") ) }}"' 21 | {{- if empty .Values.secret.apikey }} 22 | SREVIEW_API_KEY: 'null' 23 | {{- else }} 24 | SREVIEW_API_KEY: '"{{ .Values.secret.apikey }}"' 25 | {{ end }} 26 | SREVIEW_S3_ACCESS_CONFIG: '{{ .Values.secret.s3_access_config | default "{{ .Values.minio.accessKey }}" }}' 27 | --- 28 | apiVersion: v1 29 | kind: ConfigMap 30 | metadata: 31 | name: {{ .Release.Name }}-run-kube-config 32 | data: 33 | template.ep: {{ .Values.run_kube_template | quote }} 34 | -------------------------------------------------------------------------------- /helm/sreview/templates/cronjobs.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.no_detect_on_cluster }} 2 | --- 3 | apiVersion: batch/v1 4 | kind: CronJob 5 | metadata: 6 | name: {{ .Release.Name }}-detect 7 | spec: 8 | concurrencyPolicy: Forbid 9 | schedule: 0,30 * * * * 10 | jobTemplate: 11 | metadata: 12 | labels: 13 | app: {{ .Release.Name }}-detect 14 | spec: 15 | template: 16 | metadata: 17 | labels: 18 | app: {{ .Release.Name }}-detect 19 | spec: 20 | restartPolicy: OnFailure 21 | containers: 22 | - name: detect 23 | image: '{{ .Values.containerRoot | default "registry.salsa.debian.org/debconf-video-team/sreview" }}/detect:{{ .Values.containerLabel | default "latest" }}' 24 | imagePullPolicy: {{ .Values.pullPolicy | default "IfNotPresent" }} 25 | command: ["/usr/bin/sreview-detect"] 26 | {{- include "sreview.envvals" . | indent 12 }} 27 | {{- end }} 28 | --- 29 | apiVersion: batch/v1 30 | kind: CronJob 31 | metadata: 32 | name: {{ .Release.Name }}-import 33 | spec: 34 | concurrencyPolicy: Forbid 35 | schedule: 0,30 * * * * 36 | jobTemplate: 37 | metadata: 38 | labels: 39 | app: {{ .Release.Name }}-import 40 | spec: 41 | template: 42 | metadata: 43 | labels: 44 | app: {{ .Release.Name }}-import 45 | spec: 46 | restartPolicy: OnFailure 47 | containers: 48 | - name: import 49 | image: '{{ .Values.containerRoot | default "registry.salsa.debian.org/debconf-video-team/sreview" }}/detect:{{ .Values.containerLabel | default "latest" }}' 50 | imagePullPolicy: {{ .Values.pullPolicy | default "IfNotPresent" }} 51 | command: ["/usr/bin/sreview-import"] 52 | {{- include "sreview.envvals" . | indent 12 }} 53 | -------------------------------------------------------------------------------- /helm/sreview/templates/deployments.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ .Release.Name }}-web 6 | spec: 7 | replicas: {{ .Values.webReplicas | default 3 }} 8 | selector: 9 | matchLabels: 10 | app: {{ .Release.Name }}-web 11 | template: 12 | metadata: 13 | labels: 14 | app: {{ .Release.Name }}-web 15 | spec: 16 | containers: 17 | - name: web 18 | image: '{{ .Values.containerRoot | default "registry.salsa.debian.org/debconf-video-team/sreview" }}/web:{{ .Values.containerLabel | default "latest" }}' 19 | imagePullPolicy: {{ .Values.pullPolicy | default "IfNotPresent" }} 20 | {{- include "sreview.envvals" . | indent 8 }} 21 | resources: 22 | limits: 23 | cpu: 500m 24 | requests: 25 | cpu: 200m 26 | livenessProbe: 27 | httpGet: 28 | path: / 29 | port: 8080 30 | --- 31 | apiVersion: apps/v1 32 | kind: Deployment 33 | metadata: 34 | name: {{ .Release.Name }}-master 35 | annotations: 36 | checksum/configmaps: {{ include (print .Template.BasePath "/configmaps.yaml") . | sha256sum }} 37 | spec: 38 | replicas: 1 39 | selector: 40 | matchLabels: 41 | app: {{ .Release.Name }}-master 42 | template: 43 | metadata: 44 | labels: 45 | app: {{ .Release.Name }}-master 46 | spec: 47 | serviceAccountName: {{ .Release.Name }}-master 48 | containers: 49 | - name: master 50 | image: '{{ .Values.containerRoot | default "registry.salsa.debian.org/debconf-video-team/sreview" }}/master-kube:{{ .Values.containerLabel | default "latest" }}' 51 | imagePullPolicy: {{ .Values.pullPolicy | default "IfNotPresent" }} 52 | {{- include "sreview.envvals" . | indent 8 }} 53 | volumeMounts: 54 | - name: rkconfig 55 | readOnly: true 56 | mountPath: /opt/sreview 57 | volumes: 58 | - name: rkconfig 59 | configMap: 60 | name: {{ .Release.Name }}-run-kube-config 61 | -------------------------------------------------------------------------------- /helm/sreview/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ .Release.Name }}-ingress 6 | {{- with .Values.ingress.labels }} 7 | labels: 8 | {{ toYaml . | indent 4 }} 9 | {{- end }} 10 | {{- with .Values.ingress.annotations }} 11 | annotations: 12 | {{ toYaml . | indent 4 }} 13 | {{- end }} 14 | spec: 15 | rules: 16 | - host: {{ .Values.webHostname | default "sreview.example.com" }} 17 | http: 18 | paths: 19 | - path: / 20 | pathType: Prefix 21 | backend: 22 | service: 23 | name: {{ .Release.Name }}-web 24 | port: 25 | number: 8080 26 | {{- if .Values.ingress.tls }} 27 | tls: 28 | {{- range .Values.ingress.tls }} 29 | - hosts: 30 | {{- range .hosts }} 31 | - {{ . | quote }} 32 | {{- end }} 33 | secretName: {{ .secretName }} 34 | {{- end }} 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /helm/sreview/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ .Release.Name }}-master 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: {{ .Release.Name }}-manage-jobs 11 | rules: 12 | - apiGroups: [""] 13 | resources: ["pods"] 14 | verbs: ["get", "watch", "list"] 15 | - apiGroups: ["batch", "extensions"] 16 | resources: ["jobs"] 17 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 18 | - apiGroups: ["kubequeue.grep.be"] 19 | resources: ["jobentries"] 20 | verbs: ["get","list","watch","create","update","patch","delete"] 21 | --- 22 | apiVersion: rbac.authorization.k8s.io/v1 23 | kind: RoleBinding 24 | metadata: 25 | name: {{ .Release.Name }}-manage-jobs 26 | subjects: 27 | - kind: ServiceAccount 28 | name: {{ .Release.Name }}-master 29 | apiGroup: "" 30 | roleRef: 31 | kind: Role 32 | name: {{ .Release.Name }}-manage-jobs 33 | apiGroup: rbac.authorization.k8s.io 34 | -------------------------------------------------------------------------------- /helm/sreview/templates/services.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ .Release.Name }}-web 6 | spec: 7 | type: NodePort 8 | selector: 9 | app: {{ .Release.Name }}-web 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 8080 14 | targetPort: 8080 15 | -------------------------------------------------------------------------------- /helm/sreview/values.yaml: -------------------------------------------------------------------------------- 1 | # Set SReview configuration values here. For a full list of 2 | # all the possible configuration values with their documentation, run 3 | # docker run --rm -ti registry.salsa.debian.org/debconf-video-team/sreview/encoder sreview-config -a dump 4 | # Note that everything needs to be JSON-encoded; that includes 5 | # strings, which means you have to double-quote them. See the 6 | # "adminuser" configuration value for an example. 7 | config: 8 | adminuser: '"admin@example.com"' 9 | accessmethods: '{"input": "S3", "intermediate": "S3", "output": "S3"}' 10 | inputglob: '"input/*"' 11 | pubdir: '"inter"' 12 | outputdir: '"output"' 13 | secret: 14 | # Set this to override the administrator password. If left unset, will 15 | # generate one. 16 | adminpw: 17 | # Set this to configure an external database (see below) 18 | dbistring: 19 | # Set this if you want a working API key. If left unset, will generate 20 | # one. 21 | apikey: 22 | # Set this to configure an external S3 store (see below). 23 | s3_access_config: 24 | # if you want to use a PostgreSQL database outside of Kubernetes, set 25 | # the following to false and make sreview.secret.dbistring point to 26 | # your external server. If you want to run PostgreSQL on your 27 | # Kubernetes cluster, keep it at true; in that case, secret.dbistring 28 | # will be ignored. 29 | use_internal_pg: true 30 | # if you want to use an object store outside of Kubernetes (highly 31 | # recommended), set the following to false and make 32 | # secret.s3_access_config point to your external object store. If you 33 | # want to run a minio on your Kubernetes cluster, keep it at true; 34 | # secret.s3_access_config will be ignored. 35 | use_internal_minio: true 36 | -------------------------------------------------------------------------------- /helm/testvals.yaml: -------------------------------------------------------------------------------- 1 | pullPolicy: Always 2 | containerLabel: main 3 | webHostname: sreviewk.example.com 4 | config: 5 | schedule_format: '"penta"' 6 | schedule_options: '{"url":"https://fosdem.org/2021/schedule/xml"}' 7 | event: '"FOSDEM 2021"' 8 | -------------------------------------------------------------------------------- /javascript-test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SReview javascript tests 7 | 8 | 9 | 10 |
11 |
12 | 13 | 24 | 25 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/SReview.pm: -------------------------------------------------------------------------------- 1 | package SReview; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | our $VERSION; 7 | 8 | $VERSION = "0.11.1"; 9 | 10 | =head1 NAME 11 | 12 | SReview - a video review and transcoding system 13 | 14 | =head1 DESCRIPTION 15 | 16 | SReview is a system to review and transcode conference videos. You feed 17 | it a bunch of timestamped videos and a schedule, and it creates initial 18 | cuts based on that schedule. Next, you review (or ask reviewers) to 19 | decide on the actual start- and end times of the talks, through a 20 | webinterface. Once those start- and endtimes have been decided upon, 21 | SReview prepends opening and closing credits, transcodes the videos to 22 | archive quality, and publishes them. 23 | 24 | For more information, see L 25 | 26 | =cut 27 | -------------------------------------------------------------------------------- /lib/SReview/Access.pm: -------------------------------------------------------------------------------- 1 | package SReview::Access; 2 | 3 | use Exporter; 4 | 5 | @ISA = qw(Exporter); 6 | @EXPORT_OK = qw(admin_for); 7 | 8 | sub admin_for($$) { 9 | my $c = shift; 10 | my $talk = shift; 11 | 12 | if(exists($c->session->{admin})) { 13 | return 1; 14 | } 15 | if(exists($c->session->{id})) { 16 | my $st = $c->dbh->prepare("SELECT room FROM users WHERE id = ?"); 17 | $st->execute($c->session->{id}); 18 | my $row = $st->fetchrow_hashref; 19 | if($talk->roomid = $row->{room}) { 20 | return 1; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/SReview/CodecMap.pm: -------------------------------------------------------------------------------- 1 | package SReview::CodecMap; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Exporter 'import'; 7 | 8 | our @EXPORT_OK=qw/detect_to_write/; 9 | 10 | my %writemap = ( 11 | 'vorbis' => 'libvorbis', 12 | 'vp8' => 'libvpx', 13 | 'vp9' => 'libvpx-vp9', 14 | 'h264' => 'libx264', 15 | 'hevc' => 'libx265', 16 | 'opus' => 'libopus', 17 | ); 18 | 19 | open CHECK_FDK, "ffmpeg -hide_banner -h encoder=libfdk_aac|"; 20 | if( !~ /is not recognized/) { 21 | $writemap{aac} = 'libfdk_aac'; 22 | } 23 | close CHECK_FDK; 24 | 25 | sub detect_to_write($) { 26 | my $detected = shift; 27 | if(exists($writemap{$detected})) { 28 | return $writemap{$detected}; 29 | } else { 30 | return $detected; 31 | } 32 | } 33 | 34 | 1; 35 | -------------------------------------------------------------------------------- /lib/SReview/Files/Collection/HTTP.pm: -------------------------------------------------------------------------------- 1 | package SReview::Files::Access::HTTP; 2 | 3 | use Moose; 4 | use Carp; 5 | use Mojo::UserAgent; 6 | use File::Temp qw/tempfile tempdir mktemp/; 7 | 8 | extends 'SReview::Files::Access::Base'; 9 | 10 | has '+filename' => ( 11 | predicate => 'has_download', 12 | ); 13 | 14 | has 'workdir' => ( 15 | is => 'ro', 16 | lazy => 1, 17 | builder => '_get_workdir', 18 | ); 19 | 20 | sub _get_workdir { 21 | return tempdir(CLEANUP => 1); 22 | } 23 | 24 | sub _get_file { 25 | my $self = shift; 26 | my @parts = split('\.', $self->relname); 27 | my $ext = pop(@parts); 28 | my $dir = $self->workdir; 29 | 30 | if($self->has_data) { 31 | if($self->download_verbose) { 32 | print "Downloading " . $self->url . "\n"; 33 | } 34 | my ($fh, $file) = tempfile("http-XXXXXX", dir => $dir, SUFFIX => ".$ext"); 35 | my $ua = Mojo::UserAgent->new; 36 | my $res = $ua->get($self->url)->result; 37 | if($res->is_success) { 38 | $res->save_to($file); 39 | return $file; 40 | } else { 41 | croak "could not download file:" . $res->message; 42 | } 43 | } else { 44 | croak "Can't create files with the HTTP access method"; 45 | } 46 | } 47 | 48 | sub _probe_basepath { 49 | return shift->workdir; 50 | } 51 | 52 | sub DEMOLISH { 53 | my $self = shift; 54 | if($self->has_download) { 55 | if($self->download_verbose) { 56 | print "Deleting " . $self->filename . "\n"; 57 | } 58 | unlink($self->filename); 59 | } 60 | } 61 | 62 | package SReview::Files::Collection::HTTP; 63 | 64 | use Moose; 65 | use Carp; 66 | 67 | extends 'SReview::Files::Collection::Base'; 68 | 69 | has '+fileclass' => ( 70 | default => 'SReview::Files::Access::HTTP', 71 | ); 72 | 73 | sub add_file { 74 | croak "Creating files is not supported on an HTTP collection"; 75 | } 76 | 77 | sub _probe_children { 78 | croak "Discovering children is not supported on an HTTP collection"; 79 | } 80 | 81 | 1; 82 | -------------------------------------------------------------------------------- /lib/SReview/Files/Collection/HTTP/nginx/JSON.pm: -------------------------------------------------------------------------------- 1 | package SReview::Files::Collection::HTTP::nginx::JSON; 2 | 3 | use Moose; 4 | use Mojo::UserAgent; 5 | use DateTime::Format::Strptime; 6 | 7 | extends 'SReview::Files::Collection::HTTP'; 8 | 9 | sub _probe_children { 10 | my $self = shift; 11 | my $ua = Mojo::UserAgent->new; 12 | my $baseurl = $self->baseurl; 13 | my $parser = DateTime::Format::Strptime->new( 14 | pattern => '%a, %d %b %Y %H:%M:%S %Z', 15 | locale => 'C', 16 | on_error => 'croak' 17 | ); 18 | if(substr($baseurl, -1) ne "/") { 19 | $baseurl .= "/"; 20 | } 21 | my $return = []; 22 | my $res = $ua->get($baseurl)->result; 23 | if($res->is_success) { 24 | foreach my $obj(@{$res->json}) { 25 | if($obj->{type} eq "directory") { 26 | foreach my $child(@{SReview::Files::Collection::HTTP::nginx::JSON->new(baseurl => join("/", $self->baseurl, $obj->{name}), download_verbose => $self->download_verbose)->children}) { 27 | push @$return, SReview::Files::Access::HTTP->new(baseurl => $self->baseurl, relname => join('/', $obj->{name}, $child->{relname}), mtime => $child->mtime); 28 | } 29 | } else { 30 | push @$return, SReview::Files::Access::HTTP->new(baseurl => $self->baseurl, relname => $obj->{name}, mtime => $parser->parse_datetime($obj->{mtime}), download_verbose => $self->download_verbose); 31 | } 32 | } 33 | } 34 | 35 | return $return; 36 | } 37 | -------------------------------------------------------------------------------- /lib/SReview/Files/Collection/Net.pm: -------------------------------------------------------------------------------- 1 | package SReview::Files::Access::Net; 2 | 3 | use Moose; 4 | use File::Temp qw/tempfile tempdir mktemp/; 5 | use File::Path qw/make_path/; 6 | use File::Basename; 7 | use Carp; 8 | 9 | use SReview::Config::Common; 10 | 11 | extends 'SReview::Files::Access::Base'; 12 | 13 | has '+filename' => ( 14 | predicate => 'has_download', 15 | ); 16 | 17 | has 'workdir' => ( 18 | is => 'ro', 19 | lazy => 1, 20 | builder => '_get_workdir', 21 | ); 22 | 23 | sub _get_workdir { 24 | return tempdir(DIR => SReview::Config::Common::setup()->get("workdir"), CLEANUP => 1); 25 | } 26 | 27 | sub _get_file { 28 | ... 29 | } 30 | 31 | sub _probe_mtime { 32 | ... 33 | } 34 | 35 | sub _probe_basepath { 36 | return shift->workdir; 37 | } 38 | 39 | sub store_file { 40 | my $self = shift; 41 | $self->stored; 42 | return 1; 43 | } 44 | 45 | sub delete { 46 | ... 47 | } 48 | 49 | sub valid_path_filename { 50 | my $self = shift; 51 | 52 | my $path = join('/', $self->workdir, $self->relname); 53 | make_path(dirname($path)); 54 | symlink($self->filename, $path); 55 | return $path; 56 | } 57 | 58 | sub DEMOLISH { 59 | my $self = shift; 60 | if($self->has_download) { 61 | if($self->download_verbose) { 62 | print "removing " . $self->filename . "\n"; 63 | } 64 | unlink($self->filename); 65 | } 66 | } 67 | 68 | no Moose; 69 | 70 | package SReview::Files::Collection::Net; 71 | 72 | use Moose; 73 | 74 | extends 'SReview::Files::Collection::Base'; 75 | 76 | sub _probe_children { 77 | ... 78 | } 79 | 80 | no Moose; 81 | 82 | 1; 83 | -------------------------------------------------------------------------------- /lib/SReview/Files/Collection/S3.pm: -------------------------------------------------------------------------------- 1 | package SReview::Files::Access::S3; 2 | 3 | use Moose; 4 | use File::Temp qw/tempfile tempdir mktemp/; 5 | use File::Path qw/make_path/; 6 | use File::Basename; 7 | use DateTime::Format::ISO8601; 8 | use Carp; 9 | 10 | use SReview::Files::Collection::Net; 11 | 12 | extends 'SReview::Files::Access::Net'; 13 | 14 | has 's3object' => ( 15 | is => 'ro', 16 | required => 1, 17 | isa => 'Net::Amazon::S3::Bucket', 18 | ); 19 | 20 | sub _get_file { 21 | my $self = shift; 22 | my @parts = split('\.', $self->relname); 23 | my $ext = pop(@parts); 24 | my $dir = $self->workdir; 25 | 26 | if($self->has_data) { 27 | if($self->download_verbose) { 28 | print "downloading " . $self->relname . " to " . $self->filename . "\n"; 29 | } 30 | my ($fh, $file) = tempfile("s3-XXXXXX", dir => $dir, SUFFIX => ".$ext"); 31 | $self->s3object->get_key_filename($self->relname, "GET", $file); 32 | return $file; 33 | } else { 34 | my $file = join("/", $self->workdir, basename($self->relname)); 35 | return $file; 36 | } 37 | } 38 | 39 | sub _probe_mtime { 40 | my $self = shift; 41 | my $meta = $self->s3object->head_key($self->relname); 42 | 43 | return DateTime::Format::ISO8601->parse_datetime($meta->{last_modified}); 44 | } 45 | 46 | sub store_file { 47 | my $self = shift; 48 | return if(!$self->has_download); 49 | 50 | if($self->download_verbose) { 51 | print "uploading " . $self->filename . " to " . $self->onhost_pathname . " via s3\n"; 52 | } 53 | 54 | $self->s3object->add_key_filename($self->relname, $self->filename, {}) or croak($self->s3object->errstr); 55 | 56 | $self->stored; 57 | } 58 | 59 | sub delete { 60 | my $self = shift; 61 | $self->s3object->delete_key($self->relname) 62 | } 63 | 64 | no Moose; 65 | 66 | package SReview::Files::Collection::S3; 67 | 68 | use Moose; 69 | use Net::Amazon::S3; 70 | use DateTime::Format::ISO8601; 71 | use SReview::Config::Common; 72 | 73 | extends "SReview::Files::Collection::Net"; 74 | 75 | has 's3object' => ( 76 | is => 'ro', 77 | isa => 'Net::Amazon::S3::Bucket', 78 | lazy => 1, 79 | builder => '_probe_s3obj', 80 | ); 81 | 82 | has '+fileclass' => ( 83 | default => 'SReview::Files::Access::S3', 84 | ); 85 | 86 | sub _probe_s3obj { 87 | my $self = shift; 88 | my $config = SReview::Config::Common::setup(); 89 | my $bucket; 90 | if($self->has_baseurl) { 91 | $bucket = $self->baseurl; 92 | } else { 93 | my @elements = split('\/', $self->globpattern); 94 | do { 95 | $bucket = shift(@elements) 96 | } while(!length($bucket)); 97 | $self->_set_baseurl($bucket); 98 | } 99 | my $aconf = $config->get('s3_access_config'); 100 | if(exists($aconf->{$bucket})) { 101 | $aconf = $aconf->{$bucket}; 102 | } else { 103 | if(!exists($aconf->{default})) { 104 | croak("S3 access configuration does not exist for $bucket, and nor does a default exist"); 105 | } 106 | $aconf = $aconf->{default}; 107 | } 108 | return Net::Amazon::S3->new($aconf)->bucket($bucket); 109 | } 110 | 111 | sub _probe_children { 112 | my $self = shift; 113 | my $return = []; 114 | my $baseurl; 115 | 116 | eval { 117 | foreach my $key(@{$self->s3object->list_all->{keys}}) { 118 | push @$return, SReview::Files::Access::S3->new( 119 | s3object => $self->s3object, 120 | baseurl => $self->baseurl, 121 | mtime => DateTime::Format::ISO8601->parse_datetime($key->{last_modified}), 122 | relname => $key->{key}, 123 | download_verbose => $self->download_verbose 124 | ); 125 | } 126 | }; 127 | return $return; 128 | } 129 | 130 | sub _create { 131 | my $self = shift; 132 | my %options = @_; 133 | 134 | $options{s3object} = $self->s3object; 135 | 136 | return $self->SUPER::_create(%options); 137 | } 138 | 139 | no Moose; 140 | 141 | 1; 142 | -------------------------------------------------------------------------------- /lib/SReview/Job.pm: -------------------------------------------------------------------------------- 1 | package SReview::Job; 2 | 3 | use SReview::Config; 4 | use Scalar::Util qw(weaken); 5 | use Moose; 6 | 7 | has 'steps' => ( 8 | is => 'ro', 9 | isa => 'ArrayRef[SReview::Job::Step]', 10 | required => 1, 11 | traits => ['Array'], 12 | handles => { 13 | stepcount => 'count', 14 | ); 15 | 16 | has 'jobid' => ( 17 | is => 'ro', 18 | isa => 'Str', 19 | lazy => 1, 20 | builder => '_load_jobid', 21 | ); 22 | 23 | has 'talk' => ( 24 | is => 'ro', 25 | isa => 'SReview::Talk', 26 | required => 1, 27 | ); 28 | 29 | sub _load_jobid { 30 | my $self = shift; 31 | 32 | return join('-', $$, $self->talk->talkid, $self->talk->state); 33 | } 34 | 35 | has 'db' => ( 36 | is => 'ro', 37 | isa => 'DBI::db'; 38 | required => 1, 39 | ); 40 | 41 | sub run { 42 | my $self = shift; 43 | 44 | my $db = $self->db; 45 | 46 | my $joblog = $db->prepare("INSERT INTO joblog(talk, jobid) VALUES(?, ?, 0) RETURNING id"); 47 | $joblog->execute($self->talk->talkid, $self->jobid); 48 | 49 | my $row = $joblog->fetchrow_arrayref; 50 | my $joblog_id = $row->[0]; 51 | my $step_id; 52 | 53 | my $step = $db->prepare("INSERT INTO joblog_step(joblogid, stepname) VALUES(?, ?) RETURNING id"); 54 | my $progress = $db->prepare("UPDATE joblog_step SET progress = ? WHERE id = ?"); 55 | 56 | my $talk_steps = $db->prepare("UPDATE talks SET perc=? WHERE id=?"); 57 | 58 | sub progress { 59 | my $perc = shift; 60 | 61 | $progress->execute($perc, $step_id); 62 | } 63 | 64 | my $done = 0; 65 | foreach my $step (@{$self->steps}) { 66 | eval { 67 | $step->run(\&progress); 68 | }; 69 | if($@) { 70 | $db->prepare("UPDATE talks SET progress='failed' WHERE id = ?")->execute($self->talk->talkid); 71 | die "Step failed: $@"; 72 | } 73 | $done++; 74 | $talk_steps->execute($self->done / $self->stepcount, $self->talk->talkid); 75 | } 76 | } 77 | 78 | 1; 79 | -------------------------------------------------------------------------------- /lib/SReview/Model/DbElement.pm: -------------------------------------------------------------------------------- 1 | package SReview::Model::DbElement; 2 | 3 | use Moose; 4 | 5 | use SReview::Config::Common; 6 | use SReview::Db; 7 | 8 | has 'config' => ( 9 | is => 'rw', 10 | isa => 'SReview::Config', 11 | builder => '_get_config', 12 | lazy => 1, 13 | ); 14 | 15 | sub _get_config { 16 | my $self = shift; 17 | return SReview::Config::Common::setup; 18 | } 19 | 20 | has 'dbh' => ( 21 | is => 'rw', 22 | isa => 'Mojo::Pg', 23 | builder => '_get_dbh', 24 | lazy => 1, 25 | ); 26 | 27 | sub _get_dbh { 28 | my $self = shift; 29 | my $pg = Mojo::Pg->new()->dsn($self->config->get('dbistring')); 30 | } 31 | 32 | no Moose; 33 | 34 | 1; 35 | -------------------------------------------------------------------------------- /lib/SReview/Model/Event.pm: -------------------------------------------------------------------------------- 1 | package SReview::Model::Event; 2 | 3 | use Moose; 4 | 5 | extends 'SReview::Model::DbElement'; 6 | 7 | has 'name' => ( 8 | is => 'ro', 9 | builder => '_get_name', 10 | ); 11 | 12 | sub _get_name { 13 | return shift->config->get('event'); 14 | } 15 | 16 | has 'inputdir' => ( 17 | is => 'ro', 18 | builder => '_get_inputdir', 19 | lazy => 1, 20 | ); 21 | 22 | sub _get_inputdir { 23 | my $self = shift; 24 | my $st = $self->dbh->db->dbh->prepare("SELECT inputdir FROM events WHERE name = ?"); 25 | $st->execute($self->name); 26 | if($st->rows == 0) { 27 | return $self->name; 28 | } 29 | my $dir = $st->fetchrow_hashref()->{inputdir}; 30 | if(!defined($dir)) { 31 | return $self->name; 32 | } 33 | return $dir; 34 | } 35 | 36 | no Moose; 37 | 38 | 1; 39 | -------------------------------------------------------------------------------- /lib/SReview/Schedule/Filtered.pm: -------------------------------------------------------------------------------- 1 | package SReview::Schedule::Filtered::FilteredTalk; 2 | 3 | use Moose; 4 | use SReview::Schedule::WithShadow; 5 | 6 | extends 'SReview::Schedule::WithShadow::ShadowedTalk'; 7 | 8 | has 'require_match' => ( 9 | is => 'ro', 10 | isa => 'HashRef[Str]', 11 | default => sub { {} }, 12 | ); 13 | 14 | has 'forbid_match' => ( 15 | is => 'ro', 16 | isa => 'HashRef[Str]', 17 | default => sub { {} }, 18 | ); 19 | 20 | sub _load_filtered { 21 | my $self = shift; 22 | 23 | foreach my $filter(keys %{$self->require_match}) { 24 | if($self->shadow->meta->find_attribute_by_name($filter)->get_value($self->shadow) !~ $self->require_match->{$filter}) { 25 | return 1; 26 | } 27 | } 28 | foreach my $filter(keys %{$self->forbid_match}) { 29 | if($self->shadow->meta->find_attribute_by_name($filter)->get_value($self->shadow) =~ $self->forbid_match->{$filter}) { 30 | return 1; 31 | } 32 | } 33 | return 0; 34 | } 35 | 36 | no Moose; 37 | 38 | package SReview::Schedule::Filtered::FilteredEvent; 39 | 40 | use Moose; 41 | 42 | extends 'SReview::Schedule::WithShadow::ShadowedEvent'; 43 | 44 | sub _load_talk_type { 45 | return 'SReview::Schedule::Filtered::FilteredTalk'; 46 | } 47 | 48 | package SReview::Schedule::Filtered; 49 | 50 | use Moose; 51 | use SReview::Schedule::WithShadow; 52 | 53 | extends 'SReview::Schedule::WithShadow'; 54 | 55 | sub _load_event_type { 56 | return 'SReview::Schedule::Filtered::FilteredEvent'; 57 | } 58 | 59 | no Moose; 60 | 61 | 1; 62 | -------------------------------------------------------------------------------- /lib/SReview/Schedule/Ics.pm: -------------------------------------------------------------------------------- 1 | package SReview::Schedule::Ics::Speaker; 2 | 3 | use Moose; 4 | use Mojo::Util 'slugify'; 5 | 6 | extends 'SReview::Schedule::Base::Speaker'; 7 | 8 | sub _load_upstreamid { 9 | return slugify(shift->name); 10 | } 11 | 12 | package SReview::Schedule::Ics::Track; 13 | 14 | use Moose; 15 | use Mojo::Util 'slugify'; 16 | 17 | extends 'SReview::Schedule::Base::Track'; 18 | 19 | sub _load_upstreamid { 20 | return slugify(shift->name); 21 | } 22 | 23 | package SReview::Schedule::Ics::Talk; 24 | 25 | use Moose; 26 | 27 | extends 'SReview::Schedule::Base::Talk'; 28 | 29 | has 'room_name' => ( 30 | is => 'ro', 31 | isa => 'Str', 32 | default => 'main', 33 | ); 34 | 35 | sub _load_room { 36 | my $self = shift; 37 | return $self->event_object->root_object->room_type->new(name => $self->room_name, event_object => $self->event_object); 38 | } 39 | 40 | has 'track_name' => ( 41 | is => 'ro', 42 | isa => 'Str', 43 | default => 'main', 44 | ); 45 | 46 | sub _load_track { 47 | my $self = shift; 48 | return $self->event_object->root_object->track_type->new(name => $self->track_name, talk_object => $self); 49 | } 50 | 51 | has 'speaker_name' => ( 52 | is => 'ro', 53 | isa => 'Str', 54 | predicate => 'have_speaker_name', 55 | ); 56 | 57 | sub _load_speakers { 58 | my $self = shift; 59 | if($self->have_speaker_name) { 60 | return [$self->event_object->root_object->speaker_type->new(name => $self->speaker_name, talk_object => $self)]; 61 | } 62 | return undef; 63 | } 64 | 65 | has 'schedref' => ( 66 | is => 'ro', 67 | isa => 'HashRef', 68 | required => 1, 69 | ); 70 | 71 | sub _load_starttime { 72 | return shift->schedref->{DTSTART}; 73 | } 74 | 75 | sub _load_endtime { 76 | return shift->schedref->{DTEND}; 77 | } 78 | 79 | sub _load_filtered { 80 | return 0; 81 | } 82 | 83 | sub _load_title { 84 | return shift->schedref->{SUMMARY}; 85 | } 86 | 87 | package SReview::Schedule::Ics::Event; 88 | 89 | use Moose; 90 | 91 | extends 'SReview::Schedule::Base::Event'; 92 | 93 | has 'schedref' => ( 94 | is => 'ro', 95 | isa => 'HashRef', 96 | required => 1, 97 | ); 98 | 99 | has "talk_opts" => ( 100 | is => "ro", 101 | isa => "HashRef", 102 | ); 103 | 104 | has 'summary_regex' => ( 105 | is => 'ro', 106 | isa => 'Str', 107 | predicate => 'have_regex', 108 | ); 109 | 110 | sub _load_talks { 111 | my $self = shift; 112 | my $rv = []; 113 | my $talk_opts = $self->talk_opts; 114 | foreach my $year(values %{$self->schedref->{events}}) { 115 | foreach my $month(values %$year) { 116 | foreach my $day(values %$month) { 117 | foreach my $talk(values %$day) { 118 | my $talk_obj = $self->root_object->talk_type->new(schedref => $talk, %$talk_opts, event_object => $self); 119 | if($self->have_regex) { 120 | my $summary = $talk->{SUMMARY}; 121 | next unless $summary =~ $self->summary_regex; 122 | foreach my $field(keys %+) { 123 | $talk_obj->meta->find_attribute_by_name($field)->set_value($talk_obj, $+{$field}); 124 | } 125 | } 126 | push @$rv, $talk_obj; 127 | } 128 | } 129 | } 130 | } 131 | return $rv; 132 | } 133 | 134 | package SReview::Schedule::Ics; 135 | 136 | =head1 NAME 137 | 138 | SReview::Schedule::Ics - sreview-import schedule parser for schedules in ICS format 139 | 140 | =head1 SYNOPSIS 141 | 142 | $schedule_format = "ics"; 143 | $schedule_options = { url => "https://...", event_opts => { name => 'My conference', talk_opts => { track_name => "My track", room_name => "My room"} } }; 144 | 145 | =cut 146 | 147 | use Moose; 148 | use iCal::Parser; 149 | use SReview::Schedule::Base; 150 | 151 | extends 'SReview::Schedule::Base'; 152 | 153 | has "event_opts" => ( 154 | is => 'ro', 155 | isa => 'HashRef[Any]', 156 | default => sub { {} }, 157 | ); 158 | 159 | sub _load_talk_type { 160 | return "SReview::Schedule::Ics::Talk"; 161 | } 162 | 163 | sub _load_speaker_type { 164 | return "SReview::Schedule::Ics::Speaker"; 165 | } 166 | 167 | sub _load_track_type { 168 | return "SReview::Schedule::Ics::Track"; 169 | } 170 | 171 | sub _load_event_type { 172 | return "SReview::Schedule::Ics::Event"; 173 | } 174 | 175 | sub _load_events { 176 | my $self = shift; 177 | my $ics = iCal::Parser->new; 178 | $ics->parse_strings($self->_get_raw); 179 | my $event_opts = $self->event_opts; 180 | return [$self->event_type->new(schedref => $ics->calendar, %$event_opts, root_object => $self)]; 181 | } 182 | 183 | 1; 184 | -------------------------------------------------------------------------------- /lib/SReview/Schedule/Wafer.pm: -------------------------------------------------------------------------------- 1 | package SReview::Schedule::Wafer::Talk; 2 | 3 | use Moose; 4 | use Mojo::Util 'slugify'; 5 | use DateTime::Format::ISO8601; 6 | 7 | extends 'SReview::Schedule::Penta::Talk'; 8 | 9 | sub _load_upstreamid { 10 | return shift->schedref->attribute('guid'); 11 | } 12 | 13 | sub _load_slug { 14 | return shift->xml_helper('slug'); 15 | } 16 | 17 | sub _load_speakers { 18 | my $self = shift; 19 | 20 | return [] if (grep(/^persons$/, $self->schedref->children_names) == 0); 21 | return $self->SUPER::_load_speakers; 22 | } 23 | 24 | sub _load_filtered { 25 | my $rec = shift->schedref->child("recording"); 26 | return 0 unless defined($rec); 27 | return 1 if $rec->child("optout")->value eq "true"; 28 | return 0; 29 | } 30 | 31 | sub _load_starttime { 32 | return DateTime::Format::ISO8601->parse_datetime(shift->xml_helper('date')); 33 | } 34 | 35 | no Moose; 36 | 37 | package SReview::Schedule::Wafer::Event; 38 | 39 | use Moose; 40 | use DateTime::TimeZone; 41 | 42 | extends 'SReview::Schedule::Penta::Event'; 43 | 44 | sub _load_timezone { 45 | my $self = shift; 46 | my $timezone = DateTime::TimeZone->new(name => $self->schedref->child('time_zone_name')); 47 | return DateTime::TimeZone->new(name => $timezone->value) if defined($timezone); 48 | return $self->SUPER::_load_timezone; 49 | } 50 | 51 | no Moose; 52 | 53 | package SReview::Schedule::Wafer; 54 | 55 | use Moose; 56 | use SReview::Schedule::Penta; 57 | 58 | extends 'SReview::Schedule::Penta'; 59 | 60 | =head1 NAME 61 | 62 | SReview::Schedule::Wafer - sreview-import schedule parser for the Pentabarf XML format as created by the Wafer conference management system. 63 | 64 | =head1 DESCRIPTION 65 | 66 | The Wafer conference management system has the ability to create an XML 67 | version of its schedule that is compatible with the Pentabarf XML 68 | format. However, it is mildly different in the way it creates it, most 69 | significantly in the way it creates unique IDs. As such, importing such 70 | a schedule with the L parser will fail to 71 | create a stable schedule in SReview. 72 | 73 | This parser uses the L parser with the minimal 74 | required changes to make it work with the Wafer schedule parser. 75 | 76 | =head1 OPTIONS 77 | 78 | C only supports one option: 79 | 80 | =head2 url 81 | 82 | The URL where the schedule can be found. 83 | 84 | =head1 SEE ALSO 85 | 86 | L, L 87 | 88 | =cut 89 | 90 | sub _load_talk_type { 91 | return 'SReview::Schedule::Wafer::Talk'; 92 | } 93 | 94 | sub _load_event_type { 95 | return 'SReview::Schedule::Wafer::Event'; 96 | } 97 | 98 | no Moose; 99 | 100 | 1; 101 | -------------------------------------------------------------------------------- /lib/SReview/Talk/Progress.pm: -------------------------------------------------------------------------------- 1 | package SReview::Talk::Progress; 2 | 3 | use Class::Type::Enum values => [qw( 4 | waiting 5 | scheduled 6 | running 7 | done 8 | failed 9 | )]; 10 | 11 | use overload '<=>' => 'cmp', '++' => "incr", '--' => "decr"; 12 | 13 | sub incr { 14 | ++${$_[0]}; 15 | } 16 | 17 | sub decr { 18 | --${$_[0]}; 19 | } 20 | 21 | 1; 22 | -------------------------------------------------------------------------------- /lib/SReview/Talk/State.pm: -------------------------------------------------------------------------------- 1 | package SReview::Talk::State; 2 | 3 | use Class::Type::Enum values => [qw( 4 | waiting_for_files 5 | cutting 6 | generating_previews 7 | notification 8 | preview 9 | transcoding 10 | fixuping 11 | uploading 12 | publishing 13 | notify_final 14 | finalreview 15 | announcing 16 | transcribing 17 | syncing 18 | done 19 | injecting 20 | remove 21 | removing 22 | broken 23 | needs_work 24 | lost 25 | ignored 26 | uninteresting 27 | )]; 28 | 29 | 30 | use overload '<=>' => 'cmp', '++' => "incr", '--' => "decr"; 31 | 32 | sub incr { 33 | if($_[0] eq "injecting") { 34 | ${$_[0]} = $_[0]->sym_to_ord->{generating_previews}; 35 | } else { 36 | ++${$_[0]}; 37 | } 38 | } 39 | 40 | sub decr { 41 | --${$_[0]}; 42 | } 43 | 44 | 1; 45 | -------------------------------------------------------------------------------- /lib/SReview/Template.pm: -------------------------------------------------------------------------------- 1 | package SReview::Template; 2 | 3 | use Moose; 4 | use Mojo::Template; 5 | use SReview::Config::Common; 6 | 7 | =head1 NAME 8 | 9 | SReview::Template - process a string or a file and apply changes to it 10 | 11 | =head1 SYNOPSIS 12 | 13 | use SReview::Template; 14 | use SReview::Talk; 15 | 16 | my $talk = SReview::Talk->new(...); 17 | my $template = SReview::Template->new(talk => $talk, vars => { foo => "bar" }, regexvars => {"@FOO@" => "foo"}); 18 | my $processed = $template->string("The @FOO@ is <%== $foo %>, and the talk is titled <%== $talk->title %>"); 19 | # $processed now contains "The foo is bar, and the talk is titled ..." 20 | # (with the actual talk title there) 21 | $template->file("inputfile.txt", "outputfile.txt" 22 | 23 | =head1 DESCRIPTION 24 | 25 | C is a simple wrapper around L. All 26 | the variables that are passed in to the "vars" parameter are passed as 27 | named variables to L. 28 | 29 | In addition, some bits of SReview previously did some simple sed-like 30 | search-and-replace templating. For backwards compatibility, this module 31 | also supports such search-and-replace templating (e.g., 32 | L has a few of those). These, however, are now 33 | deprecated; the C<$talk> variable and L-style templates 34 | should be used instead. 35 | 36 | =head1 ATTRIBUTES 37 | 38 | C objects support the following attributes: 39 | 40 | =head2 talk 41 | 42 | An L object for the talk that this template is for. 43 | Required. Will be passed on to the template as the C<$talk> variable. 44 | 45 | =cut 46 | 47 | has 'talk' => ( 48 | is => 'ro', 49 | isa => 'SReview::Talk', 50 | required => 1, 51 | ); 52 | 53 | has '_mt' => ( 54 | is => 'ro', 55 | isa => 'Mojo::Template', 56 | default => sub { my $mt = Mojo::Template->new(); $mt->vars(1); }, 57 | ); 58 | 59 | =head2 vars 60 | 61 | Additional L variables to be made available to the 62 | template. 63 | 64 | =cut 65 | 66 | has 'vars' => ( 67 | is => 'ro', 68 | isa => 'HashRef', 69 | ); 70 | 71 | =head2 regexvars 72 | 73 | Variables to be replaced by search-and-replace. 74 | 75 | =cut 76 | 77 | has 'regexvars' => ( 78 | is => 'ro', 79 | isa => 'HashRef[Str]', 80 | predicate => '_has_regexes', 81 | ); 82 | 83 | =head1 METHODS 84 | 85 | =head2 file 86 | 87 | A method to process an input file through the templating engine into an 88 | output file. 89 | 90 | Takes to arguments: the name of the input file, followed by the name of 91 | the output file. 92 | 93 | Is implemented in terms of the C method. 94 | 95 | =cut 96 | 97 | sub file { 98 | my $self = shift; 99 | my $inputname = shift; 100 | my $outputname = shift; 101 | 102 | local $_; 103 | 104 | open my $input, '<:encoding(UTF-8)', $inputname; 105 | open my $output, '>:encoding(UTF-8)', $outputname; 106 | while(<$input>) { 107 | $_ = $self->string($_); 108 | print $output $_; 109 | } 110 | close $input; 111 | close $output; 112 | } 113 | 114 | =head2 string 115 | 116 | A function to process a string, passed as the only argument. Returns the 117 | result of the template function. 118 | 119 | =cut 120 | 121 | sub string { 122 | my $self = shift; 123 | my $string = shift; 124 | my $vars = $self->vars; 125 | $vars->{talk} = $self->talk; 126 | 127 | my $rendered = $self->_mt->render($string, $vars); 128 | if($self->_has_regexes) { 129 | my $revals = $self->regexvars; 130 | foreach my $key(keys %{$revals}) { 131 | $rendered =~ s/$key/$revals->{$key}/g; 132 | } 133 | } 134 | return $rendered; 135 | } 136 | 137 | no Moose; 138 | 139 | 1; 140 | -------------------------------------------------------------------------------- /lib/SReview/Template/SVG.pm: -------------------------------------------------------------------------------- 1 | package SReview::Template::SVG; 2 | 3 | use SReview::Template; 4 | use Mojo::UserAgent; 5 | use Mojo::Util qw/xml_escape/; 6 | use File::Temp qw/tempdir/; 7 | 8 | use Exporter 'import'; 9 | our @EXPORT_OK = qw/process_template/; 10 | 11 | =head1 NAME 12 | 13 | SReview::Template::SVG - module to process an SVG template into a PNG 14 | file 15 | 16 | =head1 SYNOPSIS 17 | 18 | use SReview::Template::SVG qw/process_template/; 19 | use SReview::Talk; 20 | use SReview::Config::Common; 21 | 22 | my $talk = SReview::Talk->new(talkid => ...); 23 | my $config = SReview::Config::Common::setup(); 24 | 25 | process_template($input_svg_template, $output_png_filename, $talk, $config); 26 | 27 | # now a PNG file is written to $output_png_filename 28 | 29 | =head1 DESCRIPTION 30 | 31 | C uses L to process an input 32 | file into a templated SVG file, and then runs inkscape over it to 33 | convert the templated SVG file to a PNG file at the given location. 34 | 35 | The input file can either be a file on the local file system, or it can 36 | be an HTTP or HTTPS URL (in which case the template at that location 37 | will first be downloaded, transparently). 38 | 39 | =head1 TEMPLATE TAGS 40 | 41 | In addition to the L syntax on C<$talk> that 42 | L provides, C also passes the 43 | these regexvars to L (for more information, see 44 | SReview::Template): 45 | 46 | =over 47 | 48 | =item @SPEAKERS@ 49 | 50 | The value of C<$talk-Espeakers> 51 | 52 | =item @ROOM@ 53 | 54 | The value of C<$talk-Eroom> 55 | 56 | =item @TITLE@ 57 | 58 | The value of C<$talkEtitle> 59 | 60 | =item @SUBTITLE@ 61 | 62 | The value of C<$talkEsubtitle> 63 | 64 | =item @DATE@ 65 | 66 | The value of C<$talkEdate> 67 | 68 | =item @APOLOGY@ 69 | 70 | The value of C<$talkEapology> 71 | 72 | =back 73 | 74 | Note that all these values are XML-escaped first. 75 | 76 | =head1 CONFIGURATION 77 | 78 | This module checks the following configuration values: 79 | 80 | =head2 command_tune 81 | 82 | If the value C in this hash is set to "C<0.9>", then the C 83 | command is invoked with command-line parameters for Inkscape version 0.9 84 | or below. In all other cases, command-line parameters for Inkscape 85 | version 1.0 or above are used instead. 86 | 87 | =head2 workdir 88 | 89 | The location for temporary files that the module needs. 90 | 91 | =cut 92 | 93 | sub process_template($$$$) { 94 | my $input = shift; 95 | my $output = shift; 96 | my $talk = shift; 97 | my $config = shift; 98 | 99 | my $tempdir = tempdir('svgXXXXXX', DIR => $config->get("workdir"), CLEANUP => 1); 100 | my $outputsvg = "$tempdir/tmp.svg"; 101 | my $speakers = xml_escape($talk->speakers); 102 | my $room = xml_escape($talk->room); 103 | my $title = xml_escape($talk->title); 104 | my $subtitle = xml_escape($talk->subtitle); 105 | my $startdate = xml_escape($talk->date); 106 | my $apology = xml_escape($talk->apology); 107 | my $regexvars = { 108 | '@SPEAKERS@' => $speakers, 109 | '@ROOM@' => $room, 110 | '@TITLE@' => $title, 111 | '@SUBTITLE@' => $subtitle, 112 | '@DATE@' => $startdate, 113 | '@APOLOGY@' => $apology, 114 | }; 115 | my $content = ""; 116 | my $template_engine = SReview::Template->new(talk => $talk, regexvars => $regexvars); 117 | 118 | if($input =~ /^http(s)?:\/\//) { 119 | my $ua = Mojo::UserAgent->new->connect_timeout(60)->max_redirects(10); 120 | my $res = $ua->get($input)->result; 121 | if(!$res->is_success) { 122 | die "could not download: " . $res->message; 123 | } 124 | $content = $res->body; 125 | } else { 126 | open INPUT, '<:encoding(UTF-8)', $input; 127 | while() { 128 | $content .= $_; 129 | } 130 | close INPUT; 131 | } 132 | open my $fh, ">:encoding(UTF-8)", $outputsvg; 133 | print "creating $output from $input\n"; 134 | print $fh $template_engine->string($content); 135 | close $fh; 136 | my $inkscape_options = "-o $output"; 137 | $command_tune = $config->get('command_tune'); 138 | if(exists($command_tune->{inkscape}) && $command_tune->{inkscape} eq "0.9") { 139 | $inkscape_options = "--export-png=$output"; 140 | } 141 | system("inkscape $inkscape_options $outputsvg"); 142 | } 143 | 144 | 1; 145 | -------------------------------------------------------------------------------- /lib/SReview/Template/Synfig.pm: -------------------------------------------------------------------------------- 1 | package SReview::Template::Synfig; 2 | 3 | use SReview::Template; 4 | use Mojo::UserAgent; 5 | use Mojo::Util qw/xml_escape/; 6 | use File::Temp qw/tempdir/; 7 | 8 | use Exporter 'import'; 9 | our @EXPORT_OK = qw/process_template/; 10 | 11 | =head1 NAME 12 | 13 | SReview::Template::Synfig - module to process a Synfig template into a PNG 14 | file 15 | 16 | =head1 SYNOPSIS 17 | 18 | use SReview::Template::Synfig qw/process_template/; 19 | use SReview::Talk; 20 | use SReview::Config::Common; 21 | 22 | my $talk = SReview::Talk->new(talkid => ...); 23 | my $config = SReview::Config::Common::setup(); 24 | 25 | process_template($input_svg_template, $output_png_filename, $talk, $config); 26 | 27 | # now a PNG file is written to $output_png_filename 28 | 29 | =head1 DESCRIPTION 30 | 31 | C uses L to process an input 32 | file into a templated Synfig file, and then runs synfig over it to 33 | convert the templated Synfig file to a PNG file at the given location. 34 | 35 | The input file can either be a file on the local file system, or it can 36 | be an HTTP or HTTPS URL (in which case the template at that location 37 | will first be downloaded, transparently). 38 | 39 | =head1 CONFIGURATION 40 | 41 | This module checks the following configuration values: 42 | 43 | =head2 workdir 44 | 45 | The location for temporary files that the module needs. 46 | 47 | =cut 48 | 49 | sub process_template($$$$) { 50 | my $input = shift; 51 | my $output = shift; 52 | my $talk = shift; 53 | my $config = shift; 54 | 55 | my $tempdir = tempdir('svgXXXXXX', DIR => $config->get("workdir"), CLEANUP => 1); 56 | my $outputsif = "$tempdir/tmp.sif"; 57 | my $content = ""; 58 | my $template_engine = SReview::Template->new(talk => $talk); 59 | 60 | if($input =~ /^http(s)?:\/\//) { 61 | my $ua = Mojo::UserAgent->new->connect_timeout(60)->max_redirects(10); 62 | my $res = $ua->get($input)->result; 63 | if(!$res->is_success) { 64 | die "could not download: " . $res->message; 65 | } 66 | $content = $res->body; 67 | } else { 68 | open INPUT, '<:encoding(UTF-8)', $input; 69 | while() { 70 | $content .= $_; 71 | } 72 | close INPUT; 73 | } 74 | open my $fh, ">:encoding(UTF-8)", $outputsif; 75 | print "creating $output from $input\n"; 76 | print $fh $template_engine->string($content); 77 | close $fh; 78 | system("synfig -o $output $outputsif"); 79 | } 80 | 81 | 1; 82 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Admin.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Admin; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use Mojo::Collection 'c'; 5 | 6 | sub main { 7 | my $c = shift; 8 | my $st; 9 | my $talks = (); 10 | my $room; 11 | my $lastroom = ''; 12 | 13 | if(defined($c->session->{room})) { 14 | $st = $c->dbh->prepare('SELECT nonce, room, name, starttime, speakers, state FROM talk_list WHERE eventid = ? AND roomid = ? ORDER BY starttime'); 15 | $st->execute($c->eventid, $c->session->{room}); 16 | } else { 17 | $st = $c->dbh->prepare('SELECT nonce, room, name, starttime, speakers, state FROM talk_list WHERE eventid = ? ORDER BY room, starttime'); 18 | $st->execute($c->eventid); 19 | } 20 | while(my $row = $st->fetchrow_hashref("NAME_lc")) { 21 | if ($row->{'room'} ne $lastroom) { 22 | if(defined($room)) { 23 | push @$talks, c($lastroom => $room); 24 | } 25 | $room = []; 26 | } 27 | $lastroom = $row->{'room'}; 28 | next unless defined($row->{nonce}); 29 | push @$room, [$row->{'starttime'} . ': ' . $row->{'name'} . ' by ' . $row->{'speakers'} . ' (' . $row->{'state'} . ')' => $row->{'nonce'}]; 30 | } 31 | if(defined($room)) { 32 | push @$talks, c($lastroom => $room); 33 | } 34 | $c->stash(email => $c->session->{email}); 35 | $c->stash(talks => $talks); 36 | $c->render; 37 | } 38 | 39 | 1; 40 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Config.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Config; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | 5 | sub get_config { 6 | my $c = shift->openapi->valid_input; 7 | 8 | my $eventid = $c->eventid; 9 | my $config; 10 | if(defined($eventid)) { 11 | $config = { event => $c->eventid }; 12 | } else { 13 | $config = {}; 14 | } 15 | 16 | return $c->render(openapi => $config); 17 | } 18 | 19 | my $legend = [ 20 | { name => "waiting_for_files", expl => 'Still waiting for content for these talks' }, 21 | { name => "cutting", expl => 'Talk is being cut' }, 22 | { name => "generating_previews", expl => 'Talk previews are being generated' }, 23 | { name => "notification", expl => 'Sending out notifications' }, 24 | { name => "preview", expl => 'Talk ready for review, waiting for reviewer' }, 25 | { name => "transcoding", expl => 'High-quality transcodes running' }, 26 | { name => "fixuping", expl => 'Fixups running' }, 27 | { name => "uploading", expl => 'Uploading results' }, 28 | { name => "publishing", expl => 'Waiting for upload to appear in download area' }, 29 | { name => "notify_final", expl => 'Sending out notifications for final review' }, 30 | { name => "finalreview", expl => 'Ready for final review, waiting for reviewer' }, 31 | { name => "announcing", expl => 'Announcing completion of publication' }, 32 | { name => "transcribing", expl => 'Transcription running' }, 33 | { name => "syncing", expl => 'Syncing Uploads' }, 34 | { name => "done", expl => 'Videos published, all done' }, 35 | { name => "injecting", expl => 'Injecting manually-edited video' }, 36 | { name => "remove", expl => 'Final review found problems, talk being removed' }, 37 | { name => "removing", expl => 'Waiting for removal to finalize' }, 38 | { name => "broken", expl => 'Review found problems, administrator required' }, 39 | { name => "needs_work", expl => 'Fixable problems exist, manual intervention required' }, 40 | { name => "lost", expl => 'Nonfixable problems exist, talk lost' }, 41 | { name => "ignored", expl => 'Talk disappeared from the schedule' }, 42 | { name => "uninteresting", expl => 'Talk marked as not relevant' }, 43 | ]; 44 | 45 | sub get_legend { 46 | my $c = shift->openapi->valid_input or return; 47 | 48 | return $c->render(openapi => $legend); 49 | } 50 | 51 | 1; 52 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/CreditPreviews.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::CreditPreviews; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | 5 | use SReview::Talk; 6 | use SReview::Template::SVG qw/process_template/; 7 | use SReview::Files::Factory; 8 | 9 | my %valid_suffix = (pre => ["preroll_template"], post => ["postroll_template", "postroll"], sorry => , ["apology_template"]); 10 | 11 | sub serve_png { 12 | my $c = shift->openapi->valid_input or return;; 13 | my $slug = $c->param("slug"); 14 | my $suffix = $c->stash("suffix"); 15 | my $nonce = $c->param("nonce"); 16 | my $talk; 17 | if(defined($slug)) { 18 | $talk = SReview::Talk->by_slug($slug); 19 | } elsif(defined($nonce)) { 20 | $talk = SReview::Talk->by_nonce($nonce); 21 | } else { 22 | $c->app->log->debug("no slug or nonce, can't generate a preview"); 23 | return $c->reply->not_found; 24 | } 25 | if(!defined($talk)) { 26 | $c->app->log->debug("talk not found"); 27 | return $c->reply->not_found; 28 | } 29 | my $input_coll = SReview::Files::Factory->create("intermediate", $c->srconfig->get("pubdir")); 30 | my $template; 31 | if(!exists($valid_suffix{$suffix})) { 32 | $c->app->log->debug("invalid suffix, ignored"); 33 | return $c->reply->not_found; 34 | } 35 | if(scalar(@{$valid_suffix{$suffix}}) > 1) { 36 | $c->app->log->debug("checking if static file exists"); 37 | my $png = $c->srconfig->get($valid_suffix{$suffix}[1]); 38 | if(defined $png && -f $png) { 39 | $c->app->log->debug("using prerendered file"); 40 | return $c->reply->file($png); 41 | } 42 | } 43 | $template = $c->srconfig->get($valid_suffix{$suffix}[0]); 44 | if(!defined $template) { 45 | $c->app->log->debug("template not configured, ignored"); 46 | return $c->reply->not_found; 47 | } 48 | $c->app->log->debug("looking for render of template $template"); 49 | my $relname = $talk->relative_name . "/" . $suffix . ".png"; 50 | my $force = $c->param("force"); 51 | if((defined($force) && $force ne "false") || !($input_coll->has_file($relname))) { 52 | $c->app->log->debug("file does not exist or force specified, rerendering"); 53 | my $preroll_file = $input_coll->add_file(relname => $relname); 54 | process_template($template, $preroll_file->filename, $talk, $c->srconfig); 55 | $preroll_file->store_file; 56 | } 57 | $c->app->log->debug("serving rendered file..."); 58 | return $c->reply->file($input_coll->get_file(relname => $relname)->filename); 59 | } 60 | 61 | 1; 62 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Event.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Event; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use SReview::API::Helpers; 5 | use Data::Dumper; 6 | 7 | sub add { 8 | my $c = shift->openapi->valid_input or return; 9 | 10 | return add_with_json($c, $c->req->json, "events", $c->openapi->spec('/components/schemas/Event/properties')); 11 | } 12 | 13 | sub update { 14 | my $c = shift->openapi->valid_input or return; 15 | 16 | my $eventId = $c->param("eventId"); 17 | 18 | my $event = $c->req->json; 19 | 20 | $event->{id} = $eventId; 21 | 22 | return update_with_json($c, $event, "events", $c->openapi->spec('/components/schemas/Event/properties')); 23 | } 24 | 25 | sub delete { 26 | my $c = shift->openapi->valid_input or return; 27 | 28 | my $eventId = $c->param('eventId'); 29 | my $query = "DELETE FROM events WHERE id = ? RETURNING id"; 30 | 31 | return delete_with_query($c, $query, $eventId); 32 | } 33 | 34 | sub getById { 35 | my $c = shift->openapi->valid_input or return; 36 | 37 | my $eventId = $c->param("eventId"); 38 | my $event = db_query($c->dbh, "SELECT events.* FROM events WHERE id = ?", $eventId); 39 | 40 | if(scalar(@$event) < 1) { 41 | return $c->render(openapi => {errors => [{message => "not found"}]}, status => 404); 42 | } 43 | 44 | $c->render(openapi => $event->[0]); 45 | } 46 | 47 | sub list { 48 | my $c = shift->openapi->valid_input or return; 49 | 50 | my $events = db_query($c->dbh, "SELECT events.* FROM events ORDER BY events.name"); 51 | 52 | $c->render(openapi => $events); 53 | } 54 | 55 | sub overview { 56 | my $c = shift->openapi->valid_input or return; 57 | 58 | my $eventId = $c->param("eventId"); 59 | my $query; 60 | my $st = $c->dbh->prepare("SELECT id FROM events WHERE id = ?"); 61 | $st->execute($eventId); 62 | if($st->rows < 1) { 63 | return $c->render(openapi => {errors => [{message => "not found"}]}, status => 404); 64 | } 65 | 66 | if($c->srconfig->get("anonreviews") || (exists($c->session->{admin}) && $c->session->{admin} > 0)) { 67 | $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, track FROM talk_list WHERE eventid = ? AND state IS NOT NULL ORDER BY state, progress, room, starttime"; 68 | } else { 69 | $query = "SELECT name, speakers, room, starttime::timestamp, endtime::timestamp, state, progress, track FROM talk_list WHERE eventid = ? AND state IS NOT NULL ORDER BY state, progress, room, starttime"; 70 | } 71 | 72 | my $res = db_query($c->dbh, $query, $eventId); 73 | 74 | $c->render(openapi => $res); 75 | } 76 | 77 | 1; 78 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Finalreview.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Finalreview; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use Mojo::Collection 'c'; 5 | 6 | use SReview::Talk; 7 | use SReview::Access qw/admin_for/; 8 | 9 | sub view { 10 | my $c = shift; 11 | 12 | my $id = $c->stash("id"); 13 | my $talk; 14 | $c->stash(adminspecial => 0); 15 | eval { 16 | if(defined($id)) { 17 | $talk = SReview::Talk->new(talkid => $id); 18 | } else { 19 | $talk = SReview::Talk->by_nonce($c->stash("nonce")); 20 | } 21 | }; 22 | if($@) { 23 | $c->stash(error => $@); 24 | $c->render(variant => "error"); 25 | return; 26 | } 27 | my $variant; 28 | my $nonce = $talk->nonce; 29 | if($talk->state eq "finalreview") { 30 | $variant = undef; 31 | } elsif(admin_for($c, $talk)) { 32 | $variant = undef; 33 | $c->stash(adminspecial => 1); 34 | } elsif($talk->state gt "finalreview" && $talk->state lt "done") { 35 | $variant = 'working'; 36 | } elsif($talk->state ge 'remove' && $talk->state le 'removing') { 37 | $variant = 'working'; 38 | } else { 39 | $variant = 'done'; 40 | } 41 | $c->stash(talk => $talk); 42 | $c->stash(stylesheets => ['/review.css']); 43 | $c->render(variant => $variant); 44 | } 45 | 46 | sub update { 47 | my $c = shift; 48 | my $id = $c->stash("id"); 49 | my $talk; 50 | 51 | $c->stash(stylesheets => ['/review.css']); 52 | eval { 53 | if(defined($id)) { 54 | $talk = SReview::Talk->new(talkid => $id); 55 | } else { 56 | $talk = SReview::Talk->by_nonce($c->stash("nonce")); 57 | } 58 | }; 59 | if($@) { 60 | $c->stash(error => $@); 61 | $c->render(variant => "error"); 62 | return; 63 | } 64 | $c->stash(talk => $talk); 65 | if(!admin_for($c, $talk) && $talk->state ne 'finalreview') { 66 | $c->stash(error => 'This talk is not currently available for final review. Please try again later!'); 67 | $c->render(variant => 'error'); 68 | return; 69 | } 70 | $talk->add_correction(serial => 0); 71 | if($c->param("serial") != $talk->corrections->{serial}) { 72 | $c->stash(error => 'This talk was updated (probably by someone else) since you last loaded it. Please reload the page, and try again.'); 73 | $c->render(variant => 'error'); 74 | return; 75 | } 76 | if(!defined($c->param("video_state"))) { 77 | $c->stash(error => 'Invalid submission data; missing parameter video_state.'); 78 | $c->render(variant => "error"); 79 | return; 80 | } 81 | if(defined($c->param("comment_text")) && length($c->param("comment_text")) > 0) { 82 | $talk->comment($c->param("comment_text")); 83 | } 84 | if($c->param("video_state") eq "ok") { 85 | $talk->state_done("finalreview"); 86 | $c->render(variant => "done"); 87 | return; 88 | } 89 | $talk->set_state("remove"); 90 | $c->render(variant => "unpublish"); 91 | } 92 | 93 | 1; 94 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Room.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Room; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use SReview::API::Helpers; 5 | 6 | sub add { 7 | my $c = shift->openapi->valid_input or return; 8 | 9 | my $room = $c->req->json; 10 | 11 | return add_with_json($c, $room, "rooms", $c->openapi->spec('/components/schemas/Room/properties')); 12 | } 13 | 14 | sub update { 15 | my $c = shift->openapi->valid_input or return; 16 | 17 | my $roomId = $c->param('roomId'); 18 | my $room = $c->req->json; 19 | 20 | $room->{id} = $roomId; 21 | 22 | return update_with_json($c, $room, "rooms", $c->openapi->spec('/components/schemas/Room/properties')); 23 | } 24 | 25 | sub getById { 26 | my $c = shift->openapi->valid_input or return; 27 | 28 | my $roomId = $c->param('roomId'); 29 | 30 | my $room = db_query($c->dbh, "SELECT rooms.* FROM rooms WHERE id = ?", $roomId); 31 | 32 | if(scalar(@$room) < 1) { 33 | $c->render(openapi => { errors => [ { message => "not found" } ]}, status => 404); 34 | return; 35 | } 36 | 37 | $c->render(openapi => $room->[0]); 38 | } 39 | 40 | sub delete { 41 | my $c = shift->openapi->valid_input or return; 42 | 43 | my $roomId = $c->param("roomId"); 44 | 45 | return delete_with_query($c, "DELETE FROM rooms WHERE id = ? RETURNING id", $roomId); 46 | } 47 | 48 | sub list { 49 | my $c = shift->openapi->valid_input or return; 50 | 51 | my $rooms = db_query($c->dbh, "SELECT rooms.* FROM rooms"); 52 | 53 | $c->render(openapi => $rooms); 54 | } 55 | 56 | 1; 57 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Schedule.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Schedule; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use Mojo::Log; 5 | 6 | sub talks { 7 | my $self = shift; 8 | 9 | my $db = $self->dbh; 10 | 11 | my $eventdata = $db->prepare("SELECT * FROM talk_list WHERE eventid = ? ORDER BY id"); 12 | $eventdata->execute($self->eventid()); 13 | 14 | $self->app->log->debug("finding talks for event " . $self->eventid()); 15 | 16 | my $rv = (); 17 | 18 | while(my $row = $eventdata->fetchrow_hashref()) { 19 | $self->app->log->debug("found talk with id: " . $row->{id}); 20 | push @$rv, $row; 21 | } 22 | 23 | $self->render(json => $rv); 24 | } 25 | 26 | sub index { } 27 | 28 | 1; 29 | 30 | __DATA__ 31 | @@ schedule/index.html.ep 32 | % layout 'admin' 33 |

Schedule management

34 |

Possitble actions:

35 |
GET /admin/schedule/list
36 |
Creates a JSON list of all talks in the current event
37 |
DELETE /admin/schedule/talk/:id
38 |
Delete the talk with the given ID
39 |
PUT /admin/schedule/talk/
40 |
Create a new talk (requires JSON object)
41 |
PUT /admin/schedule/talk/:id
42 |
Update the data of the talk with the given ID
43 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Speaker.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Speaker; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use SReview::API::Helpers; 5 | 6 | sub listByTalk { 7 | my $c = shift->openapi->valid_input or return; 8 | 9 | my $eventId = $c->param("eventId"); 10 | my $talkId = $c->param("talkId"); 11 | 12 | my $talk = db_query($c->dbh, "SELECT id FROM talks WHERE event = ? AND id = ?", $eventId, $talkId); 13 | 14 | if(scalar(@$talk) < 1) { 15 | $c->render(openapi => { errors => [ { message => 'not found' } ] }, status => 404); 16 | return; 17 | } 18 | 19 | my $speakers = db_query($c->dbh, "SELECT speakers.* FROM speakers JOIN speakers_talks ON speakers.id = speakers_talks.speaker WHERE speakers_talks.talk = ?", $talkId); 20 | 21 | $c->render(openapi => $speakers); 22 | } 23 | 24 | sub search { 25 | my $c = shift->openapi->valid_input or return; 26 | 27 | my $searchString = "%" . $c->param("searchString") . "%"; 28 | 29 | $c->render(openapi => db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE name ILIKE ? OR email ILIKE ?", $searchString, $searchString)); 30 | } 31 | 32 | sub getByUpstream { 33 | my $c = shift->openapi->valid_input or return; 34 | 35 | my $speaker = db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE upstreamid = ?", $c->param("upstreamId")); 36 | 37 | if(scalar(@$speaker) < 1) { 38 | $c->render(openapi => { errors => [ { message => 'not found' } ] }, status => 404); 39 | return; 40 | } 41 | 42 | $c->render(openapi => $speaker->[0]); 43 | } 44 | 45 | sub add { 46 | my $c = shift->openapi->valid_input or return; 47 | 48 | my $speaker = $c->req->json; 49 | $c->app->log->debug(join(',', keys %$speaker)); 50 | 51 | return add_with_json($c, $speaker, "speakers", $c->openapi->spec('/components/schemas/Speaker/properties')); 52 | } 53 | 54 | sub update { 55 | my $c = shift->openapi->valid_input or return; 56 | 57 | my $speakerId = $c->param("speakerId"); 58 | 59 | my $speaker = $c->req->json; 60 | 61 | $speaker->{id} = $speakerId; 62 | 63 | return update_with_json($c, $speaker, "speakers", $c->openapi->spec('/components/schemas/Speaker/properties')); 64 | } 65 | 66 | sub getById { 67 | my $c = shift->openapi->valid_input or return; 68 | 69 | my $speakerId = $c->param("speakerId"); 70 | 71 | my $speaker = db_query($c->dbh, "SELECT speakers.* FROM speakers WHERE id = ?", $speakerId); 72 | 73 | if(scalar(@$speaker) < 1) { 74 | $c->render(openapi => { errors => [ { message => 'not found' } ] }, status => 404); 75 | return; 76 | } 77 | 78 | $c->render(openapi => $speaker->[0]); 79 | } 80 | 81 | sub delete { 82 | my $c = shift->openapi->valid_input or return; 83 | 84 | my $speakerId = $c->param('speakerId'); 85 | 86 | return delete_with_query($c, "DELETE FROM speakers WHERE id = ?", $speakerId); 87 | } 88 | 89 | 1; 90 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Track.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Track; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use SReview::API::Helpers; 5 | 6 | sub add { 7 | my $c = shift->openapi->valid_input or return; 8 | 9 | my $track = $c->req->json; 10 | 11 | return add_with_json($c, $track, "tracks", $c->openapi->spec('/components/schemas/Track/properties')); 12 | } 13 | 14 | sub update { 15 | my $c = shift->openapi->valid_input or return; 16 | 17 | my $track = $c->req->json; 18 | my $trackId = $c->param('trackId'); 19 | 20 | $track->{id} = $trackId; 21 | 22 | return update_with_json($c, $track, "tracks", $c->openapi->spec('/components/schemas/Track/properties')); 23 | } 24 | 25 | sub list { 26 | my $c = shift->openapi->valid_input or return; 27 | 28 | $c->render(openapi => db_query($c->dbh, "SELECT tracks.* FROM tracks")); 29 | } 30 | 31 | sub getById { 32 | my $c = shift->openapi->valid_input or return; 33 | 34 | my $trackId = $c->param('trackId'); 35 | 36 | my $track = db_query($c->dbh, "SELECT tracks.* FROM tracks WHERE id = ?", $trackId); 37 | 38 | if(scalar(@$track) < 1) { 39 | $c->render(openapi => { errors => [ { message => "not found" } ]}, status => 404); 40 | return; 41 | } 42 | 43 | $c->render(openapi => $track->[0]); 44 | } 45 | 46 | sub delete { 47 | my $c = shift->openapi->valid_input or return; 48 | 49 | my $trackId = $c->param("trackId"); 50 | 51 | return delete_with_query($c, "DELETE FROM tracks WHERE id = ?", $trackId); 52 | } 53 | 54 | 1; 55 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/User.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::User; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | use SReview::API::Helpers; 5 | 6 | sub add { 7 | my $c = shift->openapi->valid_input or return; 8 | 9 | my $user = $c->req->json; 10 | 11 | return add_with_json($c, $user, "users", $c->openapi->spec('/components/schemas/User/properties')); 12 | } 13 | 14 | sub update { 15 | my $c = shift->openapi->valid_input or return; 16 | 17 | my $userId = $c->param("userId"); 18 | my $user = $c->req->json; 19 | 20 | $user->{id} = $userId; 21 | 22 | return update_with_json($c, $user, "users", $c->openapi->spec('/components/schemas/User/properties')); 23 | } 24 | 25 | sub getById { 26 | my $c = shift->openapi->valid_input or return; 27 | 28 | my $userId = $c->param('userId'); 29 | 30 | my $user = db_query($c->dbh, "SELECT users.* FROM users WHERE id = ?", $userId); 31 | 32 | if(scalar(@$user) < 1) { 33 | $c->render(openapi => { errors => [ { message => "not found" } ]}, status => 404); 34 | return; 35 | } 36 | 37 | $c->render(openapi => $user->[0]); 38 | } 39 | 40 | sub delete { 41 | my $c = shift->openapi->valid_input or return; 42 | 43 | my $userId = $c->param('userId'); 44 | 45 | return delete_with_query($c, "DELETE FROM users WHERE id = ?", $userId); 46 | } 47 | 48 | sub list { 49 | my $c = shift->openapi->valid_input or return; 50 | 51 | $c->render(openapi => db_query($c->dbh, "SELECT users.* FROM users")); 52 | } 53 | 54 | sub login { 55 | my $c = shift->openapi->valid_input or return; 56 | 57 | my $email = $c->param('email'); 58 | my $pass = $c->param('pass'); 59 | 60 | my $st = $c->dbh->prepare("SELECT id, isadmin, isvolunteer, name, room FROM users WHERE email=? AND password=crypt(?, password)"); 61 | my $rv; 62 | if(!($rv = $st->execute($email, $pass))) { 63 | return $c->render(openapi => { errors => [ { message => "unknown user or password" } ] }, status => 403); 64 | } 65 | if($rv == 0) { 66 | return $c->render(openapi => { errors => [ { message => "unknown user or password" } ] }, status => 403); 67 | } 68 | if($st->rows < 1) { 69 | return $c->render(openapi => { errors => [ { message => "unknown user or password" } ] }, status => 403); 70 | } 71 | my $row = $st->fetchrow_arrayref; 72 | $c->session->{id} = $row->[0]; 73 | $c->session->{email} = $email; 74 | $c->session->{admin} = $row->[1]; 75 | $c->session->{volunteer} = $row->[2]; 76 | $c->session->{name} = $row->[3]; 77 | $c->session->{room} = $row->[4]; 78 | 79 | my $json = {}; 80 | 81 | if(!$c->session->{volunteer}) { 82 | my $apikey = random_string(); 83 | $json->{apiKey} = $apikey; 84 | $c->session->{apikey} = $apikey; 85 | } 86 | return $c->render(openapi => $json); 87 | } 88 | 89 | sub logout { 90 | my $c = shift->openapi->valid_input or return; 91 | 92 | foreach my $field ("id", "email", "admin", "volunteer", "name", "room", "apikey") { 93 | delete $c->session->{$field}; 94 | } 95 | 96 | return $c->render(openapi => ""); 97 | } 98 | 99 | 1; 100 | -------------------------------------------------------------------------------- /lib/SReview/Web/Controller/Volunteer.pm: -------------------------------------------------------------------------------- 1 | package SReview::Web::Controller::Volunteer; 2 | 3 | use Mojo::Base 'Mojolicious::Controller'; 4 | 5 | sub list { 6 | my $c = shift; 7 | my @talks; 8 | $c->dbh->begin_work; 9 | my $already = $c->dbh->prepare("SELECT nonce, title, id, state FROM talks WHERE reviewer = ? AND state <= 'preview'"); 10 | my $new = $c->dbh->prepare("SELECT nonce, title, id, state FROM talks WHERE reviewer IS NULL AND state = 'preview'::talkstate LIMIT ? FOR UPDATE"); 11 | my $claim = $c->dbh->prepare("UPDATE talks SET reviewer = ? WHERE id = ?"); 12 | $already->execute($c->session->{id}); 13 | my $count = $already->rows; 14 | if($count < 5) { 15 | $new->execute(5 - $count); 16 | } 17 | for(my $i = 0; $i < $count; $i++) { 18 | my $row = [ $already->fetchrow_array ]; 19 | push @talks, $row; 20 | } 21 | for(my $i = 0; $i < $new->rows; $i++) { 22 | my $row = [ $new->fetchrow_array ]; 23 | $claim->execute($c->session->{id}, $row->[2]); 24 | push @talks, $row; 25 | } 26 | $c->stash(talks => \@talks); 27 | $c->stash(layout => 'admin'); 28 | $c->dbh->commit; 29 | } 30 | 31 | 1; 32 | -------------------------------------------------------------------------------- /openshift/README.md: -------------------------------------------------------------------------------- 1 | The `sreview.yaml` file in this directory contains an Openshift (v3) 2 | template that uses a set of BuildConfig etc objects to rebuild the 3 | SReview containers inside Openshift. 4 | 5 | It requires administrator access to create the Role and the RoleBinding 6 | so that the sreview-master deployment can start encoder jobs. 7 | Everything else should work fine without those. 8 | 9 | Tested on minishift, not in production (yet?) 10 | -------------------------------------------------------------------------------- /scripts/sreview-autoreview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Mojo::JSON qw/decode_json/; 7 | use SReview::Config::Common; 8 | use SReview::Talk; 9 | use SReview::Template::SVG; 10 | use SReview::Files::Factory; 11 | use DBI; 12 | 13 | my $config = SReview::Config::Common::setup(); 14 | 15 | my $talkid = shift; 16 | 17 | my $db = DBI->connect($config->get("dbistring")); 18 | $db->prepare("UPDATE talks SET state='preview', progress='running' WHERE id = ?")->execute($talkid); 19 | 20 | my $talk = SReview::Talk->new(talkid => $talkid); 21 | 22 | if($talk->get_flag("manual_review")) { 23 | exit 0; 24 | } 25 | 26 | my $detector = $config->get("autoreview_detect"); 27 | my $coll = SReview::Files::Factory->create("intermediate", $config->get("pubdir")); 28 | $coll->delete_files(relnames => [dirname($talk->relative_name)]); 29 | my $prefile = $coll->get_file(relname => $talk->relative_name . "/pre.mkv"); 30 | my $mainfile = $coll->get_file(relname => $talk->relative_name . "/main.mkv"); 31 | my $postfile = $coll->get_file(relname => $talk->relative_name . "/post.mkv"); 32 | 33 | open JSON, "-|:encoding(UTF-8)", $detector, $talkid, $prefile->filename, $mainfile->filename, $postfile->filename; 34 | my $json; 35 | { 36 | local $/ = undef; 37 | $json = ; 38 | } 39 | close JSON; 40 | $json = decode_json($json); 41 | 42 | if(exists($json->{done})) { 43 | $talk->state_done("preview"); 44 | } elsif(exists($json->{broken})) { 45 | $talk->comment($json->{broken}); 46 | $talk->done_correcting; 47 | $talk->set_state("broken"); 48 | } else { 49 | foreach my $key(keys %$json) { 50 | $talk->add_correction($key => $json->{$key}); 51 | } 52 | $talk->done_correcting; 53 | $talk->set_state("cutting"); 54 | } 55 | -------------------------------------------------------------------------------- /scripts/sreview-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Config; 7 | use SReview::Config::Common; 8 | use SReview::Db; 9 | use Getopt::Long; 10 | use Pod::Usage; 11 | 12 | my $cfile; 13 | my $action = undef; 14 | my $help = 0; 15 | my @extrasettings; 16 | my @dumpkeys; 17 | 18 | GetOptions( 19 | "config-file=s" => \$cfile, 20 | "action=s" => \$action, 21 | "help" => \$help, 22 | "set=s" => \@extrasettings, 23 | "get=s" => \@dumpkeys, 24 | ) or pod2usage("command line invalid"); 25 | 26 | =head1 NAME 27 | 28 | sreview-config - manage the SReview configuration 29 | 30 | =head1 SYNOPSIS 31 | 32 | sreview-config --help|--config-file=FILE|--action=ACTION|--set=KEY|VALUE 33 | 34 | =head1 DESCRIPTION 35 | 36 | sreview-config is used to manage the SReview configuration from the 37 | command line. It takes up to two options: the current configuration file 38 | to read defaults from, and the action to perform on that configuration 39 | file. 40 | 41 | It can be used on upgrade of SReview to a newer version, to initialize the 42 | configuration with working settings, or to initialize the database. 43 | 44 | =head1 OPTIONS 45 | 46 | =head2 B<--help> 47 | 48 | Produce help output. 49 | 50 | =head2 B<--config-file>=FILE 51 | 52 | Use C as the configuration file to read defaults from. If this 53 | parameter is not specified, then sreview-config will try the file 54 | C in the directory pointed to by the C 55 | environment variable, followed by C, and then 56 | fall back on the builtin defaults. 57 | 58 | =head2 B<--set>=KEY=VALUE 59 | 60 | After reading the selected config file (see C<--config-file>) and before 61 | performing the requested action, set the value of configuration setting 62 | C to C. 63 | 64 | This option can be repeated multiple times as needed. 65 | 66 | =head2 B<--action>=ACTION 67 | 68 | Perform ACTION, which can be one of: 69 | 70 | =head3 dump 71 | 72 | Write the current configuration to standard output. B: Do I 73 | redirect the output of this command to the active configuration file, 74 | since that will overwrite the active configuration file with empty data 75 | before it is read by C, which will not work. See 76 | C for that. 77 | 78 | =head3 initdb 79 | 80 | Read the configuration file, then initialize the database that is 81 | configured. 82 | 83 | Note that this action is not strictly necessary; sreview-web will 84 | implicitly initialize and upgrade the database to the latest version at 85 | startup. 86 | 87 | =head3 update 88 | 89 | Read the configuration file, then rewrite it with new settings that are 90 | not found in the current configuration file, as well as incorporating 91 | configuration settings that were set with C<--set>. 92 | 93 | This is useful on upgrade of SReview, so that new configuration 94 | options can be added to the configuration file without loss of old 95 | options. It can also be used as a way to configure C. 96 | 97 | =head2 B<--get>=KEY 98 | 99 | Get the value of KEY, in JSON format 100 | 101 | =cut 102 | 103 | if($help) { 104 | pod2usage(0); 105 | } 106 | 107 | my $config = SReview::Config::Common::setup($cfile); 108 | 109 | foreach my $setting(@extrasettings) { 110 | die "Missing = in setting $setting" unless $setting =~ /.*=.*/; 111 | 112 | my ($key, $value) = split(/=/, $setting, 2); 113 | $config->set($key => $value); 114 | } 115 | 116 | foreach my $key(@dumpkeys) { 117 | $config->dump_item($key); 118 | print "\n"; 119 | } 120 | 121 | exit 0 if scalar(@dumpkeys) > 0; 122 | 123 | if(!defined($action)) { 124 | pod2usage("Need an action"); 125 | } 126 | if($action eq "dump") { 127 | print $config->dump(); 128 | } elsif($action eq "initdb") { 129 | SReview::Db::init($config); 130 | } elsif($action eq "update") { 131 | $cfile = SReview::Config::Common::get_default_cfile() unless defined($cfile); 132 | open CFILE, ">$cfile"; 133 | print CFILE $config->dump(); 134 | close CFILE; 135 | } else { 136 | pod2usage("invalid action"); 137 | } 138 | -------------------------------------------------------------------------------- /scripts/sreview-copy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Config::Common; 7 | use SReview::Files::Factory; 8 | use Getopt::Long; 9 | use File::Copy; 10 | 11 | my $config = SReview::Config::Common::setup; 12 | 13 | sub get_collection($) { 14 | my $collname = shift; 15 | my $relname; 16 | if($collname eq "input") { 17 | return SReview::Files::Factory->create($collname, $config->get('inputglob')); 18 | } 19 | if($collname eq "pub") { 20 | return SReview::Files::Factory->create("intermediate", $config->get('pubdir')); 21 | } 22 | if($collname eq "output") { 23 | return SReview::Files::Factory->create("output", $config->get('outputdir')); 24 | } 25 | return SReview::Files::Factory->create($collname, $config->get('extra_collections')->{$collname}); 26 | } 27 | 28 | my $inputcoll; 29 | my $outputcoll; 30 | my $filename; 31 | my $targetname; 32 | my $move; 33 | 34 | GetOptions( 35 | "inputcollection|i=s" => \$inputcoll, 36 | "outputcollection|o=s" => \$outputcoll, 37 | "filename|f=s" => \$filename, 38 | "targetname|t=s" => \$targetname, 39 | "move|m" => \$move, 40 | ); 41 | 42 | if(!defined($targetname)) { 43 | $targetname = $filename; 44 | } 45 | 46 | die "input collection required" unless defined($inputcoll); 47 | die "output collection required" unless defined($outputcoll); 48 | die "filename required" unless defined($filename); 49 | 50 | $inputcoll = get_collection($inputcoll); 51 | $outputcoll = get_collection($outputcoll); 52 | my $inputfile = $inputcoll->get_file(relname => $filename); 53 | my $outputfile = $outputcoll->add_file(relname => $targetname); 54 | copy($inputfile->filename, $outputfile->filename); 55 | $outputfile->store_file; 56 | if($move) { 57 | $inputfile->delete; 58 | } 59 | -------------------------------------------------------------------------------- /scripts/sreview-inject: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use File::Basename; 7 | use File::Temp qw/tempdir/; 8 | use Media::Convert::Asset; 9 | use Media::Convert::Asset::ProfileFactory; 10 | use Media::Convert::Normalizer; 11 | use Media::Convert::Pipe; 12 | use SReview::Talk; 13 | use SReview::Config::Common; 14 | use SReview::Files::Factory; 15 | use Getopt::Long; 16 | use Pod::Usage; 17 | use Mojo::JSON qw/true/; 18 | 19 | my $inputfile = undef; 20 | my $talkid = undef; 21 | my $talknonce = undef; 22 | my $talkslug = undef; 23 | my $help = undef; 24 | my $audionormal = undef; 25 | 26 | GetOptions("t|talkid=i" => \$talkid, 27 | "n|nonce=s" => \$talknonce, 28 | "s|slug=s" => \$talkslug, 29 | "i|input=s" => \$inputfile, 30 | "a|audionormal" => \$audionormal, 31 | "h|help" => \$help) or pod2usage("command line invalid"); 32 | 33 | if($help) { 34 | pod2usage(0); 35 | } 36 | 37 | die "require an input file name\n" unless defined($inputfile); 38 | die "Require exactly one of a nonce, a talk ID, or a talk slug\n" unless scalar(grep({defined}($talkid, $talknonce, $talkslug))==1); 39 | 40 | my $config = SReview::Config::Common::setup(); 41 | my $talk; 42 | if(defined($talknonce)) { 43 | $talk = SReview::Talk->by_nonce($talknonce); 44 | } elsif(defined($talkid)) { 45 | $talk = SReview::Talk->new(talkid => $talkid); 46 | } elsif(defined($talkslug)) { 47 | $talk = SReview::Talk->by_slug($talkslug); 48 | } 49 | $talk->set_state("injecting", "running"); 50 | $talk = SReview::Talk->new(talkid => $talk->talkid); 51 | my $input = Media::Convert::Asset->new(url => $inputfile); 52 | my $output_coll = SReview::Files::Factory->create("intermediate", $config->get("pubdir")); 53 | my $outputfile = $output_coll->add_file(relname => $talk->relative_name . "/main.mkv"); 54 | 55 | $output_coll->delete_files(relnames => [dirname($talk->relative_name)]); 56 | 57 | my $checks = $config->get("inject_transcode_skip_checks"); 58 | my $do_transcode = 0; 59 | if(defined($checks)) { 60 | foreach my $prop(keys %$checks) { 61 | my $attr = $input->meta->find_attribute_by_name($prop); 62 | my $val = $attr->get_value($input); 63 | if(!defined($val)) { 64 | print("Skip check failed: value for $prop not defined for input video. Retranscoding.\n"); 65 | $do_transcode = 1; 66 | last; 67 | } 68 | if(exists($checks->{$prop}{min}) && exists($checks->{$prop}{max})) { 69 | if(($val > $checks->{$prop}{max}) || ($val < $checks->{$prop}{min})) { 70 | print("Skip check failed: value for $prop out of bounds on input video. Retranscoding.\n"); 71 | $do_transcode = 1; 72 | last; 73 | } else { 74 | next; 75 | } 76 | } 77 | if(exists($checks->{$prop}{val})) { 78 | if($val ne $checks->{$prop}{val}) { 79 | print("Skip check failed: value for $prop does not string-equal expected value. Retranscoding.\n"); 80 | $do_transcode = 1; 81 | last; 82 | } else { 83 | next; 84 | } 85 | } 86 | print("Skip check failed: configuration for $prop does not have both a minimum and maximum, or misses exact value. Retranscoding.\n"); 87 | last; 88 | } 89 | } 90 | 91 | if(!$do_transcode) { 92 | print("Skip check successful; not transcoding, just copying data around.\n"); 93 | Media::Convert::Pipe->new(inputs => [$input], output => Media::Convert::Asset->new(url => $outputfile->filename), vcopy => 1, acopy => 1)->run(); 94 | } else { 95 | if($audionormal) { 96 | my $dirname = tempdir("injectXXXXXX", DIR => $config->get("workdir"), CLEANUP => 1); 97 | my $normalized = Media::Convert::Asset->new(url => join("/", $dirname, basename($inputfile))); 98 | Media::Convert::Normalizer->new(input => $input, output => $normalized)->run(); 99 | $input = Media::Convert::Asset->new(url => $normalized->url); 100 | } 101 | my $profile = Media::Convert::Asset::ProfileFactory->create($config->get("input_profile"), $input, $config->get('extra_profiles')); 102 | Media::Convert::Pipe->new(inputs => [$input], output => Media::Convert::Asset->new(url => $outputfile->filename, reference => $profile), vcopy => 0, acopy => 0)->run(); 103 | } 104 | $outputfile->store_file; 105 | $talk->set_flag('is_injected' => true); 106 | $talk->add_correction(serial => -1); 107 | $talk->done_correcting; 108 | $talk->state_done("injecting"); 109 | -------------------------------------------------------------------------------- /scripts/sreview-inject-job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Talk; 7 | use SReview::Config::Common; 8 | use SReview::Files::Factory; 9 | use DBI; 10 | use File::Basename; 11 | 12 | my $talkid = shift; 13 | my $config = SReview::Config::Common::setup(); 14 | my $talk = SReview::Talk->new(talkid => $talkid); 15 | my $collname = $config->get("inject_collection"); 16 | my $input; 17 | if($collname eq "input") { 18 | $input = SReview::Files::Factory->create("input", $config->get("inputglob"), $config); 19 | } elsif($collname eq "pub") { 20 | $input = SReview::Files::Factory->create("intermediate", $config->get("pubdir"), $config); 21 | } else { 22 | $input = SReview::Files::Factory->create($collname, $config->get("extra_collections")->{$collname}, $config); 23 | } 24 | die "talk not in correct stream" unless $talk->active_stream eq 'injected'; 25 | my $db = DBI->connect($config->get("dbistring"), '', '') or die "Cannot connect to database!"; 26 | my $st = $db->prepare("SELECT filename FROM raw_files WHERE room = ? AND stream = ? AND filename LIKE ?"); 27 | $st->execute($talk->roomid, $talk->active_stream, '%' . dirname($talk->relative_name) . '%'); 28 | my $row = $st->fetchrow_arrayref; 29 | my $inputfile = $input->get_file(relname => $row->[0]); 30 | my @command = ("sreview-inject", "-t", $talkid, "-i", $inputfile->filename); 31 | if(!$talk->get_flag("keep_audio")) { 32 | push @command, "-a"; 33 | } 34 | system(@command); 35 | -------------------------------------------------------------------------------- /scripts/sreview-keys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Model::Event; 7 | use Net::SSH::AuthorizedKeysFile; 8 | use Net::SSH::AuthorizedKey; 9 | use Getopt::Long; 10 | use Pod::Usage; 11 | 12 | use SReview::Config::Common; 13 | 14 | die 'Need $HOME to be set!' unless exists($ENV{HOME}); 15 | 16 | my $config = SReview::Config::Common::setup; 17 | 18 | my $eventname = $config->get('event'); 19 | my $action = "add"; 20 | my $oknodo = 0; 21 | my $help = 0; 22 | my $keyfile = undef; 23 | my $bindir = $ENV{HOME} . "/bin"; 24 | 25 | GetOptions( 26 | "event|e=s" => \$eventname, 27 | "action|a=s" => \$action, 28 | "help" => \$help, 29 | "oknodo|o" => \$oknodo, 30 | "file|f=s" => \$keyfile, 31 | ) or pod2usage("command line invalid"); 32 | 33 | =head1 NAME 34 | 35 | sreview-keys - manage keys in SReview's C file. 36 | 37 | =head1 SYNOPSIS 38 | 39 | sreview-keys --event="event name" --action="add" --file=./id_rsa.pub 40 | sreview-keys -a "remove" --file=./id_rsa.pub 41 | 42 | =head1 DESCRIPTION 43 | 44 | sreview-keys is a simple tool to manage keys in an C 45 | file so that when you run C to sync input data to the SReview 46 | master server, the files are written to that event's own input directory 47 | (and not elsewhere). 48 | 49 | The default event name is taken from the SReview configuration file (see 50 | L, or it can be overridden with C<--event> (alias: 51 | C<-e>). 52 | 53 | The default action is to add a key; to remove one, either edit the file, 54 | or use the C<--action=remove> (alias: C<-a remove>) option. 55 | 56 | =cut 57 | 58 | if($help) { 59 | pod2usage(0); 60 | } 61 | 62 | if(!defined($keyfile)) { 63 | pod2usage("key file not specified"); 64 | } 65 | 66 | if($action ne "add" && $action ne "remove") { 67 | print STDERR "Unknown action: $action\n"; 68 | exit 1; 69 | } 70 | 71 | my $event = SReview::Model::Event->new(config => $config, name => $eventname); 72 | 73 | my $akf = Net::SSH::AuthorizedKeysFile->new(); 74 | my $file = $config->get('authkeyfile'); 75 | $akf->read($file); 76 | 77 | open KEY, "<", $keyfile; 78 | my $mkey = ""; 79 | while() { 80 | chomp; 81 | $mkey .= $_; 82 | } 83 | close KEY; 84 | 85 | $mkey = Net::SSH::AuthorizedKey->parse($mkey); 86 | 87 | my @newkeys = (); 88 | 89 | foreach my $key($akf->keys()) { 90 | if($key->fingerprint() eq $mkey->fingerprint()) { 91 | if($action eq "add") { 92 | if(!$oknodo) { 93 | print STDERR "The provided key already exists in the file! Please remove it first\n"; 94 | exit 1; 95 | } else { 96 | print "Key already added, ignoring\n"; 97 | exit 0; 98 | } 99 | } else { 100 | # don't add to @mkeys 101 | next; 102 | } 103 | } 104 | push @newkeys, $key; 105 | } 106 | 107 | if($action eq "add") { 108 | if(! -x "$bindir/rrsync") { 109 | print STDERR "E: please install rrsync as $bindir/rrsync, and make sure it's executable (hint: /usr/share/doc/rsync/scripts/rrsync.gz)"; 110 | exit 1; 111 | } 112 | my $iglob = $config->get('inputglob'); 113 | my @input = split('/', $iglob); 114 | my @dirs = (); 115 | foreach my $in(@input) { 116 | if($in =~ /\*/) { 117 | last; 118 | } 119 | push @dirs, $in; 120 | } 121 | $mkey->option("command", "$bindir/rrsync '" . join('/', @dirs, $event->inputdir) . "'", 1); 122 | $mkey->option("no-agent-forwarding", 1, 1); 123 | $mkey->option("no-port-forwarding", 1, 1); 124 | $mkey->option("no-pty", 1, 1); 125 | $mkey->option("no-user-rc", 1, 1); 126 | $mkey->option("no-X11-forwarding", 1, 1); 127 | push @newkeys, $mkey; 128 | } 129 | 130 | if(!defined($file)) { 131 | $file = $akf->path_locate; 132 | } 133 | $akf = Net::SSH::AuthorizedKeysFile->new(keys => \@newkeys, file => $file); 134 | $akf->save(); 135 | -------------------------------------------------------------------------------- /scripts/sreview-previews: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use DBI; 7 | use File::Path qw/make_path/; 8 | use SReview::Talk; 9 | use Media::Convert::Asset; 10 | use Media::Convert::Pipe; 11 | use Media::Convert::Asset::ProfileFactory; 12 | use SReview::Config::Common; 13 | use SReview::Files::Factory; 14 | 15 | =head1 NAME 16 | 17 | sreview-previews - create previews from the C output 18 | 19 | =head1 SYNOPSIS 20 | 21 | sreview-previews TALKID 22 | 23 | =head1 DESCRIPTION 24 | 25 | C performs the following actions: 26 | 27 | =over 28 | 29 | =item * 30 | 31 | Look up the talk with id TALKID in the database. 32 | 33 | =item * 34 | 35 | Verify if the codecs in the pre, main, and post videos as produced by 36 | L are HTML5-compatible. If they are, copy them to a MP4 37 | or WebM container from the Matroska one. 38 | 39 | =item * 40 | 41 | If they are not, convert them to the C profile 42 | 43 | =item * 44 | 45 | Update the database to set the current talk's C field to 46 | C. 47 | 48 | =back 49 | 50 | =head1 CONFIGURATION 51 | 52 | C considers the following configuration values: 53 | 54 | =over 55 | 56 | =cut 57 | 58 | my $config = SReview::Config::Common::setup; 59 | my $collection = SReview::Files::Factory->create("intermediate", $config->get("pubdir")); 60 | 61 | sub convert($) { 62 | my $relname = shift; 63 | return unless ($collection->has_file($relname . ".mkv")); 64 | my $input_file = $collection->get_file(relname => $relname . ".mkv"); 65 | my $input = Media::Convert::Asset->new(url => $input_file->filename); 66 | my $vc = $input->video_codec; 67 | my $ac = $input->audio_codec; 68 | 69 | if (!$config->get("force_preview_transcode")) { 70 | if (($vc eq "vp8" && $ac eq "vorbis") || ($vc eq "vp9" && $ac eq "vorbis") || ($vc eq "vp9" && $ac eq "opus")) { 71 | my $output_file = $collection->add_file(relname => $relname . ".webm"); 72 | my $output = Media::Convert::Asset->new(url => $output_file->filename); 73 | Media::Convert::Pipe->new(inputs => [$input], output => $output, vcopy => 1, acopy => 1)->run(); 74 | $output_file->store_file; 75 | return; 76 | } 77 | if ($vc eq "h264" && $ac eq "aac") { 78 | my $output_file = $collection->add_file(relname => $relname . ".mp4"); 79 | my $output = Media::Convert::Asset->new(url => $output_file->filename); 80 | Media::Convert::Pipe->new(inputs => [$input], output => $output, vcopy => 1, acopy => 1)->run(); 81 | $output_file->store_file; 82 | return; 83 | } 84 | } 85 | my $profile = Media::Convert::Asset::ProfileFactory->create('vp8_lq', $input, $config->get('extra_profiles')); 86 | my $output_file = $collection->add_file(relname => $relname . ".webm"); 87 | my $output = Media::Convert::Asset->new(url => $output_file->filename, reference => $profile); 88 | Media::Convert::Pipe->new(inputs => [$input], output => $output)->run(); 89 | $output_file->store_file; 90 | } 91 | 92 | =item dbistring 93 | 94 | The DBI string used to connect to the database. 95 | 96 | =cut 97 | 98 | my $dbh = DBI->connect($config->get('dbistring'), '', '') or die "Cannot connect to database!"; 99 | my $talkid = $ARGV[0]; 100 | 101 | $dbh->prepare("UPDATE talks SET progress='running', state='generating_previews' WHERE id=?")->execute($talkid); 102 | 103 | my $talk = SReview::Talk->new(talkid => $talkid); 104 | 105 | =item pubdir 106 | 107 | The directory in which to find the output of C, and in 108 | which to write the previews 109 | 110 | =cut 111 | 112 | my $relname = $talk->relative_name; 113 | 114 | convert($relname . "/pre"); 115 | convert($relname . "/main"); 116 | convert($relname . "/post"); 117 | 118 | $dbh->prepare("UPDATE talks SET progress='done' WHERE id=? AND state='generating_previews'")->execute($talkid); 119 | 120 | =back 121 | 122 | =head1 SEE ALSO 123 | 124 | C, C, C, C 125 | 126 | =cut 127 | -------------------------------------------------------------------------------- /scripts/sreview-reply: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use v5.28; 7 | 8 | use SReview::Config::Common; 9 | use Email::Stuffer; 10 | use Email::Address; 11 | use Email::Sender::Simple qw/sendmail/; 12 | use DBI; 13 | use Text::Format; 14 | 15 | my $config = SReview::Config::Common::setup; 16 | 17 | my $dbh = DBI->connect($config->get('dbistring'), '', '') or die "Cannot connect to database!"; 18 | my $event = $config->get('event'); 19 | 20 | my $nonce = shift; 21 | 22 | die "Need nonce!" unless defined $nonce; 23 | 24 | my $mailers = $dbh->prepare("SELECT * FROM mailers WHERE nonce=?"); 25 | $mailers->execute($nonce) or die "Cannot fetch emails!"; 26 | my $comment = $dbh->prepare("SELECT comment FROM commentlog JOIN talks ON commentlog.talk=talks.id WHERE nonce=? ORDER BY logdate DESC LIMIT 1"); 27 | $comment->execute($nonce) or die "Cannot fetch comments!"; 28 | 29 | my $formatter = Text::Format->new(firstIndent => 0); 30 | open my $start, ">", "/tmp/reply-$nonce" or die "Cannot open reply file!"; 31 | print $start "# $nonce\n#\n"; 32 | my $instructions = $formatter->paragraphs("Please reply to this email with your comments. Any use of a '#' character introduces a comment; comments will be removed before sending. If you wish to abort, make sure the word 'ABORT' appears in the (not-commented part of the) reply.\n\nAny (not-commented) lines that appear after the '#----' line will be entered as a comment in the database"); 33 | $instructions .= "\n"; 34 | $instructions =~ s/^/# /gm; 35 | print $start "$instructions"; 36 | my $para = $formatter->paragraphs("Hi! Someone receiving this email entered this comment into the $event video review system:"); 37 | print $start "$para\n"; 38 | while(my $row = $comment->fetchrow_hashref()) { 39 | $para = $formatter->paragraphs($row->{comment}); 40 | $para =~ s/^/ /gm; 41 | print $start "$para\n"; 42 | } 43 | print $start "#----\n"; 44 | close $start; 45 | 46 | system("sensible-editor", "/tmp/reply-$nonce"); 47 | 48 | my $reply = ""; 49 | my $dbcomment = ""; 50 | my $is_db = 0; 51 | my $found_db = 0; 52 | open my $reply_file, "<", "/tmp/reply-$nonce" or die "Cannot open reply file!"; 53 | LINE: 54 | foreach my $line (<$reply_file>) { 55 | if($line =~ /^#----$/) { 56 | $is_db = 1; 57 | } 58 | next if $line =~ /^#/; 59 | $line =~ s/#.*//; 60 | $reply .= $line; 61 | if($is_db) { 62 | $dbcomment .= $line; 63 | if($line =~ /^.+$/) { 64 | $found_db = 1; 65 | } 66 | } 67 | } 68 | close $reply_file; 69 | unlink "/tmp/reply-$nonce"; 70 | 71 | if($reply =~ /ABORT/) { 72 | exit 0; 73 | } 74 | 75 | if($found_db) { 76 | chomp $dbcomment; 77 | my $add_comment = $dbh->prepare("WITH talkdata(comment, id, state) AS (SELECT ?, id, state FROM talks WHERE nonce = ?) INSERT INTO commentlog(comment, talk, state) SELECT * FROM talkdata"); 78 | $add_comment->execute($dbcomment, $nonce) or die $!; 79 | $reply .= "\n\n(this comment was also entered into the system)"; 80 | } else { 81 | $reply .= "\n\n(this comment was NOT entered into the system)"; 82 | } 83 | 84 | my @recips; 85 | my $title; 86 | foreach my $mailer (@{$mailers->fetchall_arrayref({})}) { 87 | push @recips, Email::Address->new(undef, $mailer->{email}); 88 | $title = $mailer->{title}; 89 | } 90 | 91 | my $subject = "Re: comment regarding talk '$title'"; 92 | 93 | my $email = Email::Stuffer->from($config->get('email_from')) 94 | ->to(@recips) 95 | ->subject($subject) 96 | ->text_body($reply); 97 | 98 | sendmail($email->as_string); 99 | -------------------------------------------------------------------------------- /scripts/sreview-skip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use DBI; 7 | use SReview::Config::Common; 8 | 9 | =head1 NAME 10 | 11 | sreview-skip - skip a state in the SReview video review system 12 | 13 | =head1 SYNOPSIS 14 | 15 | sreview-skip B 16 | 17 | =head1 DESCRIPTION 18 | 19 | sreview-skip is used for skipping a state in the SReview video review 20 | system. It simply marks the given talk as "done" in the current state, 21 | and quits, relying on sreview-dispatch to move it to the next used 22 | state. 23 | 24 | While this may seem quite useless, it can be useful if a number of talks 25 | need to be retranscoded, but their cuts have already been removed and 26 | you don't want to have to manually okay every one of them. 27 | 28 | It can also be useful when no notification is to be used. 29 | 30 | =cut 31 | 32 | my $config = SReview::Config::Common::setup; 33 | 34 | my $dbh = DBI->connect($config->get('dbistring'), '', '') or die "Cannot connect to database!"; 35 | 36 | my $talkid = $ARGV[0]; 37 | 38 | $dbh->prepare("UPDATE talks SET progress='done' WHERE id=?")->execute($talkid); 39 | -------------------------------------------------------------------------------- /scripts/sreview-titles: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Template::SVG qw/process_template/; 7 | use SReview::Talk; 8 | use SReview::Config::Common; 9 | use SReview::Files::Factory; 10 | use DBI; 11 | 12 | my $config = SReview::Config::Common::setup(); 13 | 14 | my $dbh = DBI->connect($config->get("dbistring")); 15 | 16 | my $talklist = $dbh->prepare("SELECT talks.id FROM talks JOIN events ON talks.event = events.id WHERE events.name = ?"); 17 | my $rv = $talklist->execute($config->get("event")); 18 | 19 | my %templates = ("pre" => $config->get("preroll_template"), 20 | "post" => $config->get("postroll_template"), 21 | "sorry" => $config->get("apology_template")); 22 | 23 | my $coll = SReview::Files::Factory->create("intermediate", $config->get("pubdir")); 24 | 25 | while(my $row = $talklist->fetchrow_hashref) { 26 | my $talk = SReview::Talk->new(talkid => $row->{id}); 27 | foreach my $type("pre", "post", "sorry") { 28 | next unless(-f $templates{$type}); 29 | my $relname = $talk->relative_name . "/" . $type . ".png"; 30 | my $file = $coll->add_file(relname => $relname); 31 | process_template($templates{$type}, $file->filename, $talk, $config); 32 | $file->store_file 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/sreview-transcribe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use DBI; 7 | use File::Basename qw/dirname basename/; 8 | use File::Copy; 9 | use Mojo::Template; 10 | 11 | use SReview::Files::Factory; 12 | use SReview::Talk; 13 | 14 | =head1 NAME 15 | 16 | sreview-transcribe - create transcriptions from the C output 17 | 18 | =head1 SYNOPSIS 19 | 20 | sreview-transcribe TALKID 21 | 22 | =head1 DESCRIPTION 23 | 24 | C performs the following actions: 25 | 26 | =over 27 | 28 | =item * 29 | 30 | Look up the talk with id TALKID in the database. 31 | 32 | =item * 33 | 34 | Run openai-whisper on the video file, outputting a .vtt (WebVTT) file. 35 | 36 | =back 37 | 38 | =over 39 | 40 | =cut 41 | 42 | my $config = SReview::Config::Common::setup; 43 | 44 | my $transcribe_cmd_tpl = $config->get("transcribe_command"); 45 | # example: '/venv/bin/whisper --language en --output_format vtt --model tiny --output_dir <%== $output_dir %> <%== $input_filename %>'; 46 | 47 | die "no transcribe_command in config" unless $transcribe_cmd_tpl; 48 | 49 | sub run_transcribe { 50 | my $input_filename = shift; 51 | my $output_dir = shift; 52 | 53 | my $mt = Mojo::Template->new; 54 | 55 | my $transcribe_cmd = $mt->vars(1)->render($transcribe_cmd_tpl, {input_filename => $input_filename, output_dir => $output_dir}); 56 | 57 | print "Running $transcribe_cmd\n"; 58 | system $transcribe_cmd; 59 | } 60 | 61 | =item dbistring 62 | 63 | The DBI string used to connect to the database. 64 | 65 | =cut 66 | 67 | my $dbh = DBI->connect($config->get('dbistring'), '', '') or die "Cannot connect to database!"; 68 | my $talkid = $ARGV[0]; 69 | 70 | die "no talk id" unless $talkid; 71 | 72 | $dbh->prepare("UPDATE talks SET progress='running', state='transcribing' WHERE id=?")->execute($talkid); 73 | 74 | my $talk = SReview::Talk->new(talkid => $talkid); 75 | 76 | my $data = $dbh->prepare("SELECT eventid, event, event_output, room, room_output, starttime, starttime::date AS date, to_char(starttime, 'yyyy') AS year, speakers, name AS title, subtitle, description, apologynote FROM talk_list WHERE id = ?"); 77 | $data->execute($talkid); 78 | my $drow = $data->fetchrow_hashref(); 79 | 80 | my $slug = $talk->slug; 81 | 82 | my $output_coll = SReview::Files::Factory->create("output", $config->get("outputdir")); 83 | 84 | my @elems = (); 85 | foreach my $subdir(@{$config->get('output_subdirs')}) { 86 | push @elems, $drow->{$subdir}; 87 | } 88 | my $relprefix = join('/', @elems); 89 | 90 | my $input_file = $output_coll->get_file(relname => join('/', $relprefix, $slug . "." . $config->get("transcribe_source_extension"))); 91 | my $output_file = $output_coll->add_file(relname => join('/', $relprefix, $slug . '.vtt')); 92 | 93 | my $workdir = dirname($input_file->filename); 94 | my $expected_file = basename($input_file->filename, '.webm') . '.vtt'; 95 | 96 | run_transcribe($input_file->filename, $workdir); 97 | 98 | my $vtt_file = "$workdir/$expected_file"; 99 | if (-f $vtt_file) { 100 | move($vtt_file, $output_file->filename); 101 | } else { 102 | die "could not find $vtt_file"; 103 | } 104 | 105 | $output_file->store_file; 106 | 107 | $dbh->prepare("UPDATE talks SET progress='done' WHERE id=? AND state='transcribing'")->execute($talkid); 108 | 109 | =back 110 | 111 | =head1 SEE ALSO 112 | 113 | C, C, C, C 114 | 115 | =cut 116 | -------------------------------------------------------------------------------- /scripts/sreview-upload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use DBI; 7 | use Mojo::Template; 8 | use SReview::Config::Common; 9 | use Media::Convert::Asset::ProfileFactory; 10 | use SReview::Files::Factory; 11 | use SReview::Talk; 12 | 13 | my $config = SReview::Config::Common::setup; 14 | 15 | my $dbh = DBI->connect($config->get('dbistring'), '', '') or die "Cannot connect to database!"; 16 | 17 | my $talkid = shift; 18 | 19 | die "need talk ID!" unless defined($talkid); 20 | 21 | my $configprefix = shift; 22 | $configprefix = 'upload' unless defined($configprefix); 23 | 24 | $dbh->prepare("UPDATE talks SET progress='running' WHERE id=?")->execute($talkid); 25 | 26 | my $mt = Mojo::Template->new; 27 | $mt->vars(1); 28 | 29 | sub run_command($$$) { 30 | my $file = shift; 31 | my $relative = shift; 32 | my $base = shift; 33 | 34 | foreach my $command(@{$config->get("${configprefix}_actions")}) { 35 | my @run; 36 | foreach my $component(@$command) { 37 | my $rendered = $mt->render($component, {file => $file, relative_file => $relative, base => $base }); 38 | chomp($rendered); 39 | push @run, $rendered; 40 | } 41 | system(@run); 42 | } 43 | } 44 | 45 | my $actions = $config->get("${configprefix}_actions"); 46 | 47 | exit 0 if(scalar(@$actions) < 1); 48 | 49 | my $raw_file = $dbh->prepare("SELECT filename FROM raw_files JOIN talks ON raw_files.room = talks.room JOIN events ON talks.event = events.id WHERE events.name = ? AND stream='' LIMIT 1"); 50 | $raw_file->execute($config->get("event")); 51 | $raw_file = $raw_file->fetchrow_hashref(); 52 | my $r_file = SReview::Files::Factory->create("input", $config->get('inputglob'))->get_file(relname => $raw_file->{filename}); 53 | $raw_file = Media::Convert::Asset->new(url => $r_file->filename); 54 | my $talk_st = $dbh->prepare("SELECT event, event_output, room, room_output, starttime::date AS date, to_char(starttime, 'yyyy') AS year, name AS title, subtitle, slug FROM talk_list WHERE id = ?"); 55 | $talk_st->execute($talkid); 56 | my $talkdata = $talk_st->fetchrow_hashref; 57 | 58 | my $output_coll = SReview::Files::Factory->create("output", $config->get('outputdir')); 59 | my $inter_coll = SReview::Files::Factory->create("intermediate", $config->get('pubdir')); 60 | 61 | my $subdirs = $config->get('output_subdirs'); 62 | my @elems = (); 63 | 64 | foreach my $subdir(@$subdirs) { 65 | push @elems, $talkdata->{$subdir}; 66 | } 67 | 68 | my $reldir = join('/', @elems); 69 | 70 | my $talk = SReview::Talk->new(talkid => $talkid); 71 | 72 | my @files = (); 73 | 74 | if($configprefix eq "upload" && ($config->get('cleanup') eq "all" || $config->get('cleanup') eq 'previews')) { 75 | push @files, ( 76 | $inter_coll->get_file(relname => $talk->relative_name . "/main.mkv"), 77 | $inter_coll->get_file(relname => $talk->relative_name . "/main.webm"), 78 | $inter_coll->get_file(relname => $talk->relative_name . "/main.mp4"), 79 | $inter_coll->get_file(relname => $talk->relative_name . "/post.mkv"), 80 | $inter_coll->get_file(relname => $talk->relative_name . "/post.webm"), 81 | $inter_coll->get_file(relname => $talk->relative_name . "/post.mp4"), 82 | $inter_coll->get_file(relname => $talk->relative_name . "/post.png"), 83 | $inter_coll->get_file(relname => $talk->relative_name . "/pre.mkv"), 84 | $inter_coll->get_file(relname => $talk->relative_name . "/pre.webm"), 85 | $inter_coll->get_file(relname => $talk->relative_name . "/pre.mp4"), 86 | $inter_coll->get_file(relname => $talk->relative_name . "/pre.png"), 87 | $inter_coll->get_file(relname => $talk->relative_name . "/sorry.png"), 88 | ); 89 | for my $count(0, 1, 2) { 90 | for my $ext(qw/wav mp3 ogg/) { 91 | push @files, $inter_coll->get_file(relname => $talk->relative_name . "/audio$count.$ext"); 92 | } 93 | } 94 | } 95 | 96 | my $exts = []; 97 | 98 | if($configprefix ne "sync") { 99 | foreach my $profile(@{$config->get('output_profiles')}) { 100 | push @$exts, Media::Convert::Asset::ProfileFactory->create($profile, $raw_file, $config->get('extra_profiles'))->exten(); 101 | } 102 | } else { 103 | $exts = $config->get('sync_extensions'); 104 | } 105 | 106 | foreach my $ext(@$exts) { 107 | my $basename = join('.', $talkdata->{slug}, $ext); 108 | my $file = $output_coll->get_file(relname => join('/', $reldir, $basename)); 109 | run_command($file->valid_path_filename, $file->relname, $file->basepath); 110 | if($config->get('cleanup') eq "all" || $config->get('cleanup') eq 'output') { 111 | push @files, ( 112 | $file, 113 | $output_coll->get_file(relname => $file->relname . "/multipass-0.log"), 114 | ); 115 | } 116 | } 117 | 118 | if (scalar(@files) > 0) { 119 | foreach my $file(@files) { 120 | $file->delete; 121 | } 122 | } 123 | 124 | $dbh->prepare("UPDATE talks SET progress='done' WHERE id = ?")->execute($talkid); 125 | -------------------------------------------------------------------------------- /scripts/sreview-user: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use SReview::Config::Common; 4 | use DBI; 5 | use Getopt::Long; 6 | 7 | my $config = SReview::Config::Common::setup; 8 | 9 | my $dbh = DBI->connect($config->get('dbistring')); 10 | 11 | my $action = ""; 12 | my $admin = 0; 13 | my $user = ""; 14 | my $volunteer = 0; 15 | 16 | GetOptions( 17 | "user|u=s" => \$user, 18 | "admin|d" => \$admin, 19 | "action|a=s" => \$action, 20 | ); 21 | 22 | =head1 NAME 23 | 24 | sreview-user - SReview user management 25 | 26 | =head1 SYNOPSIS 27 | 28 | sreview-user [--user|-u username] [--admin|-d] [--action|-a ACTION] 29 | 30 | =head1 DESCRIPTION 31 | 32 | sreview-user is a simple script to manage SReview users. It allows you 33 | to create, destroy, and set passwords for users. Optionally, it also 34 | allows to mark newly-created users as administrators. 35 | 36 | More detailed user management should be done through the SReview 37 | webinterface, however. 38 | 39 | =cut 40 | 41 | if($action eq "create") { 42 | open PASSWORD, "pwgen -s 10 -n 1|"; 43 | my $password=; 44 | close(PASSWORD); 45 | chomp $password; 46 | $dbh->prepare("INSERT INTO users(email,password,isadmin) VALUES(?,crypt(?, gen_salt('bf', 8)),?)")->execute($user,$password,$admin ? "true" : "false") or die $!; 47 | print "New password is $password\n"; 48 | } elsif ($action eq "delete") { 49 | $dbh->prepare("DELETE FROM users WHERE email = ?")->execute($user) or die $!; 50 | } elsif ($action eq "pwreset") { 51 | open PASSWORD, "pwgen -s 10 -n 1|"; 52 | my $password=; 53 | close(PASSWORD); 54 | chomp $password; 55 | $dbh->prepare("UPDATE users SET password=crypt(?,gen_salt('bf',8)) WHERE email=?")->execute($password, $user) or die $!; 56 | print "New password is $password\n"; 57 | } else { 58 | die "unknown action"; 59 | } 60 | -------------------------------------------------------------------------------- /scripts/util/export_states_prometheus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use DBI; 7 | use File::Temp qw/tempfile/; 8 | 9 | our $config; 10 | require "./config.pl"; 11 | 12 | my $dbh = DBI->connect($config->{dbistring}, '', '') or die "Cannot connect to database!"; 13 | 14 | my $tst = $dbh->prepare("SELECT count(*) AS count, state FROM talks GROUP BY state"); 15 | my $rst = $dbh->prepare("SELECT rooms.altname AS name, talks.id FROM rooms LEFT JOIN talks ON (rooms.id = talks.room AND talks.starttime < now() AND talks.endtime > now()) WHERE altname IS NOT NULL"); 16 | 17 | $tst->execute(); 18 | $rst->execute(); 19 | 20 | open PROM, ">/srv/node_exporter/textfiles/sreview.prom"; 21 | while(my $row = $tst->fetchrow_hashref()) { 22 | print PROM "sreview_talkstate{state=\"" . $row->{state} . "\"} " . $row->{count} . "\n"; 23 | } 24 | while(my $row = $rst->fetchrow_hashref()) { 25 | print PROM "sreview_roomstate{stream=\"vocto-" . $row->{name} . "\"} "; 26 | if(defined($row->{id})) { 27 | print PROM "1\n"; 28 | } else { 29 | print PROM "0\n"; 30 | } 31 | } 32 | close PROM; 33 | -------------------------------------------------------------------------------- /scripts/util/poke_track_managers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # SReview, a web-based video review and transcoding system 4 | # Copyright (c) 2016-2017, Wouter Verhelst 5 | # 6 | # SReview is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public 17 | # License along with this program. If not, see 18 | # . 19 | 20 | use strict; 21 | use warnings; 22 | 23 | use Email::Sender::Simple qw(sendmail); 24 | use Email::Simple; 25 | use Email::MIME::Encodings; 26 | 27 | use DBI; 28 | 29 | our $config; 30 | 31 | require './config.pl'; 32 | 33 | my $dbh = DBI->connect($config->{dbistring}, '', '') or die "Cannot connect to database!"; 34 | 35 | my $recps = $dbh->prepare("SELECT title,speakeremail(talks.id) as speakers,nonce,name,email FROM talks JOIN tracks ON talks.track = tracks.id WHERE state='preview' ORDER BY email"); 36 | 37 | $recps->execute(); 38 | my $trackmgr_email; 39 | my $trackmgr_name; 40 | my @talks; 41 | 42 | sub send_email { 43 | my $talkdescs = join("\n",@talks); 44 | my $body = < $body); 69 | my $email = Email::Simple->create(header => 70 | [ 71 | From => '', 72 | To => $trackmgr_email, 73 | Subject => "Video review: please help keep the momentum going!", 74 | "Reply-To" => '', 75 | "Content-Transfer-Encoding" => "Quoted-Printable", 76 | ], 77 | body => $body 78 | ); 79 | 80 | sendmail($email); 81 | } 82 | 83 | while(my $row=$recps->fetchrow_hashref) { 84 | if(defined($trackmgr_email)) { 85 | if($row->{email} ne $trackmgr_email) { 86 | send_email; 87 | @talks = (); 88 | } 89 | } 90 | $trackmgr_name = $row->{name}; 91 | $trackmgr_email = $row->{email}; 92 | my $speakers = defined($row->{speakers}) ? $row->{speakers} : "(not known)"; 93 | $speakers =~ s/[^[:ascii:]]//g; 94 | my $title = $row->{title}; 95 | $title =~ s/[^[:ascii:]]//g; 96 | push @talks, $title . ":\n\tspeakers:" . $speakers 97 | . "\n\thttps://review.video.fosdem.org/review/" . $row->{nonce} . "\n"; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /scripts/util/sync-states: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use SReview::Config::Common; 7 | use DBI; 8 | 9 | my $config = SReview::Config::Common::setup(); 10 | 11 | my $records = shift; 12 | my $qa = shift; 13 | my $final = shift; 14 | 15 | my $db = DBI->connect($config->get("dbistring")); 16 | 17 | while(1) { 18 | # Ignore final talks where recording exists 19 | $db->prepare("UPDATE talks final SET state='ignored', flags = flags::jsonb || '{\"is_injected\":true}'::jsonb FROM talks rec WHERE rec.event = ? AND final.event = ? AND rec.upstreamid = final.upstreamid AND coalesce((rec.flags->>'is_injected')::boolean, false) AND final.state <= 'preview'")->execute($records, $final); 20 | # Unignore final talks where no recording exists 21 | $db->prepare("UPDATE talks final SET state='waiting_for_files',active_stream='',flags=flags::jsonb - 'is_injected' FROM talks rec WHERE rec.event = ? AND final.event = ? AND rec.upstreamid = final.upstreamid AND coalesce((rec.flags->>'is_injected')::boolean, false) AND final.state = 'ignored'")->execute 22 | # Ignore qa talks where no recording exists 23 | $db->prepare("UPDATE talks qa SET state='ignored',flags=flags::jsonb - 'is_injected' FROM talks rec WHERE rec.event = ? AND qa.event = ? AND rec.upstreamid = qa.upstreamid AND coalesce((rec.flags->>'is_injected')::boolean, false) = false")->execute($records, $qa); 24 | # Unignore qa talks where recording exists 25 | $db->prepare("UPDATE talks qa SET state='waiting_for_files',flags=flags::jsonb||'{\"is_injected\":false}'::jsonb FROM talks rec WHERE rec.event = ? AND qa.event = ? AND rec.upstreamid = qa.upstreamid AND coalesce((rec.flags->>'is_injected')::boolean, false) AND qa.state = 'ignored'")->execute($records, $qa); 26 | # Create raw files where necessary 27 | my $added = $db->prepare("WITH talk AS (SELECT event, starttime, slug, room FROM talks WHERE event=? AND coalesce((flags->>'is_injected')::boolean, false) = true) INSERT INTO raw_files(filename, room, starttime, stream) SELECT '/srv/sreview/web/public/video/' || event || '/' || date_trunc('day', starttime) || '/' || slug || '.mkv', room, starttime, 'injected' FROM talk ON CONFLICT ON CONSTRAINT unique_filename DO NOTHING RETURNING raw_files.filename"); 28 | $added->execute($records); 29 | my $endtime = $db->prepare("UPDATE raw_files SET endtime = starttime + ?::interval WHERE filename = ?"); 30 | while(my $row = $added->fetchrow_arrayref) { 31 | my $filename = $row->[0]; 32 | next unless(-f $filename); 33 | my $vid = SReview::Video->new(url => $filename); 34 | $endtime->execute($vid->duration . " seconds", $filename); 35 | } 36 | sleep(30); 37 | } 38 | -------------------------------------------------------------------------------- /t/010-config.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | BEGIN { 4 | foreach my $key(keys %ENV) { 5 | if($key =~ /^SREVIEW_/) { 6 | delete $ENV{$key}; 7 | } 8 | } 9 | } 10 | 11 | use strict; 12 | use warnings; 13 | 14 | use Test::More tests => 9; 15 | use File::Temp qw/tempfile/; 16 | use_ok('SReview::Config'); 17 | 18 | my $val; 19 | local $SIG{__WARN__} = sub { $val = shift; }; 20 | 21 | my $config = SReview::Config->new('config'); 22 | ok($val =~ /^Warning: could not find configuration file config, falling back to defaults at t\/010-config\.t line \d+\.$/, 'loading nonexisting config produces a warning but succeeds'); 23 | isa_ok($config, 'SReview::Config'); 24 | 25 | $val = ''; 26 | $config = SReview::Config->new('./t/test.cf'); 27 | ok(length($val) == 0, 'loading an existing config file succeeds and prints no warning'); 28 | 29 | $config->define('test', 'testingk', 1); 30 | my $rv = $config->dump(); 31 | my $expect = '# SReview configuration file 32 | # ========================== 33 | # This configuration file contains all the configuration options known to 34 | # SReview. To change any configuration setting, you may either modify this 35 | # configuration file, or you can run \'sreview-config --set=key=value -a 36 | # update\'. The latter method will rewrite the whole configuration file, 37 | # removing any custom comments. It is therefore recommended that you use 38 | # one or the other, but not both. However, it will also write the default 39 | # values for all known configuration items to this config file (in a 40 | # commented-out fashion). 41 | # 42 | # Every configuration option is preceded by a comment explaining what it 43 | # does, and the legal values it can accept. 44 | 45 | # test 46 | # ---- 47 | # testingk 48 | #$test = 1; 49 | 50 | # Do not remove this, perl needs it 51 | 1; 52 | '; 53 | ok($expect eq $rv, "Config dump output is as expected"); 54 | ok($config->describe('test') eq 'testingk', "Description of configuration value is as expected"); 55 | my ($f, $filename) = tempfile('configtest-XXXXXXXX', UNLINK => 1); 56 | print $f '{'; 57 | eval { 58 | my $config = SReview::Config->new($filename); 59 | }; 60 | ok(defined($@), "Trying to parse a syntactically invalid perl script produces an exception"); 61 | eval { 62 | my $val = $config->get('foo'); 63 | }; 64 | ok(defined($@), "Trying to read a config variable that does not exist produces an exception"); 65 | $val = $config->get('test'); 66 | ok($val == 1, "Reading data that does not exist yet produces the default"); 67 | -------------------------------------------------------------------------------- /t/040-db.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 7; 7 | 8 | use SReview::Db; 9 | use SReview::Config; 10 | use SReview::Config::Common; 11 | 12 | use DBI; 13 | 14 | SKIP: { 15 | skip("Can't test database work unless the SREVIEWTEST_DB environment variable points to a database which we may clobber and recreate", 7) unless defined($ENV{SREVIEWTEST_DB}); 16 | 17 | my $warn; 18 | local $SIG{__WARN__} = sub { $warn = shift }; 19 | 20 | my $config = SReview::Config::Common::setup; 21 | 22 | isa_ok($config, 'SReview::Config'); 23 | 24 | $config->set(dbistring => 'dbi:Pg:dbname=' . $ENV{SREVIEWTEST_DB}); 25 | 26 | ok(SReview::Db::init($config), "Initializing the database was successful"); 27 | ok(SReview::Db::selfdestruct(code => 0, init => 0), "Clobbering the database works"); 28 | ok(SReview::Db::init($config), "Re-initializing the database after clobbering it was successful"); 29 | my $db = DBI->connect($config->get('dbistring'), '', '', {AutoCommit => 1}); 30 | ok(defined($db), "connecting to the database was successful"); 31 | my $q = $db->prepare("SELECT * FROM raw_files"); 32 | ok(defined($q), "preparing a query succeeds"); 33 | ok(defined($q->execute), "running a query succeeds, and the tables exist"); 34 | } 35 | -------------------------------------------------------------------------------- /t/085-schedule-export.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use v5.28; 4 | 5 | use Test::More; 6 | use Test::Mojo; 7 | use Data::Dumper; 8 | use DateTime::Format::Strptime; 9 | use DateTime::Format::Pg; 10 | use Mojo::File qw/path/; 11 | use Cwd qw/abs_path/; 12 | use File::Path qw/make_path remove_tree/; 13 | 14 | use SReview::Config::Common; 15 | 16 | my $config = SReview::Config::Common::setup; 17 | 18 | $config->set(secret => "foo", 19 | outputdir => abs_path('t/outputdir'), 20 | inputglob => abs_path('t/inputdir') . "/*/*/*.mp4", 21 | pubdir => abs_path('t/pubdir'), 22 | preroll_template => abs_path('t/testvids/just-title.svg'), 23 | postroll_template => abs_path('t/testvids/just-title.svg'), 24 | apology_template => abs_path('t/testvids/just-title.svg'), 25 | event => "Test event", 26 | ); 27 | 28 | if(exists($ENV{SREVIEWTEST_DB})) { 29 | $config->set(dbistring => 'dbi:Pg:dbname=' . $ENV{SREVIEWTEST_DB}); 30 | } 31 | 32 | use_ok 'SReview::Talk'; 33 | 34 | SKIP: { 35 | skip("Need a database to play with", 1) unless (exists($ENV{SREVIEWTEST_DB}) or exists($ENV{SREVIEWTEST_INSTALLED}) or exists($ENV{AUTOPKGTEST_TMP})); 36 | 37 | my $script; 38 | if(exists($ENV{SREVIEWTEST_INSTALLED}) or exists($ENV{AUTOPKGTEST_TMP})) { 39 | $script = "SReview::Web"; 40 | } else { 41 | $script = path(__FILE__); 42 | $script = $script->dirname->child('..')->child('web')->child('sreview-web')->to_abs; 43 | symlink "../t", "web/t"; 44 | chdir($script->dirname); 45 | } 46 | 47 | make_path('t/inputdir/room1/2017-11-10'); 48 | symlink('../../../testvids/bbb.mp4', 't/inputdir/room1/2017-11-10/17:00:00.mp4'); 49 | my $talk = SReview::Talk->new(talkid => 1); 50 | $talk->set_state('done'); 51 | my $t = Test::Mojo->new($script); 52 | my $tx = $t->get_ok("/released")->status_is(200); 53 | my $parser = DateTime::Format::Strptime->new(pattern => "%F %T"); 54 | my $start = $parser->parse_datetime('2017-11-10 17:00:00'); 55 | $start->set_time_zone("local"); 56 | my $end = $parser->parse_datetime('2017-11-10 17:00:10'); 57 | $end->set_time_zone("local"); 58 | $tx->json_is({ 59 | conference => { 60 | title => 'Test event', 61 | date => [ '2017-11-10', '2017-11-10' ], 62 | video_formats => { 63 | default => { 64 | resolution => "854x480", 65 | vcodec => "vp9", 66 | acodec => "opus", 67 | bitrate => "750k", 68 | } 69 | } 70 | }, 71 | videos => [{ 72 | room => 'room1', 73 | video => 'Test event/room1/2017-11-10/test-talk.webm', 74 | title => 'Test talk', 75 | description => 'Test talk description', 76 | speakers => [ 77 | 'Speaker 1', 78 | 'Speaker 2', 79 | 'Speaker 3', 80 | ], 81 | eventid => '1', 82 | start => substr(DateTime::Format::Pg->format_timestamptz($start), 0, -2), 83 | end => substr(DateTime::Format::Pg->format_timestamptz($end), 0, -2), 84 | }] 85 | }); 86 | } 87 | 88 | remove_tree("t/inputdir"); 89 | 90 | done_testing; 91 | -------------------------------------------------------------------------------- /t/090-talk.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 11; 7 | use Test::Deep; 8 | use SReview::Config::Common; 9 | use Data::Dumper; 10 | 11 | my $config = SReview::Config::Common::setup; 12 | 13 | SKIP: { 14 | skip("Can't test database work unless the SREVIEWTEST_DB environment varialbe points to a database which we may clobber and recreate", 11) unless defined($ENV{SREVIEWTEST_DB}); 15 | 16 | $config->set(dbistring => 'dbi:Pg:dbname=' . $ENV{SREVIEWTEST_DB}); 17 | $config->set('output_subdirs' => [ 'eventid', 'event', 'room', 'date', 'year' ]); 18 | $config->set('pubdir' => '/srv/sreview/web/public/video'); 19 | 20 | use_ok('SReview::Talk'); 21 | 22 | my $talk = SReview::Talk->new(talkid => 1); 23 | isa_ok($talk, 'SReview::Talk'); 24 | 25 | my $relname = join("/", substr($talk->nonce, 0, 1), substr($talk->nonce, 1, 2), substr($talk->nonce, 3), $talk->has_correction("serial") ? $talk->corrections->{serial} : 0); 26 | 27 | ok($talk->workdir eq "/srv/sreview/web/public/video/$relname", 'The workdir resolves to the correct value'); 28 | 29 | ok($talk->finaldir eq "/srv/sreview/output/1/Test event/room1/2017-11-10/2017", 'The output directory resolves to the correct value'); 30 | 31 | ok($talk->slug eq 'test-talk', 'The talk slug resolves to the correct value'); 32 | 33 | cmp_deeply($talk->corrections, { offset_start => num(0), length_adj => num(0), offset_audio => num(0), audio_channel => num(0)}, 'Corrections are set correctly'); 34 | cmp_deeply($talk->video_fragments, [ 35 | { talkid => num(-1), rawid => num(1), raw_filename => 'room1/2017-11-10/17:00:00.mp4', fragment_start => num(0), raw_length => num(20, .025), raw_length_corrected => num(0) }, 36 | { talkid => num(1), rawid => num(1), raw_filename => 'room1/2017-11-10/17:00:00.mp4', fragment_start => num(0), raw_length => num(20, .025), raw_length_corrected => num(10) }, 37 | { talkid => num(-2), rawid => num(1), raw_filename => 'room1/2017-11-10/17:00:00.mp4', fragment_start => num(10), raw_length => num(20, .025), raw_length_corrected => num(10, .025) }], 38 | 'Video fragments are found correctly'); 39 | 40 | $talk->add_correction(offset_start => 2); 41 | $talk->done_correcting; 42 | ok($talk->corrections->{offset_start} == 2, 'Corrections are accepted'); 43 | 44 | my $newtalk = SReview::Talk->new(talkid => 1); 45 | ok($newtalk->corrections->{offset_start} == 2, 'Corrections are written to the database'); 46 | ok($newtalk->corrections->{length_adj} == -2, 'Start offset changes length adjustment'); 47 | ok($newtalk->corrections->{serial} == 1, 'Setting corrections bumps the serial'); 48 | } 49 | -------------------------------------------------------------------------------- /t/095-files.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 17; 7 | use SReview::Config::Common; 8 | use Data::Dumper; 9 | use File::Copy; 10 | use Mojo::JSON qw/decode_json/; 11 | 12 | my $config = SReview::Config::Common::setup; 13 | 14 | $config->set("outputdir", "t/testvids"); 15 | mkdir("t/target"); 16 | $config->set("pubdir", "t/target"); 17 | $config->set("accessmethods", {input => "direct", output => "direct", intermediate => "direct"}); 18 | 19 | use_ok("SReview::Files::Factory"); 20 | 21 | my $coll = SReview::Files::Factory->create("output", "t/testvids"); 22 | 23 | isa_ok($coll, "SReview::Files::Collection::Base"); 24 | isa_ok($coll, "SReview::Files::Collection::direct"); 25 | my $children = $coll->children; 26 | isa_ok($children, "ARRAY"); 27 | my $child = $children->[0]; 28 | isa_ok($child, "SReview::Files::Access::Base"); 29 | isa_ok($child, "SReview::Files::Access::direct"); 30 | ok(defined($child->filename), "child has a filename"); 31 | 32 | my $coll2 = SReview::Files::Factory->create("intermediate", "t/target"); 33 | my $newfile = $coll2->add_file(relname => $child->relname); 34 | copy($child->filename, $newfile->filename); 35 | $newfile->store_file; 36 | 37 | ok($coll2->has_file($child->relname), "file copies to new collection"); 38 | 39 | my $subfile = $coll2->add_file(relname => "foo/" . $child->relname); 40 | copy($child->filename, $subfile->filename); 41 | ok($coll2->has_file("foo/" . $child->relname), "file copies to new collection in subdir"); 42 | $subfile->store_file; 43 | 44 | $coll2->delete_files(files => ["t/target/foo/"]); 45 | 46 | ok(!($coll2->has_file("foo" . $child->relname)), "file can be deleted by prefix"); 47 | 48 | $coll2->delete_files(files => [join("/", $coll2->baseurl, $child->relname)]); 49 | 50 | SKIP: { 51 | skip("Can't test S3 work unless the s3_access_config configuration is valid", 7) unless (exists($ENV{SREVIEWTEST_BUCKET}) && exists($ENV{SREVIEWTEST_S3_CONFIG})); 52 | 53 | $config->set("s3_access_config", decode_json($ENV{SREVIEWTEST_S3_CONFIG})); 54 | $config->set("accessmethods", {input => "S3", output => "S3", intermediate => "S3"}); 55 | $config->set("outputdir", $ENV{SREVIEWTEST_BUCKET}); 56 | $coll = SReview::Files::Factory->create("output", $ENV{SREVIEWTEST_BUCKET}); 57 | 58 | isa_ok($coll, "SReview::Files::Collection::Base"); 59 | isa_ok($coll, "SReview::Files::Collection::S3"); 60 | 61 | my $new = $coll->add_file(relname => $child->relname); 62 | ok($new->relname eq $child->relname, "creating a file with a relname from another bucket creates the same relname"); 63 | copy($child->filename, $new->filename); 64 | $new->store_file; 65 | $children = $coll->children; 66 | ok($coll->has_file($new->relname), "adding a file creates it in the bucket"); 67 | $new->delete; 68 | $coll = SReview::Files::Factory->create("output", $ENV{SREVIEWTEST_BUCKET}); 69 | ok(!($coll->has_file($new->relname)), "deleting a file removes it"); 70 | 71 | $new = $coll->add_file(relname => "foo/" . $child->relname); 72 | copy($child->filename, $new->filename); 73 | $new->store_file; 74 | $coll = SReview::Files::Factory->create("output", $ENV{SREVIEWTEST_BUCKET}); 75 | ok($coll->has_file($new->relname), "adding a file with a subdir works"); 76 | $coll->delete_files(files => [$ENV{SREVIEWTEST_BUCKET} . "/foo"]); 77 | $coll = SReview::Files::Factory->create("output", $ENV{SREVIEWTEST_BUCKET}); 78 | ok(!($coll->has_file($new->relname)), "file can be deleted by prefix"); 79 | } 80 | -------------------------------------------------------------------------------- /t/095-synfig.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use v5.28; 4 | use strict; 5 | use warnings; 6 | 7 | use SReview::Talk; 8 | use SReview::Config::Common; 9 | use File::Which; 10 | use Test::More; 11 | 12 | my $config = SReview::Config::Common::setup; 13 | 14 | SKIP: { 15 | skip "no synfig installed" unless defined(which('synfig')); 16 | use_ok("SReview::Template::Synfig"); 17 | 18 | my $talk = SReview::Talk->new(talkid => 1); 19 | isa_ok($talk, "SReview::Talk"); 20 | 21 | mkdir("animations"); 22 | 23 | SReview::Template::Synfig::process_template("t/testvids/animated.sif", "animations/anim.png", $talk, $config); 24 | 25 | ok(-f "animations/anim.0120.png", "processing a Synfig animation generates 120 files"); 26 | ok(! -f "animations/anim.0121.png", "processing a Synfig animation does not generate too many files"); 27 | 28 | unlink(glob "animations/*png"); 29 | rmdir("animations"); 30 | } 31 | 32 | done_testing; 33 | -------------------------------------------------------------------------------- /t/120-types.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | use DBI; 8 | use SReview::Talk::State; 9 | use YAML::XS; 10 | use SReview::API; 11 | 12 | SKIP: { 13 | skip("Can't test database work unless the SREVIEWTEST_DB environment variable points to a database which we may clobber and recreate", 1) unless defined($ENV{SREVIEWTEST_DB}); 14 | 15 | # Fetch values from the database 16 | my $db = DBI->connect('dbi:Pg:dbname=' . $ENV{SREVIEWTEST_DB}); 17 | my $st = $db->prepare("SELECT enum_range(null::talkstate)"); 18 | $st->execute; 19 | my $val = $st->fetchrow_arrayref()->[0]; 20 | $val =~ s/(\{|\})//g; 21 | my @values_db = split /,/, $val; 22 | 23 | # Fetch values from the API definition 24 | $_ = ; 25 | my $obj; 26 | { 27 | local $/ = undef; 28 | my $yaml = ; 29 | $obj = Load $yaml; 30 | close SReview::API::DATA; 31 | } 32 | my @values_api = @{$obj->{components}{schemas}{Talk}{properties}{state}{enum}}; 33 | 34 | # Fetch values from the module 35 | my @values_mod = @{SReview::Talk::State->values}; 36 | foreach my $value(@values_db) { 37 | isa_ok(SReview::Talk::State->new($value), "SReview::Talk::State"); 38 | ok($value eq shift @values_mod, "$value exists in db and module at same location"); 39 | ok($value eq shift @values_api, "$value exists in db and API at same location"); 40 | } 41 | ok(scalar(@values_mod) == 0, "no spurious values in module"); 42 | ok(scalar(@values_api) == 0, "no spurious values in api"); 43 | } 44 | 45 | done_testing; 46 | -------------------------------------------------------------------------------- /t/130-schedule.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | use_ok("SReview::Schedule::Yaml"); 8 | 9 | my $parser = SReview::Schedule::Yaml->new(url => "file://./t/testvids/schedule.yaml"); 10 | 11 | ok(scalar(@{$parser->events}) == 1, "we parsed exactly one event"); 12 | my $event = $parser->events->[0]; 13 | ok(scalar(@{$event->talks}) == 3, "we parsed 3 talks"); 14 | my $talks = $event->talks; 15 | 16 | ok($talks->[0]->title eq "Test talk", "the test talk was parsed correctly"); 17 | 18 | done_testing; 19 | -------------------------------------------------------------------------------- /t/test.cf: -------------------------------------------------------------------------------- 1 | # subroutine test 2 | $mangle = sub { }; 3 | 4 | # The DBI connection string used to connect to the database 5 | #$dbistring = 'dbi:Pg:dbname=sreview'; 6 | 7 | # Do not remove this, perl needs it 8 | 1; 9 | -------------------------------------------------------------------------------- /t/testvids/README.md: -------------------------------------------------------------------------------- 1 | The video in this directory is used for testing SReview. 2 | 3 | It is a fragment of the "Big Buck Bunny" movie, which is copyright 2008 the 4 | Blender Foundation / www.bigbuckbunny.org. It is released under the Creative 5 | Commons "by" license, version 3.0. 6 | 7 | It's quite funny, too. Have a look! :-) 8 | -------------------------------------------------------------------------------- /t/testvids/bbb.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoe/SReview/3c1308778094b799e0f2d6684b0b5fccb058ca2a/t/testvids/bbb.mp4 -------------------------------------------------------------------------------- /t/testvids/just-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 32 | 34 | 36 | 37 | 39 | image/svg+xml 40 | 42 | 43 | 44 | 45 | 46 | 50 | 57 | @TITLE@ @DATE@ 80 | 81 | -------------------------------------------------------------------------------- /t/testvids/schedule.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is used in the test suite for the SReview::Schedule::Yaml 3 | # parser. It also serves as an example for how to write one. 4 | name: test event 5 | talks: 6 | - title: Test talk 7 | speakers: 8 | - name: Wouter Verhelst 9 | email: w@uter.be 10 | - Urbec 11 | starttime: 2022-06-07 13:00 12 | length_minutes: 55 13 | description: This is the long-form description of the test talk. It may be used in the output somewhere, but that depends on what you decide to do with it. 14 | room: main 15 | - title: Test talk 2 16 | starttime: 2022-06-07 14:00 17 | endtime: 2022-06-07 14:55 18 | room: main 19 | - room: 20 | name: main 21 | altname: mainroom 22 | outputname: main_room 23 | slug: test-talk-with-all-optional-fields 24 | starttime: 2022-06-07 15:00 25 | endtime: 2022-06-07 15:15 26 | title: Test talk with all optional fields 27 | # The "id" field should not change, *evah*. If no id field is given, the 28 | # slug is used. If no slug is given, it is autogenerated from the title. 29 | # 30 | # This means that, if the title is ever changed, the talk will be seen 31 | # as new (and the old one will be marked as ignored) unless either the 32 | # slug or the id is set to the slug/id that was generated from the 33 | # title. 34 | id: foobar 35 | subtitle: Demonstrating SReview::Schedule::Yaml's optional fields 36 | track: 37 | name: main 38 | email: main@tracks.example.com 39 | id: 1 40 | description: A test talk with all optional fields filled out. Note that we did leave out the "length_minutes" field, as that one is implied by the endtime one. 41 | flags: 42 | can_inject: true 43 | speakers: 44 | - name: Wouter Verhelst 45 | email: w@uter.be 46 | - Urbec 47 | filtered: false 48 | -------------------------------------------------------------------------------- /web/public/credits.js: -------------------------------------------------------------------------------- 1 | Vue.component("talk-preview", { 2 | template: ` 3 |
4 | 5 | 6 |
`, 7 | props: ["talk", "which"], 8 | methods: { 9 | setForce: function() { 10 | this.force = Date.now(); 11 | } 12 | }, 13 | data: function() { 14 | return { 15 | force: false 16 | } 17 | }, 18 | }) 19 | 20 | const load_event = function() { 21 | fetch("/api/v1/event/" + app.event + "/overview") 22 | .then(response => response.json()) 23 | .then((data) => {app.rows = data.filter((row) => row.state !== "ignored")}) 24 | .catch(error => console.error(error)); 25 | }; 26 | 27 | var app = new Vue({ 28 | el: '#preview', 29 | data: { 30 | title: "", 31 | rows: [], 32 | events: [], 33 | event: undefined, 34 | }, 35 | methods: { 36 | reloadEvent: load_event, 37 | }, 38 | watch: { 39 | event: load_event, 40 | }, 41 | created: function() { 42 | fetch("/api/v1/config") 43 | .then(response => response.json()) 44 | .then(data => {this.event = data.event}) 45 | .catch(error => console.error(error)); 46 | fetch("/api/v1/event/list") 47 | .then(response => response.json()) 48 | .then(data => {this.events = data}) 49 | .catch(error => console.error(error)); 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /web/public/mangler.js: -------------------------------------------------------------------------------- 1 | /* @licstart The following is the entire license notice for this 2 | * project, including all its JavaScript. 3 | * 4 | * SReview, a web-based video review and transcoding system. 5 | * Copyright (c) 2016-2017 Wouter Verhelst 6 | * 7 | * SReview is free software; you can redistribute it and/or modify it 8 | * under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation; either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, but 13 | * WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | * Affero General Public License for more detilas. 16 | * 17 | * You should have received a copy of the GNU Affero General Public 18 | * License along with SReview. If not, see 19 | * . 20 | * 21 | * @licend The above is the entire license notice for this project, 22 | * including all its JavaScript. 23 | */ 24 | sreview_viddata.init = function() { 25 | this.current_length_adj = this.corrvals.length_adj; 26 | this.current_offset = this.corrvals.offset_start; 27 | this.lengths = { 28 | "pre": this.prelen, 29 | "main_initial": this.mainlen, 30 | "post": this.postlen 31 | }; 32 | this.startpoints = { 33 | "pre": 0, 34 | "main": this.lengths.pre + this.corrvals.offset_start, 35 | "post": this.lengths.pre + this.corrvals.offset_start + this.lengths.main_initial + this.current_length_adj 36 | }; 37 | this.newpoints = { 38 | "start": this.startpoints.main, 39 | "end": this.startpoints.post 40 | }; 41 | }; 42 | 43 | sreview_viddata.point_to_abs = function(which, where) { 44 | // which = which video (pre/main/post) 45 | // where = where the time value of the video should be (fractional seconds) 46 | return where + this.startpoints[which]; 47 | }; 48 | 49 | sreview_viddata.abs_to_offset = function(abs) { 50 | return abs - this.startpoints.main + this.current_offset; 51 | }; 52 | 53 | sreview_viddata.abs_to_adj = function(abs) { 54 | let newlen = abs - this.newpoints.start; 55 | return newlen - this.lengths.main_initial; 56 | }; 57 | 58 | sreview_viddata.set_point = function(which, what, where) { 59 | // which = which video (pre/main/post) 60 | // what = what point to set (start/end) 61 | // where = where the time value of the video should be (fractional seconds) 62 | this.newpoints[what] = this.point_to_abs(which, where); 63 | }; 64 | 65 | sreview_viddata.get_start_offset = function() { 66 | return this.abs_to_offset(this.newpoints.start); 67 | }; 68 | 69 | sreview_viddata.get_length_adjust = function() { 70 | return this.abs_to_adj(this.newpoints.end); 71 | }; 72 | 73 | sreview_viddata.set_start_offset = function(off) { 74 | this.newpoints.start = this.startpoints.main + off; 75 | }; 76 | 77 | sreview_viddata.set_length_adj = function(adj) { 78 | this.newpoints.end = this.newpoints.start + this.lengths.main_initial + adj; 79 | }; 80 | 81 | sreview_viddata.init(); 82 | -------------------------------------------------------------------------------- /web/public/review.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | } 4 | 5 | .eventname { 6 | display: block; 7 | margin-bottom: 0.625rem; 8 | } 9 | 10 | img { 11 | max-width: 100%; 12 | height: auto; 13 | display: block; 14 | } 15 | 16 | .container { 17 | margin-bottom: 2.5rem; 18 | } 19 | 20 | h1 > small { 21 | display: block; 22 | margin-bottom:0.625rem; 23 | } 24 | 25 | #main_video, #video_starts_too_late, #av_delay, #talk_info, .help-text, #video_ends_too_early, #instructions, #video_state_information { 26 | margin-top: 1.25rem; 27 | } 28 | 29 | #talk_info .dl-horizontal { 30 | margin-bottom: 0.3125rem; 31 | } 32 | 33 | .dl-horizontal dt { 34 | width: auto; 35 | padding-right: 0.3125rem; 36 | } 37 | 38 | .dl-horizontal dd { 39 | margin-left: 0; 40 | } 41 | 42 | #video_starts_too_early, #video_ends_too_late { 43 | margin-top: 1.25rem; 44 | margin-bottom: 1.25rem; 45 | } 46 | 47 | .btn-block + .alert { 48 | margin-top: 0.9375rem; 49 | } 50 | 51 | .hidden { 52 | display: none; 53 | } 54 | 55 | .alert_with_control { 56 | padding:0.3125rem 0.9375rem; 57 | } 58 | 59 | #video_state_information .alert + .alert-info { 60 | padding: 1.25rem; 61 | } 62 | 63 | .audio_player { 64 | width: 90%; 65 | padding: 0.625rem 0 1.25rem 0; 66 | } 67 | 68 | .jumbotron { 69 | padding-top: 1.875rem; 70 | padding-bottom: 1.875rem; 71 | } 72 | 73 | .jumbotron h2 { 74 | font-size: 2.1875rem; 75 | margin-bottom: 2.5rem; 76 | } 77 | 78 | .jumbotron p { 79 | font-size: 1.3125rem; 80 | } 81 | 82 | .jumbotron ul { 83 | margin-bottom: 1.25rem; 84 | } 85 | 86 | .jumbotron ul > li { 87 | font-size: 1.3125rem; 88 | } 89 | 90 | .restore_original .btn { 91 | margin-top: 1.25rem; 92 | } 93 | 94 | .restore_original, .error_div, .video_has_problems, .other_brokennes, .info_row { 95 | margin-top: 1.25rem; 96 | } 97 | 98 | .video_has_problems legend { 99 | margin-bottom: 0.625rem; 100 | } 101 | 102 | #av_delay .form-inline { 103 | width: 40%; 104 | } 105 | 106 | #main_action { 107 | margin: 1.875rem 0; 108 | } 109 | 110 | #no_audio h5 { 111 | margin-top: 0.9375rem; 112 | } 113 | 114 | .fa-question-circle { 115 | color: #999999; 116 | margin-left: 0.1875rem; 117 | } 118 | 119 | a .fa-question-circle:hover { 120 | color: #000000; 121 | } 122 | -------------------------------------------------------------------------------- /web/public/style.css: -------------------------------------------------------------------------------- 1 | #version { 2 | font-size: 0.75rem; 3 | color: #999999; 4 | } 5 | -------------------------------------------------------------------------------- /web/sreview-web: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | # SReview, a web-based video review and transcoding system 4 | # Copyright (c) 2016-2017, Wouter Verhelst 5 | # 6 | # SReview is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public 17 | # License along with this program. If not, see 18 | # . 19 | 20 | use strict; 21 | use warnings; 22 | 23 | use lib '../lib'; 24 | 25 | use Mojolicious::Commands; 26 | 27 | Mojolicious::Commands->start_app('SReview::Web'); 28 | -------------------------------------------------------------------------------- /web/templates/admin/dashboard.html.ep: -------------------------------------------------------------------------------- 1 |

Hi <%= $email%>!

2 | <% if (my $message = flash 'msg') { %> 3 |

<%= $message %>

4 | <% } %> 5 |

The following actions are possible:

6 |
Create a new user
7 |
8 | %= form_for adduser => begin 9 | %= label_for email => 'Username (email address):' 10 | %= email_field 'email' 11 |
12 | %= label_for name => 'Name:' 13 | %= text_field 'name' 14 |
15 | %= label_for isadmin => 'Is administrator?' 16 | %= check_box isadmin => 'true' 17 |
18 | %= label_for isvolunteer => 'Is volunteer?' 19 | %= check_box isvolunteer => 'true' 20 |
21 | %= label_for rooms => 'Limited to room:' 22 | %= select_field rooms => $rooms 23 |
24 | %= submit_button 25 | %end 26 |
27 |
Reset a password
28 |
29 | %= form_for chpw => begin 30 | %= label_for email => 'User email:' 31 | %= email_field 'email' 32 | %= submit_button 33 | %end 34 |
35 |
Change my password
36 |
37 | %= form_for setpw => begin 38 | %= label_for password1 => 'New password:' 39 | %= password_field 'password1' 40 | %= label_for password2 => 'Again:' 41 | %= password_field 'password2' 42 | %= submit_button 43 | %end 44 |
45 |
46 | -------------------------------------------------------------------------------- /web/templates/admin/main.html.ep: -------------------------------------------------------------------------------- 1 |

Hi <%= $email %>!

2 |

Please select a talk to work with:

3 | %= form_for admin_talk => begin 4 | %= select_field nonce => $talks, size => 25 5 |
6 | %= submit_button 7 | %end 8 | -------------------------------------------------------------------------------- /web/templates/credits.html.ep: -------------------------------------------------------------------------------- 1 | %layout "default", scripts_extra => [ "/vue/vue.min.js" ]; 2 |
3 |

Video credit previews

4 |

Event: 5 | 8 | 9 |

10 | 11 | 12 |
13 |
14 | "{{row.name}}", by {{row.speakers}} 15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |

Anonymous reviews are not enabled, therefore we can't see previews here. Sorry!

25 |
26 |
27 |
28 |
29 |
30 | 31 | -------------------------------------------------------------------------------- /web/templates/error.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'default'; 2 | %title 'SReview error'; 3 |

Error

4 |

<%== $message %>

5 |

Go to <%= link_to 'the SReview home page' => '/' %> or <%= link_to 'the video status overview page' => '/overview' %>.

6 | -------------------------------------------------------------------------------- /web/templates/finalreview/update.html+done.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your review 11 |

12 |

You have told us the video looks good, so we'll mark it as complete.

13 |

Thanks for your contribution!

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /web/templates/finalreview/update.html+error.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | There was an error processing your request 11 |

12 |

The following error occurred while trying to process your request:

13 |
<%= $error =%>
14 |

If you do not know what to do with the above error, please 15 | contact the video team.

16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/finalreview/update.html+unpublish.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your review 11 |

12 |

You have told us there were issues with the video, so we will unpublish it and then send it back to the review queue.

13 |

Thanks for your contribution!

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /web/templates/finalreview/view.html.ep: -------------------------------------------------------------------------------- 1 | % if ( $adminspecial ) { 2 |
3 | × 4 |

Note: this talk is currently in the state <%= $talk->state %>, not in the finalreview state. You can only see this review page because you are admin!

5 |

Please use caution when making changes.

6 |
7 | % } 8 |

9 | <%= $talk->eventname . " videos" =%> 10 | <%= $talk->title =%> 11 |

12 |
13 |
14 |
15 |
Speakers:
16 |
<%= $talk->speakers =%>
17 |
Date:
18 |
<%= $talk->readable_date =%>
19 |
Room:
20 |
<%= $talk->room =%>
21 |
22 |
23 |
24 |
25 |
26 |

What to check when doing final review of this video

27 |
    28 |
  1. Check the beginning of the video to make sure it starts at the right time.
  2. 29 |
  3. Check the end of the video to make sure the transcode completed correctly.
  4. 30 |
  5. Check the titles to make sure they match the content
  6. 31 |
  7. Check the rendering of the titles to make sure they look good
  8. 32 |
33 |

Then answer the How is this video question at the bottom of the page.

34 |
35 |
36 | % foreach my $url(@{$talk->output_video_urls}) { 37 |
38 |
39 |

Profile <%= $url->{prof} %>:

40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | % } 48 |
49 |
50 |

How is this video?

51 |
52 |
53 |
54 | 58 |
59 |
60 | 64 |
65 |
66 | 67 | 68 |
69 | Comments 70 |
71 |
72 | 73 | 74 |

Previous comments:

75 |
76 | <%= $talk->comment %>
77 |             
78 |
79 |
80 |
81 |
82 |
83 |
84 | -------------------------------------------------------------------------------- /web/templates/index.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'default'; 2 | %title 'SReview'; 3 |

SReview video review system

4 |

If you have an account, <%= link_to 'log in here' => 'login' %>. If you received a review link, please follow that link instead.

5 |

If you simply wish to know the state of a talk, go to the <%= link_to 'video overview page' => 'overview' %>.

6 | -------------------------------------------------------------------------------- /web/templates/inject/update.html+error.ep: -------------------------------------------------------------------------------- 1 |

Error

2 |
3 |
4 |

<%= $short_error =%>

5 |
6 |
7 |
8 |
9 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /web/templates/inject/update.html.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your files 11 |

12 |

These files will now be injected into the system.

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /web/templates/inject/view.html+error.ep: -------------------------------------------------------------------------------- 1 |

Error

2 |
3 |
4 |

<%= $short_error =%>

5 |
6 |
7 |
8 |
9 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /web/templates/inject/view.html.ep: -------------------------------------------------------------------------------- 1 | % if ( $adminspecial ) { 2 |
3 | × 4 |

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!

5 |

Please use caution when making changes.

6 |
7 | % } 8 |

9 | <%= $talk->eventname =%> 10 | <%= $talk->title =%> 11 |

12 |
13 |
14 |
15 |
Speakers:
16 |
<%= $talk->speakers =%>
17 |
Date:
18 |
<%= $talk->readable_date =%>
19 |
Room:
20 |
<%= $talk->room =%>
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /web/templates/layouts/admin.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | %= stylesheet '/style.css' 6 | % if (my $scripts = stash 'scripts_raw') { 7 | % foreach my $script(@$scripts) { 8 | 9 | % } 10 | % } 11 | 12 | 13 | 14 | 15 | 16 | 17 | % if (my $extra = stash 'scripts_extra') { 18 | % foreach my $script(@$extra) { 19 | 20 | % } 21 | % } 22 | 23 | 24 |
25 | <%= content %> 26 |

<%= link_to 'Log out' => 'logout' %> | <%= link_to 'Volunteer review' => '/volunteer/list' %> 27 | % if (!$c->session->{volunteer} ) { 28 | | <%= link_to 'Admin review: overview' => '/admin' %> | <%= link_to 'Admin review: broken talks' => 'broken_table' %> 29 | % } 30 | % if ($c->session->{admin}) { 31 | | <%= link_to 'Admin: actions' => '/admin/system' %> 32 | % } 33 |

34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /web/templates/layouts/default.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | %= stylesheet '/style.css' 6 | % if (my $scripts = stash 'scripts_raw') { 7 | % foreach my $script(@$scripts) { 8 | 9 | % } 10 | % } 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | % if (my $sheets = stash 'stylesheets') { 19 | % foreach my $stylesheet(@$sheets) { 20 | %= stylesheet $stylesheet 21 | % } 22 | % } 23 | % if (my $extra = stash 'scripts_extra') { 24 | % foreach my $script(@$extra) { 25 | 26 | % } 27 | % } 28 | 29 | 30 |
<%= content %> 31 |

SReview <%= $c->version %>. Code is available under the GNU AGPLv3. Patches welcome :-)

32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /web/templates/login.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'default'; 2 | %title 'Log in'; 3 | %= form_for login_post => (method => 'POST') => begin 4 | %= label_for email => 'Email address' 5 | %= email_field 'email' 6 | %= label_for pass => 'Password' 7 | %= password_field 'pass' 8 | %= submit_button 9 | %= end 10 | -------------------------------------------------------------------------------- /web/templates/msg.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'default'; 2 | %title "SReview: $title"; 3 | % if (my $cmsg = flash 'completion_message') { 4 |
5 | × 6 | <%= $cmsg =%> 7 |
8 | % } 9 | % if (my $emsg = flash 'error_message') { 10 |
11 | × 12 | <%= $emsg =%> 13 |
14 | % } 15 |

<%= $title %>

16 |

<%== $message %>

17 |

Go to <%= link_to 'the SReview home page' => '/' %> or <%= link_to 'the video status overview page' => '/overview' %>.

18 | -------------------------------------------------------------------------------- /web/templates/review/confirm.html+done.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 26 | -------------------------------------------------------------------------------- /web/templates/review/confirm.html+error.ep: -------------------------------------------------------------------------------- 1 |

Error

2 |
3 |
4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /web/templates/review/confirm.html+injecting.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 25 | -------------------------------------------------------------------------------- /web/templates/review/confirm.html+preparing.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /web/templates/review/confirm.html+transcode.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 26 | -------------------------------------------------------------------------------- /web/templates/review/full.html+done.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 26 | -------------------------------------------------------------------------------- /web/templates/review/full.html+error.ep: -------------------------------------------------------------------------------- 1 |

Error

2 |
3 |
4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /web/templates/review/full.html+injecting.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 25 | -------------------------------------------------------------------------------- /web/templates/review/full.html+preparing.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /web/templates/review/full.html+transcode.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos" =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |
Speakers:
9 |
<%= $talk->speakers =%>
10 |
Date:
11 |
<%= $talk->readable_date =%>
12 |
Room:
13 |
<%= $talk->room =%>
14 |
15 |
16 |
17 | 18 |
19 |
20 | 26 | -------------------------------------------------------------------------------- /web/templates/review/update.html+done.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your review 11 |

12 |

You have told us the video looks good, so we'll process it for publication.

13 |

We will send you an email when the video is published.

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /web/templates/review/update.html+error.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | There was an error processing your request 11 |

12 |

The following error occurred while trying to process your request:

13 |
<%= $error =%>
14 |

If you do not know what to do with the above error, please 15 | contact the video team.

16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/review/update.html+newreview.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your review 11 |

12 |

Once we have fixed the video, we will send you an email so that you can review the changes.

13 |

Summary of video problems

14 |

You have told us the video has some problems. Here is a summary of what needs fixing:

15 |
    16 | % if (exists($corrections->{start})) { 17 |
  • The start time should be corrected with an offset of <%= $corrections->{start} %> seconds
  • 18 | % } 19 | % if (exists($corrections->{end})) { 20 |
  • The end time should be corrected with an offset of <%= $corrections->{end} %> seconds
  • 21 | % } 22 | % if (exists($corrections->{audio_channel})) { 23 |
  • The audio channel should be changed to channel <%= $corrections->{audio_channel} %>
  • 24 | % } 25 | % if (exists($corrections->{audio_offset})) { 26 |
  • An A/V synchronisation value of <%= $corrections->{audio_offset} %> seconds should be applied
  • 27 | % } 28 |
29 |

Don't worry if the numbers don't make much sense: the video processing scripts will know what to do.

30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /web/templates/review/update.html+other.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for sending your review 11 |

12 |

You have told us the video is broken in an unusual way. This is what you wrote:

13 |
14 | "<%= $other_msg %>" 15 |
16 |

Someone from the video team will look into the problem and send you an email.

17 |

Hopefully we can fix it.

18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /web/templates/review/update.html+reset.ep: -------------------------------------------------------------------------------- 1 |

2 | <%= $talk->eventname . " videos " =%> 3 | <%= $talk->title =%> 4 |

5 |
6 |
7 |
8 |

9 | 10 | Thank you for reviewing this video! 11 |

12 |

You have asked us to restore the original video. 13 |

Once it's done, We will send you an email.

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /web/templates/schedule/index.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'admin'; 2 |

Schedule management

3 |

Possitble actions:

4 |
5 |
GET /admin/schedule/talk/list
6 |
Creates a JSON list of all talk IDs with their titles and upstream IDs in the current event
7 |
GET /admin/schedule/talk/:id
8 |
Returns all the details on the given talk
9 |
DELETE /admin/schedule/talk/:id
10 |
Delete the talk with the given ID
11 |
PUT /admin/schedule/talk/
12 |
Create a new talk (requires JSON object)
13 |
PUT /admin/schedule/talk/:id
14 |
Update the data of the talk with the given ID
15 |
GET /admin/schedule/speaker/list
16 |
Retrieves a list of all known speakers, with their ID and name
17 |
PUT /admin/schedule/speaker/
18 |
Create a new speaker (requires JSON object)
19 |
20 | -------------------------------------------------------------------------------- /web/templates/table.html.ep: -------------------------------------------------------------------------------- 1 | %layout "default"; 2 | % if($autorefresh) { 3 | 6 | % } 7 |

<%= $header %>

8 | 9 | % if(defined($titlerow)) { 10 | 11 | % foreach my $cell(@$titlerow) { 12 | 13 | % } 14 | 15 | % } 16 | % my $rowtype = 0; 17 | % foreach my $row(@$rows) { 18 | 19 | % $rowtype = 1 - $rowtype; 20 | % foreach my $cell(@$row) { 21 | 22 | % } 23 | 24 | % } 25 |
<%= $cell %>
<%== $cell %>
26 | % if(defined($totals)) { 27 |

Totals

28 | 29 | % if(defined($tottitrow)) { 30 | 31 | % foreach my $cell(@$tottitrow) { 32 | 33 | % } 34 | 35 | % } 36 | % foreach my $row(@$totals) { 37 | 38 | % $rowtype = 1 - $rowtype; 39 | % foreach my $cell(@$row) { 40 | 41 | % } 42 | % } 43 | 44 | % } 45 |
<%= $cell %>
<%= $cell %>
46 | -------------------------------------------------------------------------------- /web/templates/volunteer/list.html.ep: -------------------------------------------------------------------------------- 1 | %layout 'admin'; 2 | %title 'SReview volunteer overview'; 3 |

SReview volunteer overview

4 | 5 | 6 | % foreach my $talk(@$talks) { 7 | 8 | % } 9 |
TalkState
<%= $talk->[1] %><%= $talk->[3] %>
10 | --------------------------------------------------------------------------------