* icecast working locally on my mac (VRFS-1002)

This commit is contained in:
Seth Call 2014-01-21 14:51:03 +00:00
parent c91940852b
commit fcec0a776b
68 changed files with 1548 additions and 312 deletions

View File

@ -54,6 +54,7 @@ gem 'gon'
gem 'resque' gem 'resque'
gem 'resque-retry' gem 'resque-retry'
gem 'resque-failed-job-mailer' gem 'resque-failed-job-mailer'
gem 'resque-lonely_job', '~> 1.0.0'
gem 'eventmachine', '1.0.3' gem 'eventmachine', '1.0.3'
gem 'amqp', '0.9.8' gem 'amqp', '0.9.8'

View File

@ -3,16 +3,18 @@ ActiveAdmin.register_page "Bootstrap" do
page_action :create_server, :method => :post 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] hostname = params[:jam_ruby_icecast_server][:hostname]
server = IcecastServer.new server = IcecastServer.new
server.template = template server.template = template
server.mount_template = mount_template
server.hostname = hostname server.hostname = hostname
server.server_id = hostname server.server_id = hostname
server.save! 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 end
page_action :brew_template, :method => :post do page_action :brew_template, :method => :post do
@ -81,23 +83,85 @@ ActiveAdmin.register_page "Bootstrap" do
template.save! template.save!
end end
redirect_to admin_bootstrap_path, :notice => "Brew template created. Now, create a mount template."
redirect_to admin_bootstrap_path, :notice => "Brew template created. Create a server now with that template specified."
end 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 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 end
content do content do
if IcecastTemplate.count == 0 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 else
semantic_form_for IcecastServer.new, :url => admin_bootstrap_create_server_path, :builder => ActiveAdmin::FormBuilder do |f| semantic_form_for IcecastServer.new, :url => admin_bootstrap_create_server_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "New Server" do f.inputs "New Icecast Server" do
f.input :hostname f.input :hostname, :hint => "Just the icecast hostname; no port"
f.input :template 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 end
f.actions f.actions
end end

View File

@ -0,0 +1,3 @@
ActiveAdmin.register JamRuby::IcecastMountTemplate, :as => 'IcecastMountTemplate' do
menu :parent => 'Icecast'
end

View File

@ -0,0 +1,3 @@
ActiveAdmin.register JamRuby::IcecastServerGroup, :as => 'IcecastServerGroup' do
menu :parent => 'Icecast'
end

View File

@ -1,3 +1,4 @@
module ApplicationHelper module ApplicationHelper
end end

View File

@ -3,4 +3,5 @@ module Utils
chars = ((('a'..'z').to_a + ('0'..'9').to_a) - %w(i o 0 1 l 0)) 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 (1..size).collect{|a| cc = chars[rand(chars.size)]; 0==rand(2) ? cc.upcase : cc }.join
end end
end end

View File

@ -19,7 +19,7 @@ rm -rf $TARGET
mkdir -p $PG_BUILD_OUT mkdir -p $PG_BUILD_OUT
mkdir -p $PG_RUBY_PACKAGE_OUT mkdir -p $PG_RUBY_PACKAGE_OUT
#bundle update bundle update
echo "building migrations" echo "building migrations"
bundle exec pg_migrate build --source . --out $PG_BUILD_OUT --test --verbose bundle exec pg_migrate build --source . --out $PG_BUILD_OUT --test --verbose

View File

@ -89,3 +89,5 @@ home_page_promos.sql
mix_job_watch.sql mix_job_watch.sql
music_session_constraints.sql music_session_constraints.sql
mixes_drop_manifest_add_retry.sql mixes_drop_manifest_add_retry.sql
music_sessions_unlogged.sql
integrate_icecast_into_sessions.sql

View File

@ -1,129 +1,96 @@
-- this manifest update makes every table associated with music_sessions UNLOGGED
-- tables to mark UNLOGGED CREATE TABLE icecast_mount_templates(
-- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
name VARCHAR(256) NOT NULL,
-- tables to just get rid of source_username VARCHAR(64),
-- session_plays source_pass VARCHAR(64),
max_listeners INTEGER DEFAULT 4,
-- breaking foreign keys for tables max_listener_duration INTEGER DEFAULT 3600,
-- connections: user_id dump_file VARCHAR(1024),
-- fan_invitations: receiver_id, sender_id intro VARCHAR(1024),
-- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id fallback_mount VARCHAR(1024),
-- genres_music_sessions: genre_id fallback_override INTEGER DEFAULT 1,
-- invitations: sender_id, receiver_id fallback_when_full INTEGER DEFAULT 1,
-- fan_invitations: user_id charset VARCHAR(1024) DEFAULT 'ISO8859-1',
-- notifications: invitation_id, join_request_id, session_id is_public INTEGER DEFAULT 0,
stream_name VARCHAR(1024),
DROP TABLE sessions_plays; stream_description VARCHAR(10000),
stream_url VARCHAR(1024),
-- divorce notifications from UNLOGGED tables genre VARCHAR(256),
bitrate INTEGER,
-- NOTIFICATIONS mime_type VARCHAR(64) NOT NULL DEFAULT 'audio/mpeg',
---------------- subtype VARCHAR(64),
-- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE burst_size INTEGER,
ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey; mp3_metadata_interval INTEGER,
-- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE hidden INTEGER DEFAULT 1,
ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey; on_connect VARCHAR(1024),
-- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE on_disconnect VARCHAR(1024),
ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey; authentication_id varchar(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- FAN_INVITATIONS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
------------------
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 ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP NOT NULL;
-------------- ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP DEFAULT;
CREATE UNLOGGED TABLE connections ( ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP NOT NULL;
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP DEFAULT;
user_id VARCHAR(64), ALTER TABLE icecast_mounts ADD COLUMN music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE CASCADE;
client_id VARCHAR(64) UNIQUE NOT NULL, ALTER TABLE icecast_mounts ADD COLUMN icecast_server_id VARCHAR(64) NOT NULL REFERENCES icecast_servers(id);
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, ALTER TABLE icecast_mounts ADD COLUMN icecast_mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id);
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, ALTER TABLE icecast_mounts ADD COLUMN sourced_needs_changing_at TIMESTAMP;
music_session_id VARCHAR(64), ;
ip_address VARCHAR(64), CREATE TABLE icecast_server_groups (
as_musician BOOLEAN, id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL 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 ALTER TABLE users ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default';
------------------------
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 ( -- and by default, all servers and users are in this group
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, ALTER TABLE icecast_servers ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default';
sender_id VARCHAR(64), ALTER TABLE icecast_servers ADD COLUMN mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id);
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 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;
CREATE UNLOGGED TABLE invitations ( ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_mount_template_id_fkey";
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, 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;
sender_id VARCHAR(64), ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_directory_id_fkey";
receiver_id VARCHAR(64), ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL;
music_session_id VARCHAR(64), ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_icecast_server_group_id_fkey";
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, 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;
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_limit_id_fkey";
join_request_id VARCHAR(64) 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 ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); 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 ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_master_relay_id_fkey";
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 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 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;
CREATE UNLOGGED TABLE tracks ( ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_server_id_fkey";
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, 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;
connection_id VARCHAR(64),
instrument_id VARCHAR(64), ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_admin_auth_id_fkey";
sound VARCHAR(64) NOT NULL, 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;
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_directory_id_fkey";
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL;
client_track_id VARCHAR(64) NOT 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 ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; 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;

View File

@ -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;

View File

@ -9,67 +9,69 @@ package jampb;
message ClientMessage { message ClientMessage {
enum Type { enum Type {
LOGIN = 100; LOGIN = 100;
LOGIN_ACK = 105; LOGIN_ACK = 105;
LOGIN_MUSIC_SESSION = 110; LOGIN_MUSIC_SESSION = 110;
LOGIN_MUSIC_SESSION_ACK = 115; LOGIN_MUSIC_SESSION_ACK = 115;
LEAVE_MUSIC_SESSION = 120; LEAVE_MUSIC_SESSION = 120;
LEAVE_MUSIC_SESSION_ACK = 125; LEAVE_MUSIC_SESSION_ACK = 125;
HEARTBEAT = 130; HEARTBEAT = 130;
HEARTBEAT_ACK = 135; HEARTBEAT_ACK = 135;
// friend notifications // friend notifications
FRIEND_UPDATE = 140; FRIEND_UPDATE = 140;
FRIEND_REQUEST = 145; FRIEND_REQUEST = 145;
FRIEND_REQUEST_ACCEPTED = 150; FRIEND_REQUEST_ACCEPTED = 150;
FRIEND_SESSION_JOIN = 155; FRIEND_SESSION_JOIN = 155;
NEW_USER_FOLLOWER = 160; NEW_USER_FOLLOWER = 160;
NEW_BAND_FOLLOWER = 161; NEW_BAND_FOLLOWER = 161;
// session invitations // session invitations
SESSION_INVITATION = 165; SESSION_INVITATION = 165;
SESSION_ENDED = 170; SESSION_ENDED = 170;
JOIN_REQUEST = 175; JOIN_REQUEST = 175;
JOIN_REQUEST_APPROVED = 180; JOIN_REQUEST_APPROVED = 180;
JOIN_REQUEST_REJECTED = 185; JOIN_REQUEST_REJECTED = 185;
SESSION_JOIN = 190; SESSION_JOIN = 190;
SESSION_DEPART = 195; SESSION_DEPART = 195;
MUSICIAN_SESSION_JOIN = 196; MUSICIAN_SESSION_JOIN = 196;
// recording notifications // recording notifications
MUSICIAN_RECORDING_SAVED = 200; MUSICIAN_RECORDING_SAVED = 200;
BAND_RECORDING_SAVED = 205; BAND_RECORDING_SAVED = 205;
RECORDING_STARTED = 210; RECORDING_STARTED = 210;
RECORDING_ENDED = 215; RECORDING_ENDED = 215;
RECORDING_MASTER_MIX_COMPLETE = 220; RECORDING_MASTER_MIX_COMPLETE = 220;
DOWNLOAD_AVAILABLE = 221; DOWNLOAD_AVAILABLE = 221;
// band notifications // band notifications
BAND_INVITATION = 225; BAND_INVITATION = 225;
BAND_INVITATION_ACCEPTED = 230; BAND_INVITATION_ACCEPTED = 230;
BAND_SESSION_JOIN = 235; BAND_SESSION_JOIN = 235;
MUSICIAN_SESSION_FRESH = 240; MUSICIAN_SESSION_FRESH = 240;
MUSICIAN_SESSION_STALE = 245; MUSICIAN_SESSION_STALE = 245;
// icecast notifications // icecast notifications
SOURCE_UP_REQUESTED = 250; SOURCE_UP_REQUESTED = 250;
SOURCE_DOWN_REQUESTED = 255; SOURCE_DOWN_REQUESTED = 251;
SOURCE_UP = 252;
SOURCE_DOWN = 253;
TEST_SESSION_MESSAGE = 295; TEST_SESSION_MESSAGE = 295;
PING_REQUEST = 300; PING_REQUEST = 300;
PING_ACK = 305; PING_ACK = 305;
PEER_MESSAGE = 310; PEER_MESSAGE = 310;
TEST_CLIENT_MESSAGE = 315; TEST_CLIENT_MESSAGE = 315;
SERVER_BAD_STATE_RECOVERED = 900; SERVER_BAD_STATE_RECOVERED = 900;
SERVER_GENERIC_ERROR = 1000; SERVER_GENERIC_ERROR = 1000;
SERVER_REJECTION_ERROR = 1005; SERVER_REJECTION_ERROR = 1005;
SERVER_PERMISSION_ERROR = 1010; SERVER_PERMISSION_ERROR = 1010;
SERVER_BAD_STATE_ERROR = 1015; SERVER_BAD_STATE_ERROR = 1015;
} }
// Identifies which inner message is filled in // Identifies which inner message is filled in
@ -126,7 +128,9 @@ message ClientMessage {
// icecast notifications // icecast notifications
optional SourceUpRequested source_up_requested = 250; 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) // Client-Session messages (to/from)
optional TestSessionMessage test_session_message = 295; optional TestSessionMessage test_session_message = 295;
@ -381,15 +385,26 @@ message MusicianSessionStale {
} }
message SourceUpRequested { message SourceUpRequested {
optional string host = 1; // icecast server host optional string music_session = 1; // music session id
optional int32 port = 2; // icecast server port optional string host = 2; // icecast server host
optional string mount = 3; // mount name optional int32 port = 3; // icecast server port
optional string source_user = 4; // source user optional string mount = 4; // mount name
optional string source_pass = 5; // source pass optional string source_user = 5; // source user
optional string source_pass = 6; // source pass
optional int32 bitrate = 7;
} }
message SourceDownRequested { 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 // route_to: session

View File

@ -29,6 +29,7 @@ gem 'postgres_ext'
gem 'resque' gem 'resque'
gem 'resque-retry' gem 'resque-retry'
gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer" gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer"
gem 'resque-lonely_job', '~> 1.0.0'
gem 'oj' gem 'oj'
gem 'builder' gem 'builder'

View File

@ -33,6 +33,7 @@ require "jam_ruby/resque/audiomixer"
require "jam_ruby/resque/icecast_config_writer" require "jam_ruby/resque/icecast_config_writer"
require "jam_ruby/resque/scheduled/audiomixer_retry" require "jam_ruby/resque/scheduled/audiomixer_retry"
require "jam_ruby/resque/scheduled/icecast_config_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/mq_router"
require "jam_ruby/base_manager" require "jam_ruby/base_manager"
require "jam_ruby/connection_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_relay"
require "jam_ruby/models/icecast_server_socket" require "jam_ruby/models/icecast_server_socket"
require "jam_ruby/models/icecast_template_socket" require "jam_ruby/models/icecast_template_socket"
require "jam_ruby/models/icecast_server_group"
require "jam_ruby/models/icecast_mount_template"
include Jampb include Jampb

View File

@ -37,6 +37,12 @@ module JamRuby
return friend_ids return friend_ids
end 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, # reclaim the existing connection,
def reconnect(conn, reconnect_music_session_id) def reconnect(conn, reconnect_music_session_id)
music_session_id = nil music_session_id = nil
@ -218,6 +224,7 @@ SQL
# same for session-if we are down to the last participant, delete the session # same for session-if we are down to the last participant, delete the session
unless music_session_id.nil? 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]) 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 if result.cmd_tuples == 1
music_session_id = nil music_session_id = nil
@ -258,6 +265,7 @@ SQL
end end
if num_participants == 0 if num_participants == 0
# delete the music_session # delete the music_session
before_destroy_music_session(previous_music_session_id)
conn.exec("DELETE from music_sessions WHERE id = $1", conn.exec("DELETE from music_sessions WHERE id = $1",
[previous_music_session_id]) do |result| [previous_music_session_id]) do |result|
if result.cmd_tuples == 1 if result.cmd_tuples == 1

View File

@ -558,13 +558,15 @@ module JamRuby
# create a source up requested message to send to clients in a session, # 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 # 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( source_up_requested = Jampb::SourceUpRequested.new(
music_session: session_id,
host: host, host: host,
port: port, port: port,
mount: mount, mount: mount,
source_user: source_user, source_user: source_user,
source_pass: source_pass) source_pass: source_pass,
bitrate: bitrate)
Jampb::ClientMessage.new( Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_UP_REQUESTED, 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, # 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 # so that one of the clients will start sending source audio to icecast
def source_down_requested (session_id, mount) 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( Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_DOWN_REQUESTED, type: ClientMessage::Type::SOURCE_DOWN_REQUESTED,
@ -583,6 +585,27 @@ module JamRuby
source_down_requested: source_down_requested) source_down_requested: source_down_requested)
end 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 # create a test message to send in session
def test_session_message(session_id, msg) def test_session_message(session_id, msg)
test = Jampb::TestSessionMessage.new(:msg => msg) test = Jampb::TestSessionMessage.new(:msg => msg)

View File

@ -11,6 +11,5 @@ module JamRuby
# music sessions # music sessions
has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions" has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions"
end end
end end

View File

@ -12,7 +12,8 @@ module JamRuby
validates :relay_pass, presence: true, length: {minimum: 5} validates :relay_pass, presence: true, length: {minimum: 5}
validates :admin_user, 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 def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -9,7 +9,8 @@ module JamRuby
validates :yp_url_timeout, presence: true, numericality: {only_integer: true}, length: {in: 1..30} validates :yp_url_timeout, presence: true, numericality: {only_integer: true}, length: {in: 1..30}
validates :yp_url, presence: true validates :yp_url, presence: true
after_save :poke_config before_destroy :poke_config
after_save :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -15,7 +15,8 @@ module JamRuby
validates :source_timeout, presence: true, numericality: {only_integer: true} validates :source_timeout, presence: true, numericality: {only_integer: true}
validates :burst_size, 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 def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -12,7 +12,8 @@ module JamRuby
validates :port, presence: true, numericality: {only_integer: true}, length: {in: 1..65535} validates :port, presence: true, numericality: {only_integer: true}, length: {in: 1..65535}
validates :shoutcast_compat, :inclusion => {:in => [nil, 0, 1]} validates :shoutcast_compat, :inclusion => {:in => [nil, 0, 1]}
after_save :poke_config before_destroy :poke_config
after_save :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -12,7 +12,8 @@ module JamRuby
validates :log_archive, :inclusion => {:in => [nil, 0, 1]} validates :log_archive, :inclusion => {:in => [nil, 0, 1]}
validates :log_size, numericality: {only_integer: true}, if: lambda {|m| m.log_size.present?} 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 def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -14,7 +14,8 @@ module JamRuby
validates :master_pass, presence: true, length: {minimum: 5} validates :master_pass, presence: true, length: {minimum: 5}
validates :relays_on_demand, :inclusion => {:in => [0, 1]} validates :relays_on_demand, :inclusion => {:in => [0, 1]}
after_save :poke_config before_destroy :poke_config
after_save :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -1,17 +1,22 @@
module JamRuby module JamRuby
class IcecastMount < ActiveRecord::Base class IcecastMount < ActiveRecord::Base
@@log = Logging.logger[IcecastMount]
attr_accessible :authentication_id, :name, :source_username, :source_pass, :max_listeners, :max_listener_duration, 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, :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, :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 :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' belongs_to :server, class_name: "JamRuby::IcecastServer", inverse_of: :mounts, foreign_key: 'icecast_server_id'
has_many :servers, :class_name => "JamRuby::IcecastServer", :through => :server_mounts, :source => :server 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_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 :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_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_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 :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 :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 :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 :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 :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?}
validates :hidden, :inclusion => {:in => [0, 1]} 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 before_save :sanitize_active_admin
after_save :after_save after_save :after_save
after_commit :after_commit after_save :poke_config
before_destroy :poke_config
def name_has_correct_format def name_has_correct_format
errors.add(:name, "must start with /") unless name && name.start_with?('/') errors.add(:name, "must start with /") unless name && name.start_with?('/')
end end
def after_save def poke_config
IcecastServer.update(servers, config_changed: 1) 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 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 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 end
def sanitize_active_admin def sanitize_active_admin
self.authentication_id = nil if self.authentication_id == '' 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 end
def source_up def source_up
with_lock do with_lock do
self.sourced = true self.sourced = true
self.save(:validate => false) self.sourced_needs_changing_at = nil
save(validate: false)
end end
end end
def source_down def source_down
with_lock do with_lock do
sourced = false self.sourced = false
save(:validate => false) self.sourced_needs_changing_at = nil
save(validate: false)
end end
end end
def listener_add def listener_add
with_lock do 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
end end
def listener_remove 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 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
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) def dumpXml(builder)
builder.tag! 'mount' do |mount| builder.tag! 'mount' do |mount|
mount.tag! 'mount-name', name mount.tag! 'mount-name', name
mount.tag! 'username', source_username if !source_username.nil? && !source_username.empty? mount.tag! 'username', resolve_string(:source_username) if string_present?(:source_username)
mount.tag! 'password', source_pass if !source_pass.nil? && !source_pass.empty? mount.tag! 'password', resolve_string(:source_pass) if string_present?(:source_pass)
mount.tag! 'max-listeners', max_listeners unless max_listeners.nil? mount.tag! 'max-listeners', resolve_int(:max_listeners) if int_present?(:max_listeners)
mount.tag! 'max-listener-duration', max_listener_duration unless max_listener_duration.nil? mount.tag! 'max-listener-duration', resolve_string(:max_listener_duration) if int_present?(:max_listener_duration)
mount.tag! 'dump-file', dump_file if !dump_file.nil? && !dump_file.empty? mount.tag! 'dump-file', resolve_string(:dump_file) if string_present?(:dump_file)
mount.tag! 'intro', intro if !intro.nil? && !intro.empty? mount.tag! 'intro', resolve_string(:intro) if string_present?(:intro)
mount.tag! 'fallback-mount', fallback_mount if !fallback_mount.nil? && !fallback_mount.empty? mount.tag! 'fallback-mount', resolve_string(:fallback_mount) if string_present?(:fallback_mount)
mount.tag! 'fallback-override', fallback_override if fallback_override mount.tag! 'fallback-override', resolve_int(:fallback_override) if int_present?(:fallback_override)
mount.tag! 'fallback-when-full', fallback_when_full if fallback_when_full mount.tag! 'fallback-when-full', resolve_int(:fallback_when_full) if int_present?(:fallback_when_full)
mount.tag! 'charset', charset if charset mount.tag! 'charset', resolve_string(:charset) if string_present?(:charset)
mount.tag! 'public', is_public mount.tag! 'public', resolve_int(:is_public) if int_present?(:is_public)
mount.tag! 'stream-name', stream_name if !stream_name.nil? && !stream_name.empty? mount.tag! 'stream-name', resolve_string(:stream_name) if string_present?(:stream_name)
mount.tag! 'stream-description', stream_description if !stream_description.nil? && !stream_description.empty? mount.tag! 'stream-description', resolve_string(:stream_description) if string_present?(:stream_description)
mount.tag! 'stream-url', stream_url if !stream_url.nil? && !stream_url.empty? mount.tag! 'stream-url', resolve_string(:stream_url) if string_present?(:stream_url)
mount.tag! 'genre', genre unless genre.empty? mount.tag! 'genre', resolve_string(:genre) if string_present?(:genre)
mount.tag! 'bitrate', bitrate if bitrate mount.tag! 'bitrate', resolve_int(:bitrate) if int_present?(:bitrate)
mount.tag! 'type', mime_type mount.tag! 'type', resolve_string(:mime_type) if string_present?(:mime_type)
mount.tag! 'subtype', subtype mount.tag! 'subtype', resolve_string(:subtype) if string_present?(:subtype)
mount.tag! 'burst-size', burst_size if burst_size mount.tag! 'burst-size', resolve_int(:burst_size) if int_present?(:burst_size)
mount.tag! 'mp3-metadata-interval', mp3_metadata_interval unless mp3_metadata_interval.nil? mount.tag! 'mp3-metadata-interval', resolve_int(:mp3_metadata_interval) if int_present?(:mp3_metadata_interval)
mount.tag! 'hidden', hidden mount.tag! 'hidden', resolve_int(:hidden) if int_present?(:hidden)
mount.tag! 'on-connect', on_connect if on_connect mount.tag! 'on-connect', resolve_string(:on_connect) if string_present?(:on_connect)
mount.tag! 'on-disconnect', on_disconnect if on_disconnect mount.tag! 'on-disconnect', resolve_string(:on_disconnect) if string_present?(:on_disconnect)
authentication.dumpXml(builder) if authentication authentication.dumpXml(builder) if authentication
end end
@ -117,5 +196,23 @@ module JamRuby
"http://" + server_mount.server.hostname + self.name "http://" + server_mount.server.hostname + self.name
end 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
end end

View File

@ -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

View File

@ -12,7 +12,8 @@ module JamRuby
validates :web_root, presence: true validates :web_root, presence: true
validates :admin_root, presence: true validates :admin_root, presence: true
after_save :poke_config after_save :poke_config
before_destroy :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -13,7 +13,8 @@ module JamRuby
validates :relay_shoutcast_metadata, :inclusion => {:in => [0, 1]} validates :relay_shoutcast_metadata, :inclusion => {:in => [0, 1]}
validates :on_demand, presence: true, :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 def poke_config
IcecastServer.update(servers, :config_changed => true) IcecastServer.update(servers, :config_changed => true)

View File

@ -8,7 +8,8 @@ module JamRuby
validates :chroot, :inclusion => {:in => [0, 1]} validates :chroot, :inclusion => {:in => [0, 1]}
after_save :poke_config before_destroy :poke_config
after_save :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -3,28 +3,30 @@ module JamRuby
attr_accessor :skip_config_changed_flag 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 :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 # 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 :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 :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 :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 :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 :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 :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 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_socket_servers, class_name: "JamRuby::IcecastServerSocket", inverse_of: :server
has_many :listen_sockets, :class_name => "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket has_many :listen_sockets, class_name: "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket
# mounts and relays are naturally server-specific, though # mounts and relays are naturally server-specific, though
has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :server #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 :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 :server_relays, class_name: "JamRuby::IcecastServerRelay", inverse_of: :relay
has_many :relays, :class_name => "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay has_many :relays, class_name: "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay
validates :config_changed, :inclusion => {:in => [0, 1]} validates :config_changed, :inclusion => {:in => [0, 1]}
validates :hostname, presence: true validates :hostname, presence: true
@ -32,6 +34,7 @@ module JamRuby
validates :server_id, presence: true validates :server_id, presence: true
validates :template, presence: true validates :template, presence: true
validates :mount_template, presence: true
before_save :before_save, unless: lambda { skip_config_changed_flag } before_save :before_save, unless: lambda { skip_config_changed_flag }
before_save :sanitize_active_admin before_save :sanitize_active_admin
@ -75,8 +78,48 @@ module JamRuby
end end
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 def to_s
return server_id server_id
end end
def dumpXml (output=$stdout, indent=1) def dumpXml (output=$stdout, indent=1)
@ -84,29 +127,21 @@ module JamRuby
builder = ::Builder::XmlMarkup.new(:target => output, :indent => indent) builder = ::Builder::XmlMarkup.new(:target => output, :indent => indent)
builder.tag! 'icecast' do |root| builder.tag! 'icecast' do |root|
root.tag! 'hostname', hostname root.tag! 'hostname', hostname
root.tag! 'location', (location.nil? || location.empty?) ? template.location : location root.tag! 'server-id', server_id
root.tag! 'server-id', server_id root.tag! 'location', resolve_string(:location) if string_present?(:location)
root.tag! 'admin', (admin_email.nil? || admin_email.empty?) ? template.admin_email : admin_email root.tag! 'admin', resolve_string(:admin_email) if string_present?(:admin_email)
root.tag! 'fileserve', fileserve.nil? ? template.fileserve : fileserve 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_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| current_listen_sockets.each do |listen_socket|
listen_socket.dumpXml(builder) listen_socket.dumpXml(builder)
end end
@ -120,5 +155,32 @@ module JamRuby
end end
end 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
end end

View File

@ -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

View File

@ -11,5 +11,11 @@ module JamRuby
validates :socket, :presence => true validates :socket, :presence => true
validates :server, :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
end end

View File

@ -33,8 +33,8 @@ module JamRuby
validates :listen_sockets, length: {minimum: 1} validates :listen_sockets, length: {minimum: 1}
before_save :sanitize_active_admin before_save :sanitize_active_admin
after_save :poke_config after_save :poke_config
before_destroy :poke_config
def poke_config def poke_config
IcecastServer.update(servers, config_changed: 1) IcecastServer.update(servers, config_changed: 1)

View File

@ -11,5 +11,11 @@ module JamRuby
validates :socket, :presence => true validates :socket, :presence => true
validates :template, :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
end end

View File

@ -17,10 +17,11 @@ module JamRuby
validates :auth_header, presence: true, if: :url_auth? validates :auth_header, presence: true, if: :url_auth?
validates :timelimit_header, presence: true, if: :url_auth? validates :timelimit_header, presence: true, if: :url_auth?
before_destroy :poke_config
after_save :poke_config after_save :poke_config
def 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 end
def to_s def to_s

View File

@ -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, :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" 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 :connections, :class_name => "JamRuby::Connection"
has_many :users, :through => :connections, :class_name => "JamRuby::User" has_many :users, :through => :connections, :class_name => "JamRuby::User"
has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions" 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 :creator_is_musician
validate :no_new_playback_while_playing validate :no_new_playback_while_playing
def before_destroy
self.mount.destroy if self.mount
end
def creator_is_musician def creator_is_musician
unless creator.musician? unless creator.musician?
@ -53,6 +58,21 @@ module JamRuby
end end
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 # 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. # 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) def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil)

View File

@ -766,10 +766,10 @@ module JamRuby
@@mq_router.publish_to_user(user_id, msg) @@mq_router.publish_to_user(user_id, msg)
end end
def send_source_up_requested(music_session, 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) 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 end
def send_source_down_requested(music_session, mount) def send_source_down_requested(music_session, mount)
@ -777,6 +777,18 @@ module JamRuby
@@mq_router.server_publish_to_session(music_session, msg) @@mq_router.server_publish_to_session(music_session, msg)
end 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 end
end end

View File

@ -18,6 +18,8 @@ module JamRuby
# updating_password corresponds to a lost_password # 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 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) # authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"

View File

@ -26,11 +26,10 @@ class MQRouter
# sends a message to a session on behalf of a user # 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" # 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) # 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) access_music_session(music_session, user)
# gather up client_ids in the session client_ids = music_session.get_connection_ids(as_musician: true, exclude_client_id: sender[:client_id])
client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] }
publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender)
end end
@ -38,13 +37,21 @@ class MQRouter
# sends a message to a session from the server # sends a message to a session from the server
# no access check as with user_publish_to_session # no access check as with user_publish_to_session
# client_msg should be a well-structure message (jam-pb message) # 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 # 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) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender)
end 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) # sends a message to a client with no checking of permissions (RAW USAGE)
# this method deliberately has no database interactivity/active_record objects # this method deliberately has no database interactivity/active_record objects
def publish_to_client(client_id, client_msg, sender = {:client_id => ""}) 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) # sends a message to a session with no checking of permissions (RAW USAGE)
# this method deliberately has no database interactivity/active_record objects # 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 EM.schedule do
sender_client_id = sender[:client_id] sender_client_id = sender[:client_id]

View File

@ -1,6 +1,7 @@
require 'json' require 'json'
require 'resque' require 'resque'
require 'resque-retry'
require 'resque-lonely_job'
require 'net/http' require 'net/http'
require 'digest/md5' require 'digest/md5'
@ -8,6 +9,7 @@ module JamRuby
# executes a mix of tracks, creating a final output mix # executes a mix of tracks, creating a final output mix
class IcecastConfigWriter class IcecastConfigWriter
extend Resque::Plugins::LonelyJob
@@log = Logging.logger[IcecastConfigWriter] @@log = Logging.logger[IcecastConfigWriter]

View File

@ -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

View File

@ -14,7 +14,11 @@ module JamRuby
@@log = Logging.logger[IcecastConfigRetry] @@log = Logging.logger[IcecastConfigRetry]
def self.perform def self.perform
@@log.debug("waking up")
IcecastConfigWriter.queue_jobs_needing_retry IcecastConfigWriter.queue_jobs_needing_retry
@@log.debug("done")
end end
end end

View File

@ -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

View File

@ -195,7 +195,7 @@ FactoryGirl.define do
factory :icecast_mount, :class => JamRuby::IcecastMount do factory :icecast_mount, :class => JamRuby::IcecastMount do
name "/" + Faker::Lorem.characters(10) name "/" + Faker::Lorem.characters(10)
source_username 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_listeners 100
max_listener_duration 3600 max_listener_duration 3600
fallback_mount Faker::Lorem.characters(10) fallback_mount Faker::Lorem.characters(10)
@ -207,10 +207,21 @@ FactoryGirl.define do
stream_url Faker::Lorem.characters(10) stream_url Faker::Lorem.characters(10)
genre Faker::Lorem.characters(10) genre Faker::Lorem.characters(10)
hidden 0 hidden 0
association :server, factory: :icecast_server_with_overrides
factory :icecast_mount_with_auth do factory :icecast_mount_with_auth do
association :authentication, :factory => :icecast_user_authentication 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
end end
factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do
@ -227,7 +238,7 @@ FactoryGirl.define do
factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do
authentication_type 'url' authentication_type 'url'
unused_username Faker::Lorem.characters(10) 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_add Faker::Lorem.characters(10)
mount_remove Faker::Lorem.characters(10) mount_remove Faker::Lorem.characters(10)
listener_add Faker::Lorem.characters(10) listener_add Faker::Lorem.characters(10)
@ -242,6 +253,7 @@ FactoryGirl.define do
factory :icecast_server_minimal do factory :icecast_server_minimal do
association :template, :factory => :icecast_template_minimal association :template, :factory => :icecast_template_minimal
association :mount_template, :factory => :icecast_mount_template
factory :icecast_server_with_overrides do factory :icecast_server_with_overrides do
association :limit, :factory => :icecast_limit association :limit, :factory => :icecast_limit
@ -274,4 +286,23 @@ FactoryGirl.define do
end end
end 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 end

View File

@ -50,10 +50,22 @@ describe IcecastAdminAuthentication do
server.config_changed.should == 1 server.config_changed.should == 1
end 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 it "success via server" do
server.admin_auth.save! server.admin_auth.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "success when deleted via server" do
server.admin_auth.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -57,11 +57,23 @@ describe IcecastDirectory do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.directory.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.directory.save! server.directory.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "destroy via server" do
server.directory.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -66,10 +66,22 @@ describe IcecastLimit do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.limit.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.limit.save! server.limit.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.limit.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -27,10 +27,22 @@ describe IcecastListenSocket do
server.config_changed.should == 1 server.config_changed.should == 1
end 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 it "success via server" do
server.listen_sockets.first.save! server.listen_sockets.first.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.listen_sockets.first.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -40,10 +40,22 @@ describe IcecastLogging do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.logging.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.logging.save! server.logging.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "deete via server" do
server.logging.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -53,11 +53,23 @@ describe IcecastMasterServerRelay do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.master_relay.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.master_relay.save! server.master_relay.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.master_relay.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -10,10 +10,6 @@ describe IcecastMount do
mount = IcecastMount.new mount = IcecastMount.new
mount.save.should be_false mount.save.should be_false
mount.errors[:name].should == ["can't be blank", "must start with /"] 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 end
@ -35,6 +31,7 @@ describe IcecastMount do
mount.max_listeners = 1000 mount.max_listeners = 1000
mount.max_listener_duration = 3600 mount.max_listener_duration = 3600
mount.authentication = FactoryGirl.create(:icecast_user_authentication) mount.authentication = FactoryGirl.create(:icecast_user_authentication)
mount.server = FactoryGirl.create(:icecast_server_with_overrides)
mount.save! 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 xml.css('mount authentication').length.should == 1 # no reason to test futher; it's tested in that model
end 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 describe "poke configs" do
let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) }
before(:each) do before(:each) do
server.mounts << FactoryGirl.create(:icecast_mount) server.mounts << FactoryGirl.create(:icecast_mount, server: server)
server.save! server.save!
server.config_updated server.config_updated
server.reload server.reload
@ -86,6 +111,12 @@ describe IcecastMount do
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "success when deleted" do
server.mounts.first.destroy
server.reload
server.config_changed.should == 1
end
end end
describe "icecast server callbacks" do describe "icecast server callbacks" do
@ -93,4 +124,125 @@ describe IcecastMount do
icecast_mount.source_up icecast_mount.source_up
end end
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 end

View File

@ -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

View File

@ -64,10 +64,22 @@ describe IcecastPath do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.path.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.path.save! server.path.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.path.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -48,5 +48,11 @@ describe IcecastRelay do
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.relays.first.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -40,10 +40,22 @@ describe IcecastSecurity do
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via template" do
server.template.security.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do it "success via server" do
server.security.save! server.security.save!
server.reload server.reload
server.config_changed.should == 1 server.config_changed.should == 1
end end
it "delete via server" do
server.security.destroy
server.reload
server.config_changed.should == 1
end
end end
end end

View File

@ -30,4 +30,34 @@ describe IcecastServer do
xml.css('icecast security').length.should == 1 xml.css('icecast security').length.should == 1
xml.css('icecast listen-socket').length.should == 1 xml.css('icecast listen-socket').length.should == 1
end 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 end

View File

@ -1,10 +1,20 @@
require 'spec_helper' require 'spec_helper'
describe IcecastListenSocket do describe IcecastTemplate do
let(:template) { template = FactoryGirl.create(:icecast_template_minimal) } let(:template) { template = FactoryGirl.create(:icecast_template_minimal) }
it "save" do it "save" do
template.errors.any?.should be_false template.errors.any?.should be_false
end 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 end

View File

@ -480,5 +480,36 @@ describe MusicSession do
end end
end 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 end

View File

@ -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

View File

@ -79,8 +79,8 @@ Spork.prefork do
config.filter_run_excluding aws: true unless run_tests? :aws config.filter_run_excluding aws: true unless run_tests? :aws
config.before(:suite) do config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] }
DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] })
end end
config.before(:each) do config.before(:each) do

View File

@ -37,6 +37,10 @@ def app_config
2 * 60 # 2 minutes 2 * 60 # 2 minutes
end end
def icecast_max_sourced_changed
15 # 15 seconds
end
def rabbitmq_host def rabbitmq_host
"localhost" "localhost"
end end

View File

@ -62,6 +62,7 @@ gem 'resque'
gem 'resque-retry' gem 'resque-retry'
gem 'resque-failed-job-mailer' gem 'resque-failed-job-mailer'
gem 'resque-dynamic-queues' gem 'resque-dynamic-queues'
gem 'resque-lonely_job', '~> 1.0.0'
gem 'quiet_assets', :group => :development gem 'quiet_assets', :group => :development
gem "bugsnag" gem "bugsnag"

View File

@ -48,6 +48,12 @@
BAND_INVITATION : "BAND_INVITATION", BAND_INVITATION : "BAND_INVITATION",
BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED", 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", TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE",
PING_REQUEST : "PING_REQUEST", PING_REQUEST : "PING_REQUEST",
PING_ACK : "PING_ACK", PING_ACK : "PING_ACK",

View File

@ -571,6 +571,14 @@
function CloseRecording() {} function CloseRecording() {}
function OnDownloadAvailable() {} 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 // Javascript Bridge seems to camel-case
// Set the instance functions: // Set the instance functions:
@ -703,6 +711,10 @@
this.CloseRecording = CloseRecording; this.CloseRecording = CloseRecording;
this.OnDownloadAvailable = OnDownloadAvailable; this.OnDownloadAvailable = OnDownloadAvailable;
// Broadcasting
this.SessionLiveBroadcastStart = SessionLiveBroadcastStart;
this.SessionLiveBroadcastStop = SessionLiveBroadcastStop;
// fake calls; not a part of the actual jam client // fake calls; not a part of the actual jam client
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
this.SetFakeRecordingImpl = SetFakeRecordingImpl; this.SetFakeRecordingImpl = SetFakeRecordingImpl;

View File

@ -233,10 +233,95 @@
acceptBandInvitation({ "band_invitation_id": payload.band_invitation_id, "band_id": payload.band_id, "notification_id": payload.notification_id }); 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) { else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
$notification.find('#div-actions').hide(); $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) { function deleteNotificationHandler(evt) {

View File

@ -7,15 +7,14 @@ class ApiIcecastController < ApiController
def mount_add def mount_add
mount = IcecastMount.find(@mount_id) mount = IcecastMount.find_by_name!(@mount_id)
mount.source_up mount.source_up
render text: '', :status => :ok render text: '', :status => :ok
end end
def mount_remove def mount_remove
mount = IcecastMount.find(@mount_id) mount = IcecastMount.find_by_name!(@mount_id)
mount.source_down mount.source_down
render text: '', :status => :ok render text: '', :status => :ok
@ -28,7 +27,7 @@ class ApiIcecastController < ApiController
remote_ip = params[:ip] remote_ip = params[:ip]
remote_user_agent = params[:agent] remote_user_agent = params[:agent]
mount = IcecastMount.find(@mount_id) mount = IcecastMount.find_by_name!(@mount_id)
mount.listener_add mount.listener_add
render text: '', :status => :ok render text: '', :status => :ok
@ -40,7 +39,7 @@ class ApiIcecastController < ApiController
pass = params[:pass] pass = params[:pass]
duration = params[:duration] # seconds connected to the listen stream duration = params[:duration] # seconds connected to the listen stream
mount = IcecastMount.find(@mount_id) mount = IcecastMount.find_by_name!(@mount_id)
mount.listener_remove mount.listener_remove
render text: '', :status => :ok render text: '', :status => :ok

View File

@ -177,6 +177,7 @@ include JamRuby
# this will be the qualifier on the IcecastConfigWorker queue name # this will be the qualifier on the IcecastConfigWorker queue name
config.icecast_server_id = ENV['ICECAST_SERVER_ID'] || 'localhost' config.icecast_server_id = ENV['ICECAST_SERVER_ID'] || 'localhost'
config.icecast_max_missing_check = 2 * 60 # 2 minutes 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_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' config.email_generic_from = 'nobody@jamkazam.com'

View File

@ -8,3 +8,10 @@ IcecastConfigRetry:
cron: 0 * * * * cron: 0 * * * *
class: "JamRuby::IcecastConfigRetry" class: "JamRuby::IcecastConfigRetry"
description: "Finds icecast servers that have had their config_changed, but no IcecastConfigWriter check recently" 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"

View File

@ -14,7 +14,8 @@ MusicSessionManager < BaseManager
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
# check if we are connected to rabbitmq # 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.creator = user
music_session.description = description music_session.description = description
music_session.musician_access = musician_access music_session.musician_access = musician_access
@ -24,9 +25,6 @@ MusicSessionManager < BaseManager
music_session.band = band music_session.band = band
music_session.legal_terms = legal_terms music_session.legal_terms = legal_terms
#genres = genres
@log.debug "Genres class: " + genres.class.to_s
unless genres.nil? unless genres.nil?
genres.each do |genre_id| genres.each do |genre_id|
loaded_genre = Genre.find(genre_id) loaded_genre = Genre.find(genre_id)
@ -34,6 +32,13 @@ MusicSessionManager < BaseManager
end end
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 music_session.save
unless music_session.errors.any? unless music_session.errors.any?

View File

@ -41,6 +41,7 @@ gem 'postgres_ext'
gem 'resque' gem 'resque'
gem 'resque-retry' gem 'resque-retry'
gem 'resque-failed-job-mailer' gem 'resque-failed-job-mailer'
gem 'resque-lonely_job', '~> 1.0.0'
group :development do group :development do
gem 'pry' gem 'pry'