├── .codeclimate.yml ├── .dockerignore ├── .env.development ├── .env.test ├── .github ├── dependabot.yml ├── graphql-inspector.yaml └── workflows │ ├── ci.yml │ ├── container-images.yml │ └── deploy.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── Capfile ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Procfile ├── README.md ├── Rakefile ├── Vagrant.md ├── Vagrantfile ├── app ├── admin │ ├── admin_user.rb │ ├── api_key.rb │ ├── conference.rb │ ├── dashboard.rb │ ├── event.rb │ ├── news.rb │ └── recording.rb ├── assets │ ├── config │ │ └── manifest.js │ ├── fonts │ │ ├── GabriellaHeavy.otf │ │ ├── Mona-Sans.ttf │ │ ├── Mona-Sans.woff2 │ │ ├── VCROCDFaux.ttf │ │ ├── VCROCDFaux.woff │ │ └── VCROCDFaux.woff2 │ ├── images │ │ ├── .keep │ │ ├── bxslider │ │ │ ├── bx_loader.gif │ │ │ └── controls.png │ │ └── frontend │ │ │ ├── .gitkeep │ │ │ ├── feed-banner.png │ │ │ ├── promoted_bg-rc3.jpg │ │ │ ├── promoted_bg.jpg │ │ │ ├── voctocat-header.svg │ │ │ └── voctocat.svg │ ├── javascripts │ │ ├── activate-timelens.js │ │ ├── active_admin.js.coffee │ │ ├── application.js │ │ ├── clappr-dash-shaka-playback.js │ │ ├── feed_toggle.js │ │ ├── jquery.bxslider-v4.2.1d-ssfrontend.js │ │ ├── mastodon-share.js │ │ ├── mirrorbrain-fix.js │ │ ├── oembed-player.js │ │ ├── relive-seek.js │ │ ├── replacehash.js │ │ ├── slider.js │ │ └── theme.js │ └── stylesheets │ │ ├── active_admin.css.scss │ │ ├── application.css │ │ ├── embed.css.scss │ │ ├── frontend │ │ ├── base │ │ │ ├── _base.scss │ │ │ ├── _breadcrumb.scss │ │ │ ├── _feeds_dropdown.scss │ │ │ ├── _footer.scss │ │ │ ├── _navbar.scss │ │ │ ├── _theme.scss │ │ │ └── _typography.scss │ │ ├── lib │ │ │ ├── _fonts.scss │ │ │ └── _variables.scss │ │ ├── pages │ │ │ ├── _browse.scss │ │ │ ├── _list.scss │ │ │ ├── _page_not_found.scss │ │ │ ├── _show.scss │ │ │ └── _start.scss │ │ ├── shared │ │ │ ├── _audioplayer.scss │ │ │ ├── _eventpreview_meta.scss │ │ │ ├── _label.scss │ │ │ ├── _promoted.scss │ │ │ └── _videoplayer.scss │ │ └── styles.scss │ │ ├── jquery.bxslider.css.scss │ │ ├── player-fixes.css │ │ └── timelens-custom.css ├── backstage.yml ├── controllers │ ├── api │ │ ├── backstage.yml │ │ ├── conferences_controller.rb │ │ ├── events_controller.rb │ │ └── recordings_controller.rb │ ├── api_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ ├── api_error_responses.rb │ │ └── throttle_connections.rb │ ├── frontend │ │ ├── conferences_controller.rb │ │ ├── events_controller.rb │ │ ├── feeds_controller.rb │ │ ├── home_controller.rb │ │ ├── news_controller.rb │ │ ├── popular_controller.rb │ │ ├── recent_controller.rb │ │ ├── search_controller.rb │ │ ├── sitemap_controller.rb │ │ ├── tags_controller.rb │ │ └── unpopular_controller.rb │ ├── frontend_controller.rb │ ├── graphql_controller.rb │ ├── public │ │ ├── backstage.yml │ │ ├── conferences_controller.rb │ │ ├── events_controller.rb │ │ └── recordings_controller.rb │ └── public_controller.rb ├── graphql │ ├── backstage.yml │ ├── media_backend_schema.rb │ ├── mutations │ │ └── .keep │ ├── resolvers │ │ ├── conference.rb │ │ └── search_lectures.rb │ ├── schema.graphql │ └── types │ │ ├── .keep │ │ ├── base_enum.rb │ │ ├── base_field.rb │ │ ├── base_input_object.rb │ │ ├── base_interface.rb │ │ ├── base_object.rb │ │ ├── base_scalar.rb │ │ ├── base_union.rb │ │ ├── conference_type.rb │ │ ├── date_time_type.rb │ │ ├── json_type.rb │ │ ├── lecture_type.rb │ │ ├── mutation_type.rb │ │ ├── query_type.rb │ │ ├── resource_type.rb │ │ └── url_type.rb ├── helpers │ ├── application_helper.rb │ ├── frontend │ │ ├── application_helper.rb │ │ ├── event_recording_filter.rb │ │ ├── event_recording_filter_high_quality.rb │ │ ├── event_recording_filter_low_quality.rb │ │ ├── event_recording_filter_master.rb │ │ ├── feed_quality.rb │ │ ├── feeds_helper.rb │ │ ├── feeds_navigation_bar_structure_helper.rb │ │ └── tagging_helper.rb │ ├── public_json_helper.rb │ └── view_helper.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── admin_user.rb │ ├── api_key.rb │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ ├── elasticsearch_event.rb │ │ ├── fahrplan_parser.rb │ │ ├── fahrplan_updater.rb │ │ ├── recent.rb │ │ └── storage.rb │ ├── conference.rb │ ├── event.rb │ ├── event_view_count.rb │ ├── frontend.rb │ ├── frontend │ │ ├── conference.rb │ │ ├── event.rb │ │ ├── news.rb │ │ └── recording.rb │ ├── news.rb │ ├── recording.rb │ ├── recording_view.rb │ └── web_feed.rb ├── views │ ├── api │ │ ├── conferences │ │ │ ├── _fields.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ │ ├── events │ │ │ ├── _fields.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ │ └── recordings │ │ │ ├── _fields.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ ├── frontend │ │ ├── conferences │ │ │ ├── browse.html.haml │ │ │ ├── list.haml │ │ │ └── show.html.haml │ │ ├── events │ │ │ ├── _event.html.haml │ │ │ ├── oembed.html.haml │ │ │ ├── playlist.html.haml │ │ │ ├── postroll.html.haml │ │ │ └── show.html.haml │ │ ├── home │ │ │ ├── about.haml │ │ │ ├── index.html.haml │ │ │ └── page_not_found.haml │ │ ├── popular │ │ │ └── index.haml │ │ ├── recent │ │ │ └── index.haml │ │ ├── search │ │ │ └── index.html.haml │ │ ├── shared │ │ │ ├── _download.haml │ │ │ ├── _embedshare.haml │ │ │ ├── _event_metadata.haml │ │ │ ├── _event_persons.haml │ │ │ ├── _event_thumb.haml │ │ │ ├── _event_title.html.haml │ │ │ ├── _event_with_conference.html.haml │ │ │ ├── _folder_feeds.haml │ │ │ ├── _footer.haml │ │ │ ├── _header.haml │ │ │ ├── _live.html.haml │ │ │ ├── _navbar.haml │ │ │ ├── _navbar_feeds.haml │ │ │ ├── _noscript.haml │ │ │ ├── _player_audio.haml │ │ │ ├── _player_playlist_audio.haml │ │ │ ├── _player_playlist_video.haml │ │ │ ├── _player_relive.html.haml │ │ │ ├── _player_video.haml │ │ │ ├── _player_video_clappr.haml │ │ │ ├── _player_video_native.haml │ │ │ ├── _promoted.haml │ │ │ ├── _related.html.haml │ │ │ └── _short_about.haml │ │ ├── sitemap │ │ │ └── index.xml.haml │ │ ├── tags │ │ │ └── show.html.haml │ │ └── unpopular │ │ │ └── index.haml │ ├── layouts │ │ └── frontend │ │ │ ├── frontend.html.haml │ │ │ └── oembed.html.haml │ └── public │ │ ├── _html5player.html.erb │ │ ├── conferences │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ │ ├── events │ │ ├── index.json.jbuilder │ │ ├── search.json.jbuilder │ │ └── show.json.jbuilder │ │ ├── index.json │ │ ├── oembed.json.jbuilder │ │ ├── oembed.xml.builder │ │ ├── recordings │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ │ └── shared │ │ ├── _conference.json.jbuilder │ │ ├── _event.json.jbuilder │ │ ├── _event_recordings.json.jbuilder │ │ └── _recording.json.jbuilder └── workers │ ├── conference_relive_download_worker.rb │ ├── conference_streaming_download_worker.rb │ ├── download_worker.rb │ ├── event_update_worker.rb │ ├── feed.rb │ ├── feed │ ├── archive_legacy_worker.rb │ ├── archive_worker.rb │ ├── audio_worker.rb │ ├── base.rb │ ├── folder_worker.rb │ ├── legacy_worker.rb │ ├── podcast_worker.rb │ └── recent_worker.rb │ └── schedule_download_worker.rb ├── bin ├── bundle ├── docker-dev-up ├── rails ├── rake ├── rubocop ├── setup ├── update └── update-data ├── catalog-info.yaml ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml.template ├── deploy.rb ├── deploy │ ├── production.rb │ └── staging.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── action_mailer.rb │ ├── active_admin.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── cors_public.rb │ ├── devise.rb │ ├── devise_secret_token.rb │ ├── exception_notifier.rb │ ├── filter_parameter_logging.rb │ ├── graphql.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── multi_json.rb │ ├── permissions_policy.rb │ ├── sass_skip.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── custom.en.yml │ ├── devise.en.yml │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── settings.yml.template ├── sidekiq.yml ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20130820182037_devise_create_admin_users.rb │ ├── 20130820182039_create_active_admin_comments.rb │ ├── 20130820192118_create_conferences.rb │ ├── 20130820210349_create_api_keys.rb │ ├── 20130820221709_add_conference_indexes.rb │ ├── 20130820222430_create_events.rb │ ├── 20130820222724_create_recordings.rb │ ├── 20130820223153_add_path_to_recording.rb │ ├── 20130820233230_add_title_to_conference.rb │ ├── 20130820233237_add_title_to_event.rb │ ├── 20130820233659_add_indexes_to_events.rb │ ├── 20130820233759_add_indexes_to_recordings.rb │ ├── 20130821000003_add_recording_states.rb │ ├── 20130821121327_create_delayed_jobs.rb │ ├── 20130821123704_create_event_infos.rb │ ├── 20130821124234_add_schedule_url_to_conference.rb │ ├── 20130822181227_change_recording_model.rb │ ├── 20130824135552_add_thumb_filename_to_event.rb │ ├── 20130826215505_add_slug_to_event_info.rb │ ├── 20140101230549_add_event_info_fields_to_event.rb │ ├── 20140101231325_move_event_info_data_to_event.rb │ ├── 20140101232111_remove_event_info_table.rb │ ├── 20140102031811_add_conference_logo.rb │ ├── 20140103000033_add_folder_to_recording.rb │ ├── 20140103000127_fill_recordings_folder_from_mime_types.rb │ ├── 20140120020012_add_release_date_to_event.rb │ ├── 20140502191657_add_dimensions_to_recording.rb │ ├── 20140502222555_add_promoted_flag_to_events.rb │ ├── 20140502223824_create_news.rb │ ├── 20140609012419_create_import_templates.rb │ ├── 20140611215347_remove_promoted_from_import_template.rb │ ├── 20140611215723_change_release_date_to_date.rb │ ├── 20140615151738_change_conference_schedule_xml_to_text.rb │ ├── 20140622020440_add_view_count_to_events.rb │ ├── 20140622085720_remove_released_state_from_recording.rb │ ├── 20140626220827_create_recording_views.rb │ ├── 20140626223140_add_view_count_to_recordings.rb │ ├── 20140627185401_remove_view_count_from_recordings.rb │ ├── 20140907121133_remove_event_gif.rb │ ├── 20141111120848_add_time_to_event_date.rb │ ├── 20150913230424_drop_delayed_jobs_table.rb │ ├── 20150919123544_rename_conference_webgen_location_to_slug.rb │ ├── 20150919124229_rename_import_template_webgen_location_to_slug.rb │ ├── 20151011213109_add_duration_to_event.rb │ ├── 20151018212350_add_downloaded_events_count_to_conferences.rb │ ├── 20151027160631_add_indexes.rb │ ├── 20151105165721_add_downloaded_recordings_counter_on_events.rb │ ├── 20151227115938_add_language_string_to_recording.rb │ ├── 20151230105406_add_original_language_to_event.rb │ ├── 20160102191700_add_quality_flag_on_recording.rb │ ├── 20160102191709_add_html5_flag_on_recording.rb │ ├── 20160102232606_drop_import_templates.rb │ ├── 20160104122727_mime_types_to_flags.rb │ ├── 20160130001036_remove_original_url.rb │ ├── 20160203134927_high_quality.rb │ ├── 20160205214028_default_language_is_eng.rb │ ├── 20160827151338_add_metadata_to_conference.rb │ ├── 20161127165450_add_event_last_releases_at_to_conference.rb │ ├── 20161231215656_add_metadata_to_event.rb │ ├── 20170103122951_add_identifiers_to_recording_views.rb │ ├── 20171111152136_add_streaming_to_conference.rb │ ├── 20171111211412_create_event_view_counts.rb │ ├── 20180914181622_add_timelens_to_event.rb │ ├── 20190928125825_add_doi_to_events.rb │ ├── 20191226021730_create_web_feeds.rb │ ├── 20200119000347_change_release_date_type.rb │ ├── 20200119002951_extend_conference_attributes.rb │ ├── 20200410231759_add_custom_css_to_conference.rb │ ├── 20230131143553_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20230131143554_create_active_storage_variant_records.active_storage.rb │ ├── 20230131143555_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── 20241229193141_add_notes_field_to_events.rb │ ├── 20241230111051_add_global_event_notes_field_to_conference.rb │ └── 20250120233533_add_translated_to_recordings.rb ├── schema.rb └── seeds.rb ├── deploy-staging.sh ├── deploy.sh ├── docker-compose.yml ├── docker ├── .gitkeep ├── database.yml ├── nginx.conf └── settings.yml ├── docs ├── architecture-overview-apis.png ├── architecture-overview-subtitles.png ├── architecture-overview.drawio └── architecture-overview.png ├── elasticsearch.yml ├── env.example ├── lib ├── assets │ └── .keep ├── downloader.rb ├── feeds.rb ├── feeds │ ├── helper.rb │ ├── news_feed_generator.rb │ ├── podcast_generator.rb │ └── rdf_generator.rb ├── frontend │ ├── folder_tree.rb │ └── playlist.rb ├── languages.rb ├── mime_type.rb ├── settings.rb ├── tasks │ ├── .keep │ ├── db_dump_fixtures.rake │ ├── db_load_fixtures_with_jsonb.rake │ ├── related_events.rake │ ├── relive_update.rake │ └── streaming_update.rake └── update_related_events.rb ├── log └── .keep ├── public ├── 403.html ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-57x57.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-196x196.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico └── robots.txt ├── run_tests_graphql.sh ├── schema.graphql ├── system.yaml ├── test ├── controllers │ ├── .keep │ ├── admin │ │ ├── admin_users_controller_test.rb │ │ ├── api_keys_controller_test.rb │ │ ├── conferences_controller_test.rb │ │ ├── dashboard_controller_test.rb │ │ ├── events_controller_test.rb │ │ └── recordings_controller_test.rb │ ├── api │ │ ├── conferences_controller_test.rb │ │ ├── events_controller_test.rb │ │ └── recordings_controller_test.rb │ ├── frontend │ │ ├── conferences_controller_test.rb │ │ ├── events_controller_test.rb │ │ ├── feeds_controller_test.rb │ │ ├── home_controller_test.rb │ │ ├── news_controller_test.rb │ │ ├── recent_controller_test.rb │ │ ├── search_controller_test.rb │ │ ├── sitemap_controller_test.rb │ │ └── tags_controller_test.rb │ ├── graphql_controller_test.rb │ ├── public │ │ ├── conferences_controller_test.rb │ │ ├── events_controller_test.rb │ │ └── recordings_controller_test.rb │ └── public_controller_test.rb ├── factories.rb ├── fixtures │ ├── audio.mp3 │ ├── relive-gpn18.json │ ├── schedule.xml │ └── streaming.json ├── helpers │ ├── .keep │ ├── application_helper_test.rb │ └── public_json_helper_test.rb ├── integration │ ├── .keep │ ├── conferences_api_test.rb │ ├── events_api_test.rb │ ├── events_public_api_test.rb │ ├── frontend │ │ ├── browse_integration_test.rb │ │ └── events_integration_test.rb │ ├── graphql │ │ ├── conferences_test.rb │ │ ├── lecture_test.rb │ │ └── schema_test.rb │ └── recordings_api_test.rb ├── lib │ ├── feeds │ │ └── podcast_generator_test.rb │ └── frontend │ │ └── folder_tree_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── admin_user_test.rb │ ├── api_key_test.rb │ ├── concerns │ │ └── storage_test.rb │ ├── conference_test.rb │ ├── event_test.rb │ ├── frontend │ │ └── event_test.rb │ ├── news_test.rb │ ├── recording_test.rb │ ├── recording_view_test.rb │ └── web_feed_test.rb ├── test_helper.rb └── workers │ ├── conference_relive_download_worker_test.rb │ ├── conference_streaming_download_worker_test.rb │ └── feed │ ├── archive_legacy_worker_test.rb │ ├── archive_worker_test.rb │ ├── audio_worker_test.rb │ ├── folder_worker_test.rb │ ├── legacy_worker_test.rb │ ├── podcast_worker_test.rb │ └── recent_worker_test.rb └── vendor └── assets ├── fonts ├── estre.eot ├── estre.otf ├── estre.svg ├── estre.ttf └── estre.woff ├── icomoon-font ├── Read Me.txt ├── demo-files │ ├── demo.css │ └── demo.js ├── demo.html ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── icomoon-font.scss ├── selection.json └── style.css ├── javascripts ├── .keep ├── clappr-playback-rate-plugin.js ├── clappr-thumbnails-plugin.js ├── handlebars.min-latest.js ├── mediaelement-and-player.js ├── player.umd.js ├── purl.min.js └── timelens.js ├── mediaelement-plugins ├── airplay │ ├── airplay.css │ ├── airplay.js │ ├── airplay.min.css │ ├── airplay.min.js │ ├── airplay.png │ └── airplay.svg ├── chromecast │ ├── chromecast-i18n.js │ ├── chromecast.css │ ├── chromecast.js │ ├── chromecast.min.css │ ├── chromecast.min.js │ ├── chromecast.png │ └── chromecast.svg ├── context-menu │ ├── context-menu-i18n.js │ ├── context-menu.css │ ├── context-menu.js │ ├── context-menu.min.css │ └── context-menu.min.js ├── jump-forward │ ├── jump-forward-i18n.js │ ├── jump-forward.css.scss │ ├── jump-forward.js │ ├── jumpforward.png │ └── jumpforward.svg ├── loop │ ├── loop-i18n.js │ ├── loop.css │ ├── loop.js │ ├── loop.min.css │ ├── loop.min.js │ ├── loop.png │ └── loop.svg ├── markers │ ├── markers.js │ └── markers.min.js ├── playlist │ ├── playlist-controls.svg │ ├── playlist-i18n.js │ ├── playlist.css.scss │ └── playlist.js ├── postroll │ ├── postroll-i18n.js │ ├── postroll.css │ ├── postroll.js │ ├── postroll.min.css │ └── postroll.min.js ├── preview │ ├── preview.js │ └── preview.min.js ├── quality │ ├── quality-i18n.js │ ├── quality.css │ ├── quality.js │ ├── quality.min.css │ └── quality.min.js ├── skip-back │ ├── skip-back-i18n.js │ ├── skip-back.css.scss │ ├── skip-back.js │ ├── skipback.png │ └── skipback.svg ├── source-chooser │ ├── settings.png │ ├── settings.svg │ ├── source-chooser-i18n.js │ ├── source-chooser.css.scss │ └── source-chooser.js ├── speed │ ├── speed-i18n.js │ ├── speed.css │ ├── speed.js │ ├── speed.min.css │ └── speed.min.js ├── stop │ ├── stop-i18n.js │ ├── stop.css │ ├── stop.js │ ├── stop.min.css │ ├── stop.min.js │ └── stop.svg └── vrview │ ├── cardboard.png │ ├── cardboard.svg │ ├── vrview.css │ ├── vrview.js │ ├── vrview.min.css │ └── vrview.min.js ├── mediaelement ├── background.png ├── bigplay-divoc.svg ├── bigplay.fw.png ├── bigplay.png ├── bigplay.svg ├── jumpforward.png ├── loading-divoc.gif ├── loading-rc3.gif ├── loading.gif ├── mejs-controls.png ├── mejs-controls.svg └── skipback.png └── stylesheets ├── .keep ├── mediaelementplayer.scss └── timelens.css /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | eslint: 5 | enabled: true 6 | csslint: 7 | enabled: true 8 | duplication: 9 | enabled: false 10 | ratings: 11 | paths: 12 | - app/** 13 | - lib/** 14 | - "**.rb" 15 | - "**.js" 16 | - "**.css" 17 | exclude_paths: 18 | - spec/**/* 19 | - vendor/**/* 20 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | 4 | Dockerfile 5 | docker-compose.yml 6 | 7 | *.txt 8 | *.md 9 | 10 | docker/ 11 | log/ 12 | test/ 13 | tmp/ 14 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | SECRET_KEY_BASE=abcde 2 | DEVISE_SECRET_KEY=12345 3 | DEVISE_FROM=test@example.org 4 | SMTP_HOST=localhost 5 | STREAMING_URL=https://streaming.media.ccc.de/streams/v2.json 6 | RELIVE_URL=http://relive.c3voc.de/relive/index.json 7 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | SECRET_KEY_BASE=abcde 2 | DEVISE_SECRET_KEY=12345 3 | DEVISE_FROM=test@example.org 4 | SMTP_HOST=localhost 5 | STREAMING_URL=file:///test/fixtures/streaming.json 6 | RELIVE_URL= 7 | TZ=UTC 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: elasticsearch-model 10 | versions: 11 | - ">= 7.a, < 8" 12 | - dependency-name: sidekiq 13 | versions: 14 | - ">= 6.1.a, < 6.2" 15 | - dependency-name: coffee-rails 16 | versions: 17 | - 5.0.0 18 | - dependency-name: listen 19 | versions: 20 | - 3.4.1 21 | - dependency-name: sdoc 22 | versions: 23 | - 2.0.4 24 | - dependency-name: goldiloader 25 | versions: 26 | - 4.0.0 27 | - dependency-name: exception_notification 28 | versions: 29 | - 4.4.3 30 | - dependency-name: graphql 31 | versions: 32 | - 1.12.3 33 | - dependency-name: bullet 34 | versions: 35 | - 6.1.3 36 | - dependency-name: foreman 37 | versions: 38 | - 0.87.2 39 | - package-ecosystem: "github-actions" 40 | directory: "/" 41 | schedule: 42 | interval: "monthly" 43 | -------------------------------------------------------------------------------- /.github/graphql-inspector.yaml: -------------------------------------------------------------------------------- 1 | branch: 'master' 2 | schema: 'schema.graphql' 3 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | require "capistrano/scm/git" 7 | install_plugin Capistrano::SCM::Git 8 | 9 | # Include tasks from other gems included in your Gemfile 10 | require 'capistrano/rvm' 11 | require 'capistrano/bundler' 12 | require 'capistrano/rails/assets' 13 | require 'capistrano/rails/migrations' 14 | require 'capistrano/sidekiq' 15 | require 'airbrussh/capistrano' 16 | require 'mqtt' 17 | 18 | require 'dotenv' 19 | Dotenv.load 20 | 21 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 22 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma 2 | jobs: bundle exec sidekiq 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | MediaBackend::Application.load_tasks 7 | -------------------------------------------------------------------------------- /Vagrant.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Setup Vagrant Development Server 4 | 5 | ``` 6 | $ sudo apt-get install vagrant virtualbox 7 | 8 | # or add media.ccc.vm to /etc/hosts instead 9 | $ vagrant plugin install vagrant-hostsupdater 10 | ``` 11 | 12 | Start VM and download live data 13 | 14 | ``` 15 | $ vagrant up 16 | $ vagrant ssh -c 'cd /vagrant && ./bin/update-data' 17 | ``` 18 | 19 | * http://media.ccc.vm:3000/ <- Frontend 20 | * http://media.ccc.vm:3000/admin/ <- Backend 21 | Username: admin@example.org 22 | Password: media123 23 | 24 | Running tests: 25 | ``` 26 | $ vagrant ssh 27 | $ rails test 28 | ``` 29 | -------------------------------------------------------------------------------- /app/admin/admin_user.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register AdminUser do 2 | menu :parent => "Misc" 3 | config.comments = false 4 | 5 | index do 6 | column :email 7 | column :current_sign_in_at 8 | column :last_sign_in_at 9 | column :sign_in_count 10 | actions 11 | end 12 | 13 | filter :email 14 | 15 | form do |f| 16 | f.inputs "Admin Details" do 17 | f.input :email 18 | f.input :password 19 | f.input :password_confirmation 20 | end 21 | f.actions 22 | end 23 | 24 | controller do 25 | def permitted_params 26 | params.permit admin_user: [:email, :password, :password_confirmation] 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/admin/api_key.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register ApiKey do 2 | menu :parent => "Misc" 3 | 4 | index do 5 | column :key 6 | column :description 7 | column :created_at 8 | actions 9 | end 10 | 11 | form do |f| 12 | f.inputs "API Key Details" do 13 | f.input :description 14 | end 15 | f.actions 16 | end 17 | 18 | controller do 19 | def permitted_params 20 | params.permit api_key: [:description] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/admin/news.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register News do 2 | menu :parent => "Misc" 3 | 4 | index do 5 | column :date 6 | column :title 7 | column :body 8 | column :updated_at 9 | column :created_at 10 | actions 11 | end 12 | 13 | form do |f| 14 | f.inputs "News Details" do 15 | f.input :date 16 | f.input :title 17 | f.input :body #, input_html: { class: 'tinymce' } 18 | end 19 | f.actions 20 | end 21 | 22 | controller do 23 | def permitted_params 24 | params.permit news: [:date, :title, :body] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | //= link graphiql/rails/application.js 5 | //= link graphiql/rails/application.css -------------------------------------------------------------------------------- /app/assets/fonts/GabriellaHeavy.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/GabriellaHeavy.otf -------------------------------------------------------------------------------- /app/assets/fonts/Mona-Sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/Mona-Sans.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Mona-Sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/Mona-Sans.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/VCROCDFaux.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/VCROCDFaux.ttf -------------------------------------------------------------------------------- /app/assets/fonts/VCROCDFaux.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/VCROCDFaux.woff -------------------------------------------------------------------------------- /app/assets/fonts/VCROCDFaux.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/fonts/VCROCDFaux.woff2 -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/bxslider/bx_loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/bxslider/bx_loader.gif -------------------------------------------------------------------------------- /app/assets/images/bxslider/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/bxslider/controls.png -------------------------------------------------------------------------------- /app/assets/images/frontend/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/frontend/.gitkeep -------------------------------------------------------------------------------- /app/assets/images/frontend/feed-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/frontend/feed-banner.png -------------------------------------------------------------------------------- /app/assets/images/frontend/promoted_bg-rc3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/frontend/promoted_bg-rc3.jpg -------------------------------------------------------------------------------- /app/assets/images/frontend/promoted_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/assets/images/frontend/promoted_bg.jpg -------------------------------------------------------------------------------- /app/assets/javascripts/activate-timelens.js: -------------------------------------------------------------------------------- 1 | $(document).on("turbolinks:load", function () { 2 | $(".timelens:empty").each(function () { 3 | const t = this; 4 | timelens(this, { 5 | timeline: this.dataset.timeline, 6 | thumbnails: this.dataset.thumbnails, 7 | seek: function (position) { 8 | location.href = 9 | "/v/" + t.dataset.slug + "#t=" + Math.round(position); 10 | }, 11 | lazyThumbnails: this.dataset.lazy 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js.coffee: -------------------------------------------------------------------------------- 1 | #= require active_admin/base 2 | #= require tinymce 3 | 4 | $(document).ready -> 5 | $('textarea.tinymce').prev('label').css('float', 'none') 6 | tinyMCE.init 7 | selector: 'textarea.tinymce' 8 | plugins: 'link autolink lists paste help wordcount code' 9 | menubar: false 10 | toolbar: 'undo redo | styleselect | bold italic | link openlink | bullist outdent indent | removeformat code help' 11 | relative_urls: false 12 | convert_urls: false 13 | remove_script_host: false 14 | theme: 'modern' 15 | width: '70%' 16 | height: '200' 17 | return 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/feed_toggle.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | function isSufficientlyLargeScreen() { 3 | if (! window.matchMedia) { 4 | return false; 5 | } 6 | 7 | return window.matchMedia('(min-width: 600px)').matches; 8 | } 9 | 10 | $('#feedMenu').on('click', function(event) { 11 | event.stopImmediatePropagation(); 12 | var $feedMenu = $('#feedMenu'); 13 | if (isSufficientlyLargeScreen()) { 14 | $('#feedMenuMobile').hide(); 15 | $feedMenu.dropdown('toggle'); 16 | } 17 | else { 18 | $feedMenu.parent().removeClass('open'); 19 | $("#feedMenuMobile").slideToggle("fast"); 20 | } 21 | 22 | return false; 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/mastodon-share.js: -------------------------------------------------------------------------------- 1 | function mastodonShare(text, url) { 2 | const enteredDomain = prompt("Please enter the domain of your mastodon instance, e.g. chaos.social")?.trim(); 3 | if (!enteredDomain) return; 4 | 5 | const domainURL = "https://" + enteredDomain.replace("https://", ""); 6 | 7 | const shareURL = new URL(domainURL); 8 | shareURL.pathname = "/share"; 9 | if (text) shareURL.searchParams.set("text", text); 10 | if (url) shareURL.searchParams.set("url", url); 11 | 12 | const windowHandle = window.open(shareURL, "_blank"); 13 | if (windowHandle) windowHandle.focus(); 14 | } -------------------------------------------------------------------------------- /app/assets/javascripts/mirrorbrain-fix.js: -------------------------------------------------------------------------------- 1 | var MirrorbrainFix = { 2 | selectMirror: function(url, cb) { 3 | // Always request CDN via https 4 | url = url.replace(/^http:/, 'https:'); 5 | //console.log('asking cdn for first mirror of', url); 6 | return $.ajax({ 7 | url: url, 8 | dataType: 'json', 9 | success: function(dom) { 10 | var mirror = dom.MirrorList[0].HttpURL + dom.FileInfo.Path; 11 | //console.log('using mirror', mirror); 12 | cb(mirror); 13 | } 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/oembed-player.js: -------------------------------------------------------------------------------- 1 | //= require replacehash 2 | //= require jquery 3 | //= require mirrorbrain-fix 4 | //= require mediaelement-and-player 5 | 6 | //= require source-chooser/source-chooser 7 | //= require speed/speed 8 | //= require skip-back/skip-back 9 | //= require jump-forward/jump-forward 10 | 11 | //= require timelens 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/replacehash.js: -------------------------------------------------------------------------------- 1 | // Closure to protect local variable "var hash" 2 | (function(ns) { 3 | 4 | ns.replaceHash = function(newhash) { 5 | if ((''+newhash).charAt(0) !== '#') 6 | newhash = '#' + newhash; 7 | 8 | if(window.history && window.history.replaceState) { 9 | history.replaceState({url: window.location.href}, '', newhash); 10 | } 11 | else 12 | { 13 | window.location.hash = newhash; 14 | } 15 | } 16 | 17 | })(window.location); 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/theme.js: -------------------------------------------------------------------------------- 1 | let currentTheme = localStorage.getItem('color-theme') || 'system'; 2 | 3 | function toggleTheme(newTheme) { 4 | localStorage.setItem('color-theme', newTheme); 5 | document.documentElement.classList.toggle('dark', newTheme === 'dark'); 6 | document.documentElement.classList.toggle('light', newTheme === 'light'); 7 | currentTheme = newTheme; 8 | } 9 | toggleTheme(currentTheme); 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require frontend/styles 13 | *= require icomoon-font 14 | *= require jquery.bxslider 15 | *= require mediaelementplayer 16 | 17 | *= require timelens 18 | *= require timelens-custom 19 | 20 | *= require source-chooser/source-chooser 21 | *= require speed/speed 22 | *= require postroll/postroll 23 | *= require skip-back/skip-back 24 | *= require jump-forward/jump-forward 25 | *= require chromecast/chromecast 26 | *= require airplay/airplay 27 | *= require playlist/playlist 28 | * when adding plugins here, also add them to embed.css.scss 29 | 30 | *= require player-fixes 31 | 32 | */ 33 | -------------------------------------------------------------------------------- /app/assets/stylesheets/embed.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require_self 3 | *= require mediaelementplayer 4 | 5 | *= require source-chooser/source-chooser 6 | *= require speed/speed 7 | *= require skip-back/skip-back 8 | *= require jump-forward/jump-forward 9 | 10 | *= require timelens 11 | 12 | *= require player-fixes 13 | */ 14 | 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/base/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | overflow: hidden; 5 | overflow-y: scroll; 6 | 7 | /* poor support - but for the future! */ 8 | hyphens: auto; 9 | -webkit-hyphens: auto; 10 | -moz-hyphens: auto; 11 | -ms-hyphens: auto; 12 | } 13 | 14 | body { 15 | padding-top: $navbar-height; 16 | 17 | @media #{$xs-or-smaller} { 18 | padding-top: 0; 19 | } 20 | 21 | display: flex; 22 | flex-direction: column; 23 | 24 | min-height: 100%; 25 | width: 100%; 26 | overflow: hidden; 27 | overflow-y: initial; 28 | 29 | > main { 30 | flex-grow: 1; 31 | } 32 | } 33 | 34 | .container, .container-fluid { 35 | max-width: $max-width; 36 | } 37 | 38 | .btn-secondary { 39 | @include button-variant(white, $gray, $gray-dark); 40 | } 41 | 42 | .alert { 43 | border-radius: .5em; 44 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/base/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | background-color: var(--surface-300); 3 | font-size: 16px; 4 | padding: 0; 5 | margin-bottom: 0; 6 | 7 | ol { 8 | max-width: $max-width; 9 | padding: $padding-small-vertical $padding-large-horizontal; 10 | margin-bottom: 0; 11 | 12 | li + li:before { 13 | content: ''; 14 | display: none; 15 | } 16 | 17 | .icon { 18 | font-size: 9px; 19 | color: var(--text-secondary); 20 | padding: 0 3px; 21 | } 22 | 23 | a { 24 | color: var(--text-primary); 25 | } 26 | } 27 | 28 | > .active { 29 | color: var(--text-primary); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/base/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | padding: 0.7em; 3 | margin-top: 2em; 4 | color: var(--text-secondary); 5 | background-color: var(--surface-200); 6 | text-align: center; 7 | 8 | a:any-link { 9 | color: var(--text-primary); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/base/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border: none; 3 | 4 | @media #{$xs-or-smaller} { 5 | position: initial; 6 | 7 | & > .container-fluid { 8 | display: inline-table; 9 | width: 100%; 10 | } 11 | form { 12 | width: 100%; 13 | } 14 | .input-group { 15 | margin-bottom: $padding-small-vertical; 16 | } 17 | } 18 | 19 | .navbar-brand { 20 | padding: 9px 15px; 21 | img { 22 | height: 33px; 23 | } 24 | } 25 | 26 | .btn.btn-default { 27 | /* the button is always white, so no theme-dependent color is needed here */ 28 | color: var(--neutral-800); 29 | 30 | padding: 0.2em; 31 | height: 1.8em; 32 | max-width: 2em; 33 | } 34 | 35 | .navbar-form { 36 | padding-left: 0.2em; 37 | margin-top: 0.8em; 38 | margin-bottom: 0.8em; 39 | 40 | &.compact { 41 | padding-right: 0.0em; 42 | } 43 | } 44 | 45 | input { 46 | height: 1.8em; 47 | padding-top: 6px; 48 | } 49 | 50 | .icon { 51 | min-width: 20px; 52 | font-size: 1.3em; 53 | display: inline-block; 54 | } 55 | } 56 | 57 | .navbar-default { 58 | background-color: var(--surface-200); 59 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/base/_typography.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: $gray-dark; 3 | margin-top: 60px; 4 | line-height: 0.9em; 5 | 6 | @media #{$xs-or-smaller} { 7 | font-size: 34px; 8 | margin-top: 30px; 9 | } 10 | } 11 | 12 | h1 + h2 { 13 | margin-top: -0.2em; 14 | } 15 | 16 | h2 { 17 | margin-top: 1.2em; 18 | 19 | @media #{$xs-or-smaller} { 20 | font-size: 23px; 21 | } 22 | } 23 | 24 | p { 25 | margin-bottom: 0.5em; 26 | } 27 | 28 | a.inverted { 29 | color: var(--text-inverted); 30 | } 31 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/pages/_page_not_found.scss: -------------------------------------------------------------------------------- 1 | body.page-not-found { 2 | form.search { 3 | width: 50%; 4 | 5 | .input-group-btn button { 6 | border-color: $input-border; 7 | height: 26px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/shared/_audioplayer.scss: -------------------------------------------------------------------------------- 1 | audio.audio { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/shared/_label.scss: -------------------------------------------------------------------------------- 1 | a.label { 2 | display: inline-block; 3 | 4 | border-radius: $border-radius-base; 5 | 6 | padding-top: 0.3em; 7 | padding-bottom: 0.1em; 8 | 9 | font-size: 14px; 10 | font-weight: normal; 11 | text-decoration:none; 12 | 13 | margin-right: 2px; 14 | margin-bottom: $padding-small-vertical; 15 | 16 | &.label-default { 17 | background-color: var(--neutral-700); 18 | /* white color here as the background is always dark */ 19 | color: white; 20 | 21 | &:hover, &:focus, &:active { 22 | background-color: var(--neutral-600); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontend/styles.scss: -------------------------------------------------------------------------------- 1 | // library setup 2 | @import "lib/fonts"; 3 | @import "lib/variables"; 4 | @import "bootstrap-sprockets"; 5 | @import "bootstrap"; 6 | 7 | // general page layout 8 | @import "base/typography"; 9 | @import "base/theme"; 10 | @import "base/base"; 11 | @import "base/navbar"; 12 | @import "base/feeds_dropdown"; 13 | @import "base/breadcrumb"; 14 | @import "base/footer"; 15 | 16 | // shared components 17 | @import "shared/promoted"; 18 | @import "shared/videoplayer"; 19 | @import "shared/audioplayer"; 20 | @import "shared/label"; 21 | @import "shared/eventpreview_meta"; 22 | 23 | // pages 24 | @import "pages/start"; 25 | @import "pages/page_not_found"; 26 | @import "pages/list"; 27 | @import "pages/browse"; 28 | @import "pages/show"; 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/player-fixes.css: -------------------------------------------------------------------------------- 1 | .mejs__captions-layer { 2 | line-height: 1.15em !important; 3 | font-size: 2em !important; 4 | } 5 | 6 | .postroll { 7 | width: 100% !important; 8 | height: 100% !important; 9 | padding: 1%; 10 | color: #fff; 11 | } 12 | .postroll .video-thumbnail { 13 | width: 80%; 14 | } 15 | .postroll .row { 16 | padding-bottom: 1.5em; 17 | } 18 | 19 | .playback_rate { 20 | margin-top: 9px !important; 21 | } 22 | 23 | @media all and (min-width: 550px) { 24 | .postroll h4 { 25 | height: 30%; 26 | } 27 | } 28 | @media all and (max-width: 550px) { 29 | .postroll h4 { 30 | height: 10%; 31 | } 32 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/timelens-custom.css: -------------------------------------------------------------------------------- 1 | .caption .timelens { 2 | height: 2em; 3 | cursor: pointer; 4 | } 5 | 6 | /* Undo Timelens styles from timelens.css for Clappr players inside of relive-player classes. 7 | * 8 | * To avoid this, we would need to set it up in a way so that timelens.css is only loaded for pages 9 | * with a Clappr player where a visual timeline is desired. */ 10 | 11 | .relive-player .media-control .bar-background { 12 | height: 1px !important; 13 | margin-top: 0 !important; 14 | box-shadow: none !important; 15 | } 16 | 17 | .relive-player .media-control .seek-time, 18 | .relive-player .media-control .bar-scrubber, 19 | .relive-player .media-control .bar-hover { 20 | display: block !important; 21 | } 22 | 23 | .relive-player .media-control-hide { 24 | bottom: 0 !important; 25 | } 26 | -------------------------------------------------------------------------------- /app/backstage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: voctoweb 6 | description: | 7 | Voctoweb is a rails application that provides a “YouTube like” user interface to video, audio and pdf files; a meta data editor; and APIs. For more infomation about relations to other components see c3voc Wiki. 8 | annotations: 9 | github.com/project-slug: voc/voctoweb 10 | spec: 11 | type: website 12 | lifecycle: production 13 | owner: media 14 | system: voctoweb 15 | providesApis: 16 | - voctoweb-public-api 17 | - voctoweb-graphql-api 18 | - voctoweb-private-rest-api 19 | consumesApis: 20 | - streaming-website-api -------------------------------------------------------------------------------- /app/controllers/api/backstage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: API 4 | metadata: 5 | name: voctoweb-private-rest-api 6 | description: | 7 | The private API is used by our (video) production teams. They manage the content by adding new conferences, events and other files (so called recordings). All API calls need to use the JSON format. An example API client can be found as part of our publishing-script repository: https://github.com/voc/publishing/ . The api_key has to be added as query variable, or in the JSON request body. 8 | Most REST operations work as expected. Examples for resource creation are listed on the applications dashboard page. 9 | spec: 10 | type: openapi 11 | lifecycle: production 12 | owner: media 13 | system: voctoweb 14 | definition: | 15 | {} -------------------------------------------------------------------------------- /app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiController < ApplicationController 2 | include ApiErrorResponses 3 | before_action :deny_json_request, if: :ssl_configured? 4 | before_action :authenticate_api_key! 5 | respond_to :json 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | protected 7 | 8 | def deny_request 9 | render :file => 'public/403.html', :status => :forbidden, :layout => false 10 | end 11 | 12 | def deny_json_request 13 | render json: { errors: 'ssl required' }, :status => :forbidden 14 | end 15 | 16 | def ssl_configured? 17 | Rails.env.production? and not request.ssl? 18 | end 19 | 20 | def authenticate_api_key! 21 | if params['api_key'] 22 | keys = ApiKey.find_by(key: params['api_key']) 23 | else 24 | authenticate_with_http_token do |token, options| 25 | keys = ApiKey.find_by(key: token) 26 | end 27 | end 28 | render json: { errors: 'No or invalid API key. Please add "Authorization: Token token=xxx" header or api_key=xxx param in URL or JSON request body.' }, :status => :forbidden if keys.nil? 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/throttle_connections.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module ThrottleConnections 4 | extend ActiveSupport::Concern 5 | 6 | def throttle?(recording_view) 7 | return false if Rails.env.test? 8 | 9 | Rails.cache.exist?(cache_key(recording_view)) 10 | end 11 | 12 | def add_throttling(recording_view) 13 | Rails.cache.write(cache_key(recording_view), true, expires_in: 12.hours, race_condition_ttl: 5) 14 | end 15 | 16 | private 17 | 18 | def cache_key(recording_view) 19 | ['throttle', recording_view.recording.event_id, recording_view.recording.filename, recording_view.identifier] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/frontend/home_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class HomeController < FrontendController 3 | helper_method :recent_events_for_conference 4 | CONFERENCE_LIMIT = 9 5 | EVENT_LIMIT = 3 6 | 7 | def index 8 | @news = Frontend::News.recent(1).first 9 | @hours_count = Frontend::Event.sum(:duration)/(60*60) 10 | @recordings_count = Frontend::Recording.count 11 | @events_count = Frontend::Event.count 12 | @conferences_count = Frontend::Conference.count 13 | 14 | @recent_conferences = Frontend::Conference.with_recent_events(CONFERENCE_LIMIT) 15 | 16 | @currently_streaming = Frontend::Conference.currently_streaming 17 | 18 | respond_to { |format| format.html } 19 | end 20 | 21 | def page_not_found 22 | respond_to do |format| 23 | format.json { head :no_content } 24 | format.xml { render xml: { status: :error } } 25 | format.all { render :page_not_found, status: 404, slug: params[:slug] } 26 | end 27 | end 28 | 29 | private 30 | 31 | def recent_events_for_conference(conference) 32 | conference.events.released.includes(:conference).limit(EVENT_LIMIT) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/frontend/news_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class NewsController < FrontendController 3 | include ActionView::Helpers::AssetUrlHelper 4 | 5 | def index 6 | news = News.latest_first 7 | atom_feed = Feeds::NewsFeedGenerator.generate(news, 8 | options: { 9 | author: Settings.feeds[:channel_owner], 10 | title: I18n.t('custom.news_title'), 11 | feed_url: news_url, 12 | icon: File.join(Settings.frontend_url, 'favicon.ico'), 13 | logo: image_url('frontend/voctocat.svg') 14 | }) 15 | respond_to do |format| 16 | format.xml { render xml: atom_feed } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/frontend/popular_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class PopularController < FrontendController 3 | PAGE_SIZE = 20 4 | 5 | def index 6 | @year = params[:year].to_i 7 | @page = params[:page].to_i || 0 8 | @firstyear = Frontend::Event.order('date ASC').limit(1).first.date.year 9 | if @year === 0 10 | @events = Frontend::Event.order('view_count DESC') 11 | .limit(PAGE_SIZE) 12 | .offset(@page * PAGE_SIZE) 13 | .includes(:conference) 14 | else 15 | @events = Frontend::Event.where('date between ? and ?', "#{@year}-01-01", "#{@year}-12-31") 16 | .order('view_count DESC') 17 | .limit(PAGE_SIZE) 18 | .offset(@page * PAGE_SIZE) 19 | .includes(:conference) 20 | end 21 | 22 | respond_to { |format| format.html } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/frontend/recent_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class RecentController < FrontendController 3 | def index 4 | @events = Frontend::Event.recent(20).includes(:conference) 5 | 6 | respond_to { |format| format.html } 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/frontend/search_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class SearchController < FrontendController 3 | def index 4 | @searchtype = '' 5 | @searchquery = params[:q] || params[:p] 6 | 7 | # reqular search by keyword 8 | if params[:q] 9 | # query the index 10 | result_set = Frontend::Event.query(params[:q], params[:sort]) 11 | # paginate the results 12 | @events = result_set.page(params[:page]).records 13 | # search for a person by string 14 | else 15 | @searchtype = 'person' 16 | # query the index 17 | result_set = Frontend::Event.query_persons(params[:p], params[:sort]) 18 | # paginate the results 19 | @events = result_set.page(params[:page]).records 20 | end 21 | 22 | # display the total number of found results in the view 23 | @number_of_results = result_set.results.total 24 | 25 | respond_to { |format| format.html } 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/frontend/sitemap_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class SitemapController < FrontendController 3 | def index 4 | @base_url = Settings.frontend_url 5 | respond_to { |format| format.xml } 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/frontend/tags_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class TagsController < FrontendController 3 | def show 4 | @tag = params[:tag] 5 | raise ActiveRecord::RecordNotFound unless @tag 6 | 7 | # TODO native postgresql query? 8 | @events = Frontend::Event.all.select { |event| event.tags.include? @tag } 9 | respond_to { |format| format.html } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/frontend/unpopular_controller.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class UnpopularController < FrontendController 3 | PAGE_SIZE = 50 4 | 5 | def index 6 | @year = params[:year].to_i 7 | @page = params[:page].to_i || 0 8 | @firstyear = Frontend::Event.order('date ASC').limit(1).first.date.year 9 | @events = if @year.zero? 10 | Frontend::Event.order('view_count ASC') 11 | .limit(PAGE_SIZE) 12 | .offset(@page * PAGE_SIZE) 13 | .includes(:conference) 14 | else 15 | Frontend::Event.unpopular(@year) 16 | .limit(PAGE_SIZE) 17 | .offset(@page * PAGE_SIZE) 18 | .includes(:conference) 19 | end 20 | 21 | respond_to { |format| format.html } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/frontend_controller.rb: -------------------------------------------------------------------------------- 1 | class FrontendController < ActionController::Base 2 | layout 'frontend/frontend' 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/public/backstage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: API 4 | metadata: 5 | name: voctoweb-public-api 6 | description: | 7 | The public API provides a programmatic access to the data behind media.ccc.de. Consumers of this API are typically player apps for different ecosystems, see https://media.ccc.de/about.html#apps for a 'full' list. The whole API is "discoverable" starting from https://api.media.ccc.de/public/conferences 8 | annotations: 9 | github.com/project-slug: voc/voctoweb 10 | spec: 11 | type: openapi 12 | lifecycle: production 13 | owner: media 14 | system: voctoweb 15 | definition: | 16 | {} -------------------------------------------------------------------------------- /app/controllers/public/conferences_controller.rb: -------------------------------------------------------------------------------- 1 | module Public 2 | class ConferencesController < ActionController::Base 3 | include ApiErrorResponses 4 | respond_to :json 5 | 6 | # GET /public/conferences 7 | # GET /public/conferences.json 8 | def index 9 | key = Conference.all.maximum(:updated_at) 10 | @conferences = Rails.cache.fetch([:public, :conferences, key], race_condition_ttl: 10) do 11 | Conference.all 12 | end 13 | respond_to { |format| format.json } 14 | end 15 | 16 | # GET /public/conferences/54 17 | # GET /public/conferences/54.json 18 | # GET /public/conferences/31c3 19 | # GET /public/conferences/31c3.json 20 | def show 21 | if params[:id] =~ /\A[0-9]+\z/ 22 | @conference = Conference.find(params[:id]) 23 | else 24 | @conference = Conference.find_by(acronym: params[:id]) 25 | end 26 | fail ActiveRecord::RecordNotFound unless @conference 27 | 28 | respond_to { |format| format.json } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/graphql/backstage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: API 4 | metadata: 5 | name: voctoweb-graphql-api 6 | description: | 7 | The newest API endpoint is at https://media.ccc.de/graphql, implementing a GraphQL endpoint with Apollo Federation. This allows clients to only request the attributes they need, while all data needed per screen can be fetched in a single request. We tried to clean up the type names and call talks lecture and files resources (previously known as recordings). Please create issues if you are missing anything. 8 | spec: 9 | type: graphql 10 | lifecycle: experimental 11 | owner: media 12 | system: voctoweb 13 | definition: | 14 | {} -------------------------------------------------------------------------------- /app/graphql/media_backend_schema.rb: -------------------------------------------------------------------------------- 1 | class MediaBackendSchema < GraphQL::Schema 2 | include ApolloFederation::Schema 3 | use ApolloFederation::Tracing 4 | 5 | max_depth 13 6 | query(Types::QueryType) 7 | #mutation(Types::MutationType) 8 | end 9 | -------------------------------------------------------------------------------- /app/graphql/mutations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/graphql/mutations/.keep -------------------------------------------------------------------------------- /app/graphql/resolvers/search_lectures.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::SearchLectures < GraphQL::Schema::Resolver 2 | type [Types::LectureType], null: false 3 | 4 | argument :query, String, required: true 5 | argument :page, Integer, required: false 6 | 7 | def resolve(query:, page: 1) 8 | results = Frontend::Event.query(query).page(page) 9 | @events = results.records.includes(recordings: :conference) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/graphql/types/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/graphql/types/.keep -------------------------------------------------------------------------------- /app/graphql/types/base_enum.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseEnum < GraphQL::Schema::Enum 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/graphql/types/base_field.rb: -------------------------------------------------------------------------------- 1 | require 'apollo-federation' 2 | 3 | module Types 4 | class BaseField < GraphQL::Schema::Field 5 | include ApolloFederation::Field 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/base_input_object.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseInputObject < GraphQL::Schema::InputObject 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/graphql/types/base_interface.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | module BaseInterface 3 | include GraphQL::Schema::Interface 4 | include ApolloFederation::Interface 5 | 6 | field_class BaseField 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/graphql/types/base_object.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseObject < GraphQL::Schema::Object 3 | include ApolloFederation::Object 4 | 5 | field_class BaseField 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/base_scalar.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseScalar < GraphQL::Schema::Scalar 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/graphql/types/base_union.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseUnion < GraphQL::Schema::Union 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/graphql/types/date_time_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class DateTimeType < GraphQL::Types::ISO8601DateTime 3 | graphql_name 'DateTime' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/json_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class JsonType < GraphQL::Types::String 3 | description "A untyped JSON document" 4 | graphql_name 'JSON' 5 | 6 | def self.coerce_input(input_value, context) 7 | # TODO check if input_value is already pared or not 8 | data = JSON.parse(input_value) 9 | if data.nil? 10 | raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid JSON" 11 | end 12 | 13 | # It's valid, return the URI object 14 | data 15 | end 16 | 17 | def self.coerce_result(ruby_value, context) 18 | ruby_value 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class MutationType < Types::BaseObject 3 | # TODO: remove me 4 | field :test_field, String, null: false, 5 | description: "An example field added by the generator" 6 | def test_field 7 | "Hello World" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/types/url_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class UrlType < GraphQL::Types::String 3 | description "A valid URL, transported as a string" 4 | graphql_name 'URL' 5 | 6 | def self.coerce_input(input_value, context) 7 | # Parse the incoming object into a `URI` 8 | url = URI.parse(input_value) 9 | if url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS) 10 | # It's valid, return the URI object 11 | url 12 | else 13 | raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid URL" 14 | end 15 | end 16 | 17 | def self.coerce_result(ruby_value, context) 18 | # It's transported as a string, so stringify it 19 | ruby_value.to_s 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | require "active_support/number_helper" 2 | 3 | module ApplicationHelper 4 | def line_break_filename(filename) 5 | if filename.present? 6 | filename.gsub(/_/, "_\n") 7 | else 8 | '' 9 | end 10 | end 11 | def human_readable_views_count(count) 12 | if count < 1000 13 | "#{count}" 14 | else 15 | "#{(count / 1000.0).round(1)}k" 16 | end 17 | end 18 | def delimited_views_count(count) 19 | ActiveSupport::NumberHelper.number_to_delimited(count) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/helpers/frontend/event_recording_filter_high_quality.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class EventRecordingFilterHighQuality < EventRecordingFilter 3 | def filter_by_quality(recordings) 4 | recordings.sort { 5 | |recording_a,recording_b| recording_b.number_of_pixels - recording_a.number_of_pixels 6 | } 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/frontend/event_recording_filter_low_quality.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class EventRecordingFilterLowQuality < EventRecordingFilter 3 | def filter_by_quality(recordings) 4 | recordings 5 | .select { |recording| recording.height && recording.height.to_i < 720 } 6 | .sort { |recording_a,recording_b| recording_b.number_of_pixels - recording_a.number_of_pixels } 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/frontend/event_recording_filter_master.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class EventRecordingFilterMaster < EventRecordingFilter 3 | def filter_by_quality(recordings) 4 | recordings 5 | .sort { |recording_a,recording_b| recording_b.number_of_pixels - recording_a.number_of_pixels } 6 | .sort { |recording_a,recording_b| recording_b.language.length - recording_b.language.length } 7 | .select { |recording| recording.slides? != true } 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/frontend/feed_quality.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class FeedQuality 3 | HQ = 'hq' 4 | LQ = 'lq' 5 | MASTER = 'master' 6 | 7 | def self.all 8 | [HQ, LQ, MASTER] 9 | end 10 | 11 | def self.valid?(quality) 12 | all.include?(quality) 13 | end 14 | 15 | def self.display_name(quality) 16 | case quality&.downcase 17 | when HQ 18 | 'high quality' 19 | when LQ 20 | 'low quality' 21 | when MASTER 22 | 'master' 23 | else 24 | '' 25 | end 26 | end 27 | 28 | def self.event_recording_filter(quality) 29 | case quality&.downcase 30 | when HQ then EventRecordingFilterHighQuality.new 31 | when LQ then EventRecordingFilterLowQuality.new 32 | when MASTER then EventRecordingFilterMaster.new 33 | else raise ActiveRecord::RecordNotFound, "Invalid quality argument: #{quality}" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/helpers/frontend/feeds_helper.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | module FeedsHelper 3 | include ::Feeds::Helper 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/frontend/tagging_helper.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | # Set the tags attribute on items to use this helper 3 | module TaggingHelper 4 | # link to tag page 5 | def link_for_global(tag, css: '') 6 | %[] 7 | end 8 | 9 | def link_for(conference, tag, css: '') 10 | %[] 11 | end 12 | 13 | def tag_cloud 14 | return [] if @tags.empty? 15 | 16 | tags_hash.map { |tag, count| 17 | link_for tag, css: css_class_by_size(count) 18 | } 19 | end 20 | 21 | private 22 | 23 | def css_class_by_size(n) 24 | if n < 5 25 | "xtiny" 26 | elsif n < 10 27 | "tiny" 28 | elsif n < 50 29 | "normal" 30 | elsif n < 100 31 | "large" 32 | else 33 | "xlarge" 34 | end 35 | end 36 | 37 | def tags_hash 38 | tags = {} 39 | @tags.each do |tag, events| 40 | next unless tag 41 | 42 | tags[tag] = events.count 43 | end 44 | tags 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/helpers/public_json_helper.rb: -------------------------------------------------------------------------------- 1 | module PublicJsonHelper 2 | def frontend_event_url(slug: '') 3 | return event_url(slug: slug) unless Rails.env.production? 4 | 5 | event_url(slug: slug, host: Settings.frontend_host, protocol: Settings.frontend_proto, port: nil) 6 | end 7 | 8 | def json_cached_key(identifier, *models) 9 | key = models.flatten.uniq.map { |m| "#{m.class}#{m.id}=#{m.updated_at.to_i}" }.join(';') 10 | 'js_' + identifier.to_s + Digest::SHA1.hexdigest(key) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/helpers/view_helper.rb: -------------------------------------------------------------------------------- 1 | module ViewHelper 2 | def show_recording_url(recording) 3 | "(#{recording.get_recording_url})" 4 | end 5 | 6 | def show_folder(label: 'Path', path: '/') 7 | return nil if label.empty? 8 | 9 | "#{label} (#{path})" 10 | end 11 | 12 | def show_event_folder(event, filename) 13 | label = event.send(filename) 14 | path = File.join(event.conference.get_images_url, label) 15 | show_folder label: label, path: path 16 | end 17 | 18 | def show_recording_path(recording) 19 | show_folder label: recording.filename, path: recording.get_recording_url 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/models/.keep -------------------------------------------------------------------------------- /app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | class AdminUser < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :token_authenticatable, :confirmable, 4 | # :lockable, :timeoutable and :omniauthable 5 | devise :database_authenticatable, 6 | :recoverable, :rememberable, :trackable, :validatable 7 | 8 | def self.ransackable_attributes(*) 9 | %w[email] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/api_key.rb: -------------------------------------------------------------------------------- 1 | class ApiKey < ApplicationRecord 2 | before_create :generate_guid 3 | 4 | def generate_guid 5 | self.key = SecureRandom.uuid 6 | end 7 | 8 | # keep this in sync with filters in app/admin 9 | def self.ransackable_attributes(*) 10 | %w[key description] 11 | end 12 | 13 | def self.ransackable_associations(*) 14 | [] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/fahrplan_updater.rb: -------------------------------------------------------------------------------- 1 | module FahrplanUpdater 2 | extend ActiveSupport::Concern 3 | include FahrplanParser 4 | 5 | def fill_event_info 6 | return unless conference.downloaded? 7 | 8 | fahrplan = FahrplanParser.new(conference.schedule_xml) 9 | info = fahrplan.event_info_by_guid[guid] 10 | return if info.empty? 11 | 12 | update_event_info(info) 13 | end 14 | 15 | # update event attributes from schedule XML 16 | def update_event_info(info) 17 | self.title = info.delete(:title) 18 | id = info.delete(:id) 19 | self.metadata[:remote_id] = id 20 | # fallback to link schedule url based link generation, when not set in fahrplan 21 | if info.key?('link') 22 | info.delete(:link) 23 | self.link = get_event_url(id) 24 | end 25 | update info 26 | end 27 | 28 | private 29 | 30 | def get_event_url(id) 31 | return unless conference.schedule_url.present? 32 | 33 | conference.schedule_url.sub('schedule.xml', "events/#{id}.html").freeze 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/concerns/recent.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Recent 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | scope :recent, ->(n) { order('created_at desc').limit(n) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/event_view_count.rb: -------------------------------------------------------------------------------- 1 | class EventViewCount < ApplicationRecord 2 | def self.updated_at 3 | first&.last_updated_at || Time.now 4 | end 5 | 6 | def self.touch! 7 | last_updated_at = first_or_create 8 | last_updated_at.update(last_updated_at: Time.now) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/frontend.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | end 3 | -------------------------------------------------------------------------------- /app/models/frontend/news.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class News < ::News 3 | scope :recent, ->(n) { where("now() - date < '150 days'").order('date desc').limit(n) } 4 | 5 | def date_formatted 6 | date.strftime('%d.%m.%Y') 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/frontend/recording.rb: -------------------------------------------------------------------------------- 1 | module Frontend 2 | class Recording < ::Recording 3 | belongs_to :event, class_name: 'Frontend::Event' 4 | scope :by_mime_type, ->(mime_type) { where(mime_type: mime_type) } 5 | 6 | def resolution 7 | return '' unless height 8 | 9 | if height < 720 10 | 'sd' 11 | elsif height < 1080 12 | 'hd' 13 | elsif height < 1716 14 | 'full-hd' 15 | else 16 | '4k' 17 | end 18 | end 19 | 20 | def filetype 21 | MimeType.humanized_mime_type(mime_type) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/news.rb: -------------------------------------------------------------------------------- 1 | class News < ApplicationRecord 2 | scope :latest_first, ->() { order('date desc') } 3 | 4 | validates_presence_of :date 5 | end 6 | -------------------------------------------------------------------------------- /app/models/recording_view.rb: -------------------------------------------------------------------------------- 1 | class RecordingView < ApplicationRecord 2 | validates :recording, presence: true 3 | belongs_to :recording 4 | 5 | def identifier=(val) 6 | secret = Rails.cache.fetch(:recording_view_secret, expires_in: 12.hours, race_condition_ttl: 10) do 7 | SecureRandom.random_bytes(16) 8 | end 9 | self[:identifier] = Digest::SHA1.hexdigest(val + secret) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/web_feed.rb: -------------------------------------------------------------------------------- 1 | class WebFeed < ApplicationRecord 2 | FEEDS_TIMESPAN = 1.years 3 | 4 | validates :key, :last_build, :content, presence: true 5 | 6 | scope :newer, ->(date) { where('last_build > ?', date) } 7 | 8 | def self.last_year 9 | self.round_to_quarter_hour(Time.now.ago(FEEDS_TIMESPAN)) 10 | end 11 | 12 | def self.update_with_lock(time, selector={}) 13 | feed = WebFeed.find_or_create_by(selector) 14 | feed.with_lock do 15 | return if feed.newer?(time) 16 | 17 | feed.last_build = time || Time.now 18 | yield feed 19 | feed.save 20 | end 21 | end 22 | 23 | def self.folder_key(conference, quality, mime_type) 24 | "#{conference.acronym}#{quality}#{mime_type}" 25 | end 26 | 27 | def newer?(date) 28 | return unless last_build && date 29 | 30 | last_build >= date 31 | end 32 | 33 | private 34 | 35 | def self.round_to_quarter_hour(time) 36 | seconds = 15 * 60 37 | Time.at((time.to_f / seconds).floor * seconds) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/views/api/conferences/_fields.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! conference, :id 2 | -------------------------------------------------------------------------------- /app/views/api/conferences/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@conferences) do |conference| 2 | json.partial! 'fields', conference: conference 3 | json.url api_conference_url(conference, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/conferences/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'fields', conference: @conference 2 | -------------------------------------------------------------------------------- /app/views/api/events/_fields.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! event, :id 2 | -------------------------------------------------------------------------------- /app/views/api/events/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@events) do |event| 2 | json.partial! 'fields', event: event 3 | json.url api_event_url(event, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/events/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'fields', event: @event 2 | -------------------------------------------------------------------------------- /app/views/api/recordings/_fields.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! recording, :id 2 | json.public_url recording.url 3 | json.errors recording.errors.to_a unless recording.errors.attribute_names.length 4 | -------------------------------------------------------------------------------- /app/views/api/recordings/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@recordings) do |recording| 2 | json.partial! 'fields', recording: recording 3 | json.url api_recording_url(recording, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/recordings/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'fields', recording: @recording 2 | -------------------------------------------------------------------------------- /app/views/frontend/conferences/browse.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Browse by category 3 | 4 | - content_for :body_class do 5 | page-browse 6 | 7 | %main.container-fluid 8 | %h1 Browse by category 9 | 10 | - if @folders.present? 11 | - @folders.each do |folder| 12 | - if folder.conference? 13 | %a.thumbnail.conference{href: conference_path(acronym: folder.conference.acronym)} 14 | .header 15 | %span.icon.icon-video-camera 16 | = folder.conference.downloaded_events_count 17 | %img{src: folder.conference.logo_url, alt: 'conference logo'} 18 | .caption 19 | = folder.conference.title 20 | - else 21 | %a.thumbnail.folder{href: browse_path(folder.path)} 22 | .header 23 | %span.icon.icon-th 24 | .icon.icon-folder 25 | .caption 26 | = folder.name 27 | -------------------------------------------------------------------------------- /app/views/frontend/conferences/list.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Conferences by date 3 | 4 | - content_for :body_class do 5 | page-browse 6 | 7 | %main.container-fluid 8 | %h1 All conferences and series, recently updated first 9 | 10 | - if @conferences.present? 11 | - @conferences.each do |conference| 12 | %a.thumbnail.conference{href: conference_path(acronym: conference.acronym)} 13 | .header 14 | %span.icon.icon-video-camera 15 | = conference.downloaded_events_count 16 | %img{src: conference.logo_url, alt: 'conference logo'} 17 | .caption 18 | = conference.title 19 | 20 | -------------------------------------------------------------------------------- /app/views/frontend/events/_event.html.haml: -------------------------------------------------------------------------------- 1 | - cache([event.conference, event]) do 2 | .event-preview 3 | = render partial: 'frontend/shared/event_thumb', locals: { event: event } 4 | = render partial: 'frontend/shared/event_metadata', locals: { event: event, show_conference: false } 5 | -------------------------------------------------------------------------------- /app/views/frontend/events/oembed.html.haml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/app/views/frontend/events/oembed.html.haml -------------------------------------------------------------------------------- /app/views/frontend/events/postroll.html.haml: -------------------------------------------------------------------------------- 1 | .postroll 2 | %h4 continue… 3 | .container-fluid 4 | .row 5 | .col-xs-12 6 | %b 7 | Related to 8 | - if @event.link 9 | = link_to @event.title, @event.link 10 | - else 11 | = @event.title 12 | 13 | .row 14 | - @events.each do |event| 15 | .col-xs-4.event 16 | = render partial: 'frontend/shared/event_title', locals: { event: event } 17 | = render partial: 'frontend/shared/event_thumb', locals: { event: event } 18 | -------------------------------------------------------------------------------- /app/views/frontend/home/page_not_found.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | 404 - Page not found 3 | 4 | - content_for :body_class do 5 | page-not-found 6 | 7 | %main.container-fluid 8 | %h1 Page not found 9 | 10 | %form.search{role: 'search', action: '/search/', method: 'get'} 11 | .form-group.input-group 12 | %input.form-control{type: 'search', name: 'q', placeholder: 'Search…', value: @slug} 13 | %span.input-group-btn 14 | %button.btn.btn-default{type: 'submit'} 15 | %span.icon.icon-search 16 | -------------------------------------------------------------------------------- /app/views/frontend/popular/index.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Popular Events 3 | 4 | - content_for :body_class do 5 | page-list 6 | 7 | %main.container-fluid 8 | %h1 Popular Events 9 | -if @year === 0 10 | %b All 11 | - else 12 | %a{:href => popular_path} All 13 | - for year in (@firstyear..Time.new.year).to_a.reverse 14 | %span - 15 | - if @year != year 16 | %a{:href => popular_path + "/#{year}"} #{year} 17 | - else 18 | %b #{year} 19 | - if @events.present? 20 | .event-previews 21 | - @events.each do |event| 22 | = render partial: 'frontend/shared/event_with_conference', locals: { event: event } 23 | %a{:href => "?page=" + "#{@page + 1}" } next 24 | -------------------------------------------------------------------------------- /app/views/frontend/recent/index.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Recent Videos 3 | 4 | - content_for :body_class do 5 | page-list 6 | 7 | %main.container-fluid 8 | %h1 Recent Videos 9 | - if @events.present? 10 | .event-previews 11 | - @events.each do |event| 12 | = render partial: 'frontend/shared/event_with_conference', locals: { event: event } 13 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_event_persons.haml: -------------------------------------------------------------------------------- 1 | - if not persons.empty? 2 | %span.icon{class: persons_icon(persons)} 3 | - persons.to_enum.with_index(1).each do |speaker, index| 4 | - if (persons.count - 1) == index 5 | - delimiter = ' and' 6 | - elsif persons.count != index 7 | - delimiter = ',' 8 | 9 | = succeed delimiter do 10 | %a{href: search_path(p: speaker)}= speaker 11 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_event_thumb.haml: -------------------------------------------------------------------------------- 1 | %a.thumbnail-link{href: event_path(slug: event.slug)} 2 | .thumbnail-badge-container 3 | %img.video-thumbnail{src: h(event.thumb_url), alt: h(event.title), loading: 'lazy'} 4 | - if event.duration > 0 5 | .duration.digits 6 | %span.icon.icon-clock-o 7 | = duration_in_minutes(event.duration) 8 | - if not event.recordings.video.present? and event.relive_present? 9 | .relive 10 | %span.icon.icon-cog{title: 'This is just a stream recording, the final release of this talk is still being worked on.'} 11 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_event_title.html.haml: -------------------------------------------------------------------------------- 1 | %a.title{href: event_path(slug: event.slug), title: event.title.length > 55 ? event.title : ''} 2 | = truncate(event.title, length: 55, separator: ' ', omission: '…') 3 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_event_with_conference.html.haml: -------------------------------------------------------------------------------- 1 | - cache([event.conference, event], expires_in: 2.hours) do 2 | .event-preview.has-conference 3 | = render partial: 'frontend/shared/event_thumb', locals: { event: event } 4 | = render partial: 'frontend/shared/event_metadata', locals: { event: event, show_conference: true } 5 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_folder_feeds.haml: -------------------------------------------------------------------------------- 1 | - conference.mime_type_names do |mime_type, mime_type_name| 2 | - if MimeType.is_video(mime_type) 3 | - feed_url = podcast_folder_video_feed_url(acronym: conference.acronym, mime_type: mime_type_name, quality: 'hq') 4 | - elsif MimeType.is_audio(mime_type) 5 | - feed_url = podcast_folder_feed_url(acronym: conference.acronym, mime_type: mime_type_name) 6 | %link{rel: "alternate", type: "application/rss+xml", title: "Podcast feed #{MimeType.humanized_mime_type(mime_type)} for this folder", href: feed_url} 7 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_footer.haml: -------------------------------------------------------------------------------- 1 | %footer.dark 2 | by 3 | %a.inverted{href: '//ccc.de'} Chaos Computer Club e.V 4 | –– 5 | %a.inverted{href: '/about.html'} About 6 | –– 7 | %a.inverted{href: '/about.html#apps'} Apps 8 | –– 9 | %a.inverted{href: '//ccc.de/en/imprint'} Imprint 10 | –– 11 | %a.inverted{href: '/about.html#privacy'} Privacy 12 | –– 13 | %a.inverted{href: '//c3voc.de/'} c3voc 14 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_live.html.haml: -------------------------------------------------------------------------------- 1 | .promoted.themed-banner.live 2 | %h2= "Live now – " + @currently_streaming.map{|a| a.title}.join(", ") 3 | .titlebar 4 | .slider 5 | / Declare Variable 6 | - first = true 7 | - @currently_streaming.each do |c| 8 | - c.live.each_with_index do |event, i| 9 | .slide 10 | %a.item{href: event['link'], 'class' => (first and 'active')} 11 | %img{src: h(event['thumb']), 12 | alt: event['display'], 13 | title: ( @currently_streaming.length != 1 ? c.title + '
' : '') + event['display']} 14 | / Declare Variable 15 | - first = false 16 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_navbar_feeds.haml: -------------------------------------------------------------------------------- 1 | %table.feeds_list 2 | -# Translates the feeds menu structure to HTML - see feeds_navigation_bar_structure_helper.rb for the structure 3 | - feed_structure.each do |entry| 4 | - if entry[:headline] 5 | %tr{:class => 'headline'} 6 | %td{:class => 'headline', :colspan => 2} 7 | %div 8 | %span 9 | #{entry[:headline]} 10 | - else 11 | %tr 12 | %td 13 | %a{:href => entry[:left][:href], :title => entry[:left][:title], :class => entry[:left][:indented]} 14 | #{entry[:left][:content]} 15 | - if entry[:right] 16 | %td 17 | %a{:href => entry[:right][:href], :title => entry[:right][:title]} 18 | #{entry[:right][:content]} 19 | - else 20 | %td{:class => %w(placeholder)} 21 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_noscript.haml: -------------------------------------------------------------------------------- 1 | %noscript 2 | :css 3 | .script-only { display: none !important; } 4 | .slider { display: flex; gap: 1em; } 5 | .nav-tabs { display: none; } 6 | .tab-content > .tab-pane { display: block; } 7 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_player_relive.html.haml: -------------------------------------------------------------------------------- 1 | .relive-player 2 | .video-wrap{ | 3 | style: "width 100%; height: 56.25vw;", | 4 | data: relive_data(event, '100%', '56.25vw')} 5 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_player_video_native.haml: -------------------------------------------------------------------------------- 1 | 2 | 3 | %video.video{controls: 'controls', 'data-id' => event.id, poster: event.poster_url, width: width, height: height, preload: 'none', class: 'video-native'} 4 | - event.videos_sorted_by_language.each do |recording| 5 | %source{type: recording.mime_type, src: h(recording.url), data: { lang: recording.language, quality: recording.quality_label }, title: recording.label} 6 | 7 | - event.recordings.subtitle.each do |track| 8 | %track{kind: "subtitles", src: h(track.path), srclang: track.language_iso_639_1()} 9 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_promoted.haml: -------------------------------------------------------------------------------- 1 | .promoted.dark.themed-banner 2 | .titlebar 3 | .slider 4 | - Frontend::Event.promoted(10).includes(:conference).each_with_index do |event, i| 5 | .slide 6 | %a.item{href: event_path(slug: event.slug), 'class' => (i == 0 and 'active')} 7 | %img{src: h(event.thumb_url), alt: event.title, title: event.short_title} 8 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_related.html.haml: -------------------------------------------------------------------------------- 1 | .related 2 | .slider 3 | - events.each_with_index do |event, i| 4 | .slide 5 | %a.item{href: event_path(slug: event.slug), 'class' => (i == 0 and 'active')} 6 | %img{src: h(event.thumb_url), alt: event.title, title: event.short_title} 7 | -------------------------------------------------------------------------------- /app/views/frontend/shared/_short_about.haml: -------------------------------------------------------------------------------- 1 | %p 2 | This site offers a wide variety of video and audio material distributed by the Chaos Computer Club provided in native formats (usually MPEG and/or Vorbis families) for online viewing. Older, archived recordings might require proprietary players. The media files on this site can also be downloaded for offline consumption which includes lecture slides (PDF) and subtitles (SRT/WebVTT). -------------------------------------------------------------------------------- /app/views/frontend/sitemap/index.xml.haml: -------------------------------------------------------------------------------- 1 | %urlset{xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9"} 2 | %url 3 | %loc= root_url 4 | %lastmod= News.maximum(:updated_at).try(:strftime, '%Y-%m-%d') 5 | %loc= about_url 6 | %lastmod= File.mtime(Rails.root.join('app', 'views','frontend', 'home', 'about.haml')).strftime('%Y-%m-%d') 7 | - Frontend::Conference.find_each do |conference| 8 | %url 9 | %loc= browse_url(conference.slug) 10 | %lastmod= conference.events.maximum(:updated_at).try(:strftime, '%Y-%m-%d') 11 | - Frontend::Event.find_each do |event| 12 | %url 13 | %loc= event_url(slug: event.slug) 14 | %lastmod= event.updated_at.strftime('%Y-%m-%d') 15 | -------------------------------------------------------------------------------- /app/views/frontend/tags/show.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | = @tag 3 | 4 | - content_for :body_class do 5 | page-list 6 | 7 | %main.container-fluid 8 | %h1 Events for tag "#{@tag}" 9 | 10 | .event-previews 11 | - if @events.present? 12 | - @events.each do |event| 13 | = render partial: 'frontend/shared/event_with_conference', locals: { event: event } 14 | -------------------------------------------------------------------------------- /app/views/frontend/unpopular/index.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Unpopular Events 3 | 4 | - content_for :body_class do 5 | page-list 6 | 7 | %main.container-fluid 8 | %h1 Unpopular Events 9 | -if @year === 0 10 | %b All 11 | - else 12 | %a{:href => unpopular_path} All 13 | - for year in (@firstyear..Time.new.year).to_a.reverse 14 | %span - 15 | - if @year != year 16 | %a{:href => unpopular_path + "/#{year}"} #{year} 17 | - else 18 | %b #{year} 19 | - if @events.present? 20 | .event-previews 21 | - @events.each do |event| 22 | = render partial: 'frontend/shared/event_with_conference', locals: { event: event } 23 | %a{:href => "?page=" + "#{@page + 1}" } next 24 | -------------------------------------------------------------------------------- /app/views/layouts/frontend/frontend.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html{lang: 'en'} 3 | %head 4 | = render 'frontend/shared/header' 5 | %title 6 | - if content_for?(:title) 7 | = yield :title 8 | - else 9 | = @event.try(:title) || @conference.try(:title) 10 | = t('custom.title') 11 | 12 | = render partial: 'frontend/shared/noscript' 13 | 14 | = yield :head 15 | 16 | - unless @conference.nil? || @conference.custom_css.nil? 17 | %style 18 | = @conference.try(:custom_css).html_safe 19 | 20 | %body{:class => content_for(:body_class)} 21 | = render partial: 'frontend/shared/navbar' 22 | 23 | = yield 24 | 25 | = render 'frontend/shared/footer' 26 | -------------------------------------------------------------------------------- /app/views/layouts/frontend/oembed.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html{lang: 'en', style: 'height: 100%;'} 3 | %head 4 | %title 5 | = t('custom.title') 6 | = h @event.title 7 | = javascript_include_tag 'oembed-player' 8 | = stylesheet_link_tag 'embed' 9 | 10 | %body{style: 'height: 100%; overflow: hidden;'} 11 | .player.video 12 | = render partial: 'frontend/shared/player_video', locals: { height: '450', width: '800', event: @event } 13 | -------------------------------------------------------------------------------- /app/views/public/_html5player.html.erb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/public/conferences/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:conferences, @conferences), race_condition_ttl: 30 do 2 | json.conferences(@conferences) do |conference| 3 | json.partial! 'public/shared/conference', conference: conference 4 | json.url public_conference_url(conference.acronym, format: :json) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/public/conferences/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:conference, @conference, @conference.events), race_condition_ttl: 30 do 2 | json.partial! 'public/shared/conference', conference: @conference 3 | json.events Event.recorded_at(@conference), partial: 'public/shared/event', as: :event 4 | end 5 | -------------------------------------------------------------------------------- /app/views/public/events/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:events, @events, @events.map(&:conference)), race_condition_ttl: 30 do 2 | json.events(@events) do |event| 3 | json.partial! 'public/shared/event', event: event 4 | json.url public_event_url(event.guid, format: :json) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/public/events/search.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.events(@events) do |event| 2 | json.partial! 'public/shared/event', event: event 3 | json.partial! 'public/shared/event_recordings', recordings: event.recordings 4 | json.url public_event_url(event.guid, format: :json) 5 | end 6 | -------------------------------------------------------------------------------- /app/views/public/events/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:event, @event, @event.recordings), race_condition_ttl: 30 do 2 | json.partial! 'public/shared/event', event: @event 3 | json.recordings @event.recordings, partial: 'public/shared/recording', as: :recording 4 | end 5 | -------------------------------------------------------------------------------- /app/views/public/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "conferences_url": "public/conferences{/id}", 3 | "events_url": "public/events/id", 4 | "recordings_url": "public/recordings/id" 5 | } 6 | -------------------------------------------------------------------------------- /app/views/public/oembed.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:event_oembed, @event), race_condition_ttl: 30 do 2 | json.version '1.0' 3 | json.type 'video' 4 | json.provider_name 'media.ccc.de' 5 | json.provider_url Settings.frontend_url 6 | json.width @width 7 | json.height @height 8 | json.title @event.title 9 | json.author @event.persons_text 10 | json.thumbnail_url @event.get_thumb_url 11 | json.html render(partial: 'html5player', formats: [:html], locals: { width: @width, height: @height, event: @event }) 12 | end 13 | -------------------------------------------------------------------------------- /app/views/public/oembed.xml.builder: -------------------------------------------------------------------------------- 1 | xml.oembed do 2 | xml.version '1.0' 3 | xml.type 'video' 4 | xml.provider_name 'media.ccc.de' 5 | xml.provider_url Settings.frontend_url 6 | xml.width @width 7 | xml.height @height 8 | xml.title @event.title 9 | xml.author @event.persons_text 10 | xml.thumbnail_url @event.get_thumb_url 11 | xml.html render(partial: 'html5player', formats: [:html], locals: { width: @width, height: @height, event: @event }) 12 | end 13 | -------------------------------------------------------------------------------- /app/views/public/recordings/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:recordings, @recordings.map(&:event), @recordings.map(&:conference)), race_condition_ttl: 30 do 2 | json.recordings(@recordings) do |recording| 3 | json.partial! 'public/shared/recording', recording: recording 4 | json.url public_recording_url(recording, format: :json) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/public/recordings/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! json_cached_key(:recording, @recording, @recording.event), race_condition_ttl: 30 do 2 | json.partial! 'public/shared/recording', recording: @recording 3 | end 4 | -------------------------------------------------------------------------------- /app/views/public/shared/_conference.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! conference, :acronym, :aspect_ratio, :updated_at, :title, :schedule_url, :slug, :event_last_released_at, :link, :description 2 | json.webgen_location conference.slug 3 | json.logo_url conference.logo_url 4 | json.images_url conference.get_images_url 5 | json.recordings_url conference.get_recordings_url 6 | json.url public_conference_url(id: conference.acronym, format: :json) 7 | -------------------------------------------------------------------------------- /app/views/public/shared/_event.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! event, :guid, :title, :subtitle, :slug, :link, :description, :original_language, :persons, :tags, :view_count, :promoted, :date, :release_date, :updated_at 2 | json.length event.duration 3 | json.duration event.duration 4 | json.thumb_url event.get_thumb_url 5 | json.poster_url event.get_poster_url 6 | json.timeline_url event.get_timeline_url 7 | json.thumbnails_url event.get_thumbnails_url 8 | json.frontend_link frontend_event_url(slug: event.slug) 9 | json.url public_event_url(id: event.guid, format: :json) 10 | json.conference_title event.conference.title 11 | json.conference_url public_conference_url(id: event.conference.acronym, format: :json) 12 | json.related(event.related_events) do |related_event| 13 | json.event_id related_event.id 14 | json.event_guid related_event.guid 15 | json.weight event.metadata['related'][related_event.id.to_s] 16 | end 17 | -------------------------------------------------------------------------------- /app/views/public/shared/_event_recordings.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.recordings(recordings) do |recording| 2 | json.partial! 'public/shared/recording', recording: recording 3 | json.url public_recording_url(recording, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/public/shared/_recording.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! recording, :size, :length, :mime_type, :language, :filename, :state, :folder, :high_quality, :width, :height, :updated_at 2 | json.recording_url recording.get_recording_url 3 | json.url public_recording_url(recording, format: :json) 4 | json.event_url public_event_url(id: recording.event.guid, format: :json) 5 | json.conference_url public_conference_url(id: recording.conference.acronym, format: :json) 6 | -------------------------------------------------------------------------------- /app/workers/conference_streaming_download_worker.rb: -------------------------------------------------------------------------------- 1 | class ConferenceStreamingDownloadWorker 2 | include Sidekiq::Worker 3 | include Downloader 4 | 5 | def perform 6 | url = ENV['STREAMING_URL'] 7 | return unless url 8 | 9 | logger.info "downloading streaming configs from #{url}" 10 | streaming_response = download(url) 11 | streaming = JSON.parse(streaming_response) 12 | 13 | Conference.transaction do 14 | Conference.update_all(streaming: {}) 15 | streaming.each do |conference_data| 16 | conference = Conference.find_by(acronym: conference_data['slug']) 17 | next unless conference 18 | 19 | logger.info "updating streaming config for #{conference.acronym}" 20 | conference.update(streaming: conference_data) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/workers/download_worker.rb: -------------------------------------------------------------------------------- 1 | class DownloadWorker 2 | include Sidekiq::Worker 3 | include Downloader 4 | 5 | def perform(conference_path, filename, url) 6 | FileUtils.mkdir_p conference_path 7 | path = File.join conference_path, filename 8 | logger.info "downloading #{url} to #{path}" 9 | download_to_file(url, path) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/workers/event_update_worker.rb: -------------------------------------------------------------------------------- 1 | class EventUpdateWorker 2 | include Sidekiq::Worker 3 | include FahrplanParser 4 | 5 | # bulk update several events using the saved schedule.xml files 6 | def perform(ids) 7 | logger.info "bulk updating events from XML for events: #{ids.join(', ')}" 8 | @fahrplans = {} 9 | @event_infos = {} 10 | 11 | ActiveRecord::Base.transaction do 12 | Event.where(id: ids).each do |event| 13 | conference = event.conference 14 | 15 | fahrplan = fahrplan_for_conference(conference) 16 | next unless fahrplan 17 | 18 | info = event_info(fahrplan, event.guid) 19 | next unless info.present? 20 | 21 | event.update_event_info(info) 22 | end 23 | end 24 | end 25 | 26 | private 27 | 28 | def fahrplan_for_conference(conference) 29 | @fahrplans[conference.acronym] ||= FahrplanParser.new(conference.schedule_xml) 30 | end 31 | 32 | def event_info(fahrplan, guid) 33 | @event_infos[fahrplan] ||= fahrplan.event_info_by_guid 34 | @event_infos[fahrplan][guid] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/workers/feed.rb: -------------------------------------------------------------------------------- 1 | module Feed 2 | end 3 | -------------------------------------------------------------------------------- /app/workers/feed/archive_legacy_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::ArchiveLegacyWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :podcast_archive_legacy 5 | channel_title 'archive feed' 6 | channel_summary ' This feed contains events older than two years' 7 | 8 | def perform(*args) 9 | events = downloaded_events.older(last_year) 10 | start_time = events.maximum(:updated_at) 11 | 12 | WebFeed.update_with_lock(start_time, key: key) do |feed| 13 | feed.content = generator.generate(events, &:preferred_recording) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workers/feed/archive_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::ArchiveWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :podcast_archive 5 | 6 | def perform(*args) 7 | events = downloaded_events.older(last_year) 8 | start_time = events.maximum(:updated_at) 9 | 10 | Frontend::FeedQuality.all.each do |quality| 11 | WebFeed.update_with_lock(start_time, key: key, kind: quality) do |feed| 12 | feed.content = build(events, quality) 13 | end 14 | end 15 | end 16 | 17 | private 18 | 19 | def build(events, quality) 20 | generator = Feeds::PodcastGenerator.new( 21 | title: "archive feed (#{Frontend::FeedQuality.display_name(quality)})", 22 | channel_summary: ' This feed contains events older than two years', 23 | logo_image: logo_image_url 24 | ) 25 | generator.generate(events) do |event| 26 | Frontend::EventRecordingFilter.by_quality_string(quality).filter(event) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/workers/feed/audio_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::AudioWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :podcast_audio 5 | channel_title 'recent audio-only feed' 6 | channel_summary ' This feed contains audio files from the last year' 7 | 8 | def perform(*args) 9 | events = downloaded_events.newer(last_year) 10 | start_time = events.maximum(:updated_at) 11 | 12 | WebFeed.update_with_lock(start_time, key: key) do |feed| 13 | feed.content = generator.generate(events, &:audio_recording) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workers/feed/base.rb: -------------------------------------------------------------------------------- 1 | class Feed::Base 2 | include ActionView::Helpers 3 | 4 | def initialize 5 | Rails.application.routes.default_url_options[:host] = Settings.frontend_host 6 | Rails.application.routes.default_url_options[:protocol] = Settings.frontend_proto 7 | end 8 | 9 | def self.key(n) 10 | @name = n 11 | end 12 | 13 | def self.get_key 14 | @name 15 | end 16 | 17 | def key 18 | self.class.get_key 19 | end 20 | 21 | def self.channel_title(n) 22 | @title = n 23 | end 24 | 25 | def self.get_title 26 | @title 27 | end 28 | 29 | def self.channel_summary(n) 30 | @summary = n 31 | end 32 | 33 | def self.get_summary 34 | @summary 35 | end 36 | 37 | def downloaded_events 38 | Frontend::Event.published.includes(:conference) 39 | end 40 | 41 | def last_year 42 | WebFeed.last_year 43 | end 44 | 45 | def logo_image_url 46 | image_url('frontend/feed-banner.png') 47 | end 48 | 49 | def generator 50 | Feeds::PodcastGenerator.new( 51 | title: self.class.get_title, 52 | channel_summary: self.class.get_summary, 53 | logo_image: logo_image_url 54 | ) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/workers/feed/legacy_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::LegacyWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :podcast_legacy 5 | channel_title 'recent events feed' 6 | channel_summary ' This feed contains events from the last two years' 7 | 8 | def perform(*args) 9 | events = downloaded_events.newer(last_year) 10 | start_time = events.maximum(:updated_at) 11 | 12 | WebFeed.update_with_lock(start_time, key: key) do |feed| 13 | feed.content = generator.generate(events, &:preferred_recording) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workers/feed/podcast_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::PodcastWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :podcast 5 | 6 | def perform(*args) 7 | events = downloaded_events.newer(last_year) 8 | start_time = events.maximum(:updated_at) 9 | 10 | Frontend::FeedQuality.all.each do |quality| 11 | WebFeed.update_with_lock(start_time, key: key, kind: quality) do |feed| 12 | feed.content = build(events, quality) 13 | end 14 | end 15 | end 16 | 17 | private 18 | 19 | def build(events, quality) 20 | generator = Feeds::PodcastGenerator.new( 21 | title: "recent events feed (#{Frontend::FeedQuality.display_name(quality)})", 22 | channel_summary: ' This feed contains events from the last two years', 23 | logo_image: logo_image_url 24 | ) 25 | generator.generate(events) do |event| 26 | Frontend::EventRecordingFilter.by_quality_string(quality).filter(event) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/workers/feed/recent_worker.rb: -------------------------------------------------------------------------------- 1 | class Feed::RecentWorker < Feed::Base 2 | include Sidekiq::Worker 3 | 4 | key :rdftop100 5 | channel_title 'last 100 events feed' 6 | channel_summary ' This feed the most recent 100 events' 7 | 8 | def perform(*args) 9 | events = downloaded_events.recent(100) 10 | start_time = events.maximum(:updated_at) 11 | 12 | generator = Feeds::RdfGenerator.new( 13 | config: { 14 | title: self.class.get_title, 15 | channel_summary: self.class.get_summary, 16 | logo_image: logo_image_url 17 | } 18 | ) 19 | WebFeed.update_with_lock(start_time, key: key) do |feed| 20 | feed.content = generator.generate(events) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/workers/schedule_download_worker.rb: -------------------------------------------------------------------------------- 1 | class ScheduleDownloadWorker 2 | include Sidekiq::Worker 3 | include Downloader 4 | 5 | def perform(conference_id) 6 | conference = Conference.find(conference_id) 7 | logger.info "downloading schedule for #{conference.acronym}" 8 | conference.schedule_xml = download(conference.schedule_url) 9 | if conference.schedule_xml.nil? 10 | conference.schedule_state = :new 11 | conference.save 12 | else 13 | conference.finish_download! 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/docker-dev-up: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker compose build 4 | docker compose up -d postgres 5 | docker compose run --remove-orphans voctoweb rake db:setup 6 | docker compose run --remove-orphans voctoweb bin/update-data 7 | docker compose up voctoweb -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /bin/update-data: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update the data in your development environment. 15 | 16 | puts '== Downloading Data-Dump ==' 17 | system! 'curl https://media.ccc.de/system/voctoweb.dump.tar.gz | tar xvz' 18 | 19 | puts "\n== Updating database ==" 20 | system! 'FIXTURES_DIR=../../tmp/fixtures bin/rake db:fixtures:load_jsonb' 21 | 22 | puts "\n== Updating Elasticsearch ==" 23 | puts "\n(does not really matter when it fails)" 24 | system! 'SKIP_ELASTICSEARCH_SUBTITLES=1 bin/rails runner "Event.__elasticsearch__.create_index! force: true; Event.import"' 25 | end 26 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Location 3 | metadata: 4 | name: voctoweb 5 | annotations: 6 | github.com/project-slug: voc/voctoweb 7 | spec: 8 | type: url 9 | targets: 10 | - ./system.yaml 11 | - ./app/backstage.yml 12 | - ./app/controllers/public/backstage.yml 13 | - ./app/controllers/api/backstage.yml 14 | - ./app/graphql/backstage.yml -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: media_backend_production 11 | -------------------------------------------------------------------------------- /config/database.yml.template: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: utf8 4 | username: voctoweb 5 | password: voctoweb 6 | host: 127.0.0.1 7 | 8 | development: 9 | <<: *default 10 | database: voctoweb 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | <<: *default 17 | database: voctoweb_test 18 | 19 | production: 20 | <<: *default 21 | database: voctoweb_live 22 | -------------------------------------------------------------------------------- /config/deploy/staging.rb: -------------------------------------------------------------------------------- 1 | server ENV['CAP_HOST'], roles: %w{app db web}, primary: true, port: ENV['CAP_PORT'] 2 | set :deploy_to, "/srv/media/#{fetch(:application)}" 3 | set :tmp_dir, "/srv/media/#{fetch(:application)}/tmp" 4 | 5 | namespace :fixtures do 6 | task :apply do 7 | on roles(:app) do 8 | within release_path do 9 | with rails_env: fetch(:rails_env), FIXTURES_PATH: fetch(:fixtures_path) do 10 | execute :rake, 'db:fixtures:load' 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/action_mailer.rb: -------------------------------------------------------------------------------- 1 | require 'settings' 2 | 3 | MediaBackend::Application.configure do 4 | config.action_mailer.default_url_options = { 5 | host: Settings.frontend_host, 6 | protocol: Settings.frontend_proto, 7 | } 8 | config.action_mailer.smtp_settings = { 9 | address: ENV['SMTP_HOST'], 10 | enable_starttls_auto: false, 11 | ssl: false, 12 | tls: false 13 | } 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/cors_public.rb: -------------------------------------------------------------------------------- 1 | module MediaBackend 2 | class Application < Rails::Application 3 | config.middleware.use Rack::Cors do 4 | allow do 5 | origins '*' 6 | resource '/public/*', :headers => :any, :methods => :get 7 | resource '/graphql', :headers => :any, :methods => [:get, :post, :options] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/devise_secret_token.rb: -------------------------------------------------------------------------------- 1 | Devise.setup do |config| 2 | config.secret_key = ENV['DEVISE_SECRET_KEY'] 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/exception_notifier.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.production? && ENV['NOTIFY_RECEIVER'].present? 2 | Rails.application.config.middleware.use ExceptionNotification::Rack, 3 | ignore_if: ->(env, exception) { exception.message =~ /^IP spoofing attack/ }, 4 | email: { 5 | email_prefix: '[MEDIA] ', 6 | sender_address: ENV['NOTIFY_SENDER'], 7 | exception_recipients: ENV['NOTIFY_RECEIVER'].split(/\s*,\s*/) 8 | } 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :password, :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/graphql.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Define 3 | class DefinedObjectProxy 4 | include Rails.application.routes.url_helpers 5 | Rails.application.routes.default_url_options[:host] = Settings.frontend_host 6 | Rails.application.routes.default_url_options[:protocol] = Settings.frontend_proto 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/multi_json.rb: -------------------------------------------------------------------------------- 1 | require 'multi_json' 2 | MultiJson.use :yajl 3 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /config/initializers/sass_skip.rb: -------------------------------------------------------------------------------- 1 | require "sprockets/sass_compressor" 2 | 3 | # https://stackoverflow.com/a/77544219 4 | module SkipSassCompression 5 | SEARCH = "graphiql-react".freeze 6 | 7 | def call(input) 8 | if skip_compression?(input[:data]) 9 | input[:data] 10 | else 11 | super 12 | end 13 | end 14 | 15 | def skip_compression?(body) 16 | body.include?(SEARCH) 17 | end 18 | end 19 | 20 | Sprockets::SassCompressor.prepend SkipSassCompression 21 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_media_backend_session', secure: Rails.env.production? 4 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/custom.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | custom: 3 | title: '- media.ccc.de' 4 | news_title: media.ccc.de - NEWS 5 | rss: 6 | title: 7 | atom: media.ccc.de - News 8 | last100: media.ccc.de - RSS, last 100 9 | lastyears: media.ccc.de - Podcast feed of the last two years 10 | podcast_archive: media.ccc.de - Podcast archive feed 11 | header: 12 | publisher: CCC 13 | description: Video Streaming Portal des Chaos Computer Clubs 14 | keywords: Chaos Computer Club, Video, Media, Streaming, TV, Hacker 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | date: 34 | formats: 35 | pretty: "%Y-%m-%d %H:%M" 36 | time: 37 | formats: 38 | pretty_datetime: "%Y-%m-%d %H:%M" 39 | rss: 40 | title: 41 | atom: ATOM 42 | last100: last 100 43 | podcast_archive: podcast archive 44 | 45 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 86029523f0e57d06110702393492bbc3dd7c9b16ffc39987845abb2ec0e819c8a40d24de9301b01ef82e992461062eb6f90ce2c3c7dc1218cdce03a92d701d44 15 | 16 | test: 17 | secret_key_base: 4e8f81b0b2a716b737aceb3060bbf105334db9a941e9599520b26f38e2ad769226a98c348a9277a4fd9cd4893939eb6e5968dc24d942e8ae4a3500d1a169623a 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :concurrency: 5 3 | :queues: 4 | - critical 5 | - default 6 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/migrate/20130820182039_create_active_admin_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAdminComments < ActiveRecord::Migration[4.2] 2 | def self.up 3 | create_table :active_admin_comments do |t| 4 | t.string :namespace 5 | t.text :body 6 | t.string :resource_id, :null => false 7 | t.string :resource_type, :null => false 8 | t.references :author, :polymorphic => true 9 | t.timestamps 10 | end 11 | add_index :active_admin_comments, [:namespace] 12 | add_index :active_admin_comments, [:author_type, :author_id] 13 | add_index :active_admin_comments, [:resource_type, :resource_id] 14 | end 15 | 16 | def self.down 17 | drop_table :active_admin_comments 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20130820192118_create_conferences.rb: -------------------------------------------------------------------------------- 1 | class CreateConferences < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :conferences do |t| 4 | t.string :acronym 5 | t.string :recordings_path 6 | t.string :images_path 7 | t.string :webgen_location 8 | t.string :aspect_ratio 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20130820210349_create_api_keys.rb: -------------------------------------------------------------------------------- 1 | class CreateApiKeys < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :api_keys do |t| 4 | t.string :key 5 | t.string :description 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130820221709_add_conference_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddConferenceIndexes < ActiveRecord::Migration[4.2] 2 | def up 3 | add_index :conferences, :acronym 4 | end 5 | 6 | def down 7 | remove_index :conferences, :column => :acronym 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20130820222430_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :events do |t| 4 | t.string :guid 5 | t.string :gif_filename 6 | t.string :poster_filename 7 | t.references :conference, index: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20130820222724_create_recordings.rb: -------------------------------------------------------------------------------- 1 | class CreateRecordings < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :recordings do |t| 4 | t.integer :size 5 | t.integer :length 6 | t.string :mime_type 7 | t.references :event, index: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20130820223153_add_path_to_recording.rb: -------------------------------------------------------------------------------- 1 | class AddPathToRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :path, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20130820233230_add_title_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddTitleToConference < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :conferences, :title, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20130820233237_add_title_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddTitleToEvent < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :title, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20130820233659_add_indexes_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesToEvents < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index :events, :guid 4 | add_index :events, :title 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20130820233759_add_indexes_to_recordings.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesToRecordings < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index :recordings, :path 4 | add_index :recordings, :mime_type 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20130821000003_add_recording_states.rb: -------------------------------------------------------------------------------- 1 | class AddRecordingStates < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :state, :string, default: "new", null: false 4 | add_index :events, :state 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20130821123704_create_event_infos.rb: -------------------------------------------------------------------------------- 1 | class CreateEventInfos < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :event_infos do |t| 4 | t.references :event, index: true 5 | t.string :subtitle 6 | t.string :link 7 | t.text :description 8 | t.text :persons 9 | t.text :tags 10 | t.date :date 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20130821124234_add_schedule_url_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddScheduleUrlToConference < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :conferences, :schedule_url, :string 4 | add_column :conferences, :schedule_xml, :binary 5 | add_column :conferences, :schedule_state, :string, default: "not_present", null: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130822181227_change_recording_model.rb: -------------------------------------------------------------------------------- 1 | class ChangeRecordingModel < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :recordings, :path, :filename 4 | add_column :recordings, :original_url, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20130824135552_add_thumb_filename_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddThumbFilenameToEvent < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :thumb_filename, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20130826215505_add_slug_to_event_info.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToEventInfo < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :event_infos, :slug, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140101230549_add_event_info_fields_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddEventInfoFieldsToEvent < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :date, :date 4 | add_column :events, :description, :text 5 | add_column :events, :link, :string 6 | add_column :events, :persons, :text 7 | add_column :events, :slug, :string 8 | add_column :events, :subtitle, :string 9 | add_column :events, :tags, :text 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140101231325_move_event_info_data_to_event.rb: -------------------------------------------------------------------------------- 1 | class MoveEventInfoDataToEvent < ActiveRecord::Migration[4.2] 2 | =begin 3 | def up 4 | EventInfo.all.each do |ei| 5 | next if ei.event.nil? 6 | ei.event.date = ei.date 7 | ei.event.description = ei.description 8 | ei.event.link = ei.link 9 | ei.event.persons = ei.persons 10 | ei.event.slug = ei.slug 11 | ei.event.subtitle = ei.subtitle 12 | ei.event.tags = ei.tags 13 | ei.event.save! 14 | end 15 | end 16 | 17 | def down 18 | Events.all.each do |e| 19 | ei = EventInfo.new 20 | ei.date = e.date 21 | ei.description = e.description 22 | ei.link = e.link 23 | ei.persons = e.persons 24 | ei.slug = e.slug 25 | ei.subtitle = e.subtitle 26 | ei.tags = e.tags 27 | ei.event = e 28 | ei.save! 29 | end 30 | end 31 | =end 32 | end 33 | -------------------------------------------------------------------------------- /db/migrate/20140101232111_remove_event_info_table.rb: -------------------------------------------------------------------------------- 1 | class RemoveEventInfoTable < ActiveRecord::Migration[4.2] 2 | def change 3 | drop_table :event_infos 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140102031811_add_conference_logo.rb: -------------------------------------------------------------------------------- 1 | class AddConferenceLogo < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :conferences, :logo, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140103000033_add_folder_to_recording.rb: -------------------------------------------------------------------------------- 1 | class AddFolderToRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :folder, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140103000127_fill_recordings_folder_from_mime_types.rb: -------------------------------------------------------------------------------- 1 | class FillRecordingsFolderFromMimeTypes < ActiveRecord::Migration[4.2] 2 | def up 3 | mappings = { 4 | 'application/ogg' => 'ogg', 5 | 'application/vnd.rn-realmedia' => 'realmedia', 6 | 'audio/ogg' => 'ogg', 7 | 'audio/opus' => 'opus', 8 | 'audio/mpeg' => 'mp3', 9 | 'audio/x-wav' => 'wav', 10 | 'video/mp4' => 'mp4', 11 | 'vnd.voc/h264-hd' => 'mp4-hd', 12 | 'vnd.voc/h264-lq' => 'mp4-lq', 13 | 'video/ogg' => 'ogg', 14 | 'video/quicktime' => 'qt', 15 | 'video/webm' => 'webm', 16 | 'video/x-matroska' => 'mkv', 17 | 'video/x-msvideo' => 'avi', 18 | } 19 | 20 | Recording.all.each do |r| 21 | next if r.mime_type.empty? 22 | 23 | r.folder = mappings[r.mime_type] || "" 24 | r.save! 25 | end 26 | end 27 | 28 | def down 29 | Recording.all.each do |r| 30 | r.folder = '' 31 | r.save! 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /db/migrate/20140120020012_add_release_date_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddReleaseDateToEvent < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :events, :release_date, :datetime 4 | Event.all.each { |e| 5 | e.release_date = e.created_at 6 | e.save! 7 | } 8 | end 9 | 10 | def down 11 | remove_column :events, :release_date 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20140502191657_add_dimensions_to_recording.rb: -------------------------------------------------------------------------------- 1 | class AddDimensionsToRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :width, :integer 4 | add_column :recordings, :height, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140502222555_add_promoted_flag_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddPromotedFlagToEvents < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :promoted, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140502223824_create_news.rb: -------------------------------------------------------------------------------- 1 | class CreateNews < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :news do |t| 4 | t.string :title 5 | t.text :body 6 | t.date :date 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140609012419_create_import_templates.rb: -------------------------------------------------------------------------------- 1 | class CreateImportTemplates < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :import_templates do |t| 4 | #conference 5 | 6 | t.string "acronym" 7 | t.string "title" 8 | t.string "logo" 9 | t.string "webgen_location" 10 | t.string "aspect_ratio" 11 | t.string "recordings_path" 12 | t.string "images_path" 13 | 14 | #events 15 | 16 | t.date "date" 17 | t.datetime "release_date" 18 | t.boolean "promoted" 19 | 20 | #recordings 21 | 22 | t.string "mime_type" 23 | t.string "folder" 24 | t.integer "width" 25 | t.integer "height" 26 | 27 | t.timestamps 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /db/migrate/20140611215347_remove_promoted_from_import_template.rb: -------------------------------------------------------------------------------- 1 | class RemovePromotedFromImportTemplate < ActiveRecord::Migration[4.2] 2 | def up 3 | remove_column :import_templates, :promoted 4 | end 5 | 6 | def down 7 | add_column :import_templates, :promoted, :boolean 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20140611215723_change_release_date_to_date.rb: -------------------------------------------------------------------------------- 1 | class ChangeReleaseDateToDate < ActiveRecord::Migration[4.2] 2 | def up 3 | change_column :import_templates, :release_date, :date 4 | change_column :events, :release_date, :date 5 | end 6 | 7 | def down 8 | change_column :import_templates, :release_date, :datetime 9 | change_column :events, :release_date, :datetime 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140615151738_change_conference_schedule_xml_to_text.rb: -------------------------------------------------------------------------------- 1 | class ChangeConferenceScheduleXmlToText < ActiveRecord::Migration[4.2] 2 | def up 3 | change_column :conferences, :schedule_xml, :text, limit: 10.megabyte 4 | end 5 | 6 | def down 7 | change_column :conferences, :schedule_xml, :binary 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20140622020440_add_view_count_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddViewCountToEvents < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :events, :view_count, :integer, default: 0 4 | 5 | execute %{ 6 | UPDATE events SET view_count=0; 7 | } 8 | end 9 | 10 | def down 11 | remove_column :events, :view_count 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20140622085720_remove_released_state_from_recording.rb: -------------------------------------------------------------------------------- 1 | class RemoveReleasedStateFromRecording < ActiveRecord::Migration[4.2] 2 | def up 3 | #execute %{ 4 | #UPDATE recordings SET state='downloaded' WHERE state='released' 5 | #} 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140626220827_create_recording_views.rb: -------------------------------------------------------------------------------- 1 | class CreateRecordingViews < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :recording_views do |t| 4 | t.references :recording, index: true 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20140626223140_add_view_count_to_recordings.rb: -------------------------------------------------------------------------------- 1 | class AddViewCountToRecordings < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :recordings, :view_count, :integer, default: 0 4 | 5 | execute %{ 6 | UPDATE recordings SET view_count=0; 7 | } 8 | end 9 | 10 | def down 11 | remove_column :recordings, :view_count 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20140627185401_remove_view_count_from_recordings.rb: -------------------------------------------------------------------------------- 1 | class RemoveViewCountFromRecordings < ActiveRecord::Migration[4.2] 2 | def up 3 | remove_column :recordings, :view_count 4 | end 5 | 6 | def down 7 | add_column :recordings, :view_count, :integer, default: 0 8 | execute %{ UPDATE recordings SET view_count=0; } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20140907121133_remove_event_gif.rb: -------------------------------------------------------------------------------- 1 | class RemoveEventGif < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :events, :gif_filename 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141111120848_add_time_to_event_date.rb: -------------------------------------------------------------------------------- 1 | class AddTimeToEventDate < ActiveRecord::Migration[4.2] 2 | def change 3 | change_column :events, :date, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150913230424_drop_delayed_jobs_table.rb: -------------------------------------------------------------------------------- 1 | class DropDelayedJobsTable < ActiveRecord::Migration[4.2] 2 | def change 3 | drop_table :delayed_jobs 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150919123544_rename_conference_webgen_location_to_slug.rb: -------------------------------------------------------------------------------- 1 | class RenameConferenceWebgenLocationToSlug < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :conferences, :webgen_location, :slug 4 | change_column :conferences, :slug, :string, default: '' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20150919124229_rename_import_template_webgen_location_to_slug.rb: -------------------------------------------------------------------------------- 1 | class RenameImportTemplateWebgenLocationToSlug < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :import_templates, :webgen_location, :slug 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151011213109_add_duration_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddDurationToEvent < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :duration, :integer, default: 0 4 | Event.find_each do |event| 5 | recordings = event.recordings 6 | next unless recordings.present? 7 | 8 | recording = recordings.find { |r| r.length.present? } 9 | next unless recording 10 | 11 | event.update(duration: recording.length) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20151018212350_add_downloaded_events_count_to_conferences.rb: -------------------------------------------------------------------------------- 1 | class AddDownloadedEventsCountToConferences < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :conferences, :downloaded_events_count, :integer, default: 0, null: false 4 | Conference.reset_column_information 5 | Conference.find_each do |c| 6 | c.update_column :downloaded_events_count, Event.recorded_at(c).to_a.size 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20151027160631_add_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddIndexes < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index 'events', ['slug'], name: 'index_events_on_slug' 4 | add_index 'events', ['release_date'], name: 'index_events_on_release_date' 5 | add_index 'events', %w(slug id), name: 'index_events_on_slug_and_id' 6 | #add_index 'recordings', %w(state mime_type), name: 'index_recordings_on_state_and_mime_type' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20151105165721_add_downloaded_recordings_counter_on_events.rb: -------------------------------------------------------------------------------- 1 | class AddDownloadedRecordingsCounterOnEvents < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :downloaded_recordings_count, :integer, default: 0 4 | Event.reset_column_information 5 | Event.find_each do |e| 6 | e.update downloaded_recordings_count: e.downloaded_recordings.count 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20151227115938_add_language_string_to_recording.rb: -------------------------------------------------------------------------------- 1 | class AddLanguageStringToRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :language, :string, default: 'en' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151230105406_add_original_language_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddOriginalLanguageToEvent < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :events, :original_language, :string 4 | 5 | Event.find_each do |event| 6 | languages = event.recordings.pluck(:language).uniq 7 | next if languages.empty? 8 | 9 | event.original_language = languages.max_by(&:length).split(/-/).first 10 | event.save 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20160102191700_add_quality_flag_on_recording.rb: -------------------------------------------------------------------------------- 1 | class AddQualityFlagOnRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :hd_quality, :boolean, default: true, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160102191709_add_html5_flag_on_recording.rb: -------------------------------------------------------------------------------- 1 | class AddHtml5FlagOnRecording < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :recordings, :html5, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160102232606_drop_import_templates.rb: -------------------------------------------------------------------------------- 1 | class DropImportTemplates < ActiveRecord::Migration[4.2] 2 | def change 3 | drop_table :import_templates 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160104122727_mime_types_to_flags.rb: -------------------------------------------------------------------------------- 1 | class MimeTypesToFlags < ActiveRecord::Migration[4.2] 2 | def change 3 | html5 = %w(vnd.voc/mp4-web vnd.voc/webm-web) 4 | Recording.find_each do |recording| 5 | next unless recording.valid? 6 | 7 | recording.hd_quality = hd?(recording.mime_type) 8 | recording.html5 = html5.include?(recording.mime_type) 9 | recording.mime_type = display_mime_type(recording.mime_type) 10 | recording.save! 11 | end 12 | end 13 | 14 | def display_mime_type(mime_type) 15 | case mime_type 16 | when 'vnd.voc/h264-lq' 17 | 'video/mp4' 18 | when 'vnd.voc/h264-sd' 19 | 'video/mp4' 20 | when 'vnd.voc/h264-hd' 21 | 'video/mp4' 22 | when 'vnd.voc/mp4-web' 23 | 'video/mp4' 24 | when 'vnd.voc/webm-hd' 25 | 'video/webm' 26 | when 'vnd.voc/webm-web' 27 | 'video/webm' 28 | else 29 | mime_type 30 | end 31 | end 32 | 33 | def hd?(mime_type) 34 | case mime_type 35 | when 'vnd.voc/h264-lq' 36 | false 37 | when 'vnd.voc/h264-sd' 38 | false 39 | when 'vnd.voc/h264-hd' 40 | true 41 | when 'vnd.voc/webm-hd' 42 | true 43 | else 44 | true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /db/migrate/20160130001036_remove_original_url.rb: -------------------------------------------------------------------------------- 1 | class RemoveOriginalUrl < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :recordings, :original_url 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160203134927_high_quality.rb: -------------------------------------------------------------------------------- 1 | class HighQuality < ActiveRecord::Migration[4.2] 2 | def change 3 | rename_column :recordings, :hd_quality, :high_quality 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160205214028_default_language_is_eng.rb: -------------------------------------------------------------------------------- 1 | class DefaultLanguageIsEng < ActiveRecord::Migration[4.2] 2 | def change 3 | change_column :recordings, :language, :string, default: 'eng' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160827151338_add_metadata_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddMetadataToConference < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :conferences, :metadata, :jsonb, index: true, default: {} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161127165450_add_event_last_releases_at_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddEventLastReleasesAtToConference < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :conferences, :event_last_released_at, :date, null: true 4 | 5 | reversible do |dir| 6 | dir.up do 7 | # initialize field content 8 | execute <<-SQL 9 | UPDATE conferences 10 | SET event_last_released_at = ( 11 | SELECT MAX(release_date) 12 | FROM events 13 | WHERE events.conference_id = conferences.id 14 | ); 15 | SQL 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20161231215656_add_metadata_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddMetadataToEvent < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :events, :metadata, :jsonb, index: true, default: {} 4 | add_index :events, :metadata, using: :gin 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170103122951_add_identifiers_to_recording_views.rb: -------------------------------------------------------------------------------- 1 | class AddIdentifiersToRecordingViews < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :recording_views, :user_agent, :string, default: '' 4 | add_column :recording_views, :identifier, :string, default: '' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171111152136_add_streaming_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddStreamingToConference < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :conferences, :streaming, :jsonb, index: true, default: {} 4 | add_index :conferences, :streaming, using: :gin 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171111211412_create_event_view_counts.rb: -------------------------------------------------------------------------------- 1 | class CreateEventViewCounts < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :event_view_counts do |t| 4 | t.timestamp :last_updated_at 5 | end 6 | 7 | connection.execute("INSERT INTO event_view_counts (last_updated_at) VALUES (NOW())") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180914181622_add_timelens_to_event.rb: -------------------------------------------------------------------------------- 1 | class AddTimelensToEvent < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :events, :timeline_filename, :string, :default => '' 4 | add_column :events, :thumbnails_filename, :string, :default => '' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190928125825_add_doi_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddDoiToEvents < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :events, :doi, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20191226021730_create_web_feeds.rb: -------------------------------------------------------------------------------- 1 | class CreateWebFeeds < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :web_feeds do |t| 4 | t.string :key 5 | t.string :kind 6 | t.timestamp :last_build 7 | t.text :content 8 | end 9 | add_index :web_feeds, %i{key kind}, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200119000347_change_release_date_type.rb: -------------------------------------------------------------------------------- 1 | class ChangeReleaseDateType < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :events, :release_date, :datetime; 4 | change_column :conferences, :event_last_released_at, :datetime; 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200119002951_extend_conference_attributes.rb: -------------------------------------------------------------------------------- 1 | class ExtendConferenceAttributes < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :conferences, :description, :text 4 | add_column :conferences, :link, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200410231759_add_custom_css_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddCustomCssToConference < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :conferences, :custom_css, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230131143553_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if configured_service = ActiveStorage::Blob.service.name 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20230131143555_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20241229193141_add_notes_field_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddNotesFieldToEvents < ActiveRecord::Migration[7.2] 2 | def change 3 | add_column :events, :notes, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20241230111051_add_global_event_notes_field_to_conference.rb: -------------------------------------------------------------------------------- 1 | class AddGlobalEventNotesFieldToConference < ActiveRecord::Migration[7.2] 2 | def change; 3 | add_column :conferences, :global_event_notes, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250120233533_add_translated_to_recordings.rb: -------------------------------------------------------------------------------- 1 | class AddTranslatedToRecordings < ActiveRecord::Migration[7.2] 2 | def up 3 | add_column :recordings, :translated, :boolean, default: false, null: false 4 | 5 | execute "UPDATE recordings SET translated = true WHERE folder LIKE '%transla%'" 6 | end 7 | 8 | def down 9 | remove_column :recordings, :translated 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | AdminUser.create(email: 'admin@example.org', password: 'media123') 9 | -------------------------------------------------------------------------------- /deploy-staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export CAP_REPO=https://github.com/voc/voctoweb.git 3 | export CAP_USER=media 4 | export CAP_BRANCH=staging 5 | #export CAP_BRANCH=main 6 | 7 | export CAP_HOST=app.media.test.c3voc.de 8 | export CAP_PORT=22 9 | 10 | #export MQTT_URL=mqtt://media:XXXXX@mng.c3voc.de 11 | 12 | echo "Deploying branch ${CAP_BRANCH} to ${CAP_HOST}" 13 | 14 | bundle exec cap staging deploy $* 15 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export CAP_REPO=https://github.com/voc/voctoweb.git 4 | export CAP_BRANCH=main 5 | export CAP_USER=media 6 | 7 | export CAP_HOST=app.media.ccc.de 8 | export CAP_PORT=22 9 | 10 | bundle exec cap production deploy 11 | -------------------------------------------------------------------------------- /docker/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/docker/.gitkeep -------------------------------------------------------------------------------- /docker/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: utf8 4 | username: postgres 5 | password: postgres 6 | host: postgres 7 | 8 | development: 9 | <<: *default 10 | database: voctoweb 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | <<: *default 17 | database: voctoweb_test 18 | 19 | production: 20 | <<: *default 21 | database: voctoweb_live 22 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | # unused: $NGINX_HOST $NGINX_PORT 2 | 3 | upstream voctoweb-docker { 4 | server voctoweb:3000; 5 | } 6 | 7 | server { 8 | listen [::]:80 ipv6only=off; 9 | server_name ${NGINX_HOST}; 10 | root /usr/share/nginx/html; 11 | 12 | location / { 13 | add_header 'Access-Control-Allow-Origin' '*'; 14 | proxy_set_header X-Forwarded-For $remote_addr; 15 | proxy_set_header X-Forwarded-Proto https; 16 | proxy_pass http://voctoweb-docker; 17 | } 18 | } 19 | 20 | server { 21 | listen [::]:80; 22 | 23 | server_name static.${NGINX_HOST}; 24 | root /usr/share/nginx/html; 25 | 26 | location @live { 27 | rewrite ^ https://static.media.ccc.de$request_uri?; 28 | } 29 | 30 | location / { 31 | add_header 'Access-Control-Allow-Origin' '*'; 32 | try_files $uri @live; 33 | } 34 | } 35 | 36 | server { 37 | listen [::]:80; 38 | 39 | server_name cdn.${NGINX_HOST}; 40 | root /usr/share/nginx/html; 41 | 42 | location @live { 43 | rewrite ^ https://cdn.media.ccc.de$request_uri?; 44 | } 45 | 46 | location / { 47 | add_header 'Access-Control-Allow-Origin' '*'; 48 | try_files $uri @live; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/architecture-overview-apis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/docs/architecture-overview-apis.png -------------------------------------------------------------------------------- /docs/architecture-overview-subtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/docs/architecture-overview-subtitles.png -------------------------------------------------------------------------------- /docs/architecture-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/docs/architecture-overview.png -------------------------------------------------------------------------------- /elasticsearch.yml: -------------------------------------------------------------------------------- 1 | http.host: 0.0.0.0 2 | 3 | # for http://splainer.io/ 4 | http.cors.allow-origin: "/https?:\\/\\/(.*?\\.)?(quepid\\.com|splainer\\.io)/" 5 | http.cors.enabled: true -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY_BASE=asdkjf3245jsjfakjq435jadsgjlkq4j5jwj45jasdjvlj 2 | DEVISE_SECRET_KEY=22c82812123213214328cec6c84a97980869ba1e1a6e54b5269c8d387729a7d10e4b167c3b4f30029bf6e352a0160f871258a6536e19819e05b513ee868b362e 3 | DEVISE_FROM=test@example.org 4 | SMTP_HOST=localhost 5 | CAP_REPO=gitolite@localhost:media-site.git 6 | CAP_BRANCH=deployment-media.ccc.de 7 | CAP_USER=media-site 8 | NOTIFY_SENDER='"media" ' 9 | NOTIFY_RECEIVER=user@example.org 10 | MQTT_SERVER=mqtt://user:password@host 11 | STREAMING_URL=https://streaming.media.ccc.de/streams/v2.json 12 | RELIVE_URL=http://relive.c3voc.de/relive/index.json 13 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/lib/assets/.keep -------------------------------------------------------------------------------- /lib/feeds.rb: -------------------------------------------------------------------------------- 1 | module Feeds 2 | end 3 | -------------------------------------------------------------------------------- /lib/feeds/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Feeds 4 | module Helper 5 | def merge_config(config) 6 | keep = [:title, :channel_summary] 7 | @config.channel_title = [@config.channel_title, config[:title]].join(' - ') 8 | @config.channel_summary += config[:channel_summary] 9 | 10 | config.each { |k, v| 11 | next if keep.include? k 12 | 13 | @config[k] = v 14 | } 15 | end 16 | 17 | def get_item_title(event) 18 | "#{event.title} (#{event.conference.acronym})" 19 | end 20 | 21 | def get_item_description(event) 22 | description = [] 23 | description << event.description 24 | 25 | link = event.link 26 | description << "about this event: #{link}\n" if link 27 | 28 | description.join("\n") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Settings 4 | def self.method_missing(name) 5 | fail "not implemented: #{name}" unless config.respond_to?(name) 6 | 7 | config.public_send(name).freeze 8 | end 9 | 10 | def self.respond_to?(name) 11 | config.respond_to?(name) 12 | end 13 | 14 | def self.frontend_url 15 | "#{frontend_proto}://#{frontend_host}".freeze 16 | end 17 | 18 | def self.config 19 | @config ||= OpenStruct.new(Rails.application.config_for(:settings)).freeze 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/db_dump_fixtures.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :fixtures do 3 | desc 'Dump fixtures from database' 4 | task :dump, [:include_private] => [:environment] do |t, args| 5 | fixtures_dir = ENV['FIXTURES_PATH'] || 'test/fixtures' 6 | sql = 'SELECT * FROM %s' 7 | 8 | skip_tables = %w(schema_info schema_migrations sessions recording_views active_admin_comments ar_internal_metadata web_feeds) 9 | skip_tables += %w(api_keys admin_users) unless args[:include_private] 10 | 11 | ActiveRecord::Base.establish_connection 12 | i = '000' 13 | (ActiveRecord::Base.connection.tables - skip_tables).each do |table| 14 | File.open(File.join(fixtures_dir, "#{table}.yml"), 'w') do |file| 15 | data = ActiveRecord::Base.connection.select_all(sql % table) 16 | file.write data.inject({}) { |hash, record| 17 | hash["#{table}_#{i.succ!}"] = record 18 | hash 19 | }.to_yaml 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tasks/db_load_fixtures_with_jsonb.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :fixtures do 3 | desc 'Load fixtures into database, converting jsonb' 4 | task :load_jsonb, [:include_private] => [:environment] do |t, args| 5 | Rake::Task["db:fixtures:load"].execute 6 | 7 | ActiveRecord::Base.establish_connection 8 | Conference.all.each { |c| c.update_column(:metadata, JSON.parse('{"subtitles":true}')) if c.metadata == "\"{\\\"subtitles\\\":true}\"" } 9 | Conference.all.each { |c| c.update_column(:metadata, JSON.parse('{}')) if c.metadata == "\"{}\"" } 10 | 11 | Conference.all.each { |c| c.update_column(:metadata, JSON.parse(c.metadata)) if c.metadata.is_a? String } 12 | Conference.all.each { |c| c.update_column(:streaming, JSON.parse(c.streaming)) if c.streaming.is_a? String } 13 | 14 | Event.all.each { |e| e.update_column(:metadata, JSON.parse(e.metadata)) if e.metadata.is_a? String } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tasks/related_events.rake: -------------------------------------------------------------------------------- 1 | namespace :voctoweb do 2 | namespace :related do 3 | desc 'Update related metadata on events from recording views' 4 | task update: :environment do 5 | UpdateRelatedEvents.new.update 6 | end 7 | 8 | desc 'Clean up related' 9 | task clean: :environment do 10 | UpdateRelatedEvents.new.clean 11 | end 12 | 13 | desc 'Remove related info from all events' 14 | task remove: :environment do 15 | Event.all.map { |e| 16 | metadata = e.metadata 17 | metadata = {} unless metadata.is_a?(Hash) 18 | metadata.delete('related') 19 | e.update_columns(metadata: metadata) 20 | } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/tasks/relive_update.rake: -------------------------------------------------------------------------------- 1 | namespace :voctoweb do 2 | namespace :relive do 3 | desc 'Update conferences relive data' 4 | task update: :environment do 5 | ConferenceReliveDownloadWorker.new.perform 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/streaming_update.rake: -------------------------------------------------------------------------------- 1 | namespace :voctoweb do 2 | namespace :streaming do 3 | desc 'Update conferences streaming settings' 4 | task update: :environment do 5 | ConferenceStreamingDownloadWorker.new.perform 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/log/.keep -------------------------------------------------------------------------------- /public/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/favicon-196x196.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /run_tests_graphql.sh: -------------------------------------------------------------------------------- 1 | ruby -Itest test/integration/graphql/* 2 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/schema.graphql -------------------------------------------------------------------------------- /system.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: System 4 | metadata: 5 | name: voctoweb 6 | annotations: 7 | github.com/project-slug: voc/voctoweb 8 | spec: 9 | type: service 10 | owner: media -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/admin/admin_users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::AdminUsersControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should list admin users" do 10 | create :admin_user 11 | get 'index' 12 | assert_response :success 13 | end 14 | 15 | test "should show new admin users form" do 16 | get 'new' 17 | assert_response :success 18 | end 19 | 20 | test "should show an admin user" do 21 | user = create :admin_user 22 | get 'show', params: { id: user.id } 23 | assert_response :success 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/controllers/admin/api_keys_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::ApiKeysControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should list api keys" do 10 | create :api_key 11 | get 'index' 12 | assert_response :success 13 | end 14 | 15 | test "should show new api key form" do 16 | get 'new' 17 | assert_response :success 18 | end 19 | 20 | test "should show an api key" do 21 | api_key = create :api_key 22 | get 'show', params: { id: api_key.id } 23 | assert_response :success 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/controllers/admin/conferences_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::ConferencesControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should list conferences" do 10 | create :conference 11 | get 'index' 12 | assert_response :success 13 | end 14 | 15 | test "should show new conference form" do 16 | get 'new' 17 | assert_response :success 18 | end 19 | 20 | test "should show a conference" do 21 | conference = create :conference 22 | get 'show', params: { id: conference.id } 23 | assert_response :success 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/controllers/admin/dashboard_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::DashboardControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should show dashboard" do 10 | get 'index' 11 | assert_response :success 12 | end 13 | 14 | test "should show dashboard with a conference" do 15 | create :conference 16 | get :index 17 | assert_response :success 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/controllers/admin/events_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::EventsControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should list events" do 10 | get 'index' 11 | assert_response :success 12 | end 13 | 14 | test "should show new event form" do 15 | get 'new' 16 | assert_response :success 17 | end 18 | 19 | test "should show an event" do 20 | event = create :event 21 | get 'show', params: { id: event.id } 22 | assert_response :success 23 | end 24 | 25 | test "should error for non-existing event" do 26 | assert_raise ActiveRecord::RecordNotFound do 27 | get 'show', params: { id: 1234 } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/controllers/admin/recordings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Admin::RecordingsControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :admin_user 6 | sign_in @user 7 | end 8 | 9 | test "should list recordings" do 10 | get 'index' 11 | assert_response :success 12 | end 13 | 14 | test "should show new recording form" do 15 | get 'new' 16 | assert_response :success 17 | end 18 | 19 | test "should show a recording" do 20 | recording = create :recording 21 | get 'show', params: { id: recording.id } 22 | assert_response :success 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/controllers/api/conferences_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ConferencesControllerTest < ActionController::TestCase 4 | setup do 5 | @key = create(:api_key) 6 | @conference = create(:conference, acronym: 'one') 7 | end 8 | 9 | test 'should list conferences' do 10 | get 'index', format: :json, params: { api_key: @key.key } 11 | assert_response :success 12 | assert JSON.parse(response.body) 13 | end 14 | 15 | test 'should update conference' do 16 | args = { 17 | conference: { 18 | logo: 'fake-logo', 19 | title: 'fake-title' 20 | }, 21 | api_key: @key.key, 22 | id: @conference.id 23 | } 24 | patch 'update', format: :json, params: args 25 | assert_response :success 26 | @conference.reload 27 | assert_equal 'fake-logo', @conference.logo 28 | assert_equal 'fake-title', @conference.title 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/controllers/frontend/conferences_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class ConferencesControllerTest < ActionController::TestCase 5 | test 'should redirect if slug is not found' do 6 | get :browse 7 | assert_response :redirect 8 | end 9 | 10 | test 'should get browse for slug' do 11 | create :conference, slug: 'a/b/c', downloaded_events_count: 1 12 | create :conference, slug: 'a/e', downloaded_events_count: 1 13 | get :browse, params: { slug: 'a' } 14 | assert_response :success 15 | assert_template :browse 16 | get :browse, params: { slug: 'a/e' } 17 | assert_template :show 18 | end 19 | 20 | test 'should access conference via acronym' do 21 | create :conference, acronym: 'frabcon' 22 | get :show, params: { acronym: 'frabcon' } 23 | assert_response :success 24 | assert_template :show 25 | assert assigns(:conference) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/controllers/frontend/events_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class EventsControllerTest < ActionController::TestCase 5 | def setup 6 | @conference = create :conference, slug: '123' 7 | @event = create :event, conference: @conference, slug: 'abc' 8 | @recording = create :recording, event: @event, filename: 'abc', mime_type: 'video/mp4', language: 'eng' 9 | end 10 | 11 | test 'should get show with slug' do 12 | get :show, params: { slug: @event.slug } 13 | assert_response :success 14 | assert_equal @event.id, assigns(:event).id 15 | end 16 | 17 | test 'should get show' do 18 | get :show, params: { slug: 'abc' } 19 | assert_response :success 20 | end 21 | 22 | test 'should get oembed' do 23 | get :oembed, params: { slug: 'abc' } 24 | assert_response :success 25 | get :oembed, params: { slug: 'abc', width: 12, height: 13 } 26 | assert_equal "12", assigns(:width) 27 | end 28 | 29 | test 'should get postroll' do 30 | other_event = create :event, conference: @conference, slug: 'efg' 31 | get :postroll, params: { slug: 'abc' } 32 | assert_response :success 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/controllers/frontend/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class HomeControllerTest < ActionController::TestCase 5 | test "should get index" do 6 | get :index 7 | assert_response :success 8 | end 9 | test "should get about" do 10 | get :about 11 | assert_response :success 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/controllers/frontend/news_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class NewsControllerTest < ActionController::TestCase 5 | test "should get index" do 6 | create_list :news, 5 7 | get :index, format: :xml 8 | assert_response :success, format: :xml 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/frontend/recent_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class RecentControllerTest < ActionController::TestCase 5 | test 'should redirect if slug is not found' do 6 | create :conference_with_recordings 7 | conference = create :conference_with_recordings 8 | conference.events.update_all(release_date: '2014-08-21') 9 | 10 | get :index 11 | assert_response :success 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/controllers/frontend/sitemap_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class SitemapControllerTest < ActionController::TestCase 5 | test "should get index" do 6 | get :index, format: :xml 7 | assert_response :success 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/controllers/frontend/tags_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Frontend 4 | class TagsControllerTest < ActionController::TestCase 5 | test "should get show for tag" do 6 | get :show, params: { tag: '123' } 7 | assert_response :success 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/controllers/graphql_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GraphqlControllerTest < ActionController::TestCase 4 | setup do 5 | @conference = create :conference_with_recordings 6 | end 7 | 8 | test 'should list conferences' do 9 | post 'execute', params: { 10 | query: " { 11 | allConferences(first:10) {id, title} 12 | } " 13 | } 14 | assert_response :success 15 | assert JSON.parse(response.body) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/controllers/public_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PublicControllerTest < ActionController::TestCase 4 | setup do 5 | @conference = create :conference_with_recordings 6 | end 7 | 8 | test 'should get index' do 9 | get :index, format: :json 10 | assert_response :success 11 | refute_empty JSON.parse(response.body) 12 | end 13 | 14 | test 'should get oembed' do 15 | get :oembed, params: { url: event_url(slug: @conference.events.first.slug) } 16 | assert_response :success 17 | oembed = JSON.parse(response.body) 18 | refute_empty oembed 19 | assert_equal 640, oembed['width'] 20 | assert oembed['html'].include? '640' 21 | end 22 | 23 | test 'should get oembed with dimensions' do 24 | get :oembed, params: { url: event_url(slug: @conference.events.first.slug), maxwidth: 234 } 25 | assert_response :success 26 | oembed = JSON.parse(response.body) 27 | assert_equal 234, oembed['width'] 28 | assert oembed['html'].include? '234' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/fixtures/audio.mp3: -------------------------------------------------------------------------------- 1 | ID3 2 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/test/helpers/.keep -------------------------------------------------------------------------------- /test/helpers/application_helper_test.rb: -------------------------------------------------------------------------------- 1 | # test/helpers/application_helper_test.rb 2 | require 'test_helper' 3 | 4 | class ApplicationHelperTest < ActionView::TestCase 5 | test 'returns "0" for 0 views' do 6 | assert_equal '0', human_readable_views_count(0) 7 | end 8 | 9 | test 'returns "1" for 1 view' do 10 | assert_equal '1', human_readable_views_count(1) 11 | end 12 | 13 | test 'returns "500" for 500 views' do 14 | assert_equal '500', human_readable_views_count(500) 15 | end 16 | 17 | test 'returns "999" for 999 views' do 18 | assert_equal '999', human_readable_views_count(999) 19 | end 20 | 21 | test 'returns "1.0k" for 1000 views' do 22 | assert_equal '1.0k', human_readable_views_count(1000) 23 | end 24 | 25 | test 'returns "10.0k" for 10000 views' do 26 | assert_equal '10.0k', human_readable_views_count(10000) 27 | end 28 | 29 | test 'returns "100.0k" for 100000 views' do 30 | assert_equal '100.0k', human_readable_views_count(100000) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/helpers/public_json_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PublicJsonHelperTest < ActionView::TestCase 4 | test "should return the user's full name" do 5 | fixed_time = '2016-12-12 12:12' 6 | event = build(:event_with_recordings, id: 1, updated_at: fixed_time) 7 | assert_equal 'js_event9cf8de2fe83a8092add513c99f6b9253c9fbf06d', json_cached_key(:event, event, event) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/test/integration/.keep -------------------------------------------------------------------------------- /test/integration/conferences_api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ConferencesApiTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @key = create(:api_key) 6 | @json = json_text 7 | end 8 | 9 | def json_text 10 | json = '{' 11 | json += '"api_key":"' 12 | json += @key.key 13 | json += '",' 14 | json += '"conference":' 15 | url = 'file://' + File.join(Rails.root, 'test', 'fixtures', 'schedule.xml') 16 | d = %'{"acronym":"frab666","recordings_path":"conference/frab123","images_path":"events/frab","slug":"event/frab/frab123","aspect_ratio":"16:9","title":null,"schedule_url":"#{url}"}' 17 | json += d 18 | json+= '}' 19 | json 20 | end 21 | 22 | test "should create conference" do 23 | FileUtils.mkdir_p 'tmp/tests/rec/conference/frab123' 24 | FileUtils.mkdir_p 'tmp/tests/img/events/frab' 25 | assert JSON.parse(@json) 26 | run_background_jobs_immediately do 27 | assert_difference('Conference.count') do 28 | post_json '/api/conferences.json', @json 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/integration/events_public_api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventsPublicApiTest < ActionDispatch::IntegrationTest 4 | test 'should get event via public api' do 5 | @conference = create(:conference_with_recordings) 6 | event = Event.last 7 | event.update(slug: 'test.slug') 8 | get_json "/public/events/test.slug", {} 9 | assert_response :success 10 | assert JSON.parse(response.body) 11 | assert response.body.include?(event.guid) 12 | assert_equal 'application/json', response.headers['Content-Type'] 13 | end 14 | 15 | test 'should get recent events via public api' do 16 | @conference = create(:conference_with_recordings) 17 | get_json "/public/events/recent", {} 18 | assert_response :success 19 | events = JSON.parse(response.body) 20 | assert events['events'] 21 | assert_equal 6, events['events'].count 22 | assert_equal 'application/json', response.headers['Content-Type'] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/frontend/browse_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Frontend::BrowseIntegrationTest < ActionDispatch::IntegrationTest 4 | setup do 5 | create :conference, slug: 'a/b', downloaded_events_count: 1 6 | create :conference, slug: 'a/c', downloaded_events_count: 1 7 | create :conference, slug: 'a/d/e', downloaded_events_count: 1 8 | end 9 | 10 | test 'should browse folders' do 11 | get browse_start_url 12 | assert_response :success 13 | get browse_url('a') 14 | assert_response :success 15 | get browse_url('a/d') 16 | assert_response :success 17 | get browse_url('a/d/e') 18 | assert_response :success 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/integration/frontend/events_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Frontend::EventsIntegrationTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @conference = create :conference_with_recordings 6 | end 7 | 8 | test 'should list events' do 9 | get browse_url(@conference.slug) 10 | assert_response :success 11 | assert_equal @conference.id, assigns(:conference).id 12 | assert_equal @conference.events.count, assigns(:events).count 13 | end 14 | 15 | test 'should view event' do 16 | event = @conference.events.first 17 | get event_url(slug: event.slug) 18 | assert_response :success 19 | assert_equal @conference.id, assigns(:conference).id 20 | assert_equal event.id, assigns(:event).id 21 | end 22 | 23 | test 'should view event with shorter url' do 24 | event = @conference.events.first 25 | get event_url(slug: event.slug) 26 | assert_response :success 27 | assert_equal @conference.id, assigns(:conference).id 28 | assert_equal event.id, assigns(:event).id 29 | end 30 | 31 | test 'should view recent' do 32 | get recent_url 33 | assert_response :success 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/integration/graphql/schema_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SchemaTest < ActiveSupport::TestCase 4 | # disabled due to NoMethodError: undefined method `visible?' for nil:NilClass 5 | # 6 | # def test_printout_is_up_to_date 7 | # current_defn = MediaBackendSchema.to_definition 8 | # printout_defn = File.read(Rails.root.join("app/graphql/schema.graphql")) 9 | # assert_equal(current_defn, printout_defn, "Update the printed schema with `bundle exec rake graphql:schema:dump`") 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /test/integration/recordings_api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RecordingsApiTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @key = create(:api_key) 6 | @event = create(:event) 7 | @json = json_text 8 | end 9 | 10 | def json_text 11 | json = '{' 12 | json += '"api_key":"' 13 | json += @key.key 14 | json += '",' 15 | json += '"guid":"' + @event.guid + '",' 16 | json += '"recording":' 17 | d = '{"filename":"some.mp4","folder":"","mime_type":"audio/ogg","size":"12","length":"30"}' 18 | json += d 19 | json += '}' 20 | json 21 | end 22 | 23 | test 'should create recording via json' do 24 | assert JSON.parse(@json) 25 | assert_difference('Recording.count') do 26 | post_json '/api/recordings.json', @json 27 | end 28 | assert @event.recordings.last 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/lib/feeds/podcast_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Feeds 4 | class PodcastGeneratorTest < ActiveSupport::TestCase 5 | test 'handles invalid recording.duration' do 6 | feed = PodcastGenerator.new(title: 'some-title', channel_summary: 'some-summary', logo_image: 'some-url') 7 | 8 | create_list(:event_with_recordings, 5) 9 | Recording.update_all(length: nil) 10 | events = Frontend::Event.all 11 | assert_nothing_raised { 12 | output = feed.generate(events, &:preferred_recording) 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/test/models/.keep -------------------------------------------------------------------------------- /test/models/admin_user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdminUserTest < ActiveSupport::TestCase 4 | test "should create admin user" do 5 | r = create :admin_user 6 | assert r.valid? 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/models/api_key_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ApiKeyTest < ActiveSupport::TestCase 4 | test "should create key" do 5 | k = ApiKey.new description: "key1" 6 | k.save! 7 | assert_not_nil k.key 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/models/news_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NewsTest < ActiveSupport::TestCase 4 | test "should create news" do 5 | assert_difference('News.count') do 6 | create :news 7 | end 8 | end 9 | test "should not save without date" do 10 | r = News.new 11 | r.title = 'a' 12 | r.body = 'b' 13 | assert_raises(ActiveRecord::RecordInvalid) { r.save! } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/web_feed_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WebFeedTest < ActiveSupport::TestCase 4 | test 'round_to_next_quarter_hour' do 5 | time = WebFeed.round_to_quarter_hour(Time.parse('2017-02-12 14:21:42 +00000')) 6 | assert_equal Time.parse('2017-02-12 14:15:00 +0000'), time 7 | 8 | time = WebFeed.round_to_quarter_hour(Time.parse('2017-02-12 17:38:10 +00000')) 9 | assert_equal Time.parse('2017-02-12 17:30:00 +0000'), time 10 | 11 | time = WebFeed.round_to_quarter_hour(Time.parse('2017-02-12 17:46:10 +00000')) 12 | assert_equal Time.parse('2017-02-12 17:45:00 +0000'), time 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/workers/conference_streaming_download_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ConferenceStreamingDownloadWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | create(:conference, acronym: '32c3') 6 | end 7 | 8 | def test_perform 9 | worker = ConferenceStreamingDownloadWorker.new 10 | assert worker.perform 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/workers/feed/archive_legacy_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::ArchiveLegacyWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_recordings) 6 | @worker = Feed::ArchiveLegacyWorker.new 7 | end 8 | 9 | def test_perform 10 | assert_difference('WebFeed.count') do 11 | assert @worker.perform 12 | end 13 | 14 | f = WebFeed.first 15 | assert_equal 'podcast_archive_legacy', f.key 16 | assert_nil f.kind 17 | refute_empty f.content 18 | 19 | items = xml_rss_items(f.content) 20 | assert_equal 2, items.size 21 | assert_includes items[0].elements['link'].text, Settings.frontend_url 22 | 23 | refute @worker.perform 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/workers/feed/archive_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::ArchiveWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_recordings) 6 | @worker = Feed::ArchiveWorker.new 7 | end 8 | 9 | def test_perform 10 | assert_difference('WebFeed.count', 3) do 11 | assert @worker.perform 12 | end 13 | 14 | f = WebFeed.find_by(kind: 'hq', key: 'podcast_archive') 15 | refute_empty f.content 16 | 17 | items = xml_rss_items(f.content) 18 | assert_equal 2, items.size 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/workers/feed/audio_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::AudioWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_audio_recordings) 6 | @conference.events.update_all(release_date: Time.now) 7 | 8 | @worker = Feed::AudioWorker.new 9 | end 10 | 11 | def test_perform 12 | assert_difference('WebFeed.count') do 13 | assert @worker.perform 14 | end 15 | 16 | f = WebFeed.first 17 | assert_equal 'podcast_audio', f.key 18 | assert_nil f.kind 19 | refute_empty f.content 20 | 21 | items = xml_rss_items(f.content) 22 | assert_equal 1, items.size 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/workers/feed/legacy_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::LegacyWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_recordings) 6 | @conference.events.update_all(release_date: Time.now) 7 | 8 | @worker = Feed::LegacyWorker.new 9 | end 10 | 11 | def test_perform 12 | assert_difference('WebFeed.count') do 13 | assert @worker.perform 14 | end 15 | 16 | f = WebFeed.first 17 | assert_equal 'podcast_legacy', f.key 18 | assert_nil f.kind 19 | refute_empty f.content 20 | 21 | items = xml_rss_items(f.content) 22 | assert_equal 2, items.size 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/workers/feed/podcast_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::PodcastWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_recordings) 6 | @conference.events.update_all(release_date: Time.now) 7 | 8 | @worker = Feed::PodcastWorker.new 9 | end 10 | 11 | def test_perform 12 | assert_difference('WebFeed.count', 3) do 13 | assert @worker.perform 14 | end 15 | 16 | f = WebFeed.find_by(kind: 'hq', key: 'podcast') 17 | refute_empty f.content 18 | 19 | items = xml_rss_items(f.content) 20 | assert_equal 2, items.size 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/workers/feed/recent_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Feed::RecentWorkerTest < ActiveSupport::TestCase 4 | def setup 5 | @conference = create(:conference_with_recordings) 6 | @worker = Feed::RecentWorker.new 7 | end 8 | 9 | def test_perform 10 | assert_difference('WebFeed.count') do 11 | assert @worker.perform 12 | end 13 | 14 | f = WebFeed.first 15 | assert_equal 'rdftop100', f.key 16 | assert_nil f.kind 17 | refute_empty f.content 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /vendor/assets/fonts/estre.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/fonts/estre.eot -------------------------------------------------------------------------------- /vendor/assets/fonts/estre.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/fonts/estre.otf -------------------------------------------------------------------------------- /vendor/assets/fonts/estre.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/fonts/estre.ttf -------------------------------------------------------------------------------- /vendor/assets/fonts/estre.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/fonts/estre.woff -------------------------------------------------------------------------------- /vendor/assets/icomoon-font/Read Me.txt: -------------------------------------------------------------------------------- 1 | Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. 2 | 3 | To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts 4 | 5 | You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. 6 | 7 | You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. 8 | -------------------------------------------------------------------------------- /vendor/assets/icomoon-font/demo-files/demo.js: -------------------------------------------------------------------------------- 1 | if (!('boxShadow' in document.body.style)) { 2 | document.body.setAttribute('class', 'noBoxShadow'); 3 | } 4 | 5 | document.body.addEventListener("click", function(e) { 6 | var target = e.target; 7 | if (target.tagName === "INPUT" && 8 | target.getAttribute('class').indexOf('liga') === -1) { 9 | target.select(); 10 | } 11 | }); 12 | 13 | (function() { 14 | var fontSize = document.getElementById('fontSize'), 15 | testDrive = document.getElementById('testDrive'), 16 | testText = document.getElementById('testText'); 17 | function updateTest() { 18 | testDrive.innerHTML = testText.value || String.fromCharCode(160); 19 | if (window.icomoonLiga) { 20 | window.icomoonLiga(testDrive); 21 | } 22 | } 23 | function updateSize() { 24 | testDrive.style.fontSize = fontSize.value + 'px'; 25 | } 26 | fontSize.addEventListener('change', updateSize, false); 27 | testText.addEventListener('input', updateTest, false); 28 | testText.addEventListener('change', updateTest, false); 29 | updateSize(); 30 | }()); 31 | -------------------------------------------------------------------------------- /vendor/assets/icomoon-font/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/icomoon-font/fonts/icomoon.eot -------------------------------------------------------------------------------- /vendor/assets/icomoon-font/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/icomoon-font/fonts/icomoon.ttf -------------------------------------------------------------------------------- /vendor/assets/icomoon-font/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/icomoon-font/fonts/icomoon.woff -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/airplay/airplay.css: -------------------------------------------------------------------------------- 1 | .mejs__airplay-button > button, 2 | .mejs-airplay-button > button { 3 | background: url('airplay.svg') no-repeat 0 4px; 4 | } 5 | 6 | .mejs__airplay-button > button .fill, 7 | .mejs-airplay-button > button .fill { 8 | fill: #fff; 9 | } 10 | 11 | .mejs__airplay-button > button.active .fill, 12 | .mejs-airplay-button > button.active .fill { 13 | fill: #66a8cc; 14 | } 15 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/airplay/airplay.min.css: -------------------------------------------------------------------------------- 1 | .mejs-airplay-button>button,.mejs__airplay-button>button{background:url(airplay.svg) no-repeat 0 4px}.mejs-airplay-button>button .fill,.mejs__airplay-button>button .fill{fill:#fff}.mejs-airplay-button>button.active .fill,.mejs__airplay-button>button.active .fill{fill:#66a8cc} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/airplay/airplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/airplay/airplay.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/airplay/airplay.svg: -------------------------------------------------------------------------------- 1 | 7 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/chromecast/chromecast.css: -------------------------------------------------------------------------------- 1 | .mejs__chromecast-button > button, 2 | .mejs-chromecast-button > button { 3 | --disconnected-color: #fff; 4 | background: none; 5 | display: inline-block; 6 | } 7 | .mejs__chromecast-container, 8 | .mejs-chromecast-container { 9 | background: #000; 10 | color: #fff; 11 | font-size: 10px; 12 | left: 0; 13 | padding: 5px; 14 | position: absolute; 15 | top: 0; 16 | z-index: 1; 17 | } 18 | 19 | .mejs__chromecast-layer > img, 20 | .mejs-chromecast-layer > img { 21 | left: 0; 22 | position: absolute; 23 | top: 0; 24 | z-index: 0; 25 | } 26 | 27 | .mejs__chromecast-icon, 28 | .mejs-chromecast-icon { 29 | background: url('chromecast.svg') no-repeat 0 0; 30 | display: inline-block; 31 | height: 14px; 32 | margin-right: 5px; 33 | width: 17px; 34 | } -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/chromecast/chromecast.min.css: -------------------------------------------------------------------------------- 1 | .mejs-chromecast-button>button,.mejs__chromecast-button>button{--disconnected-color:#fff;background:none;display:inline-block}.mejs-chromecast-container,.mejs__chromecast-container{background:#000;color:#fff;font-size:10px;left:0;padding:5px;position:absolute;top:0;z-index:1}.mejs-chromecast-layer>img,.mejs__chromecast-layer>img{left:0;position:absolute;top:0;z-index:0}.mejs-chromecast-icon,.mejs__chromecast-icon{background:url(chromecast.svg) no-repeat 0 0;display:inline-block;height:14px;margin-right:5px;width:17px} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/chromecast/chromecast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/chromecast/chromecast.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/chromecast/chromecast.svg: -------------------------------------------------------------------------------- 1 | 5 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/context-menu/context-menu.css: -------------------------------------------------------------------------------- 1 | .mejs__contextmenu, 2 | .mejs-contextmenu { 3 | background: #fff; 4 | border: solid 1px #999; 5 | border-radius: 4px; 6 | left: 0; 7 | padding: 10px; 8 | position: absolute; 9 | top: 0; 10 | width: 150px; 11 | z-index: 9999999999; /* make sure it shows on fullscreen */ 12 | } 13 | 14 | .mejs__contextmenu-separator, 15 | .mejs-contextmenu-separator { 16 | background: #333; 17 | font-size: 0; 18 | height: 1px; 19 | margin: 5px 6px; 20 | } 21 | 22 | .mejs__contextmenu-item, 23 | .mejs-contextmenu-item { 24 | color: #333; 25 | cursor: pointer; 26 | font-size: 12px; 27 | padding: 4px 6px; 28 | } 29 | 30 | .mejs__contextmenu-item:hover, 31 | .mejs-contextmenu-item:hover { 32 | background: #2c7c91; 33 | color: #fff; 34 | } 35 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/context-menu/context-menu.min.css: -------------------------------------------------------------------------------- 1 | .mejs-contextmenu,.mejs__contextmenu{background:#fff;border:1px solid #999;border-radius:4px;left:0;padding:10px;position:absolute;top:0;width:150px;z-index:1}.mejs-contextmenu-separator,.mejs__contextmenu-separator{background:#333;font-size:0;height:1px;margin:5px 6px}.mejs-contextmenu-item,.mejs__contextmenu-item{color:#333;cursor:pointer;font-size:12px;padding:4px 6px}.mejs-contextmenu-item:hover,.mejs__contextmenu-item:hover{background:#2c7c91;color:#fff} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/jump-forward/jump-forward.css.scss: -------------------------------------------------------------------------------- 1 | .mejs__jump-forward-button > button, 2 | .mejs-jump-forward-button > button { 3 | background: asset-url('jump-forward/jumpforward.svg') no-repeat 0 0; 4 | color: #fff; 5 | font-size: 8px; 6 | line-height: normal; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/jump-forward/jumpforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/jump-forward/jumpforward.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/jump-forward/jumpforward.svg: -------------------------------------------------------------------------------- 1 | 2 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/loop/loop.css: -------------------------------------------------------------------------------- 1 | .mejs__loop-button > button, 2 | .mejs-loop-button > button { 3 | background: url('loop.svg') no-repeat transparent; 4 | } 5 | .mejs__loop-off > button, 6 | .mejs-loop-off > button { 7 | background-position: -20px 1px; 8 | } 9 | 10 | .mejs__loop-on > button, 11 | .mejs-loop-on > button { 12 | background-position: 0 1px; 13 | } 14 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/loop/loop.min.css: -------------------------------------------------------------------------------- 1 | .mejs-loop-button>button,.mejs__loop-button>button{background:url(loop.svg) no-repeat transparent}.mejs-loop-off>button,.mejs__loop-off>button{background-position:-20px 1px}.mejs-loop-on>button,.mejs__loop-on>button{background-position:0 1px} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/loop/loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/loop/loop.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/loop/loop.svg: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/postroll/postroll.css: -------------------------------------------------------------------------------- 1 | .mejs__postroll-layer, 2 | .mejs-postroll-layer { 3 | background: rgba(50, 50, 50, 0.7); 4 | bottom: 0; 5 | height: 100%; 6 | left: 0; 7 | overflow: hidden; 8 | position: absolute; 9 | width: 100%; 10 | z-index: 1000; 11 | } 12 | 13 | .mejs__postroll-layer-content, 14 | .mejs-postroll-layer-content { 15 | height: 100%; 16 | width: 100%; 17 | } 18 | .mejs__postroll-close, 19 | .mejs-postroll-close { 20 | background: rgba(50, 50, 50, 0.7); 21 | color: #fff; 22 | cursor: pointer; 23 | padding: 4px; 24 | position: absolute; 25 | right: 0; 26 | top: 0; 27 | z-index: 100; 28 | } 29 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/postroll/postroll.min.css: -------------------------------------------------------------------------------- 1 | .mejs-postroll-layer,.mejs__postroll-layer{background:rgba(50,50,50,.7);bottom:0;height:100%;left:0;overflow:hidden;position:absolute;width:100%;z-index:2}.mejs-postroll-layer-content,.mejs__postroll-layer-content{height:100%;width:100%}.mejs-postroll-close,.mejs__postroll-close{background:rgba(50,50,50,.7);color:#fff;cursor:pointer;padding:4px;position:absolute;right:0;top:0;z-index:1} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/quality/quality.min.css: -------------------------------------------------------------------------------- 1 | .mejs-qualities-button,.mejs__qualities-button{position:relative}.mejs-qualities-button>button,.mejs__qualities-button>button{background:transparent;color:#fff;font-size:11px;line-height:normal;margin:11px 0 0;width:36px}.mejs-qualities-selector,.mejs__qualities-selector{background:rgba(50,50,50,.7);border:1px solid transparent;border-radius:0;height:100px;left:-10px;overflow:hidden;padding:0;position:absolute;top:-100px;width:60px}.mejs-qualities-selector ul,.mejs__qualities-selector ul{display:block;list-style-type:none!important;margin:0;overflow:hidden;padding:0}.mejs-qualities-selector li,.mejs__qualities-selector li{color:#fff;cursor:pointer;display:block;list-style-type:none!important;margin:0 0 6px;overflow:hidden;padding:0 10px}.mejs-qualities-selector li:hover,.mejs__qualities-selector li:hover{background-color:hsla(0,0%,100%,.2);cursor:pointer}.mejs-qualities-selector input,.mejs__qualities-selector input{clear:both;float:left;left:-1000px;margin:3px 3px 0 5px;position:absolute}.mejs-qualities-selector label,.mejs__qualities-selector label{cursor:pointer;float:left;font-size:10px;line-height:15px;padding:4px 0 0;width:55px}.mejs-qualities-selected,.mejs__qualities-selected{color:#21f8f8} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/skip-back/skip-back.css.scss: -------------------------------------------------------------------------------- 1 | .mejs__skip-back-button > button, 2 | .mejs-skip-back-button > button { 3 | background: asset-url('skip-back/skipback.svg') no-repeat 0 -1px; 4 | color: #fff; 5 | font-size: 8px; 6 | line-height: normal; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/skip-back/skipback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/skip-back/skipback.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/skip-back/skipback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/source-chooser/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/source-chooser/settings.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/source-chooser/settings.svg: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/stop/stop.css: -------------------------------------------------------------------------------- 1 | .mejs__stop > button, 2 | .mejs-stop > button { 3 | background: url('stop.svg') 0 2px no-repeat; 4 | } 5 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/stop/stop.min.css: -------------------------------------------------------------------------------- 1 | .mejs-stop>button,.mejs__stop>button{background:url(stop.svg) 0 2px no-repeat} -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/stop/stop.svg: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/vrview/cardboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement-plugins/vrview/cardboard.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/vrview/cardboard.svg: -------------------------------------------------------------------------------- 1 | 6 -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/vrview/vrview.css: -------------------------------------------------------------------------------- 1 | .mejs__vrview-button > button, 2 | .mejs-vrview-button > button { 3 | background: url('cardboard.svg') no-repeat 0 4px; 4 | } -------------------------------------------------------------------------------- /vendor/assets/mediaelement-plugins/vrview/vrview.min.css: -------------------------------------------------------------------------------- 1 | .mejs-vrview-button>button,.mejs__vrview-button>button{background:url(cardboard.svg) no-repeat 0 4px} -------------------------------------------------------------------------------- /vendor/assets/mediaelement/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/background.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement/bigplay.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/bigplay.fw.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement/bigplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/bigplay.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement/jumpforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/jumpforward.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement/loading-divoc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/loading-divoc.gif -------------------------------------------------------------------------------- /vendor/assets/mediaelement/loading-rc3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/loading-rc3.gif -------------------------------------------------------------------------------- /vendor/assets/mediaelement/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/loading.gif -------------------------------------------------------------------------------- /vendor/assets/mediaelement/mejs-controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/mejs-controls.png -------------------------------------------------------------------------------- /vendor/assets/mediaelement/skipback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/mediaelement/skipback.png -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voc/voctoweb/2bb12571403a4f846cc911730e0aa3842208e85a/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------