├── .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 | %[#{h tag}]
7 | end
8 |
9 | def link_for(conference, tag, css: '')
10 | %[#{h tag}]
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------