From fcec0a776bda3f2243e4a9b68c7ddb45c61a1ea5 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 21 Jan 2014 14:51:03 +0000 Subject: [PATCH] * icecast working locally on my mac (VRFS-1002) --- admin/Gemfile | 1 + admin/app/admin/icecast_bootstrap.rb | 82 ++++++- admin/app/admin/icecast_mount_template.rb | 3 + admin/app/admin/icecast_server_group.rb | 3 + admin/app/helpers/application_helper.rb | 1 + admin/lib/utils.rb | 1 + db/build | 2 +- db/manifest | 4 +- db/up/integrate_icecast_into_sessions.sql | 207 ++++++++---------- db/up/music_sessions_unlogged.sql | 128 +++++++++++ pb/src/client_container.proto | 119 +++++----- ruby/Gemfile | 1 + ruby/lib/jam_ruby.rb | 4 + ruby/lib/jam_ruby/connection_manager.rb | 8 + ruby/lib/jam_ruby/message_factory.rb | 29 ++- ruby/lib/jam_ruby/models/genre.rb | 1 - .../models/icecast_admin_authentication.rb | 3 +- ruby/lib/jam_ruby/models/icecast_directory.rb | 3 +- ruby/lib/jam_ruby/models/icecast_limit.rb | 3 +- .../jam_ruby/models/icecast_listen_socket.rb | 3 +- ruby/lib/jam_ruby/models/icecast_logging.rb | 3 +- .../models/icecast_master_server_relay.rb | 3 +- ruby/lib/jam_ruby/models/icecast_mount.rb | 189 ++++++++++++---- .../jam_ruby/models/icecast_mount_template.rb | 62 ++++++ ruby/lib/jam_ruby/models/icecast_path.rb | 3 +- ruby/lib/jam_ruby/models/icecast_relay.rb | 3 +- ruby/lib/jam_ruby/models/icecast_security.rb | 3 +- ruby/lib/jam_ruby/models/icecast_server.rb | 136 ++++++++---- .../jam_ruby/models/icecast_server_group.rb | 11 + .../jam_ruby/models/icecast_server_socket.rb | 6 + ruby/lib/jam_ruby/models/icecast_template.rb | 4 +- .../models/icecast_template_socket.rb | 6 + .../models/icecast_user_authentication.rb | 3 +- ruby/lib/jam_ruby/models/music_session.rb | 20 ++ ruby/lib/jam_ruby/models/notification.rb | 18 +- ruby/lib/jam_ruby/models/user.rb | 2 + ruby/lib/jam_ruby/mq_router.rb | 19 +- .../jam_ruby/resque/icecast_config_writer.rb | 4 +- .../resque/scheduled/IcecastSourceCheck.rb | 32 +++ .../resque/scheduled/icecast_config_retry.rb | 4 + .../resque/scheduled/icecast_source_check.rb | 59 +++++ ruby/spec/factories.rb | 35 ++- .../icecast_admin_authentication_spec.rb | 12 + .../jam_ruby/models/icecast_directory_spec.rb | 12 + .../jam_ruby/models/icecast_limit_spec.rb | 12 + .../models/icecast_listen_socket_spec.rb | 12 + .../jam_ruby/models/icecast_logging_spec.rb | 12 + .../icecast_master_server_relay_spec.rb | 12 + .../jam_ruby/models/icecast_mount_spec.rb | 162 +++++++++++++- .../models/icecast_mount_template_spec.rb | 37 ++++ .../spec/jam_ruby/models/icecast_path_spec.rb | 12 + .../jam_ruby/models/icecast_relay_spec.rb | 6 + .../jam_ruby/models/icecast_security_spec.rb | 12 + .../jam_ruby/models/icecast_server_spec.rb | 30 +++ .../jam_ruby/models/icecast_template_spec.rb | 12 +- .../jam_ruby/models/music_session_spec.rb | 31 +++ .../resque/icecast_source_check_spec.rb | 110 ++++++++++ ruby/spec/spec_helper.rb | 4 +- ruby/spec/support/utilities.rb | 4 + web/Gemfile | 1 + .../assets/javascripts/AAB_message_factory.js | 6 + web/app/assets/javascripts/fakeJamClient.js | 12 + web/app/assets/javascripts/sidebar.js | 87 +++++++- web/app/controllers/api_icecast_controller.rb | 9 +- web/config/application.rb | 1 + web/config/scheduler.yml | 7 + web/lib/music_session_manager.rb | 13 +- websocket-gateway/Gemfile | 1 + 68 files changed, 1548 insertions(+), 312 deletions(-) create mode 100644 admin/app/admin/icecast_mount_template.rb create mode 100644 admin/app/admin/icecast_server_group.rb create mode 100644 db/up/music_sessions_unlogged.sql create mode 100644 ruby/lib/jam_ruby/models/icecast_mount_template.rb create mode 100644 ruby/lib/jam_ruby/models/icecast_server_group.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb create mode 100644 ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb create mode 100644 ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb diff --git a/admin/Gemfile b/admin/Gemfile index 7c8ba4476..a14387965 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -54,6 +54,7 @@ gem 'gon' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' +gem 'resque-lonely_job', '~> 1.0.0' gem 'eventmachine', '1.0.3' gem 'amqp', '0.9.8' diff --git a/admin/app/admin/icecast_bootstrap.rb b/admin/app/admin/icecast_bootstrap.rb index 1cc15e22e..3b3f643b6 100644 --- a/admin/app/admin/icecast_bootstrap.rb +++ b/admin/app/admin/icecast_bootstrap.rb @@ -3,16 +3,18 @@ ActiveAdmin.register_page "Bootstrap" do page_action :create_server, :method => :post do - template = IcecastTemplate.find(params[:jam_ruby_icecast_server][:template_id]) + template = IcecastTemplate.find_by_id(params[:jam_ruby_icecast_server][:template_id]) + mount_template = IcecastMountTemplate.find_by_id(params[:jam_ruby_icecast_server][:mount_template_id]) hostname = params[:jam_ruby_icecast_server][:hostname] server = IcecastServer.new server.template = template + server.mount_template = mount_template server.hostname = hostname server.server_id = hostname server.save! - redirect_to admin_bootstrap_path, :notice => "Server created. If you start job worker (bundle exec rake all_jobs), it should update your icecast config." + redirect_to admin_bootstrap_path, :notice => "Server created. If you start a job worker (bundle exec rake all_jobs in /web), it should update your icecast config." end page_action :brew_template, :method => :post do @@ -81,23 +83,85 @@ ActiveAdmin.register_page "Bootstrap" do template.save! end - - redirect_to admin_bootstrap_path, :notice => "Brew template created. Create a server now with that template specified." + redirect_to admin_bootstrap_path, :notice => "Brew template created. Now, create a mount template." end + page_action :create_mount_template, :method => :post do + IcecastServer.transaction do + hostname = params[:jam_ruby_icecast_mount_template][:hostname] + type = params[:jam_ruby_icecast_mount_template][:default_mime_type] + + auth = IcecastUserAuthentication.new + auth.authentication_type = 'url' + auth.mount_add = 'http://' + hostname + '/api/icecast/mount_add' + auth.mount_remove = 'http://' + hostname + '/api/icecast/mount_remove' + auth.listener_add = 'http://' + hostname + '/api/icecast/listener_add' + auth.listener_remove = 'http://' + hostname + '/api/icecast/listener_remove' + auth.auth_header = 'HTTP/1.1 200 OK' + auth.timelimit_header = 'icecast-auth-timelimit:' + auth.save! + + mount_template = IcecastMountTemplate.new + mount_template.name = "#{type}-#{IcecastMountTemplate.count}" + mount_template.source_username = nil # mount will override + mount_template.source_pass = nil # mount will override + mount_template.max_listeners = 20000 # huge + mount_template.max_listener_duration = 3600 * 24 # one day + mount_template.fallback_override = 1 + mount_template.fallback_when_full = 1 + mount_template.is_public = 0 + mount_template.stream_name = nil # mount will override + mount_template.stream_description = nil # mount will override + mount_template.stream_url = nil # mount will override + mount_template.genre = nil # mount will override + mount_template.bitrate = 128 + mount_template.burst_size = 65536 + mount_template.hidden = 1 + mount_template.on_connect = nil + mount_template.on_disconnect = nil + mount_template.authentication = auth + + if type == 'ogg' + mount_template.mp3_metadata_interval = nil + mount_template.mime_type ='audio/ogg' + mount_template.subtype = 'vorbis' + mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.ogg" + else + mount_template.mp3_metadata_interval = 4096 + mount_template.mime_type ='audio/mpeg' + mount_template.subtype = nil + mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.mp3" + end + mount_template.save! + end + + redirect_to admin_bootstrap_path, :notice => "Mount template created. Create a server now with your new templates specified." + end + + action_item do - link_to "Create Brew Template", admin_bootstrap_brew_template_path, :method => :post + link_to "Create MacOSX (Brew) Template", admin_bootstrap_brew_template_path, :method => :post end content do if IcecastTemplate.count == 0 - para "You need to create at least one template for your environment" + para "You need to create at least one server template, and one mount template. Click one of the top-left buttons based on your platform" + + elsif IcecastMountTemplate.count == 0 + semantic_form_for IcecastMountTemplate.new, :url => admin_bootstrap_create_mount_template_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "New Mount Template" do + f.input :hostname, :label => "jam-web hostname:port" + f.input :default_mime_type, :as => :select, :collection => ["ogg", "mp3"] + end + f.actions + end else semantic_form_for IcecastServer.new, :url => admin_bootstrap_create_server_path, :builder => ActiveAdmin::FormBuilder do |f| - f.inputs "New Server" do - f.input :hostname - f.input :template + f.inputs "New Icecast Server" do + f.input :hostname, :hint => "Just the icecast hostname; no port" + f.input :template, :hint => "This is the template associated with the server. Not as useful for the 1st server, but subsequent servers can use this same template, and share config" + f.input :mount_template, :hint => "The mount template. When mounts are made as music sessions are created, this template will satisfy templatable values" end f.actions end diff --git a/admin/app/admin/icecast_mount_template.rb b/admin/app/admin/icecast_mount_template.rb new file mode 100644 index 000000000..3a369da11 --- /dev/null +++ b/admin/app/admin/icecast_mount_template.rb @@ -0,0 +1,3 @@ +ActiveAdmin.register JamRuby::IcecastMountTemplate, :as => 'IcecastMountTemplate' do + menu :parent => 'Icecast' +end diff --git a/admin/app/admin/icecast_server_group.rb b/admin/app/admin/icecast_server_group.rb new file mode 100644 index 000000000..2c560996d --- /dev/null +++ b/admin/app/admin/icecast_server_group.rb @@ -0,0 +1,3 @@ +ActiveAdmin.register JamRuby::IcecastServerGroup, :as => 'IcecastServerGroup' do + menu :parent => 'Icecast' +end diff --git a/admin/app/helpers/application_helper.rb b/admin/app/helpers/application_helper.rb index a2f487023..6e9385e59 100644 --- a/admin/app/helpers/application_helper.rb +++ b/admin/app/helpers/application_helper.rb @@ -1,3 +1,4 @@ module ApplicationHelper + end diff --git a/admin/lib/utils.rb b/admin/lib/utils.rb index aa92de6f7..4af46d734 100644 --- a/admin/lib/utils.rb +++ b/admin/lib/utils.rb @@ -3,4 +3,5 @@ module Utils chars = ((('a'..'z').to_a + ('0'..'9').to_a) - %w(i o 0 1 l 0)) (1..size).collect{|a| cc = chars[rand(chars.size)]; 0==rand(2) ? cc.upcase : cc }.join end + end diff --git a/db/build b/db/build index e0ece07a9..259aea93b 100755 --- a/db/build +++ b/db/build @@ -19,7 +19,7 @@ rm -rf $TARGET mkdir -p $PG_BUILD_OUT mkdir -p $PG_RUBY_PACKAGE_OUT -#bundle update +bundle update echo "building migrations" bundle exec pg_migrate build --source . --out $PG_BUILD_OUT --test --verbose diff --git a/db/manifest b/db/manifest index 5a9d3c4c8..8e39f5b24 100755 --- a/db/manifest +++ b/db/manifest @@ -88,4 +88,6 @@ icecast.sql home_page_promos.sql mix_job_watch.sql music_session_constraints.sql -mixes_drop_manifest_add_retry.sql \ No newline at end of file +mixes_drop_manifest_add_retry.sql +music_sessions_unlogged.sql +integrate_icecast_into_sessions.sql \ No newline at end of file diff --git a/db/up/integrate_icecast_into_sessions.sql b/db/up/integrate_icecast_into_sessions.sql index d92fa96c5..6c29b3562 100644 --- a/db/up/integrate_icecast_into_sessions.sql +++ b/db/up/integrate_icecast_into_sessions.sql @@ -1,129 +1,96 @@ --- this manifest update makes every table associated with music_sessions UNLOGGED --- tables to mark UNLOGGED --- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions - --- tables to just get rid of --- session_plays - --- breaking foreign keys for tables --- connections: user_id --- fan_invitations: receiver_id, sender_id --- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id --- genres_music_sessions: genre_id --- invitations: sender_id, receiver_id --- fan_invitations: user_id --- notifications: invitation_id, join_request_id, session_id - -DROP TABLE sessions_plays; - --- divorce notifications from UNLOGGED tables - --- NOTIFICATIONS ----------------- --- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey; --- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey; --- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey; - --- FAN_INVITATIONS ------------------- -DROP TABLE fan_invitations; -DROP TABLE invitations; -DROP TABLE join_requests; -DROP TABLE genres_music_sessions; -DROP TABLE tracks; -DROP TABLE connections; -DROP TABLE music_sessions; - --- MUSIC_SESSIONS ------------------ -CREATE UNLOGGED TABLE music_sessions ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - description VARCHAR(8000), - user_id VARCHAR(64) NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - musician_access BOOLEAN NOT NULL, - band_id VARCHAR(64), - approval_required BOOLEAN NOT NULL, - fan_access BOOLEAN NOT NULL, - fan_chat BOOLEAN NOT NULL, - claimed_recording_id VARCHAR(64), - claimed_recording_initiator_id VARCHAR(64) +CREATE TABLE icecast_mount_templates( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + name VARCHAR(256) NOT NULL, + source_username VARCHAR(64), + source_pass VARCHAR(64), + max_listeners INTEGER DEFAULT 4, + max_listener_duration INTEGER DEFAULT 3600, + dump_file VARCHAR(1024), + intro VARCHAR(1024), + fallback_mount VARCHAR(1024), + fallback_override INTEGER DEFAULT 1, + fallback_when_full INTEGER DEFAULT 1, + charset VARCHAR(1024) DEFAULT 'ISO8859-1', + is_public INTEGER DEFAULT 0, + stream_name VARCHAR(1024), + stream_description VARCHAR(10000), + stream_url VARCHAR(1024), + genre VARCHAR(256), + bitrate INTEGER, + mime_type VARCHAR(64) NOT NULL DEFAULT 'audio/mpeg', + subtype VARCHAR(64), + burst_size INTEGER, + mp3_metadata_interval INTEGER, + hidden INTEGER DEFAULT 1, + on_connect VARCHAR(1024), + on_disconnect VARCHAR(1024), + authentication_id varchar(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); --- CONNECTIONS --------------- -CREATE UNLOGGED TABLE connections ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - user_id VARCHAR(64), - client_id VARCHAR(64) UNIQUE NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - music_session_id VARCHAR(64), - ip_address VARCHAR(64), - as_musician BOOLEAN, - aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL +ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP NOT NULL; +ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP DEFAULT; +ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP NOT NULL; +ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP DEFAULT; +ALTER TABLE icecast_mounts ADD COLUMN music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE icecast_mounts ADD COLUMN icecast_server_id VARCHAR(64) NOT NULL REFERENCES icecast_servers(id); +ALTER TABLE icecast_mounts ADD COLUMN icecast_mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id); +ALTER TABLE icecast_mounts ADD COLUMN sourced_needs_changing_at TIMESTAMP; +; +CREATE TABLE icecast_server_groups ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + name VARCHAR(255) UNIQUE NOT NULL ); -ALTER TABLE ONLY connections ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE SET NULL; +-- bootstrap the default icecast group +INSERT INTO icecast_server_groups (id, name) VALUES ('default', 'default'); +INSERT INTO icecast_server_groups (id, name) VALUES ('unused', 'unused'); --- GENRES_MUSIC_SESSIONS ------------------------- -CREATE UNLOGGED TABLE genres_music_sessions ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - genre_id VARCHAR(64), - music_session_id VARCHAR(64) -); -ALTER TABLE ONLY genres_music_sessions ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE users ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default'; -CREATE UNLOGGED TABLE fan_invitations ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - sender_id VARCHAR(64), - receiver_id VARCHAR(64), - music_session_id VARCHAR(64), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL -); -ALTER TABLE ONLY fan_invitations ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +-- and by default, all servers and users are in this group +ALTER TABLE icecast_servers ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default'; +ALTER TABLE icecast_servers ADD COLUMN mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id); -CREATE UNLOGGED TABLE join_requests ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - user_id VARCHAR(64), - music_session_id VARCHAR(64), - text VARCHAR(2000), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL -); -ALTER TABLE ONLY join_requests ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id); -ALTER TABLE ONLY join_requests ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; --- INVITATIONS --------------- -CREATE UNLOGGED TABLE invitations ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - sender_id VARCHAR(64), - receiver_id VARCHAR(64), - music_session_id VARCHAR(64), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - join_request_id VARCHAR(64) -); -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_admin_auth_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_mount_template_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_mount_template_id_fkey" FOREIGN KEY (mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_directory_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_icecast_server_group_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_icecast_server_group_id_fkey" FOREIGN KEY (icecast_server_group_id) REFERENCES icecast_server_groups(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_limit_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_logging_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_master_relay_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_path_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_security_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_template_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_template_id_fkey" FOREIGN KEY (template_id) REFERENCES icecast_templates(id) ON DELETE SET NULL; --- TRACKS ---------- -CREATE UNLOGGED TABLE tracks ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - connection_id VARCHAR(64), - instrument_id VARCHAR(64), - sound VARCHAR(64) NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - client_track_id VARCHAR(64) NOT NULL -); -ALTER TABLE ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; +ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey"; +ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey" FOREIGN KEY (icecast_mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; +ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_server_id_fkey"; +ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_server_id_fkey" FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE SET NULL; + +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_admin_auth_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_directory_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_limit_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_logging_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_master_relay_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_path_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_security_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; diff --git a/db/up/music_sessions_unlogged.sql b/db/up/music_sessions_unlogged.sql new file mode 100644 index 000000000..fad3bc8f5 --- /dev/null +++ b/db/up/music_sessions_unlogged.sql @@ -0,0 +1,128 @@ +-- this manifest update makes every table associated with music_sessions UNLOGGED + +-- tables to mark UNLOGGED +-- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions + +-- breaking foreign keys for tables +-- connections: user_id +-- fan_invitations: receiver_id, sender_id +-- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id +-- genres_music_sessions: genre_id +-- invitations: sender_id, receiver_id +-- fan_invitations: user_id +-- notifications: invitation_id, join_request_id, session_id + + +-- divorce notifications from UNLOGGED tables + +DROP TABLE sessions_plays; + +-- NOTIFICATIONS +---------------- +-- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey; +-- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey; +-- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey; + +-- FAN_INVITATIONS +------------------ +DROP TABLE fan_invitations; +DROP TABLE invitations; +DROP TABLE join_requests; +DROP TABLE genres_music_sessions; +DROP TABLE tracks; +DROP TABLE connections; +DROP TABLE music_sessions; + +-- MUSIC_SESSIONS +----------------- +CREATE UNLOGGED TABLE music_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + description VARCHAR(8000), + user_id VARCHAR(64) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + musician_access BOOLEAN NOT NULL, + band_id VARCHAR(64), + approval_required BOOLEAN NOT NULL, + fan_access BOOLEAN NOT NULL, + fan_chat BOOLEAN NOT NULL, + claimed_recording_id VARCHAR(64), + claimed_recording_initiator_id VARCHAR(64) +); + +-- CONNECTIONS +-------------- +CREATE UNLOGGED TABLE connections ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64), + client_id VARCHAR(64) UNIQUE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + music_session_id VARCHAR(64), + ip_address VARCHAR(64), + as_musician BOOLEAN, + aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL +); +ALTER TABLE ONLY connections ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE SET NULL; + +-- GENRES_MUSIC_SESSIONS +------------------------ +CREATE UNLOGGED TABLE genres_music_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + genre_id VARCHAR(64), + music_session_id VARCHAR(64) +); +ALTER TABLE ONLY genres_music_sessions ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +CREATE UNLOGGED TABLE fan_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + sender_id VARCHAR(64), + receiver_id VARCHAR(64), + music_session_id VARCHAR(64), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); +ALTER TABLE ONLY fan_invitations ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +CREATE UNLOGGED TABLE join_requests ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64), + music_session_id VARCHAR(64), + text VARCHAR(2000), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); +ALTER TABLE ONLY join_requests ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id); +ALTER TABLE ONLY join_requests ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +-- INVITATIONS +-------------- +CREATE UNLOGGED TABLE invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + sender_id VARCHAR(64), + receiver_id VARCHAR(64), + music_session_id VARCHAR(64), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + join_request_id VARCHAR(64) +); +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +-- TRACKS +--------- +CREATE UNLOGGED TABLE tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + connection_id VARCHAR(64), + instrument_id VARCHAR(64), + sound VARCHAR(64) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + client_track_id VARCHAR(64) NOT NULL +); +ALTER TABLE ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; + diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index ca59728b2..c6669a824 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -9,67 +9,69 @@ package jampb; message ClientMessage { enum Type { - LOGIN = 100; - LOGIN_ACK = 105; - LOGIN_MUSIC_SESSION = 110; - LOGIN_MUSIC_SESSION_ACK = 115; - LEAVE_MUSIC_SESSION = 120; - LEAVE_MUSIC_SESSION_ACK = 125; - HEARTBEAT = 130; - HEARTBEAT_ACK = 135; + LOGIN = 100; + LOGIN_ACK = 105; + LOGIN_MUSIC_SESSION = 110; + LOGIN_MUSIC_SESSION_ACK = 115; + LEAVE_MUSIC_SESSION = 120; + LEAVE_MUSIC_SESSION_ACK = 125; + HEARTBEAT = 130; + HEARTBEAT_ACK = 135; // friend notifications - FRIEND_UPDATE = 140; - FRIEND_REQUEST = 145; - FRIEND_REQUEST_ACCEPTED = 150; - FRIEND_SESSION_JOIN = 155; - NEW_USER_FOLLOWER = 160; - NEW_BAND_FOLLOWER = 161; + FRIEND_UPDATE = 140; + FRIEND_REQUEST = 145; + FRIEND_REQUEST_ACCEPTED = 150; + FRIEND_SESSION_JOIN = 155; + NEW_USER_FOLLOWER = 160; + NEW_BAND_FOLLOWER = 161; // session invitations - SESSION_INVITATION = 165; - SESSION_ENDED = 170; - JOIN_REQUEST = 175; - JOIN_REQUEST_APPROVED = 180; - JOIN_REQUEST_REJECTED = 185; - SESSION_JOIN = 190; - SESSION_DEPART = 195; - MUSICIAN_SESSION_JOIN = 196; + SESSION_INVITATION = 165; + SESSION_ENDED = 170; + JOIN_REQUEST = 175; + JOIN_REQUEST_APPROVED = 180; + JOIN_REQUEST_REJECTED = 185; + SESSION_JOIN = 190; + SESSION_DEPART = 195; + MUSICIAN_SESSION_JOIN = 196; // recording notifications - MUSICIAN_RECORDING_SAVED = 200; - BAND_RECORDING_SAVED = 205; - RECORDING_STARTED = 210; - RECORDING_ENDED = 215; - RECORDING_MASTER_MIX_COMPLETE = 220; - DOWNLOAD_AVAILABLE = 221; + MUSICIAN_RECORDING_SAVED = 200; + BAND_RECORDING_SAVED = 205; + RECORDING_STARTED = 210; + RECORDING_ENDED = 215; + RECORDING_MASTER_MIX_COMPLETE = 220; + DOWNLOAD_AVAILABLE = 221; // band notifications - BAND_INVITATION = 225; - BAND_INVITATION_ACCEPTED = 230; - BAND_SESSION_JOIN = 235; + BAND_INVITATION = 225; + BAND_INVITATION_ACCEPTED = 230; + BAND_SESSION_JOIN = 235; - MUSICIAN_SESSION_FRESH = 240; - MUSICIAN_SESSION_STALE = 245; + MUSICIAN_SESSION_FRESH = 240; + MUSICIAN_SESSION_STALE = 245; // icecast notifications - SOURCE_UP_REQUESTED = 250; - SOURCE_DOWN_REQUESTED = 255; + SOURCE_UP_REQUESTED = 250; + SOURCE_DOWN_REQUESTED = 251; + SOURCE_UP = 252; + SOURCE_DOWN = 253; - TEST_SESSION_MESSAGE = 295; + TEST_SESSION_MESSAGE = 295; - PING_REQUEST = 300; - PING_ACK = 305; - PEER_MESSAGE = 310; - TEST_CLIENT_MESSAGE = 315; + PING_REQUEST = 300; + PING_ACK = 305; + PEER_MESSAGE = 310; + TEST_CLIENT_MESSAGE = 315; - SERVER_BAD_STATE_RECOVERED = 900; + SERVER_BAD_STATE_RECOVERED = 900; - SERVER_GENERIC_ERROR = 1000; - SERVER_REJECTION_ERROR = 1005; - SERVER_PERMISSION_ERROR = 1010; - SERVER_BAD_STATE_ERROR = 1015; + SERVER_GENERIC_ERROR = 1000; + SERVER_REJECTION_ERROR = 1005; + SERVER_PERMISSION_ERROR = 1010; + SERVER_BAD_STATE_ERROR = 1015; } // Identifies which inner message is filled in @@ -126,7 +128,9 @@ message ClientMessage { // icecast notifications optional SourceUpRequested source_up_requested = 250; - optional SourceDownRequested source_down_requested = 255; + optional SourceDownRequested source_down_requested = 251; + optional SourceUp source_up = 252; + optional SourceDown source_down = 253; // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -381,15 +385,26 @@ message MusicianSessionStale { } message SourceUpRequested { - optional string host = 1; // icecast server host - optional int32 port = 2; // icecast server port - optional string mount = 3; // mount name - optional string source_user = 4; // source user - optional string source_pass = 5; // source pass + optional string music_session = 1; // music session id + optional string host = 2; // icecast server host + optional int32 port = 3; // icecast server port + optional string mount = 4; // mount name + optional string source_user = 5; // source user + optional string source_pass = 6; // source pass + optional int32 bitrate = 7; } message SourceDownRequested { - optional string mount = 1; // mount name + optional string music_session = 1; // music session id + optional string mount = 2; // mount name +} + +message SourceUp { + optional string music_session = 1; // music session id +} + +message SourceDown { + optional string music_session = 1; // music session id } // route_to: session diff --git a/ruby/Gemfile b/ruby/Gemfile index ca3ab001a..f8a1643ac 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -29,6 +29,7 @@ gem 'postgres_ext' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer" +gem 'resque-lonely_job', '~> 1.0.0' gem 'oj' gem 'builder' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 9f5eb11eb..ce77bf09d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -33,6 +33,7 @@ require "jam_ruby/resque/audiomixer" require "jam_ruby/resque/icecast_config_writer" require "jam_ruby/resque/scheduled/audiomixer_retry" require "jam_ruby/resque/scheduled/icecast_config_retry" +require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/mq_router" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" @@ -113,6 +114,9 @@ require "jam_ruby/models/icecast_server_mount" require "jam_ruby/models/icecast_server_relay" require "jam_ruby/models/icecast_server_socket" require "jam_ruby/models/icecast_template_socket" +require "jam_ruby/models/icecast_server_group" +require "jam_ruby/models/icecast_mount_template" + include Jampb diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index a16ee93b8..5a13351c5 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -37,6 +37,12 @@ module JamRuby return friend_ids end + # this simulates music_session destroy callbacks with activerecord + def before_destroy_music_session(music_session_id) + music_session = MusicSession.find_by_id(music_session_id) + music_session.before_destroy if music_session + end + # reclaim the existing connection, def reconnect(conn, reconnect_music_session_id) music_session_id = nil @@ -218,6 +224,7 @@ SQL # same for session-if we are down to the last participant, delete the session unless music_session_id.nil? + before_destroy_music_session(music_session_id) result = conn.exec("DELETE FROM music_sessions WHERE id = $1 AND 0 = (select count(music_session_id) FROM connections where music_session_id = $1)", [music_session_id]) if result.cmd_tuples == 1 music_session_id = nil @@ -258,6 +265,7 @@ SQL end if num_participants == 0 # delete the music_session + before_destroy_music_session(previous_music_session_id) conn.exec("DELETE from music_sessions WHERE id = $1", [previous_music_session_id]) do |result| if result.cmd_tuples == 1 diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index f23eb7087..2c91c16da 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -558,13 +558,15 @@ module JamRuby # create a source up requested message to send to clients in a session, # so that one of the clients will start sending source audio to icecast - def source_up_requested (session_id, host, port, mount, source_user, source_pass) + def source_up_requested (session_id, host, port, mount, source_user, source_pass, bitrate) source_up_requested = Jampb::SourceUpRequested.new( + music_session: session_id, host: host, port: port, mount: mount, source_user: source_user, - source_pass: source_pass) + source_pass: source_pass, + bitrate: bitrate) Jampb::ClientMessage.new( type: ClientMessage::Type::SOURCE_UP_REQUESTED, @@ -575,7 +577,7 @@ module JamRuby # create a source up requested message to send to clients in a session, # so that one of the clients will start sending source audio to icecast def source_down_requested (session_id, mount) - source_down_requested = Jampb::SourceDownRequested.new(mount: mount) + source_down_requested = Jampb::SourceDownRequested.new(music_session: session_id, mount: mount) Jampb::ClientMessage.new( type: ClientMessage::Type::SOURCE_DOWN_REQUESTED, @@ -583,6 +585,27 @@ module JamRuby source_down_requested: source_down_requested) end + # let's someone know that the source came online. the stream activate shortly + # it might be necessary to refresh the client + def source_up (session_id) + source_up = Jampb::SourceUp.new(music_session: session_id) + + Jampb::ClientMessage.new( + type: ClientMessage::Type::SOURCE_UP, + route_to: SESSION_TARGET_PREFIX + session_id, + source_up: source_up) + end + + # let's someone know that the source went down. the stream will go offline + def source_down (session_id) + source_down = Jampb::SourceDown.new(music_session: session_id) + + Jampb::ClientMessage.new( + type: ClientMessage::Type::SOURCE_DOWN, + route_to: SESSION_TARGET_PREFIX + session_id, + source_down: source_down) + end + # create a test message to send in session def test_session_message(session_id, msg) test = Jampb::TestSessionMessage.new(:msg => msg) diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 80fb3c1f8..45776c59f 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -11,6 +11,5 @@ module JamRuby # music sessions has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions" - end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb index 4880495b5..55fb29dda 100644 --- a/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb +++ b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb @@ -12,7 +12,8 @@ module JamRuby validates :relay_pass, presence: true, length: {minimum: 5} validates :admin_user, presence: true, length: {minimum: 5} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_directory.rb b/ruby/lib/jam_ruby/models/icecast_directory.rb index 29ddd2b6a..0f425c55f 100644 --- a/ruby/lib/jam_ruby/models/icecast_directory.rb +++ b/ruby/lib/jam_ruby/models/icecast_directory.rb @@ -9,7 +9,8 @@ module JamRuby validates :yp_url_timeout, presence: true, numericality: {only_integer: true}, length: {in: 1..30} validates :yp_url, presence: true - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_limit.rb b/ruby/lib/jam_ruby/models/icecast_limit.rb index 76a95ee3e..b47475e7c 100644 --- a/ruby/lib/jam_ruby/models/icecast_limit.rb +++ b/ruby/lib/jam_ruby/models/icecast_limit.rb @@ -15,7 +15,8 @@ module JamRuby validates :source_timeout, presence: true, numericality: {only_integer: true} validates :burst_size, presence: true, numericality: {only_integer: true} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_listen_socket.rb b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb index 569740eb4..90ce3a2c1 100644 --- a/ruby/lib/jam_ruby/models/icecast_listen_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb @@ -12,7 +12,8 @@ module JamRuby validates :port, presence: true, numericality: {only_integer: true}, length: {in: 1..65535} validates :shoutcast_compat, :inclusion => {:in => [nil, 0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_logging.rb b/ruby/lib/jam_ruby/models/icecast_logging.rb index 0d41bd0e1..b9c579084 100644 --- a/ruby/lib/jam_ruby/models/icecast_logging.rb +++ b/ruby/lib/jam_ruby/models/icecast_logging.rb @@ -12,7 +12,8 @@ module JamRuby validates :log_archive, :inclusion => {:in => [nil, 0, 1]} validates :log_size, numericality: {only_integer: true}, if: lambda {|m| m.log_size.present?} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb b/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb index 0c3d04733..6215c5a03 100644 --- a/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb +++ b/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb @@ -14,7 +14,8 @@ module JamRuby validates :master_pass, presence: true, length: {minimum: 5} validates :relays_on_demand, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_mount.rb b/ruby/lib/jam_ruby/models/icecast_mount.rb index 2b19cee51..1014561fd 100644 --- a/ruby/lib/jam_ruby/models/icecast_mount.rb +++ b/ruby/lib/jam_ruby/models/icecast_mount.rb @@ -1,17 +1,22 @@ module JamRuby class IcecastMount < ActiveRecord::Base + @@log = Logging.logger[IcecastMount] + attr_accessible :authentication_id, :name, :source_username, :source_pass, :max_listeners, :max_listener_duration, :dump_file, :intro, :fallback_mount, :fallback_override, :fallback_when_full, :charset, :is_public, :stream_name, :stream_description, :stream_url, :genre, :bitrate, :mime_type, :subtype, :burst_size, - :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, as: :admin + :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, + :music_session_id, :icecast_server_id, :icecast_mount_template_id, :listeners, :sourced, + :sourced_needs_changing_at, as: :admin belongs_to :authentication, class_name: "JamRuby::IcecastUserAuthentication", inverse_of: :mount, :foreign_key => 'authentication_id' + belongs_to :music_session, class_name: "JamRuby::MusicSession", inverse_of: :mount, foreign_key: 'music_session_id' - has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :mount, :foreign_key => 'icecast_mount_id' - has_many :servers, :class_name => "JamRuby::IcecastServer", :through => :server_mounts, :source => :server + belongs_to :server, class_name: "JamRuby::IcecastServer", inverse_of: :mounts, foreign_key: 'icecast_server_id' + belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", inverse_of: :mounts, foreign_key: 'icecast_mount_template_id' - validates :name, presence: true + validates :name, presence: true, uniqueness: true validates :source_username, length: {minimum: 5}, if: lambda {|m| m.source_username.present?} validates :source_pass, length: {minimum: 5}, if: lambda {|m| m.source_pass.present?} validates :max_listeners, length: {in: 1..15000}, if: lambda {|m| m.max_listeners.present?} @@ -19,93 +24,167 @@ module JamRuby validates :fallback_override, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} validates :fallback_when_full, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} validates :is_public, presence: true, :inclusion => {:in => [-1, 0, 1]} - validates :stream_name, presence: true - validates :stream_description, presence: true - validates :stream_url, presence: true - validates :genre, presence: true validates :bitrate, numericality: {only_integer: true}, if: lambda {|m| m.bitrate.present?} - validates :mime_type, presence: true - validates :subtype, presence: true validates :burst_size, numericality: {only_integer: true}, if: lambda {|m| m.burst_size.present?} validates :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?} validates :hidden, :inclusion => {:in => [0, 1]} - validate :name_has_correct_format + validates :server, presence: true + validate :name_has_correct_format - before_save :sanitize_active_admin - after_save :after_save - after_commit :after_commit + before_save :sanitize_active_admin + after_save :after_save + after_save :poke_config + before_destroy :poke_config def name_has_correct_format errors.add(:name, "must start with /") unless name && name.start_with?('/') end - def after_save - IcecastServer.update(servers, config_changed: 1) + def poke_config + server.update_attribute(:config_changed, 1) if server + end + + def after_save + server.update_attribute(:config_changed, 1) - # transiting to sourced from not sourced if !sourced_was && sourced + # went from NOT SOURCED to SOURCED + notify_source_up + + elsif sourced_was && !sourced + + # went from SOURCED to NOT SOURCED + notify_source_down + end + + if listeners_was == 0 && listeners > 0 && !sourced + # listener count went above 0 and there is no source. ask the musician clients to source + notify_source_up_requested + end + + # Note: + # Notification.send_source_down_requested does not occur here. + # we set up a cron that checks for streams that have not been successfully source up/down (after timeout ) in IcecastSourceCheck end def sanitize_active_admin self.authentication_id = nil if self.authentication_id == '' + self.music_session_id = nil if self.music_session_id == '' + self.icecast_server_id = nil if self.icecast_server_id == '' + end + + # creates a templated + def self.build_session_mount(music_session) + + # only public sessions get mounts currently + return nil unless music_session.fan_access + + icecast_server = IcecastServer.find_best_server_for_user(music_session.creator) + + mount = nil + if icecast_server && icecast_server.mount_template_id + # we have a server with an associated mount_template; we can create a mount automatically + mount = icecast_server.mount_template.build_session_mount(music_session) + mount.server = icecast_server + end + mount end def source_up with_lock do self.sourced = true - self.save(:validate => false) + self.sourced_needs_changing_at = nil + save(validate: false) end end def source_down with_lock do - sourced = false - save(:validate => false) + self.sourced = false + self.sourced_needs_changing_at = nil + save(validate: false) end end def listener_add with_lock do - increment!(:listeners) + sourced_needs_changing_at = Time.now if listeners == 0 + + # this is completely unsafe without that 'with_lock' statement above + self.listeners = self.listeners + 1 + + save(validate: false) end end - def listener_remove + if listeners == 0 + @@log.warn("listeners is at 0, but we are being asked to remove a listener. maybe we missed a listener_add request earlier") + return + end + with_lock do - decrement!(:listeners) + sourced_needs_changing_at = Time.now if listeners == 1 + + # this is completely unsafe without that 'with_lock' statement above + self.listeners = self.listeners - 1 + + save(validations: false) end end + def notify_source_up_requested + Notification.send_source_up_requested(music_session, + server.hostname, + server.pick_listen_socket(:port), + name, + resolve_string(:source_username), + resolve_string(:source_pass), + resolve_int(:bitrate)) if music_session_id + end + + def notify_source_down_requested + Notification.send_source_down_requested(music_session, name) + end + + def notify_source_up + Notification.send_source_up(music_session) if music_session_id + end + + def notify_source_down + Notification.send_source_down(music_session) if music_session_id + end + + # Check if the icecast_mount specifies the value; if not, use the mount_template's value take effect def dumpXml(builder) builder.tag! 'mount' do |mount| mount.tag! 'mount-name', name - mount.tag! 'username', source_username if !source_username.nil? && !source_username.empty? - mount.tag! 'password', source_pass if !source_pass.nil? && !source_pass.empty? - mount.tag! 'max-listeners', max_listeners unless max_listeners.nil? - mount.tag! 'max-listener-duration', max_listener_duration unless max_listener_duration.nil? - mount.tag! 'dump-file', dump_file if !dump_file.nil? && !dump_file.empty? - mount.tag! 'intro', intro if !intro.nil? && !intro.empty? - mount.tag! 'fallback-mount', fallback_mount if !fallback_mount.nil? && !fallback_mount.empty? - mount.tag! 'fallback-override', fallback_override if fallback_override - mount.tag! 'fallback-when-full', fallback_when_full if fallback_when_full - mount.tag! 'charset', charset if charset - mount.tag! 'public', is_public - mount.tag! 'stream-name', stream_name if !stream_name.nil? && !stream_name.empty? - mount.tag! 'stream-description', stream_description if !stream_description.nil? && !stream_description.empty? - mount.tag! 'stream-url', stream_url if !stream_url.nil? && !stream_url.empty? - mount.tag! 'genre', genre unless genre.empty? - mount.tag! 'bitrate', bitrate if bitrate - mount.tag! 'type', mime_type - mount.tag! 'subtype', subtype - mount.tag! 'burst-size', burst_size if burst_size - mount.tag! 'mp3-metadata-interval', mp3_metadata_interval unless mp3_metadata_interval.nil? - mount.tag! 'hidden', hidden - mount.tag! 'on-connect', on_connect if on_connect - mount.tag! 'on-disconnect', on_disconnect if on_disconnect + mount.tag! 'username', resolve_string(:source_username) if string_present?(:source_username) + mount.tag! 'password', resolve_string(:source_pass) if string_present?(:source_pass) + mount.tag! 'max-listeners', resolve_int(:max_listeners) if int_present?(:max_listeners) + mount.tag! 'max-listener-duration', resolve_string(:max_listener_duration) if int_present?(:max_listener_duration) + mount.tag! 'dump-file', resolve_string(:dump_file) if string_present?(:dump_file) + mount.tag! 'intro', resolve_string(:intro) if string_present?(:intro) + mount.tag! 'fallback-mount', resolve_string(:fallback_mount) if string_present?(:fallback_mount) + mount.tag! 'fallback-override', resolve_int(:fallback_override) if int_present?(:fallback_override) + mount.tag! 'fallback-when-full', resolve_int(:fallback_when_full) if int_present?(:fallback_when_full) + mount.tag! 'charset', resolve_string(:charset) if string_present?(:charset) + mount.tag! 'public', resolve_int(:is_public) if int_present?(:is_public) + mount.tag! 'stream-name', resolve_string(:stream_name) if string_present?(:stream_name) + mount.tag! 'stream-description', resolve_string(:stream_description) if string_present?(:stream_description) + mount.tag! 'stream-url', resolve_string(:stream_url) if string_present?(:stream_url) + mount.tag! 'genre', resolve_string(:genre) if string_present?(:genre) + mount.tag! 'bitrate', resolve_int(:bitrate) if int_present?(:bitrate) + mount.tag! 'type', resolve_string(:mime_type) if string_present?(:mime_type) + mount.tag! 'subtype', resolve_string(:subtype) if string_present?(:subtype) + mount.tag! 'burst-size', resolve_int(:burst_size) if int_present?(:burst_size) + mount.tag! 'mp3-metadata-interval', resolve_int(:mp3_metadata_interval) if int_present?(:mp3_metadata_interval) + mount.tag! 'hidden', resolve_int(:hidden) if int_present?(:hidden) + mount.tag! 'on-connect', resolve_string(:on_connect) if string_present?(:on_connect) + mount.tag! 'on-disconnect', resolve_string(:on_disconnect) if string_present?(:on_disconnect) authentication.dumpXml(builder) if authentication end @@ -117,5 +196,23 @@ module JamRuby "http://" + server_mount.server.hostname + self.name end + + + def resolve_string(field) + self[field].present? ? self[field] : mount_template && mount_template[field] + end + + def string_present?(field) + val = resolve_string(field) + val ? val.present? : false + end + + def resolve_int(field) + !self[field].nil? ? self[field]: mount_template && mount_template[field] + end + + def int_present?(field) + resolve_int(field) + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_mount_template.rb b/ruby/lib/jam_ruby/models/icecast_mount_template.rb new file mode 100644 index 000000000..f6b844969 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_mount_template.rb @@ -0,0 +1,62 @@ +module JamRuby + class IcecastMountTemplate < ActiveRecord::Base + + attr_accessor :hostname, :default_mime_type # used by jam-admin + + attr_accessible :authentication_id, :source_username, :source_pass, :max_listeners, :max_listener_duration, + :dump_file, :intro, :fallback_mount, :fallback_override, :fallback_when_full, :charset, :is_public, + :stream_name, :stream_description, :stream_url, :genre, :bitrate, :mime_type, :subtype, :burst_size, + :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, :name, as: :admin + + belongs_to :authentication, class_name: "JamRuby::IcecastUserAuthentication", inverse_of: :mount, foreign_key: 'authentication_id' + has_many :mounts, class_name: "JamRuby::IcecastMount", inverse_of: :mount_template, foreign_key: 'icecast_mount_template_id' + has_many :servers, class_name: "JamRuby::IcecastServer", inverse_of: :mount_template, foreign_key: 'mount_template_id' + + validates :source_username, length: {minimum: 5}, if: lambda {|m| m.source_username.present?} + validates :source_pass, length: {minimum: 5}, if: lambda {|m| m.source_pass.present?} + validates :max_listeners, length: {in: 1..15000}, if: lambda {|m| m.max_listeners.present?} + validates :max_listener_duration, length: {in: 1..3600 * 48}, if: lambda {|m| m.max_listener_duration.present?} + validates :fallback_override, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} + validates :fallback_when_full, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} + validates :is_public, presence: true, :inclusion => {:in => [-1, 0, 1]} + validates :bitrate, numericality: {only_integer: true}, if: lambda {|m| m.bitrate.present?} + validates :mime_type, presence: true + validates :burst_size, numericality: {only_integer: true}, if: lambda {|m| m.burst_size.present?} + validates :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?} + validates :hidden, :inclusion => {:in => [0, 1]} + + before_save :sanitize_active_admin + after_save :poke_config + after_initialize :after_initialize + before_destroy :poke_config + + def after_initialize # used by jam-admin + self.hostname = 'localhost:3000' + self.default_mime_type = 'mp3' + end + + def poke_config + IcecastServer.update(servers, config_changed: 1) + end + + def sanitize_active_admin + self.authentication_id = nil if self.authentication_id == '' + end + + # pick a server that's in the same group as the user that is under the least load + def build_session_mount(music_session) + mount = IcecastMount.new + mount.authentication = authentication + mount.mount_template = self + mount.name = "/" + SecureRandom.urlsafe_base64 + mount.music_session_id = music_session.id + mount.source_username = 'source' + mount.source_pass = SecureRandom.urlsafe_base64 + mount.stream_name = "JamKazam music session created by #{music_session.creator.name}" + mount.stream_description = music_session.description + mount.stream_url = "http://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget + mount.genre = music_session.genres.map {|genre| genre.description}.join(',') + mount + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_path.rb b/ruby/lib/jam_ruby/models/icecast_path.rb index ab74fe45a..7b9f7cecb 100644 --- a/ruby/lib/jam_ruby/models/icecast_path.rb +++ b/ruby/lib/jam_ruby/models/icecast_path.rb @@ -12,7 +12,8 @@ module JamRuby validates :web_root, presence: true validates :admin_root, presence: true - after_save :poke_config + after_save :poke_config + before_destroy :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_relay.rb b/ruby/lib/jam_ruby/models/icecast_relay.rb index f8c6bc712..11f7bec2d 100644 --- a/ruby/lib/jam_ruby/models/icecast_relay.rb +++ b/ruby/lib/jam_ruby/models/icecast_relay.rb @@ -13,7 +13,8 @@ module JamRuby validates :relay_shoutcast_metadata, :inclusion => {:in => [0, 1]} validates :on_demand, presence: true, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, :config_changed => true) diff --git a/ruby/lib/jam_ruby/models/icecast_security.rb b/ruby/lib/jam_ruby/models/icecast_security.rb index 2980f72e4..bae4180e4 100644 --- a/ruby/lib/jam_ruby/models/icecast_security.rb +++ b/ruby/lib/jam_ruby/models/icecast_security.rb @@ -8,7 +8,8 @@ module JamRuby validates :chroot, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_server.rb b/ruby/lib/jam_ruby/models/icecast_server.rb index 39320925b..4cc3bd23d 100644 --- a/ruby/lib/jam_ruby/models/icecast_server.rb +++ b/ruby/lib/jam_ruby/models/icecast_server.rb @@ -3,28 +3,30 @@ module JamRuby attr_accessor :skip_config_changed_flag - attr_accessible :template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id, + attr_accessible :template_id, :mount_template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id, :security_id, :config_changed, :hostname, :location, :admin_email, :fileserve, as: :admin - belongs_to :template, :class_name => "JamRuby::IcecastTemplate", foreign_key: 'template_id', :inverse_of => :servers + belongs_to :template, class_name: "JamRuby::IcecastTemplate", foreign_key: 'template_id', inverse_of: :servers + belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", foreign_key: 'mount_template_id', inverse_of: :servers + belongs_to :server_group, class_name: "JamRuby::IcecastServerGroup", foreign_key: 'icecast_server_group_id', inverse_of: :servers # all are overrides, because the template defines all of these as well. When building the XML, we will prefer these if set - belongs_to :limit, :class_name => "JamRuby::IcecastLimit", foreign_key: 'limit_id', :inverse_of => :servers - belongs_to :admin_auth, :class_name => "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', :inverse_of => :servers - belongs_to :directory, :class_name => "JamRuby::IcecastDirectory", foreign_key: 'directory_id', :inverse_of => :servers - belongs_to :master_relay, :class_name => "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', :inverse_of => :servers - belongs_to :path, :class_name => "JamRuby::IcecastPath", foreign_key: 'path_id', :inverse_of => :servers - belongs_to :logging, :class_name => "JamRuby::IcecastLogging", foreign_key: 'logging_id', :inverse_of => :servers - belongs_to :security, :class_name => "JamRuby::IcecastSecurity", foreign_key: 'security_id', :inverse_of => :servers - has_many :listen_socket_servers, :class_name => "JamRuby::IcecastServerSocket", :inverse_of => :server - has_many :listen_sockets, :class_name => "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket + belongs_to :limit, class_name: "JamRuby::IcecastLimit", foreign_key: 'limit_id', inverse_of: :servers + belongs_to :admin_auth, class_name: "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', inverse_of: :servers + belongs_to :directory, class_name: "JamRuby::IcecastDirectory", foreign_key: 'directory_id', inverse_of: :servers + belongs_to :master_relay, class_name: "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', inverse_of: :servers + belongs_to :path, class_name: "JamRuby::IcecastPath", foreign_key: 'path_id', inverse_of: :servers + belongs_to :logging, class_name: "JamRuby::IcecastLogging", foreign_key: 'logging_id', inverse_of: :servers + belongs_to :security, class_name: "JamRuby::IcecastSecurity", foreign_key: 'security_id', inverse_of: :servers + has_many :listen_socket_servers, class_name: "JamRuby::IcecastServerSocket", inverse_of: :server + has_many :listen_sockets, class_name: "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket # mounts and relays are naturally server-specific, though - has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :server - has_many :mounts, :class_name => "JamRuby::IcecastMount", :through => :server_mounts, :source => :mount + #has_many :server_mounts, class_name: "JamRuby::IcecastServerMount", inverse_of: :server + has_many :mounts, class_name: "JamRuby::IcecastMount", inverse_of: :server, :foreign_key => 'icecast_server_id' - has_many :server_relays, :class_name => "JamRuby::IcecastServerRelay", :inverse_of => :relay - has_many :relays, :class_name => "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay + has_many :server_relays, class_name: "JamRuby::IcecastServerRelay", inverse_of: :relay + has_many :relays, class_name: "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay validates :config_changed, :inclusion => {:in => [0, 1]} validates :hostname, presence: true @@ -32,6 +34,7 @@ module JamRuby validates :server_id, presence: true validates :template, presence: true + validates :mount_template, presence: true before_save :before_save, unless: lambda { skip_config_changed_flag } before_save :sanitize_active_admin @@ -75,8 +78,48 @@ module JamRuby end end + + def pick_listen_socket(field) + current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets + socket = current_listen_sockets.first + socket[:field] if socket + end + + + # pick an icecast server with the least listeners * sources + def self.find_best_server_for_user(user) + chosen_server_id = nil + chosen_server_weight = nil + + ActiveRecord::Base.connection_pool.with_connection do |connection| + result = connection.execute('select SUM(listeners), SUM(sourced::int), icecast_servers.id + FROM icecast_servers + LEFT JOIN icecast_mounts ON icecast_servers.id = icecast_mounts.icecast_server_id + WHERE icecast_server_group_id = \'' + user.icecast_server_group_id + '\' + GROUP BY icecast_servers.id;') + + result.cmd_tuples.times do |i| + listeners = result.getvalue(i, 0).to_i + sourced = result.getvalue(i, 1).to_i + icecast_server_id = result.getvalue(i, 2) + + # compute weight. source is much more intensive than listener, based on load tests again 2.3.0 + # http://icecast.org/loadtest2.php + + weight = sourced * 10 + listeners + + if !chosen_server_id || (weight < chosen_server_weight) + chosen_server_id = icecast_server_id + chosen_server_weight = weight + end + end + end + + IcecastServer.find(chosen_server_id) if chosen_server_id + end + def to_s - return server_id + server_id end def dumpXml (output=$stdout, indent=1) @@ -84,29 +127,21 @@ module JamRuby builder = ::Builder::XmlMarkup.new(:target => output, :indent => indent) builder.tag! 'icecast' do |root| - root.tag! 'hostname', hostname - root.tag! 'location', (location.nil? || location.empty?) ? template.location : location - root.tag! 'server-id', server_id - root.tag! 'admin', (admin_email.nil? || admin_email.empty?) ? template.admin_email : admin_email - root.tag! 'fileserve', fileserve.nil? ? template.fileserve : fileserve + root.tag! 'hostname', hostname + root.tag! 'server-id', server_id + root.tag! 'location', resolve_string(:location) if string_present?(:location) + root.tag! 'admin', resolve_string(:admin_email) if string_present?(:admin_email) + root.tag! 'fileserve', resolve_int(:fileserve) if int_present?(:fileserve) + + resolve_association(:limit).dumpXml(builder) if association_present?(:limit) + resolve_association(:admin_auth).dumpXml(builder) if association_present?(:admin_auth) + resolve_association(:directory).dumpXml(builder) if association_present?(:directory) + resolve_association(:master_relay).dumpXml(builder) if association_present?(:master_relay) + resolve_association(:path).dumpXml(builder) if association_present?(:path) + resolve_association(:logging).dumpXml(builder) if association_present?(:logging) + resolve_association(:security).dumpXml(builder) if association_present?(:security) - # do we have an override specified? or do we go with the template - current_limit = limit ? limit : template.limit - current_admin_auth = admin_auth ? admin_auth : template.admin_auth - current_directory = directory ? directory : template.directory - current_master_relay = master_relay ? master_relay : template.master_relay - current_path = path ? path : template.path - current_logging = logging ? logging : template.logging - current_security = security ? security : template.security current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets - - current_limit.dumpXml(builder) unless current_limit.nil? - current_admin_auth.dumpXml(builder) unless current_admin_auth.nil? - current_directory.dumpXml(builder) unless current_directory.nil? - current_master_relay.dumpXml(builder) unless current_master_relay.nil? - current_path.dumpXml(builder) unless current_path.nil? - current_logging.dumpXml(builder) unless current_logging.nil? - current_security.dumpXml(builder) unless current_security.nil? current_listen_sockets.each do |listen_socket| listen_socket.dumpXml(builder) end @@ -120,5 +155,32 @@ module JamRuby end end end + + def resolve_string(field) + self[field].present? ? self[field] : template && template[field] + end + + def string_present?(field) + val = resolve_string(field) + val ? val.present? : false + end + + def resolve_int(field) + self[field] ? self[field]: template && template[field] + end + + def int_present?(field) + resolve_int(field) + end + + def resolve_association(field) + self.send(field) ? self.send(field) : template && template.send(field) + end + + def association_present?(field) + resolve_association(field) + end + + end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_server_group.rb b/ruby/lib/jam_ruby/models/icecast_server_group.rb new file mode 100644 index 000000000..cce2e329d --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_server_group.rb @@ -0,0 +1,11 @@ +module JamRuby + class IcecastServerGroup < ActiveRecord::Base + + attr_accessible :name, as: :admin + + has_many :users, class_name: "JamRuby::User", inverse_of: :icecast_server_group, foreign_key: 'icecast_server_group_id' + has_many :servers, class_name: "JamRuby::IcecastServer", inverse_of: :server_group, foreign_key: 'icecast_server_group_id' + + validates :name, presence: true, uniqueness: true + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_server_socket.rb b/ruby/lib/jam_ruby/models/icecast_server_socket.rb index b99d5a006..3468e11a1 100644 --- a/ruby/lib/jam_ruby/models/icecast_server_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_server_socket.rb @@ -11,5 +11,11 @@ module JamRuby validates :socket, :presence => true validates :server, :presence => true + after_save :poke_config + before_destroy :poke_config + + def poke_config + server.update_attribute(:config_changed, 1) if server + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_template.rb b/ruby/lib/jam_ruby/models/icecast_template.rb index 6813c2a04..b2459fe89 100644 --- a/ruby/lib/jam_ruby/models/icecast_template.rb +++ b/ruby/lib/jam_ruby/models/icecast_template.rb @@ -33,8 +33,8 @@ module JamRuby validates :listen_sockets, length: {minimum: 1} before_save :sanitize_active_admin - after_save :poke_config - + after_save :poke_config + before_destroy :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_template_socket.rb b/ruby/lib/jam_ruby/models/icecast_template_socket.rb index 91c0f2eed..49ce9fdef 100644 --- a/ruby/lib/jam_ruby/models/icecast_template_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_template_socket.rb @@ -11,5 +11,11 @@ module JamRuby validates :socket, :presence => true validates :template, :presence => true + after_save :poke_config + before_destroy :poke_config + + def poke_config + IcecastServer.update(template.servers, config_changed: 1) if template + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_user_authentication.rb b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb index 01da5b32c..bbacfa46e 100644 --- a/ruby/lib/jam_ruby/models/icecast_user_authentication.rb +++ b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb @@ -17,10 +17,11 @@ module JamRuby validates :auth_header, presence: true, if: :url_auth? validates :timelimit_header, presence: true, if: :url_auth? + before_destroy :poke_config after_save :poke_config def poke_config - IcecastServer.update(mount.servers, config_changed: 1) if mount + mount.server.update_attribute(:config_changed, 1) if mount && mount.server end def to_s diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 1a2df5603..ff4ce0c2c 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -9,6 +9,8 @@ module JamRuby belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" + has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id' + has_many :connections, :class_name => "JamRuby::Connection" has_many :users, :through => :connections, :class_name => "JamRuby::User" has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions" @@ -37,6 +39,9 @@ module JamRuby validate :creator_is_musician validate :no_new_playback_while_playing + def before_destroy + self.mount.destroy if self.mount + end def creator_is_musician unless creator.musician? @@ -53,6 +58,21 @@ module JamRuby end end + # returns an array of client_id's that are in this session + # if as_musician is nil, all connections in the session ,regardless if it's a musician or not or not + # you can also exclude a client_id from the returned set by setting exclude_client_id + def get_connection_ids(options = {}) + as_musician = options[:as_musician] + exclude_client_id = options[:exclude_client_id] + + where = { :music_session_id => self.id } + where[:as_musician] = as_musician unless as_musician.nil? + + exclude = "client_id != '#{exclude_client_id}'"unless exclude_client_id.nil? + + Connection.select(:client_id).where(where).where(exclude).map(&:client_id) + end + # This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true # If so, then it's an OR condition. If both are false, you can get sessions with anyone. def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil) diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 6587fcc25..a5a858be1 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -766,10 +766,10 @@ module JamRuby @@mq_router.publish_to_user(user_id, msg) end - def send_source_up_requested(music_session, host, port, mount, source_user, source_pass) - msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass) + def send_source_up_requested(music_session, host, port, mount, source_user, source_pass, bitrate) + msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass, bitrate) - @@mg_router.server_publish_to_session(music_session, msg) + @@mq_router.server_publish_to_session(music_session, msg) end def send_source_down_requested(music_session, mount) @@ -777,6 +777,18 @@ module JamRuby @@mq_router.server_publish_to_session(music_session, msg) end + + def send_source_up(music_session) + msg = @@message_factory.source_up(music_session.id) + + @@mq_router.server_publish_to_everyone_in_session(music_session, msg) + end + + def send_source_down(music_session) + msg = @@message_factory.source_up(music_session.id) + + @@mq_router.server_publish_to_everyone_in_session(music_session, msg) + end end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 024838e72..f1b703f10 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -18,6 +18,8 @@ module JamRuby # updating_password corresponds to a lost_password attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field + belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' + # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index e117a0974..78c8dd800 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -26,11 +26,10 @@ class MQRouter # sends a message to a session on behalf of a user # if this is originating in the context of a client, it should be specified as :client_id => "value" # client_msg should be a well-structure message (jam-pb message) - def user_publish_to_session(music_session, user, client_msg, sender = {:client_id => ""}) + def user_publish_to_session(music_session, user, client_msg, sender = {:client_id => nil}) access_music_session(music_session, user) - # gather up client_ids in the session - client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] } + client_ids = music_session.get_connection_ids(as_musician: true, exclude_client_id: sender[:client_id]) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) end @@ -38,13 +37,21 @@ class MQRouter # sends a message to a session from the server # no access check as with user_publish_to_session # client_msg should be a well-structure message (jam-pb message) - def server_publish_to_session(music_session, client_msg, sender = {:client_id => ""}) + def server_publish_to_session(music_session, client_msg, sender = {:client_id => nil}) # gather up client_ids in the session - client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] } + client_ids = music_session.get_connection_ids(as_musician: true, exclude_client_id: sender[:client_id]) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) end + # sends a message to a session AND fans/listeners from the server + # client_msg should be a well-structure message (jam-pb message) + def server_publish_to_everyone_in_session(music_session, client_msg, sender = {:client_id => nil}) + # gather up client_ids in the session + client_ids = music_session.get_connection_ids(exclude_client_id: sender[:client_id]) + publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) + end + # sends a message to a client with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_client(client_id, client_msg, sender = {:client_id => ""}) @@ -60,7 +67,7 @@ class MQRouter # sends a message to a session with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects - def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => ""}) + def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => nil}) EM.schedule do sender_client_id = sender[:client_id] diff --git a/ruby/lib/jam_ruby/resque/icecast_config_writer.rb b/ruby/lib/jam_ruby/resque/icecast_config_writer.rb index b86c9ead7..96076f41e 100644 --- a/ruby/lib/jam_ruby/resque/icecast_config_writer.rb +++ b/ruby/lib/jam_ruby/resque/icecast_config_writer.rb @@ -1,6 +1,7 @@ require 'json' require 'resque' -require 'resque-retry' + +require 'resque-lonely_job' require 'net/http' require 'digest/md5' @@ -8,6 +9,7 @@ module JamRuby # executes a mix of tracks, creating a final output mix class IcecastConfigWriter + extend Resque::Plugins::LonelyJob @@log = Logging.logger[IcecastConfigWriter] diff --git a/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb b/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb new file mode 100644 index 000000000..4d1577637 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb @@ -0,0 +1,32 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # http://blog.bignerdranch.com/1643-never-use-resque-for-serial-jobs/ + # periodically scheduled to find sources that need to be brought down, or alternatively, it seems the client failed to start sourcing + class IcecastSourceCheck + + @queue = :icecast_source_check + + @@log = Logging.logger[IcecastSourceCheck] + + def self.perform + @@log.debug("waking up") + + # if we haven't seen updated_at be tickled in 5 minutes, but config_changed is still set to TRUE, this record has gotten stale + IcecastMount.find_each(:conditions => "sourced_needs_changing_at < (NOW() - interval '#{APP_CONFIG.icecast_max_sourced_changed} second')", :batch_size => 100) do |server| + server.with_lock do + IcecastConfigWriter.enqueue(server.server_id) + end + + end + + @@log.debug("done") + end + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb b/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb index f0388d8f5..340e9a05f 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb @@ -14,7 +14,11 @@ module JamRuby @@log = Logging.logger[IcecastConfigRetry] def self.perform + @@log.debug("waking up") + IcecastConfigWriter.queue_jobs_needing_retry + + @@log.debug("done") end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb b/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb new file mode 100644 index 000000000..d832f4d2d --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb @@ -0,0 +1,59 @@ +require 'json' +require 'resque' +require 'resque-lonely_job' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # http://blog.bignerdranch.com/1643-never-use-resque-for-serial-jobs/ + # periodically scheduled to find sources that need to be brought down, or alternatively, it seems the client failed to start sourcing + class IcecastSourceCheck + extend Resque::Plugins::LonelyJob + + + @queue = :icecast_source_check + + + @@log = Logging.logger[IcecastSourceCheck] + + + def self.perform + @@log.debug("waking up") + + JamWebEventMachine.run_wait_stop do + IcecastSourceCheck.new.run + end + + @@log.debug("done") + end + + def run # if we haven't seen updated_at be tickled in 5 minutes, but config_changed is still set to TRUE, this record has gotten stale + IcecastMount.find_each(lock: true, :conditions => "sourced_needs_changing_at < (NOW() - interval '#{APP_CONFIG.icecast_max_sourced_changed} second')", :batch_size => 100) do |mount| + if mount.music_session_id + mount.with_lock do + handle_notifications(mount) + end + end + end + end + + def handle_notifications(mount) + if mount.listeners == 0 && mount.sourced + # if no listeners, but we are sourced, then ask it to stop sourcing + @@log.debug("SOURCE_DOWN_REQUEST called on mount #{mount.name}") + + mount.update_attribute(:sourced_needs_changing_at, Time.now) # we send out a source request, so we need to update the time + mount.notify_source_down_requested + + elsif mount.listeners > 0 && !mount.sourced + # if we have some listeners, and still are not sourced, then ask to start sourcing again + @@log.debug("SOURCE_UP_REQUEST called on mount #{mount.name}") + + mount.update_attribute(:sourced_needs_changing_at, Time.now) # we send out a source request, so we need to update the time + mount.notify_source_up_requested + + end + end + end +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 4a51defe2..0724273f6 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -195,7 +195,7 @@ FactoryGirl.define do factory :icecast_mount, :class => JamRuby::IcecastMount do name "/" + Faker::Lorem.characters(10) source_username Faker::Lorem.characters(10) - source_pass Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) max_listeners 100 max_listener_duration 3600 fallback_mount Faker::Lorem.characters(10) @@ -207,10 +207,21 @@ FactoryGirl.define do stream_url Faker::Lorem.characters(10) genre Faker::Lorem.characters(10) hidden 0 + association :server, factory: :icecast_server_with_overrides factory :icecast_mount_with_auth do association :authentication, :factory => :icecast_user_authentication + + factory :iceast_mount_with_template do + association :mount_template, :factory => :icecast_mount_template + + factory :iceast_mount_with_music_session do + association :music_session, :factory => :music_session + end + end end + + end factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do @@ -227,7 +238,7 @@ FactoryGirl.define do factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do authentication_type 'url' unused_username Faker::Lorem.characters(10) - unused_pass Faker::Lorem.characters(10) + unused_pass Faker::Lorem.characters(10) mount_add Faker::Lorem.characters(10) mount_remove Faker::Lorem.characters(10) listener_add Faker::Lorem.characters(10) @@ -242,6 +253,7 @@ FactoryGirl.define do factory :icecast_server_minimal do association :template, :factory => :icecast_template_minimal + association :mount_template, :factory => :icecast_mount_template factory :icecast_server_with_overrides do association :limit, :factory => :icecast_limit @@ -274,4 +286,23 @@ FactoryGirl.define do end end end + + factory :icecast_mount_template, :class => JamRuby::IcecastMountTemplate do + sequence(:name) { |n| "name-#{n}"} + source_username Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) + max_listeners 100 + max_listener_duration 3600 + fallback_mount Faker::Lorem.characters(10) + fallback_override 1 + fallback_when_full 1 + is_public -1 + stream_name Faker::Lorem.characters(10) + stream_description Faker::Lorem.characters(10) + stream_url Faker::Lorem.characters(10) + genre Faker::Lorem.characters(10) + hidden 0 + association :authentication, :factory => :icecast_user_authentication + end + end diff --git a/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb index 48ec8f43e..85f562405 100644 --- a/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb @@ -50,10 +50,22 @@ describe IcecastAdminAuthentication do server.config_changed.should == 1 end + it "success when deleted via template" do + server.template.admin_auth.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.admin_auth.save! server.reload server.config_changed.should == 1 end + + it "success when deleted via server" do + server.admin_auth.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_directory_spec.rb b/ruby/spec/jam_ruby/models/icecast_directory_spec.rb index 203cead6a..f77f65768 100644 --- a/ruby/spec/jam_ruby/models/icecast_directory_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_directory_spec.rb @@ -57,11 +57,23 @@ describe IcecastDirectory do server.config_changed.should == 1 end + it "delete via template" do + server.template.directory.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.directory.save! server.reload server.config_changed.should == 1 end + + it "destroy via server" do + server.directory.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_limit_spec.rb b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb index 910b2fca4..bab815e50 100644 --- a/ruby/spec/jam_ruby/models/icecast_limit_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb @@ -66,10 +66,22 @@ describe IcecastLimit do server.config_changed.should == 1 end + it "delete via template" do + server.template.limit.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.limit.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.limit.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb b/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb index 1de0009a0..19a1efa60 100644 --- a/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb @@ -27,10 +27,22 @@ describe IcecastListenSocket do server.config_changed.should == 1 end + it "delete via template" do + server.template.listen_sockets.first.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.listen_sockets.first.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.listen_sockets.first.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_logging_spec.rb b/ruby/spec/jam_ruby/models/icecast_logging_spec.rb index 2ad726bcb..2892bbc82 100644 --- a/ruby/spec/jam_ruby/models/icecast_logging_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_logging_spec.rb @@ -40,10 +40,22 @@ describe IcecastLogging do server.config_changed.should == 1 end + it "delete via template" do + server.template.logging.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.logging.save! server.reload server.config_changed.should == 1 end + + it "deete via server" do + server.logging.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb index 5de60553e..5dfa1231a 100644 --- a/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb @@ -53,11 +53,23 @@ describe IcecastMasterServerRelay do server.config_changed.should == 1 end + it "delete via template" do + server.template.master_relay.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.master_relay.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.master_relay.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_mount_spec.rb b/ruby/spec/jam_ruby/models/icecast_mount_spec.rb index 5a42a0397..bcd0955a7 100644 --- a/ruby/spec/jam_ruby/models/icecast_mount_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_mount_spec.rb @@ -10,10 +10,6 @@ describe IcecastMount do mount = IcecastMount.new mount.save.should be_false mount.errors[:name].should == ["can't be blank", "must start with /"] - mount.errors[:stream_name].should == ["can't be blank"] - mount.errors[:stream_description].should == ["can't be blank"] - mount.errors[:stream_url].should == ["can't be blank"] - mount.errors[:genre].should == ["can't be blank"] end @@ -35,6 +31,7 @@ describe IcecastMount do mount.max_listeners = 1000 mount.max_listener_duration = 3600 mount.authentication = FactoryGirl.create(:icecast_user_authentication) + mount.server = FactoryGirl.create(:icecast_server_with_overrides) mount.save! @@ -70,11 +67,39 @@ describe IcecastMount do xml.css('mount authentication').length.should == 1 # no reason to test futher; it's tested in that model end + describe "override xml over mount template" do + let(:mount) {FactoryGirl.create(:iceast_mount_with_template)} + + it "should allow override by mount" do + mount.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('mount mount-name').text.should == mount.name + xml.css('mount username').text.should == mount.source_username + xml.css('mount bitrate').text.should == mount.bitrate.to_s + xml.css('mount type').text.should == mount.mount_template.mime_type + xml.css('mount stream-url').text.should == mount.stream_url + + # now see the stream_url, and bitrate, go back to the template's value because we set it to nil + mount.bitrate = nil + mount.stream_url = nil + mount.save! + + output = StringIO.new + builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1) + mount.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('mount bitrate').text.should == mount.mount_template.bitrate.to_s + xml.css('mount stream-url').text.should == mount.mount_template.stream_url + end + end + describe "poke configs" do let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } before(:each) do - server.mounts << FactoryGirl.create(:icecast_mount) + server.mounts << FactoryGirl.create(:icecast_mount, server: server) server.save! server.config_updated server.reload @@ -86,6 +111,12 @@ describe IcecastMount do server.reload server.config_changed.should == 1 end + + it "success when deleted" do + server.mounts.first.destroy + server.reload + server.config_changed.should == 1 + end end describe "icecast server callbacks" do @@ -93,4 +124,125 @@ describe IcecastMount do icecast_mount.source_up end end + + describe "listener/source" do + let(:mount) {FactoryGirl.create(:iceast_mount_with_template)} + + describe "listeners" do + it "listener_add" do + mount.listener_add + mount.listeners.should == 1 + end + + it "listener_remove when at 0" do + mount.listener_remove + mount.listeners.should == 0 + end + + it "listener_remove" do + mount.listener_add + mount.listener_remove + mount.listeners.should == 0 + end + end + + describe "sources" do + it "source_up" do + mount.source_up + mount.sourced.should == true + end + end + + describe "sources" do + it "source_down" do + mount.source_up + mount.source_down + mount.sourced.should == false + end + end + end + describe "build_session_mount" do + + let(:server1) {FactoryGirl.create(:icecast_server_minimal)} + let(:server2) {FactoryGirl.create(:icecast_server_with_overrides)} + let(:server3) {FactoryGirl.create(:icecast_server_with_overrides)} + let(:hidden_music_session) { FactoryGirl.create(:music_session, :fan_access => false)} + let(:public_music_session) { FactoryGirl.create(:music_session, :fan_access => true)} + let(:public_music_session2) { FactoryGirl.create(:music_session, :fan_access => true)} + let(:public_music_session3) { FactoryGirl.create(:music_session, :fan_access => true)} + + before(:each) do + + end + + it "no fan access means no mount" do + mount = IcecastMount.build_session_mount(hidden_music_session) + mount.should be_nil + end + + it "with no servers" do + IcecastServer.count.should == 0 + mount = IcecastMount.build_session_mount(public_music_session) + mount.should be_nil + end + + it "with a server that has a mount template" do + server1.mount_template.should_not be_nil + mount = IcecastMount.build_session_mount(public_music_session) + mount.should_not be_nil + mount.save! + end + + it "with a server that already has an associated mount" do + server1.mount_template.should_not be_nil + mount = IcecastMount.build_session_mount(public_music_session) + mount.save! + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.save! + server1.reload + server1.mounts.length.should == 2 + end + + it "picks a second server once the 1st has been chosen" do + server1.touch + + mount = IcecastMount.build_session_mount(public_music_session) + mount.listeners = 1 # affect the weight + mount.save! + + server2.touch + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.save! + server1.reload + server1.mounts.length.should == 1 + server2.reload + server2.mounts.length.should == 1 + end + + it "picks the 1st server again once the 2nd has higher weight" do + server1.touch + + mount = IcecastMount.build_session_mount(public_music_session) + mount.listeners = 1 # affect the weight + mount.save! + + server2.touch + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.sourced = 1 + mount.save! + + mount = IcecastMount.build_session_mount(public_music_session3) + mount.listeners = 1 + mount.save! + + server1.reload + server1.mounts.length.should == 2 + server2.reload + server2.mounts.length.should == 1 + end + end + end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb b/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb new file mode 100644 index 000000000..2478af012 --- /dev/null +++ b/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe IcecastMountTemplate do + + let(:mount_template) { template = FactoryGirl.create(:icecast_mount_template) } + + it "save" do + mount_template.errors.any?.should be_false + end + + describe "poke configs" do + let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } + let(:music_session) { FactoryGirl.create(:music_session, :fan_access => true)} + + before(:each) do + server.touch + mount = IcecastMount.build_session_mount(music_session) + mount.save! + server.save! + server.config_updated + server.reload + server.config_changed.should == 0 + end + + it "success via server" do + server.mounts.first.mount_template.save! + server.reload + server.config_changed.should == 1 + end + + it "delete via server" do + server.mounts.first.mount_template.destroy + server.reload + server.config_changed.should == 1 + end + end +end diff --git a/ruby/spec/jam_ruby/models/icecast_path_spec.rb b/ruby/spec/jam_ruby/models/icecast_path_spec.rb index 0f73acd09..50354d38d 100644 --- a/ruby/spec/jam_ruby/models/icecast_path_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_path_spec.rb @@ -64,10 +64,22 @@ describe IcecastPath do server.config_changed.should == 1 end + it "delete via template" do + server.template.path.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.path.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.path.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_relay_spec.rb index 09c46e303..705434b3b 100644 --- a/ruby/spec/jam_ruby/models/icecast_relay_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_relay_spec.rb @@ -48,5 +48,11 @@ describe IcecastRelay do server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.relays.first.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_security_spec.rb b/ruby/spec/jam_ruby/models/icecast_security_spec.rb index 08ad498a9..e33d9e180 100644 --- a/ruby/spec/jam_ruby/models/icecast_security_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_security_spec.rb @@ -40,10 +40,22 @@ describe IcecastSecurity do server.config_changed.should == 1 end + it "delete via template" do + server.template.security.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.security.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.security.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_server_spec.rb b/ruby/spec/jam_ruby/models/icecast_server_spec.rb index a7831aa72..a9e6adbb2 100644 --- a/ruby/spec/jam_ruby/models/icecast_server_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_server_spec.rb @@ -30,4 +30,34 @@ describe IcecastServer do xml.css('icecast security').length.should == 1 xml.css('icecast listen-socket').length.should == 1 end + + it "xml overrides" do + server = FactoryGirl.create(:icecast_server_minimal) + server.save! + server.reload + server.dumpXml(output) + + output.rewind + + xml = Nokogiri::XML(output) + xml.css('icecast location').text.should == server.template.location + xml.css('icecast fileserve').text.should == server.template.fileserve.to_s + xml.css('icecast limits').length.should == 1 + xml.css('icecast limits queue-size').text.should == server.template.limit.queue_size.to_s + + server.location = "override" + server.fileserve = 1 + server.limit = FactoryGirl.create(:icecast_limit, :queue_size => 777) + server.save! + + output = StringIO.new + builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1) + server.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('icecast location').text.should == server.location + xml.css('icecast fileserve').text.should == server.fileserve.to_s + xml.css('icecast limits').length.should == 1 + xml.css('icecast limits queue-size').text.should == server.limit.queue_size.to_s + end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_template_spec.rb b/ruby/spec/jam_ruby/models/icecast_template_spec.rb index 267acdd0d..690cf9122 100644 --- a/ruby/spec/jam_ruby/models/icecast_template_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_template_spec.rb @@ -1,10 +1,20 @@ require 'spec_helper' -describe IcecastListenSocket do +describe IcecastTemplate do let(:template) { template = FactoryGirl.create(:icecast_template_minimal) } it "save" do template.errors.any?.should be_false end + + describe "poke configs" do + let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } + + it "success via template" do + server.template.save! + server.reload + server.config_changed.should == 1 + end + end end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 32909739e..d16972b6e 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -480,5 +480,36 @@ describe MusicSession do end end end + + describe "get_connection_ids" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @user2 = FactoryGirl.create(:user) + @music_session = FactoryGirl.create(:music_session, :creator => @user1, :musician_access => true) + @connection1 = FactoryGirl.create(:connection, :user => @user1, :music_session => @music_session, :as_musician => true) + @connection2 = FactoryGirl.create(:connection, :user => @user2, :music_session => @music_session, :as_musician => false) + + end + + it "get all connections" do + @music_session.get_connection_ids().should == [@connection1.client_id, @connection2.client_id] + end + + it "exclude non-musicians" do + @music_session.get_connection_ids(as_musician: true).should == [@connection1.client_id] + end + + it "exclude musicians" do + @music_session.get_connection_ids(as_musician: false).should == [@connection2.client_id] + end + + it "exclude particular client" do + @music_session.get_connection_ids(exclude_client_id: @connection1.client_id).should == [@connection2.client_id] + end + + it "exclude particular client and exclude non-musicians" do + @music_session.get_connection_ids(exclude_client_id: @connection2.client_id, as_musician: true).should == [@connection1.client_id] + end + end end diff --git a/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb b/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb new file mode 100644 index 000000000..2c528bf6d --- /dev/null +++ b/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' +require 'fileutils' + +# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests +describe IcecastSourceCheck do + + let(:check) { IcecastSourceCheck.new } + + describe "integration" do + + it "be OK with no mounts" do + IcecastMount.count().should == 0 + check.should_not_receive(:handle_notifications) + check.run + end + + + it "find no mounts if source_hanged timestamp is nil and listeners = 1/sourced = false" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced: false, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is nil and listeners = 0/sourced = true" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced: true, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is very recent and listeners = 1/sourced = false" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: Time.now, sourced: false, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is very recent and listeners = 0/sourced = true" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: Time.now, sourced: true, listeners: 0) + check.should_not_receive(:handle_notifications) + check.run + end + + it "sends notify_source_down_requested when old source_changed timestamp, and sourced = true and listeners = 0" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:true, listeners: 0) + check.stub(:handle_notifications) do |mount| + mount.should_receive(:notify_source_down_requested).once + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "does not send notify_source_down_requested when old source_changed timestamp, and sourced = true and listeners = 1" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:true, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "sends notify_source_up_requested when old source_changed timestamp, and sourced = false and listeners = 1" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_receive(:notify_source_up_requested).once + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + + it "does not send notify_source_up_requested when old source_changed timestamp, and sourced = false and listeners = 0" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 0) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "resets source_changed_at when a notification is sent out" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_receive(:notify_source_up_requested).once + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + mount.reload + (mount.sourced_needs_changing_at.to_i - Time.now.to_i).abs.should < 10 # less than 5 seconds -- just a little slop for a very slow build server + end + end +end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 2fd679cc5..bbdbd7440 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -79,8 +79,8 @@ Spork.prefork do config.filter_run_excluding aws: true unless run_tests? :aws config.before(:suite) do - DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } - DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) + DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] } + DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] }) end config.before(:each) do diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 84b51f440..4cf3ec682 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -37,6 +37,10 @@ def app_config 2 * 60 # 2 minutes end + def icecast_max_sourced_changed + 15 # 15 seconds + end + def rabbitmq_host "localhost" end diff --git a/web/Gemfile b/web/Gemfile index 8132e1b52..30e0c3cd3 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -62,6 +62,7 @@ gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' gem 'resque-dynamic-queues' +gem 'resque-lonely_job', '~> 1.0.0' gem 'quiet_assets', :group => :development gem "bugsnag" diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index b6820a8b1..a104e8e9b 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -48,6 +48,12 @@ BAND_INVITATION : "BAND_INVITATION", BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED", + // broadcast notifications + SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED", + SOURCE_DOWN_REQUESTED : "SOURCE_DOWN_REQUESTED", + SOURCE_UP : "SOURCE_UP", + SOURCE_DOWN : "SOURCE_DOWN", + TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE", PING_REQUEST : "PING_REQUEST", PING_ACK : "PING_ACK", diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 96ee3781c..bc2d0baf6 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -571,6 +571,14 @@ function CloseRecording() {} function OnDownloadAvailable() {} + function SessionLiveBroadcastStart(host, port, mount, sourceUser, sourcePass, preferredClientId, bitrate) + { + logger.debug("SessionLiveBroadcastStart requested"); + } + + function SessionLiveBroadcastStop() { + logger.debug("SessionLiveBroadcastStop requested"); + } // Javascript Bridge seems to camel-case // Set the instance functions: @@ -703,6 +711,10 @@ this.CloseRecording = CloseRecording; this.OnDownloadAvailable = OnDownloadAvailable; + // Broadcasting + this.SessionLiveBroadcastStart = SessionLiveBroadcastStart; + this.SessionLiveBroadcastStop = SessionLiveBroadcastStop; + // fake calls; not a part of the actual jam client this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index 252ab20c4..4e63c8c24 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -233,10 +233,95 @@ acceptBandInvitation({ "band_invitation_id": payload.band_invitation_id, "band_id": payload.band_id, "notification_id": payload.notification_id }); }); } - else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) { $notification.find('#div-actions').hide(); } + else if (type === context.JK.MessageType.SOURCE_UP_REQUESTED) { + var current_session_id = context.JK.CurrentSessionModel.id(); + + if (!current_session_id) { + // we are not in a session + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified"; + } + } + else { + // we are in a session + if(current_session_id == payload.music_session) { + context.jamClient.SessionLiveBroadcastStart(payload.host, payload.port, payload.mount, + payload.source_user, payload.source_pass, + '', payload.bitrate) + } + else { + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified"; + } + } + } + } + else if (type === context.JK.MessageType.SOURCE_DOWN_REQUESTED) { + var current_session_id = context.JK.CurrentSessionModel.id(); + + if (!current_session_id) { + // we are not in a session + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified"; + } + } + else { + // we are in a session + if(current_session_id == payload.music_session) { + context.jamClient.SessionLiveBroadcastStop(); + } + else { + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified"; + } + } + } + } + else if (type === context.JK.MessageType.SOURCE_UP) { + log.debug("session %o is now being broadcasted", payload.music_session); + app.notify({ + "title": "Now Broadcasting", + "text": "This session is now being broadcasted." + }); + } + else if (type === context.JK.MessageType.SOURCE_DOWN) { + log.debug("session %o is no longer being broadcasted", payload.music_session); + app.notify({ + "title": "No Longer Broadcasting", + "text": "This session is no longer being broadcasted." + }); + } } function deleteNotificationHandler(evt) { diff --git a/web/app/controllers/api_icecast_controller.rb b/web/app/controllers/api_icecast_controller.rb index 2d0986705..2340575b0 100644 --- a/web/app/controllers/api_icecast_controller.rb +++ b/web/app/controllers/api_icecast_controller.rb @@ -7,15 +7,14 @@ class ApiIcecastController < ApiController def mount_add - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.source_up - render text: '', :status => :ok end def mount_remove - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.source_down render text: '', :status => :ok @@ -28,7 +27,7 @@ class ApiIcecastController < ApiController remote_ip = params[:ip] remote_user_agent = params[:agent] - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.listener_add render text: '', :status => :ok @@ -40,7 +39,7 @@ class ApiIcecastController < ApiController pass = params[:pass] duration = params[:duration] # seconds connected to the listen stream - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.listener_remove render text: '', :status => :ok diff --git a/web/config/application.rb b/web/config/application.rb index fc11d17b8..e6394999e 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -177,6 +177,7 @@ include JamRuby # this will be the qualifier on the IcecastConfigWorker queue name config.icecast_server_id = ENV['ICECAST_SERVER_ID'] || 'localhost' config.icecast_max_missing_check = 2 * 60 # 2 minutes + config.icecast_max_sourced_changed = 15 # 15 seconds config.email_alerts_alias = 'nobody@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails config.email_generic_from = 'nobody@jamkazam.com' diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 97adc2d3c..8a6dde7df 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -8,3 +8,10 @@ IcecastConfigRetry: cron: 0 * * * * class: "JamRuby::IcecastConfigRetry" description: "Finds icecast servers that have had their config_changed, but no IcecastConfigWriter check recently" + + +IcecastSourceCheck: + cron: "10 * * * * *" + class: "JamRuby::IcecastSourceCheck" + description: "Finds icecast mounts that need their 'sourced' state to change, but haven't in some time" + diff --git a/web/lib/music_session_manager.rb b/web/lib/music_session_manager.rb index 7a71bdcf9..7889db8e2 100644 --- a/web/lib/music_session_manager.rb +++ b/web/lib/music_session_manager.rb @@ -14,7 +14,8 @@ MusicSessionManager < BaseManager ActiveRecord::Base.transaction do # check if we are connected to rabbitmq - music_session = MusicSession.new() + music_session = MusicSession.new + music_session.id = SecureRandom.uuid music_session.creator = user music_session.description = description music_session.musician_access = musician_access @@ -24,9 +25,6 @@ MusicSessionManager < BaseManager music_session.band = band music_session.legal_terms = legal_terms - #genres = genres - @log.debug "Genres class: " + genres.class.to_s - unless genres.nil? genres.each do |genre_id| loaded_genre = Genre.find(genre_id) @@ -34,6 +32,13 @@ MusicSessionManager < BaseManager end end + + if fan_access + # create an icecast mount since regular users can listen in to the broadcast + music_session.mount = IcecastMount.build_session_mount(music_session) + end + + music_session.save unless music_session.errors.any? diff --git a/websocket-gateway/Gemfile b/websocket-gateway/Gemfile index d2c796ec3..78131b08a 100644 --- a/websocket-gateway/Gemfile +++ b/websocket-gateway/Gemfile @@ -41,6 +41,7 @@ gem 'postgres_ext' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' +gem 'resque-lonely_job', '~> 1.0.0' group :development do gem 'pry'