merge develop

This commit is contained in:
Brian Smith 2014-01-22 21:21:40 -05:00
commit 30b13ed538
94 changed files with 2099 additions and 562 deletions

View File

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

View File

@ -3,16 +3,18 @@ ActiveAdmin.register_page "Bootstrap" do
page_action :create_server, :method => :post do
template = IcecastTemplate.find(params[:jam_ruby_icecast_server][:template_id])
template = IcecastTemplate.find_by_id(params[:jam_ruby_icecast_server][:template_id])
mount_template = IcecastMountTemplate.find_by_id(params[:jam_ruby_icecast_server][:mount_template_id])
hostname = params[:jam_ruby_icecast_server][:hostname]
server = IcecastServer.new
server.template = template
server.mount_template = mount_template
server.hostname = hostname
server.server_id = hostname
server.save!
redirect_to admin_bootstrap_path, :notice => "Server created. If you start job worker (bundle exec rake all_jobs), it should update your icecast config."
redirect_to admin_bootstrap_path, :notice => "Server created. If you start a job worker (bundle exec rake all_jobs in /web), it should update your icecast config."
end
page_action :brew_template, :method => :post do
@ -60,11 +62,11 @@ ActiveAdmin.register_page "Bootstrap" do
logging.save!
listen_socket1 = IcecastListenSocket.new
listen_socket1.port = 8000
listen_socket1.port = 9000
listen_socket1.save!
listen_socket2 = IcecastListenSocket.new
listen_socket2.port = 8001
listen_socket2.port = 9001
listen_socket2.save!
template = IcecastTemplate.new
@ -81,23 +83,156 @@ ActiveAdmin.register_page "Bootstrap" do
template.save!
end
redirect_to admin_bootstrap_path, :notice => "Brew template created. Now, create a mount template."
end
redirect_to admin_bootstrap_path, :notice => "Brew template created. Create a server now with that template specified."
page_action :ubuntu_template, :method => :post do
# to make this template, I installed icecast233 from jenkins (or our chef'ed apt-repo, same difference), and then based the rest of this code on what I saw in /etc/icecast2/icecast.xml
IcecastServer.transaction do
limit = IcecastLimit.new
limit.clients = 100
limit.sources = 2
limit.queue_size = 524288
limit.client_timeout = 30
limit.header_timeout = 15
limit.source_timeout = 10
limit.burst_size = 65535
limit.save!
admin_auth = IcecastAdminAuthentication.new
admin_auth.source_pass = 'blueberryjam'
admin_auth.relay_user = 'jamjam'
admin_auth.relay_pass = 'blueberryjam'
admin_auth.admin_user = 'jamjam'
admin_auth.admin_pass = 'blueberryjam'
admin_auth.save!
path = IcecastPath.new
path.base_dir = '/usr/share/icecast2'
path.log_dir = '/var/log/icecast2'
path.web_root = '/usr/share/icecast2/web'
path.admin_root = '/usr/share/icecast2/admin'
path.pid_file = nil
path.save!
security = IcecastSecurity.new
security.chroot = false
security.save!
logging = IcecastLogging.new
logging.access_log = 'access.log'
logging.error_log = 'error.log'
logging.log_level = 3 # you might want to change this after creating the template
logging.log_size = 10000
logging.save!
listen_socket1 = IcecastListenSocket.new
listen_socket1.port = 9000
listen_socket1.save!
listen_socket2 = IcecastListenSocket.new
listen_socket2.port = 9001
listen_socket2.save!
template = IcecastTemplate.new
template.name = "Ubuntu-#{IcecastTemplate.count + 1}"
template.location = '@work'
template.admin_email = 'nobody@jamkazam.com'
template.fileserve = true
template.limit = limit
template.admin_auth = admin_auth
template.path = path
template.security = security
template.logging = logging
template.listen_sockets = [listen_socket1, listen_socket2]
template.save!
end
redirect_to admin_bootstrap_path, :notice => "Ubuntu 12.04 template created. You should also install the icecast233 package: https://int.jamkazam.com/jenkins/job/icecast-debian/"
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 + 1}"
mount_template.source_username = nil # mount will override
mount_template.source_pass = nil # mount will override
mount_template.max_listeners = 20000 # huge
mount_template.max_listener_duration = 3600 * 24 # one day
mount_template.fallback_override = 1
mount_template.fallback_when_full = 1
mount_template.is_public = 0
mount_template.stream_name = nil # mount will override
mount_template.stream_description = nil # mount will override
mount_template.stream_url = nil # mount will override
mount_template.genre = nil # mount will override
mount_template.bitrate = 128
mount_template.burst_size = 65536
mount_template.hidden = 1
mount_template.on_connect = nil
mount_template.on_disconnect = nil
mount_template.authentication = auth
if type == 'ogg'
mount_template.mp3_metadata_interval = nil
mount_template.mime_type ='audio/ogg'
mount_template.subtype = 'vorbis'
mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.ogg"
else
mount_template.mp3_metadata_interval = 4096
mount_template.mime_type ='audio/mpeg'
mount_template.subtype = nil
mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.mp3"
end
mount_template.save!
end
redirect_to admin_bootstrap_path, :notice => "Mount template created. Create a server now with your new templates specified."
end
action_item do
link_to "Create MacOSX (Brew) Template", admin_bootstrap_brew_template_path, :method => :post
end
action_item do
link_to "Create Brew Template", admin_bootstrap_brew_template_path, :method => :post
link_to "Create Ubuntu 12.04 Template", admin_bootstrap_ubuntu_template_path, :method => :post
end
content do
if IcecastTemplate.count == 0
para "You need to create at least one template for your environment"
para "You need to create at least one server template, and one mount template. Click one of the top-left buttons based on your platform"
elsif IcecastMountTemplate.count == 0
semantic_form_for IcecastMountTemplate.new, :url => admin_bootstrap_create_mount_template_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "New Mount Template" do
f.input :hostname, :label => "jam-web hostname:port"
f.input :default_mime_type, :as => :select, :collection => ["ogg", "mp3"]
end
f.actions
end
else
semantic_form_for IcecastServer.new, :url => admin_bootstrap_create_server_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "New Server" do
f.input :hostname
f.input :template
f.inputs "New Icecast Server" do
f.input :hostname, :hint => "Just the icecast hostname; no port"
f.input :template, :hint => "This is the template associated with the server. Not as useful for the 1st server, but subsequent servers can use this same template, and share config"
f.input :mount_template, :hint => "The mount template. When mounts are made as music sessions are created, this template will satisfy templatable values"
end
f.actions
end

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
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))
(1..size).collect{|a| cc = chars[rand(chars.size)]; 0==rand(2) ? cc.upcase : cc }.join
end
end

View File

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

View File

@ -88,5 +88,8 @@ icecast.sql
home_page_promos.sql
mix_job_watch.sql
music_session_constraints.sql
mixes_drop_manifest_add_retry.sql
music_sessions_unlogged.sql
integrate_icecast_into_sessions.sql
ms_recording_anonymous_likes.sql
ms_user_history_add_instruments.sql

View File

@ -1,129 +1,96 @@
-- this manifest update makes every table associated with music_sessions UNLOGGED
-- tables to mark UNLOGGED
-- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions
-- tables to just get rid of
-- session_plays
-- breaking foreign keys for tables
-- connections: user_id
-- fan_invitations: receiver_id, sender_id
-- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id
-- genres_music_sessions: genre_id
-- invitations: sender_id, receiver_id
-- fan_invitations: user_id
-- notifications: invitation_id, join_request_id, session_id
DROP TABLE sessions_plays;
-- divorce notifications from UNLOGGED tables
-- NOTIFICATIONS
----------------
-- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE
ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey;
-- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE
ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey;
-- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE
ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey;
-- FAN_INVITATIONS
------------------
DROP TABLE fan_invitations;
DROP TABLE invitations;
DROP TABLE join_requests;
DROP TABLE genres_music_sessions;
DROP TABLE tracks;
DROP TABLE connections;
DROP TABLE music_sessions;
-- MUSIC_SESSIONS
-----------------
CREATE UNLOGGED TABLE music_sessions (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
description VARCHAR(8000),
user_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
musician_access BOOLEAN NOT NULL,
band_id VARCHAR(64),
approval_required BOOLEAN NOT NULL,
fan_access BOOLEAN NOT NULL,
fan_chat BOOLEAN NOT NULL,
claimed_recording_id VARCHAR(64),
claimed_recording_initiator_id VARCHAR(64)
CREATE TABLE icecast_mount_templates(
id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
name VARCHAR(256) NOT NULL,
source_username VARCHAR(64),
source_pass VARCHAR(64),
max_listeners INTEGER DEFAULT 4,
max_listener_duration INTEGER DEFAULT 3600,
dump_file VARCHAR(1024),
intro VARCHAR(1024),
fallback_mount VARCHAR(1024),
fallback_override INTEGER DEFAULT 1,
fallback_when_full INTEGER DEFAULT 1,
charset VARCHAR(1024) DEFAULT 'ISO8859-1',
is_public INTEGER DEFAULT 0,
stream_name VARCHAR(1024),
stream_description VARCHAR(10000),
stream_url VARCHAR(1024),
genre VARCHAR(256),
bitrate INTEGER,
mime_type VARCHAR(64) NOT NULL DEFAULT 'audio/mpeg',
subtype VARCHAR(64),
burst_size INTEGER,
mp3_metadata_interval INTEGER,
hidden INTEGER DEFAULT 1,
on_connect VARCHAR(1024),
on_disconnect VARCHAR(1024),
authentication_id varchar(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CONNECTIONS
--------------
CREATE UNLOGGED TABLE connections (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
user_id VARCHAR(64),
client_id VARCHAR(64) UNIQUE NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
music_session_id VARCHAR(64),
ip_address VARCHAR(64),
as_musician BOOLEAN,
aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL
ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP NOT NULL;
ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP DEFAULT;
ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP NOT NULL;
ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP DEFAULT;
ALTER TABLE icecast_mounts ADD COLUMN music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE CASCADE;
ALTER TABLE icecast_mounts ADD COLUMN icecast_server_id VARCHAR(64) NOT NULL REFERENCES icecast_servers(id);
ALTER TABLE icecast_mounts ADD COLUMN icecast_mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id);
ALTER TABLE icecast_mounts ADD COLUMN sourced_needs_changing_at TIMESTAMP;
;
CREATE TABLE icecast_server_groups (
id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
name VARCHAR(255) UNIQUE NOT NULL
);
ALTER TABLE ONLY connections ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE SET NULL;
-- bootstrap the default icecast group
INSERT INTO icecast_server_groups (id, name) VALUES ('default', 'default');
INSERT INTO icecast_server_groups (id, name) VALUES ('unused', 'unused');
-- GENRES_MUSIC_SESSIONS
------------------------
CREATE UNLOGGED TABLE genres_music_sessions (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
genre_id VARCHAR(64),
music_session_id VARCHAR(64)
);
ALTER TABLE ONLY genres_music_sessions ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE;
ALTER TABLE users ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default';
CREATE UNLOGGED TABLE fan_invitations (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
sender_id VARCHAR(64),
receiver_id VARCHAR(64),
music_session_id VARCHAR(64),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
);
ALTER TABLE ONLY fan_invitations ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE;
-- and by default, all servers and users are in this group
ALTER TABLE icecast_servers ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default';
ALTER TABLE icecast_servers ADD COLUMN mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id);
CREATE UNLOGGED TABLE join_requests (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
user_id VARCHAR(64),
music_session_id VARCHAR(64),
text VARCHAR(2000),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
);
ALTER TABLE ONLY join_requests ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id);
ALTER TABLE ONLY join_requests ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE;
-- INVITATIONS
--------------
CREATE UNLOGGED TABLE invitations (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
sender_id VARCHAR(64),
receiver_id VARCHAR(64),
music_session_id VARCHAR(64),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
join_request_id VARCHAR(64)
);
ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id);
ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_admin_auth_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_mount_template_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_mount_template_id_fkey" FOREIGN KEY (mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_directory_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_icecast_server_group_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_icecast_server_group_id_fkey" FOREIGN KEY (icecast_server_group_id) REFERENCES icecast_server_groups(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_limit_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_logging_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_master_relay_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_path_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_security_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL;
ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_template_id_fkey";
ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_template_id_fkey" FOREIGN KEY (template_id) REFERENCES icecast_templates(id) ON DELETE SET NULL;
-- TRACKS
---------
CREATE UNLOGGED TABLE tracks (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
connection_id VARCHAR(64),
instrument_id VARCHAR(64),
sound VARCHAR(64) NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
client_track_id VARCHAR(64) NOT NULL
);
ALTER TABLE ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE;
ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey";
ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey" FOREIGN KEY (icecast_mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL;
ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_server_id_fkey";
ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_server_id_fkey" FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_admin_auth_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_directory_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_limit_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_logging_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_master_relay_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_path_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL;
ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_security_id_fkey";
ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL;

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

View File

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

View File

@ -33,6 +33,7 @@ require "jam_ruby/resque/audiomixer"
require "jam_ruby/resque/icecast_config_writer"
require "jam_ruby/resque/scheduled/audiomixer_retry"
require "jam_ruby/resque/scheduled/icecast_config_retry"
require "jam_ruby/resque/scheduled/icecast_source_check"
require "jam_ruby/mq_router"
require "jam_ruby/base_manager"
require "jam_ruby/connection_manager"
@ -113,6 +114,9 @@ require "jam_ruby/models/icecast_server_mount"
require "jam_ruby/models/icecast_server_relay"
require "jam_ruby/models/icecast_server_socket"
require "jam_ruby/models/icecast_template_socket"
require "jam_ruby/models/icecast_server_group"
require "jam_ruby/models/icecast_mount_template"
include Jampb

View File

@ -37,6 +37,12 @@ module JamRuby
return friend_ids
end
# this simulates music_session destroy callbacks with activerecord
def before_destroy_music_session(music_session_id)
music_session = MusicSession.find_by_id(music_session_id)
music_session.before_destroy if music_session
end
# reclaim the existing connection,
def reconnect(conn, reconnect_music_session_id)
music_session_id = nil
@ -218,6 +224,7 @@ SQL
# same for session-if we are down to the last participant, delete the session
unless music_session_id.nil?
before_destroy_music_session(music_session_id)
result = conn.exec("DELETE FROM music_sessions WHERE id = $1 AND 0 = (select count(music_session_id) FROM connections where music_session_id = $1)", [music_session_id])
if result.cmd_tuples == 1
music_session_id = nil
@ -258,6 +265,7 @@ SQL
end
if num_participants == 0
# delete the music_session
before_destroy_music_session(previous_music_session_id)
conn.exec("DELETE from music_sessions WHERE id = $1",
[previous_music_session_id]) do |result|
if result.cmd_tuples == 1

View File

@ -558,13 +558,15 @@ module JamRuby
# create a source up requested message to send to clients in a session,
# so that one of the clients will start sending source audio to icecast
def source_up_requested (session_id, host, port, mount, source_user, source_pass)
def source_up_requested (session_id, host, port, mount, source_user, source_pass, bitrate)
source_up_requested = Jampb::SourceUpRequested.new(
music_session: session_id,
host: host,
port: port,
mount: mount,
source_user: source_user,
source_pass: source_pass)
source_pass: source_pass,
bitrate: bitrate)
Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_UP_REQUESTED,
@ -575,7 +577,7 @@ module JamRuby
# create a source up requested message to send to clients in a session,
# so that one of the clients will start sending source audio to icecast
def source_down_requested (session_id, mount)
source_down_requested = Jampb::SourceDownRequested.new(mount: mount)
source_down_requested = Jampb::SourceDownRequested.new(music_session: session_id, mount: mount)
Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_DOWN_REQUESTED,
@ -583,6 +585,27 @@ module JamRuby
source_down_requested: source_down_requested)
end
# let's someone know that the source came online. the stream activate shortly
# it might be necessary to refresh the client
def source_up (session_id)
source_up = Jampb::SourceUp.new(music_session: session_id)
Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_UP,
route_to: SESSION_TARGET_PREFIX + session_id,
source_up: source_up)
end
# let's someone know that the source went down. the stream will go offline
def source_down (session_id)
source_down = Jampb::SourceDown.new(music_session: session_id)
Jampb::ClientMessage.new(
type: ClientMessage::Type::SOURCE_DOWN,
route_to: SESSION_TARGET_PREFIX + session_id,
source_down: source_down)
end
# create a test message to send in session
def test_session_message(session_id, msg)
test = Jampb::TestSessionMessage.new(:msg => msg)

View File

@ -6,7 +6,7 @@ module JamRuby
PRODUCTS = ['JamClient/Win32', 'JamClient/MacOSX']
self.primary_key = 'id'
attr_accessible :version, :uri, :sha1, :environment, :product
attr_accessible :version, :uri, :sha1, :environment, :product, as: :admin
mount_uploader :uri, ArtifactUploader

View File

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

View File

@ -12,7 +12,8 @@ module JamRuby
validates :relay_pass, presence: true, length: {minimum: 5}
validates :admin_user, presence: true, length: {minimum: 5}
after_save :poke_config
before_destroy :poke_config
after_save :poke_config
def poke_config
IcecastServer.update(servers, config_changed: 1)

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, presence: true
after_save :poke_config
before_destroy :poke_config
after_save :poke_config
def poke_config
IcecastServer.update(servers, config_changed: 1)

View File

@ -15,7 +15,8 @@ module JamRuby
validates :source_timeout, presence: true, numericality: {only_integer: true}
validates :burst_size, presence: true, numericality: {only_integer: true}
after_save :poke_config
before_destroy :poke_config
after_save :poke_config
def poke_config
IcecastServer.update(servers, config_changed: 1)

View File

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

View File

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

View File

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

View File

@ -1,17 +1,22 @@
module JamRuby
class IcecastMount < ActiveRecord::Base
@@log = Logging.logger[IcecastMount]
attr_accessible :authentication_id, :name, :source_username, :source_pass, :max_listeners, :max_listener_duration,
:dump_file, :intro, :fallback_mount, :fallback_override, :fallback_when_full, :charset, :is_public,
:stream_name, :stream_description, :stream_url, :genre, :bitrate, :mime_type, :subtype, :burst_size,
:mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, as: :admin
:mp3_metadata_interval, :hidden, :on_connect, :on_disconnect,
:music_session_id, :icecast_server_id, :icecast_mount_template_id, :listeners, :sourced,
:sourced_needs_changing_at, as: :admin
belongs_to :authentication, class_name: "JamRuby::IcecastUserAuthentication", inverse_of: :mount, :foreign_key => 'authentication_id'
belongs_to :music_session, class_name: "JamRuby::MusicSession", inverse_of: :mount, foreign_key: 'music_session_id'
has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :mount, :foreign_key => 'icecast_mount_id'
has_many :servers, :class_name => "JamRuby::IcecastServer", :through => :server_mounts, :source => :server
belongs_to :server, class_name: "JamRuby::IcecastServer", inverse_of: :mounts, foreign_key: 'icecast_server_id'
belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", inverse_of: :mounts, foreign_key: 'icecast_mount_template_id'
validates :name, presence: true
validates :name, presence: true, uniqueness: true
validates :source_username, length: {minimum: 5}, if: lambda {|m| m.source_username.present?}
validates :source_pass, length: {minimum: 5}, if: lambda {|m| m.source_pass.present?}
validates :max_listeners, length: {in: 1..15000}, if: lambda {|m| m.max_listeners.present?}
@ -19,93 +24,167 @@ module JamRuby
validates :fallback_override, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?}
validates :fallback_when_full, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?}
validates :is_public, presence: true, :inclusion => {:in => [-1, 0, 1]}
validates :stream_name, presence: true
validates :stream_description, presence: true
validates :stream_url, presence: true
validates :genre, presence: true
validates :bitrate, numericality: {only_integer: true}, if: lambda {|m| m.bitrate.present?}
validates :mime_type, presence: true
validates :subtype, presence: true
validates :burst_size, numericality: {only_integer: true}, if: lambda {|m| m.burst_size.present?}
validates :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?}
validates :hidden, :inclusion => {:in => [0, 1]}
validate :name_has_correct_format
validates :server, presence: true
validate :name_has_correct_format
before_save :sanitize_active_admin
after_save :after_save
after_commit :after_commit
before_save :sanitize_active_admin
after_save :after_save
after_save :poke_config
before_destroy :poke_config
def name_has_correct_format
errors.add(:name, "must start with /") unless name && name.start_with?('/')
end
def after_save
IcecastServer.update(servers, config_changed: 1)
def poke_config
server.update_attribute(:config_changed, 1) if server
end
def after_save
server.update_attribute(:config_changed, 1)
# transiting to sourced from not sourced
if !sourced_was && sourced
# went from NOT SOURCED to SOURCED
notify_source_up
elsif sourced_was && !sourced
# went from SOURCED to NOT SOURCED
notify_source_down
end
if listeners_was == 0 && listeners > 0 && !sourced
# listener count went above 0 and there is no source. ask the musician clients to source
notify_source_up_requested
end
# Note:
# Notification.send_source_down_requested does not occur here.
# we set up a cron that checks for streams that have not been successfully source up/down (after timeout ) in IcecastSourceCheck
end
def sanitize_active_admin
self.authentication_id = nil if self.authentication_id == ''
self.music_session_id = nil if self.music_session_id == ''
self.icecast_server_id = nil if self.icecast_server_id == ''
end
# creates a templated
def self.build_session_mount(music_session)
# only public sessions get mounts currently
return nil unless music_session.fan_access
icecast_server = IcecastServer.find_best_server_for_user(music_session.creator)
mount = nil
if icecast_server && icecast_server.mount_template_id
# we have a server with an associated mount_template; we can create a mount automatically
mount = icecast_server.mount_template.build_session_mount(music_session)
mount.server = icecast_server
end
mount
end
def source_up
with_lock do
self.sourced = true
self.save(:validate => false)
self.sourced_needs_changing_at = nil
save(validate: false)
end
end
def source_down
with_lock do
sourced = false
save(:validate => false)
self.sourced = false
self.sourced_needs_changing_at = nil
save(validate: false)
end
end
def listener_add
with_lock do
increment!(:listeners)
sourced_needs_changing_at = Time.now if listeners == 0
# this is completely unsafe without that 'with_lock' statement above
self.listeners = self.listeners + 1
save(validate: false)
end
end
def listener_remove
if listeners == 0
@@log.warn("listeners is at 0, but we are being asked to remove a listener. maybe we missed a listener_add request earlier")
return
end
with_lock do
decrement!(:listeners)
sourced_needs_changing_at = Time.now if listeners == 1
# this is completely unsafe without that 'with_lock' statement above
self.listeners = self.listeners - 1
save(validations: false)
end
end
def notify_source_up_requested
Notification.send_source_up_requested(music_session,
server.hostname,
server.pick_listen_socket(:port),
name,
resolve_string(:source_username),
resolve_string(:source_pass),
resolve_int(:bitrate)) if music_session_id
end
def notify_source_down_requested
Notification.send_source_down_requested(music_session, name)
end
def notify_source_up
Notification.send_source_up(music_session) if music_session_id
end
def notify_source_down
Notification.send_source_down(music_session) if music_session_id
end
# Check if the icecast_mount specifies the value; if not, use the mount_template's value take effect
def dumpXml(builder)
builder.tag! 'mount' do |mount|
mount.tag! 'mount-name', name
mount.tag! 'username', source_username if !source_username.nil? && !source_username.empty?
mount.tag! 'password', source_pass if !source_pass.nil? && !source_pass.empty?
mount.tag! 'max-listeners', max_listeners unless max_listeners.nil?
mount.tag! 'max-listener-duration', max_listener_duration unless max_listener_duration.nil?
mount.tag! 'dump-file', dump_file if !dump_file.nil? && !dump_file.empty?
mount.tag! 'intro', intro if !intro.nil? && !intro.empty?
mount.tag! 'fallback-mount', fallback_mount if !fallback_mount.nil? && !fallback_mount.empty?
mount.tag! 'fallback-override', fallback_override if fallback_override
mount.tag! 'fallback-when-full', fallback_when_full if fallback_when_full
mount.tag! 'charset', charset if charset
mount.tag! 'public', is_public
mount.tag! 'stream-name', stream_name if !stream_name.nil? && !stream_name.empty?
mount.tag! 'stream-description', stream_description if !stream_description.nil? && !stream_description.empty?
mount.tag! 'stream-url', stream_url if !stream_url.nil? && !stream_url.empty?
mount.tag! 'genre', genre unless genre.empty?
mount.tag! 'bitrate', bitrate if bitrate
mount.tag! 'type', mime_type
mount.tag! 'subtype', subtype
mount.tag! 'burst-size', burst_size if burst_size
mount.tag! 'mp3-metadata-interval', mp3_metadata_interval unless mp3_metadata_interval.nil?
mount.tag! 'hidden', hidden
mount.tag! 'on-connect', on_connect if on_connect
mount.tag! 'on-disconnect', on_disconnect if on_disconnect
mount.tag! 'username', resolve_string(:source_username) if string_present?(:source_username)
mount.tag! 'password', resolve_string(:source_pass) if string_present?(:source_pass)
mount.tag! 'max-listeners', resolve_int(:max_listeners) if int_present?(:max_listeners)
mount.tag! 'max-listener-duration', resolve_string(:max_listener_duration) if int_present?(:max_listener_duration)
mount.tag! 'dump-file', resolve_string(:dump_file) if string_present?(:dump_file)
mount.tag! 'intro', resolve_string(:intro) if string_present?(:intro)
mount.tag! 'fallback-mount', resolve_string(:fallback_mount) if string_present?(:fallback_mount)
mount.tag! 'fallback-override', resolve_int(:fallback_override) if int_present?(:fallback_override)
mount.tag! 'fallback-when-full', resolve_int(:fallback_when_full) if int_present?(:fallback_when_full)
mount.tag! 'charset', resolve_string(:charset) if string_present?(:charset)
mount.tag! 'public', resolve_int(:is_public) if int_present?(:is_public)
mount.tag! 'stream-name', resolve_string(:stream_name) if string_present?(:stream_name)
mount.tag! 'stream-description', resolve_string(:stream_description) if string_present?(:stream_description)
mount.tag! 'stream-url', resolve_string(:stream_url) if string_present?(:stream_url)
mount.tag! 'genre', resolve_string(:genre) if string_present?(:genre)
mount.tag! 'bitrate', resolve_int(:bitrate) if int_present?(:bitrate)
mount.tag! 'type', resolve_string(:mime_type) if string_present?(:mime_type)
mount.tag! 'subtype', resolve_string(:subtype) if string_present?(:subtype)
mount.tag! 'burst-size', resolve_int(:burst_size) if int_present?(:burst_size)
mount.tag! 'mp3-metadata-interval', resolve_int(:mp3_metadata_interval) if int_present?(:mp3_metadata_interval)
mount.tag! 'hidden', resolve_int(:hidden) if int_present?(:hidden)
mount.tag! 'on-connect', resolve_string(:on_connect) if string_present?(:on_connect)
mount.tag! 'on-disconnect', resolve_string(:on_disconnect) if string_present?(:on_disconnect)
authentication.dumpXml(builder) if authentication
end
@ -117,5 +196,23 @@ module JamRuby
"http://" + server_mount.server.hostname + self.name
end
def resolve_string(field)
self[field].present? ? self[field] : mount_template && mount_template[field]
end
def string_present?(field)
val = resolve_string(field)
val ? val.present? : false
end
def resolve_int(field)
!self[field].nil? ? self[field]: mount_template && mount_template[field]
end
def int_present?(field)
resolve_int(field)
end
end
end

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 :admin_root, presence: true
after_save :poke_config
after_save :poke_config
before_destroy :poke_config
def poke_config
IcecastServer.update(servers, config_changed: 1)

View File

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

View File

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

View File

@ -3,28 +3,31 @@ module JamRuby
attr_accessor :skip_config_changed_flag
attr_accessible :template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id,
:security_id, :config_changed, :hostname, :location, :admin_email, :fileserve, as: :admin
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, :icecast_server_group_id, as: :admin
belongs_to :template, :class_name => "JamRuby::IcecastTemplate", foreign_key: 'template_id', :inverse_of => :servers
belongs_to :template, class_name: "JamRuby::IcecastTemplate", foreign_key: 'template_id', inverse_of: :servers
belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", foreign_key: 'mount_template_id', inverse_of: :servers
belongs_to :server_group, class_name: "JamRuby::IcecastServerGroup", foreign_key: 'icecast_server_group_id', inverse_of: :servers
# all are overrides, because the template defines all of these as well. When building the XML, we will prefer these if set
belongs_to :limit, :class_name => "JamRuby::IcecastLimit", foreign_key: 'limit_id', :inverse_of => :servers
belongs_to :admin_auth, :class_name => "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', :inverse_of => :servers
belongs_to :directory, :class_name => "JamRuby::IcecastDirectory", foreign_key: 'directory_id', :inverse_of => :servers
belongs_to :master_relay, :class_name => "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', :inverse_of => :servers
belongs_to :path, :class_name => "JamRuby::IcecastPath", foreign_key: 'path_id', :inverse_of => :servers
belongs_to :logging, :class_name => "JamRuby::IcecastLogging", foreign_key: 'logging_id', :inverse_of => :servers
belongs_to :security, :class_name => "JamRuby::IcecastSecurity", foreign_key: 'security_id', :inverse_of => :servers
has_many :listen_socket_servers, :class_name => "JamRuby::IcecastServerSocket", :inverse_of => :server
has_many :listen_sockets, :class_name => "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket
belongs_to :limit, class_name: "JamRuby::IcecastLimit", foreign_key: 'limit_id', inverse_of: :servers
belongs_to :admin_auth, class_name: "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', inverse_of: :servers
belongs_to :directory, class_name: "JamRuby::IcecastDirectory", foreign_key: 'directory_id', inverse_of: :servers
belongs_to :master_relay, class_name: "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', inverse_of: :servers
belongs_to :path, class_name: "JamRuby::IcecastPath", foreign_key: 'path_id', inverse_of: :servers
belongs_to :logging, class_name: "JamRuby::IcecastLogging", foreign_key: 'logging_id', inverse_of: :servers
belongs_to :security, class_name: "JamRuby::IcecastSecurity", foreign_key: 'security_id', inverse_of: :servers
has_many :listen_socket_servers, class_name: "JamRuby::IcecastServerSocket", inverse_of: :server
has_many :listen_sockets, class_name: "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket
# mounts and relays are naturally server-specific, though
has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :server
has_many :mounts, :class_name => "JamRuby::IcecastMount", :through => :server_mounts, :source => :mount
#has_many :server_mounts, class_name: "JamRuby::IcecastServerMount", inverse_of: :server
has_many :mounts, class_name: "JamRuby::IcecastMount", inverse_of: :server, :foreign_key => 'icecast_server_id'
has_many :server_relays, :class_name => "JamRuby::IcecastServerRelay", :inverse_of => :relay
has_many :relays, :class_name => "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay
has_many :server_relays, class_name: "JamRuby::IcecastServerRelay", inverse_of: :relay
has_many :relays, class_name: "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay
validates :config_changed, :inclusion => {:in => [0, 1]}
validates :hostname, presence: true
@ -32,6 +35,7 @@ module JamRuby
validates :server_id, presence: true
validates :template, presence: true
validates :mount_template, presence: true
before_save :before_save, unless: lambda { skip_config_changed_flag }
before_save :sanitize_active_admin
@ -75,8 +79,48 @@ module JamRuby
end
end
def pick_listen_socket(field)
current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets
socket = current_listen_sockets.first
socket[:field] if socket
end
# pick an icecast server with the least listeners * sources
def self.find_best_server_for_user(user)
chosen_server_id = nil
chosen_server_weight = nil
ActiveRecord::Base.connection_pool.with_connection do |connection|
result = connection.execute('select SUM(listeners), SUM(sourced::int), icecast_servers.id
FROM icecast_servers
LEFT JOIN icecast_mounts ON icecast_servers.id = icecast_mounts.icecast_server_id
WHERE icecast_server_group_id = \'' + user.icecast_server_group_id + '\'
GROUP BY icecast_servers.id;')
result.cmd_tuples.times do |i|
listeners = result.getvalue(i, 0).to_i
sourced = result.getvalue(i, 1).to_i
icecast_server_id = result.getvalue(i, 2)
# compute weight. source is much more intensive than listener, based on load tests again 2.3.0
# http://icecast.org/loadtest2.php
weight = sourced * 10 + listeners
if !chosen_server_id || (weight < chosen_server_weight)
chosen_server_id = icecast_server_id
chosen_server_weight = weight
end
end
end
IcecastServer.find(chosen_server_id) if chosen_server_id
end
def to_s
return server_id
server_id
end
def dumpXml (output=$stdout, indent=1)
@ -84,29 +128,21 @@ module JamRuby
builder = ::Builder::XmlMarkup.new(:target => output, :indent => indent)
builder.tag! 'icecast' do |root|
root.tag! 'hostname', hostname
root.tag! 'location', (location.nil? || location.empty?) ? template.location : location
root.tag! 'server-id', server_id
root.tag! 'admin', (admin_email.nil? || admin_email.empty?) ? template.admin_email : admin_email
root.tag! 'fileserve', fileserve.nil? ? template.fileserve : fileserve
root.tag! 'hostname', hostname
root.tag! 'server-id', server_id
root.tag! 'location', resolve_string(:location) if string_present?(:location)
root.tag! 'admin', resolve_string(:admin_email) if string_present?(:admin_email)
root.tag! 'fileserve', resolve_int(:fileserve) if int_present?(:fileserve)
resolve_association(:limit).dumpXml(builder) if association_present?(:limit)
resolve_association(:admin_auth).dumpXml(builder) if association_present?(:admin_auth)
resolve_association(:directory).dumpXml(builder) if association_present?(:directory)
resolve_association(:master_relay).dumpXml(builder) if association_present?(:master_relay)
resolve_association(:path).dumpXml(builder) if association_present?(:path)
resolve_association(:logging).dumpXml(builder) if association_present?(:logging)
resolve_association(:security).dumpXml(builder) if association_present?(:security)
# do we have an override specified? or do we go with the template
current_limit = limit ? limit : template.limit
current_admin_auth = admin_auth ? admin_auth : template.admin_auth
current_directory = directory ? directory : template.directory
current_master_relay = master_relay ? master_relay : template.master_relay
current_path = path ? path : template.path
current_logging = logging ? logging : template.logging
current_security = security ? security : template.security
current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets
current_limit.dumpXml(builder) unless current_limit.nil?
current_admin_auth.dumpXml(builder) unless current_admin_auth.nil?
current_directory.dumpXml(builder) unless current_directory.nil?
current_master_relay.dumpXml(builder) unless current_master_relay.nil?
current_path.dumpXml(builder) unless current_path.nil?
current_logging.dumpXml(builder) unless current_logging.nil?
current_security.dumpXml(builder) unless current_security.nil?
current_listen_sockets.each do |listen_socket|
listen_socket.dumpXml(builder)
end
@ -120,5 +156,32 @@ module JamRuby
end
end
end
def resolve_string(field)
self[field].present? ? self[field] : template && template[field]
end
def string_present?(field)
val = resolve_string(field)
val ? val.present? : false
end
def resolve_int(field)
self[field] ? self[field]: template && template[field]
end
def int_present?(field)
resolve_int(field)
end
def resolve_association(field)
self.send(field) ? self.send(field) : template && template.send(field)
end
def association_present?(field)
resolve_association(field)
end
end
end

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

View File

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

View File

@ -11,5 +11,11 @@ module JamRuby
validates :socket, :presence => true
validates :template, :presence => true
after_save :poke_config
before_destroy :poke_config
def poke_config
IcecastServer.update(template.servers, config_changed: 1) if template
end
end
end

View File

@ -17,10 +17,11 @@ module JamRuby
validates :auth_header, presence: true, if: :url_auth?
validates :timelimit_header, presence: true, if: :url_auth?
before_destroy :poke_config
after_save :poke_config
def poke_config
IcecastServer.update(mount.servers, config_changed: 1) if mount
mount.server.update_attribute(:config_changed, 1) if mount && mount.server
end
def to_s

View File

@ -3,7 +3,8 @@ module JamRuby
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
attr_accessible :email, :sender_id, :autofriend, :note
attr_accessible :email, :sender_id, :autofriend, :note, as: :admin
attr_accessor :accepted_twice
self.primary_key = 'id'

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_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id'
has_many :connections, :class_name => "JamRuby::Connection"
has_many :users, :through => :connections, :class_name => "JamRuby::User"
has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions"
@ -37,6 +39,9 @@ module JamRuby
validate :creator_is_musician
validate :no_new_playback_while_playing
def before_destroy
self.mount.destroy if self.mount
end
def creator_is_musician
unless creator.musician?
@ -53,6 +58,21 @@ module JamRuby
end
end
# returns an array of client_id's that are in this session
# if as_musician is nil, all connections in the session ,regardless if it's a musician or not or not
# you can also exclude a client_id from the returned set by setting exclude_client_id
def get_connection_ids(options = {})
as_musician = options[:as_musician]
exclude_client_id = options[:exclude_client_id]
where = { :music_session_id => self.id }
where[:as_musician] = as_musician unless as_musician.nil?
exclude = "client_id != '#{exclude_client_id}'"unless exclude_client_id.nil?
Connection.select(:client_id).where(where).where(exclude).map(&:client_id)
end
# This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true
# If so, then it's an OR condition. If both are false, you can get sessions with anyone.
def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil)

View File

@ -766,10 +766,10 @@ module JamRuby
@@mq_router.publish_to_user(user_id, msg)
end
def send_source_up_requested(music_session, host, port, mount, source_user, source_pass)
msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass)
def send_source_up_requested(music_session, host, port, mount, source_user, source_pass, bitrate)
msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass, bitrate)
@@mg_router.server_publish_to_session(music_session, msg)
@@mq_router.server_publish_to_session(music_session, msg)
end
def send_source_down_requested(music_session, mount)
@ -777,6 +777,18 @@ module JamRuby
@@mq_router.server_publish_to_session(music_session, msg)
end
def send_source_up(music_session)
msg = @@message_factory.source_up(music_session.id)
@@mq_router.server_publish_to_everyone_in_session(music_session, msg)
end
def send_source_down(music_session)
msg = @@message_factory.source_up(music_session.id)
@@mq_router.server_publish_to_everyone_in_session(music_session, msg)
end
end
end
end

View File

@ -74,8 +74,9 @@ module JamRuby
PARAM_MUSICIAN = :srch_m
PARAM_BAND = :srch_b
PARAM_FEED = :srch_f
B_PER_PAGE = M_PER_PAGE = 10
F_PER_PAGE = B_PER_PAGE = M_PER_PAGE = 10
M_MILES_DEFAULT = 500
B_MILES_DEFAULT = 0
@ -87,6 +88,18 @@ module JamRuby
DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [['Any', 0], [1000.to_s, 1000], [500.to_s, 500], [250.to_s, 250], [100.to_s, 100], [50.to_s, 50], [25.to_s, 25]]
F_SORT_RECENT = ['Most Recent', :recent]
F_SORT_OLDEST = ['Ending Soonest', :ending_soon]
F_SORT_LENGTH = ['Session Length', :session_length]
F_SORT_OPTS = [F_SORT_RECENT, F_SORT_LENGTH, F_SORT_OLDEST]
SHOW_BOTH = ['Both', :both]
SHOW_SESSIONS = ['Sessions', :sessions]
SHOW_RECORDINGS = ['Recordings', :recordings]
SHOW_OPTS = [SHOW_BOTH, SHOW_SESSIONS, SHOW_RECORDINGS]
DATE_OPTS = [['Today', 0], ['This week', 7], ['Past 2 weeks', 14], ['This month', 30], ['Past year', 365], ['All', -1]]
def self.order_param(params, keys=M_ORDERING_KEYS)
ordering = params[:orderby]
ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering }

View File

@ -18,6 +18,8 @@ module JamRuby
# updating_password corresponds to a lost_password
attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field
belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id'
# authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"

View File

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

View File

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

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]
def self.perform
@@log.debug("waking up")
IcecastConfigWriter.queue_jobs_needing_retry
@@log.debug("done")
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
name "/" + Faker::Lorem.characters(10)
source_username Faker::Lorem.characters(10)
source_pass Faker::Lorem.characters(10)
source_pass Faker::Lorem.characters(10)
max_listeners 100
max_listener_duration 3600
fallback_mount Faker::Lorem.characters(10)
@ -207,10 +207,21 @@ FactoryGirl.define do
stream_url Faker::Lorem.characters(10)
genre Faker::Lorem.characters(10)
hidden 0
association :server, factory: :icecast_server_with_overrides
factory :icecast_mount_with_auth do
association :authentication, :factory => :icecast_user_authentication
factory :iceast_mount_with_template do
association :mount_template, :factory => :icecast_mount_template
factory :iceast_mount_with_music_session do
association :music_session, :factory => :music_session
end
end
end
end
factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do
@ -227,7 +238,7 @@ FactoryGirl.define do
factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do
authentication_type 'url'
unused_username Faker::Lorem.characters(10)
unused_pass Faker::Lorem.characters(10)
unused_pass Faker::Lorem.characters(10)
mount_add Faker::Lorem.characters(10)
mount_remove Faker::Lorem.characters(10)
listener_add Faker::Lorem.characters(10)
@ -242,6 +253,7 @@ FactoryGirl.define do
factory :icecast_server_minimal do
association :template, :factory => :icecast_template_minimal
association :mount_template, :factory => :icecast_mount_template
factory :icecast_server_with_overrides do
association :limit, :factory => :icecast_limit
@ -274,4 +286,23 @@ FactoryGirl.define do
end
end
end
factory :icecast_mount_template, :class => JamRuby::IcecastMountTemplate do
sequence(:name) { |n| "name-#{n}"}
source_username Faker::Lorem.characters(10)
source_pass Faker::Lorem.characters(10)
max_listeners 100
max_listener_duration 3600
fallback_mount Faker::Lorem.characters(10)
fallback_override 1
fallback_when_full 1
is_public -1
stream_name Faker::Lorem.characters(10)
stream_description Faker::Lorem.characters(10)
stream_url Faker::Lorem.characters(10)
genre Faker::Lorem.characters(10)
hidden 0
association :authentication, :factory => :icecast_user_authentication
end
end

View File

@ -50,10 +50,22 @@ describe IcecastAdminAuthentication do
server.config_changed.should == 1
end
it "success when deleted via template" do
server.template.admin_auth.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do
server.admin_auth.save!
server.reload
server.config_changed.should == 1
end
it "success when deleted via server" do
server.admin_auth.destroy
server.reload
server.config_changed.should == 1
end
end
end

View File

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

View File

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

View File

@ -27,10 +27,22 @@ describe IcecastListenSocket do
server.config_changed.should == 1
end
it "delete via template" do
server.template.listen_sockets.first.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do
server.listen_sockets.first.save!
server.reload
server.config_changed.should == 1
end
it "delete via server" do
server.listen_sockets.first.destroy
server.reload
server.config_changed.should == 1
end
end
end

View File

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

View File

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

View File

@ -10,10 +10,6 @@ describe IcecastMount do
mount = IcecastMount.new
mount.save.should be_false
mount.errors[:name].should == ["can't be blank", "must start with /"]
mount.errors[:stream_name].should == ["can't be blank"]
mount.errors[:stream_description].should == ["can't be blank"]
mount.errors[:stream_url].should == ["can't be blank"]
mount.errors[:genre].should == ["can't be blank"]
end
@ -35,6 +31,7 @@ describe IcecastMount do
mount.max_listeners = 1000
mount.max_listener_duration = 3600
mount.authentication = FactoryGirl.create(:icecast_user_authentication)
mount.server = FactoryGirl.create(:icecast_server_with_overrides)
mount.save!
@ -70,11 +67,39 @@ describe IcecastMount do
xml.css('mount authentication').length.should == 1 # no reason to test futher; it's tested in that model
end
describe "override xml over mount template" do
let(:mount) {FactoryGirl.create(:iceast_mount_with_template)}
it "should allow override by mount" do
mount.dumpXml(builder)
output.rewind
xml = Nokogiri::XML(output)
xml.css('mount mount-name').text.should == mount.name
xml.css('mount username').text.should == mount.source_username
xml.css('mount bitrate').text.should == mount.bitrate.to_s
xml.css('mount type').text.should == mount.mount_template.mime_type
xml.css('mount stream-url').text.should == mount.stream_url
# now see the stream_url, and bitrate, go back to the template's value because we set it to nil
mount.bitrate = nil
mount.stream_url = nil
mount.save!
output = StringIO.new
builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1)
mount.dumpXml(builder)
output.rewind
xml = Nokogiri::XML(output)
xml.css('mount bitrate').text.should == mount.mount_template.bitrate.to_s
xml.css('mount stream-url').text.should == mount.mount_template.stream_url
end
end
describe "poke configs" do
let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) }
before(:each) do
server.mounts << FactoryGirl.create(:icecast_mount)
server.mounts << FactoryGirl.create(:icecast_mount, server: server)
server.save!
server.config_updated
server.reload
@ -86,6 +111,12 @@ describe IcecastMount do
server.reload
server.config_changed.should == 1
end
it "success when deleted" do
server.mounts.first.destroy
server.reload
server.config_changed.should == 1
end
end
describe "icecast server callbacks" do
@ -93,4 +124,125 @@ describe IcecastMount do
icecast_mount.source_up
end
end
describe "listener/source" do
let(:mount) {FactoryGirl.create(:iceast_mount_with_template)}
describe "listeners" do
it "listener_add" do
mount.listener_add
mount.listeners.should == 1
end
it "listener_remove when at 0" do
mount.listener_remove
mount.listeners.should == 0
end
it "listener_remove" do
mount.listener_add
mount.listener_remove
mount.listeners.should == 0
end
end
describe "sources" do
it "source_up" do
mount.source_up
mount.sourced.should == true
end
end
describe "sources" do
it "source_down" do
mount.source_up
mount.source_down
mount.sourced.should == false
end
end
end
describe "build_session_mount" do
let(:server1) {FactoryGirl.create(:icecast_server_minimal)}
let(:server2) {FactoryGirl.create(:icecast_server_with_overrides)}
let(:server3) {FactoryGirl.create(:icecast_server_with_overrides)}
let(:hidden_music_session) { FactoryGirl.create(:music_session, :fan_access => false)}
let(:public_music_session) { FactoryGirl.create(:music_session, :fan_access => true)}
let(:public_music_session2) { FactoryGirl.create(:music_session, :fan_access => true)}
let(:public_music_session3) { FactoryGirl.create(:music_session, :fan_access => true)}
before(:each) do
end
it "no fan access means no mount" do
mount = IcecastMount.build_session_mount(hidden_music_session)
mount.should be_nil
end
it "with no servers" do
IcecastServer.count.should == 0
mount = IcecastMount.build_session_mount(public_music_session)
mount.should be_nil
end
it "with a server that has a mount template" do
server1.mount_template.should_not be_nil
mount = IcecastMount.build_session_mount(public_music_session)
mount.should_not be_nil
mount.save!
end
it "with a server that already has an associated mount" do
server1.mount_template.should_not be_nil
mount = IcecastMount.build_session_mount(public_music_session)
mount.save!
mount = IcecastMount.build_session_mount(public_music_session2)
mount.save!
server1.reload
server1.mounts.length.should == 2
end
it "picks a second server once the 1st has been chosen" do
server1.touch
mount = IcecastMount.build_session_mount(public_music_session)
mount.listeners = 1 # affect the weight
mount.save!
server2.touch
mount = IcecastMount.build_session_mount(public_music_session2)
mount.save!
server1.reload
server1.mounts.length.should == 1
server2.reload
server2.mounts.length.should == 1
end
it "picks the 1st server again once the 2nd has higher weight" do
server1.touch
mount = IcecastMount.build_session_mount(public_music_session)
mount.listeners = 1 # affect the weight
mount.save!
server2.touch
mount = IcecastMount.build_session_mount(public_music_session2)
mount.sourced = 1
mount.save!
mount = IcecastMount.build_session_mount(public_music_session3)
mount.listeners = 1
mount.save!
server1.reload
server1.mounts.length.should == 2
server2.reload
server2.mounts.length.should == 1
end
end
end

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
end
it "delete via template" do
server.template.path.destroy
server.reload
server.config_changed.should == 1
end
it "success via server" do
server.path.save!
server.reload
server.config_changed.should == 1
end
it "delete via server" do
server.path.destroy
server.reload
server.config_changed.should == 1
end
end
end

View File

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

View File

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

View File

@ -30,4 +30,34 @@ describe IcecastServer do
xml.css('icecast security').length.should == 1
xml.css('icecast listen-socket').length.should == 1
end
it "xml overrides" do
server = FactoryGirl.create(:icecast_server_minimal)
server.save!
server.reload
server.dumpXml(output)
output.rewind
xml = Nokogiri::XML(output)
xml.css('icecast location').text.should == server.template.location
xml.css('icecast fileserve').text.should == server.template.fileserve.to_s
xml.css('icecast limits').length.should == 1
xml.css('icecast limits queue-size').text.should == server.template.limit.queue_size.to_s
server.location = "override"
server.fileserve = 1
server.limit = FactoryGirl.create(:icecast_limit, :queue_size => 777)
server.save!
output = StringIO.new
builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1)
server.dumpXml(builder)
output.rewind
xml = Nokogiri::XML(output)
xml.css('icecast location').text.should == server.location
xml.css('icecast fileserve').text.should == server.fileserve.to_s
xml.css('icecast limits').length.should == 1
xml.css('icecast limits queue-size').text.should == server.limit.queue_size.to_s
end
end

View File

@ -1,10 +1,20 @@
require 'spec_helper'
describe IcecastListenSocket do
describe IcecastTemplate do
let(:template) { template = FactoryGirl.create(:icecast_template_minimal) }
it "save" do
template.errors.any?.should be_false
end
describe "poke configs" do
let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) }
it "success via template" do
server.template.save!
server.reload
server.config_changed.should == 1
end
end
end

View File

@ -480,5 +480,36 @@ describe MusicSession do
end
end
end
describe "get_connection_ids" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@user2 = FactoryGirl.create(:user)
@music_session = FactoryGirl.create(:music_session, :creator => @user1, :musician_access => true)
@connection1 = FactoryGirl.create(:connection, :user => @user1, :music_session => @music_session, :as_musician => true)
@connection2 = FactoryGirl.create(:connection, :user => @user2, :music_session => @music_session, :as_musician => false)
end
it "get all connections" do
@music_session.get_connection_ids().should == [@connection1.client_id, @connection2.client_id]
end
it "exclude non-musicians" do
@music_session.get_connection_ids(as_musician: true).should == [@connection1.client_id]
end
it "exclude musicians" do
@music_session.get_connection_ids(as_musician: false).should == [@connection2.client_id]
end
it "exclude particular client" do
@music_session.get_connection_ids(exclude_client_id: @connection1.client_id).should == [@connection2.client_id]
end
it "exclude particular client and exclude non-musicians" do
@music_session.get_connection_ids(exclude_client_id: @connection2.client_id, as_musician: true).should == [@connection1.client_id]
end
end
end

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.before(:suite) do
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] }
DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] })
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] }
DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] })
end
config.before(:each) do

View File

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

View File

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

View File

@ -48,6 +48,12 @@
BAND_INVITATION : "BAND_INVITATION",
BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED",
// broadcast notifications
SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED",
SOURCE_DOWN_REQUESTED : "SOURCE_DOWN_REQUESTED",
SOURCE_UP : "SOURCE_UP",
SOURCE_DOWN : "SOURCE_DOWN",
TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE",
PING_REQUEST : "PING_REQUEST",
PING_ACK : "PING_ACK",

View File

@ -7,81 +7,19 @@
var logger = context.JK.logger;
var rest = context.JK.Rest();
var realtimeMessaging = context.JK.JamServer;
var friendSelectorDialog = null;
var invitationDialog = null;
var autoComplete = null;
var userNames = [];
var userIds = [];
var userPhotoUrls = [];
var inviteMusiciansUtil = null;
var MAX_GENRES = 1;
var selectedFriendIds = {};
var sessionSettings = {};
function beforeShow(data) {
userNames = [];
userIds = [];
userPhotoUrls = [];
inviteMusiciansUtil.clearSelections();
context.JK.GenreSelectorHelper.render('#create-session-genre');
resetForm();
}
function afterShow(data) {
friendSelectorDialog.setCallback(friendSelectorCallback);
var friends = rest.getFriends({ id: context.JK.currentUserId })
.done(function(friends) {
$.each(friends, function() {
userNames.push(this.name);
userIds.push(this.id);
userPhotoUrls.push(this.photo_url);
});
var autoCompleteOptions = {
lookup: { suggestions: userNames, data: userIds },
onSelect: addInvitation
};
$('#friend-input').attr("placeholder", "Type a friend\'s name").prop('disabled', false);
if (!autoComplete) {
autoComplete = $('#friend-input').autocomplete(autoCompleteOptions);
}
else {
autoComplete.setOptions(autoCompleteOptions);
}
$(".autocomplete").width("150px");
})
.fail(function() {
$('#friend-input').attr("placeholder", "Unable to lookup friends");
app.ajaxError(arguments);
});
}
function friendSelectorCallback(newSelections) {
var keys = Object.keys(newSelections);
for (var i=0; i < keys.length; i++) {
addInvitation(newSelections[keys[i]].userName, newSelections[keys[i]].userId);
}
}
function addInvitation(value, data) {
if ($('#selected-friends div[user-id=' + data + ']').length === 0) {
var template = $('#template-added-invitation').html();
var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value});
$('#selected-friends').append(invitationHtml);
$('#friend-input').select();
selectedFriendIds[data] = true;
}
else {
$('#friend-input').select();
context.alert('Invitation already exists for this musician.');
}
}
function removeInvitation(evt) {
delete selectedFriendIds[$(evt.currentTarget).parent().attr('user-id')];
$(evt.currentTarget).closest('.invitation').remove();
inviteMusiciansUtil.loadFriends();
}
function resetForm() {
@ -226,7 +164,7 @@
data: jsonData,
success: function(response) {
var newSessionId = response.id;
var invitationCount = createInvitations(newSessionId, function() {
var invitationCount = inviteMusiciansUtil.createInvitations(newSessionId, function() {
context.location = '#/session/' + newSessionId;
});
// Re-loading the session settings will cause the form to reset with the right stuff in it.
@ -248,49 +186,12 @@
return false;
}
function createInvitations(sessionId, onComplete) {
var callCount = 0;
var totalInvitations = 0;
$('#selected-friends .invitation').each(function(index, invitation) {
callCount++;
totalInvitations++;
var invite_id = $(invitation).attr('user-id');
var invite = {
music_session: sessionId,
receiver: invite_id
};
$.ajax({
type: "POST",
url: "/api/invitations",
data: invite
}).done(function(response) {
callCount--;
}).fail(app.ajaxError);
});
// TODO - this is the second time I've used this pattern.
// refactor to make a common utility for this.
function checker() {
if (callCount === 0) {
onComplete();
} else {
context.setTimeout(checker, 10);
}
}
checker();
return totalInvitations;
}
function events() {
$('#create-session-form').on('submit', submitForm);
$('#btn-create-session').on("click", submitForm);
$('#selected-friends').on("click", ".invitation a", removeInvitation);
$('#musician-access').change(toggleMusicianAccess);
$('#fan-access').change(toggleFanAccess);
$('#btn-choose-friends').click(function() {
friendSelectorDialog.showDialog(selectedFriendIds);
});
$('div[layout-id="createSession"] .btn-email-invitation').click(function() {
invitationDialog.showEmailDialog();
});
@ -373,38 +274,10 @@
});
}
function searchFriends(query) {
if (query.length < 2) {
$('#friend-search-results').empty();
return;
}
var url = "/api/search?query=" + query + "&userId=" + context.JK.currentUserId;
$.ajax({
type: "GET",
url: url,
success: friendSearchComplete
});
}
function friendSearchComplete(response) {
// reset search results each time
$('#friend-search-results').empty();
// loop through each
$.each(response.friends, function() {
// only show friends who are musicians
if (this.musician === true) {
var template = $('#template-friend-search-results').html();
var searchResultHtml = context.JK.fillTemplate(template, {userId: this.id, name: this.first_name + ' ' + this.last_name});
$('#friend-search-results').append(searchResultHtml);
$('#friend-search-results').attr('style', 'display:block');
}
});
}
function initialize(invitationDialogInstance, friendSelectorDialogInstance) {
friendSelectorDialog = friendSelectorDialogInstance;
function initialize(invitationDialogInstance, inviteMusiciansUtilInstance) {
invitationDialog = invitationDialogInstance;
inviteMusiciansUtil = inviteMusiciansUtilInstance;
inviteMusiciansUtil.inviteSessionCreate('#create-session-invite-musicians');
events();
loadBands();
loadSessionSettings();
@ -419,8 +292,6 @@
this.submitForm = submitForm;
this.validateForm = validateForm;
this.loadBands = loadBands;
this.searchFriends = searchFriends;
this.addInvitation = addInvitation;
return this;
};

View File

@ -571,6 +571,14 @@
function CloseRecording() {}
function OnDownloadAvailable() {}
function SessionLiveBroadcastStart(host, port, mount, sourceUser, sourcePass, preferredClientId, bitrate)
{
logger.debug("SessionLiveBroadcastStart requested");
}
function SessionLiveBroadcastStop() {
logger.debug("SessionLiveBroadcastStop requested");
}
// Javascript Bridge seems to camel-case
// Set the instance functions:
@ -703,6 +711,10 @@
this.CloseRecording = CloseRecording;
this.OnDownloadAvailable = OnDownloadAvailable;
// Broadcasting
this.SessionLiveBroadcastStart = SessionLiveBroadcastStart;
this.SessionLiveBroadcastStop = SessionLiveBroadcastStop;
// fake calls; not a part of the actual jam client
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
this.SetFakeRecordingImpl = SetFakeRecordingImpl;

View File

@ -89,6 +89,10 @@
for (ii=0, len=musicians.length; ii < len; ii++) {
mm = musicians[ii];
if (context.JK.currentUserId === mm.id) {
// VRFS-294.3 (David) => skip if current user is musician
continue;
}
instr_logos = '';
for (var jj=0, ilen=mm['instruments'].length; jj<ilen; jj++) {
if (mm['instruments'][jj].instrument_id in instrument_logo_map) {
@ -110,8 +114,10 @@
}
var actionVals = {
profile_url: "/#/profile/" + mm.id,
button_friend: mm['is_friend'] ? '' : 'button-orange',
button_follow: mm['is_following'] ? '' : 'button-orange',
friend_class: 'button-' + (mm['is_friend'] ? 'grey' : 'orange'),
friend_caption: (mm.is_friend ? 'DIS':'')+'CONNECT',
follow_class: 'button-' + (mm['is_following'] ? 'grey' : 'orange'),
follow_caption: (mm.is_following ? 'UN':'')+'FOLLOW',
button_message: 'button-orange'
};
var musician_actions = context.JK.fillTemplate(aTemplate, actionVals);
@ -122,7 +128,7 @@
musician_name: mm.name,
musician_location: mm.city + ', ' + mm.state,
instruments: instr_logos,
biography: mm['biography'],
biography: mm['biography'] || 'Lorum Ipsum Nulla facilisi. In vel sem. Morbi id urna in diam dignissim feugiat. Proin molestie tortor eu velit. Aliquam erat volutpat. Nullam ultrices, diam tempus vulputate egestas, eros pede varius leo, sed imperdiet lectus est ornare odio.',
follow_count: mm['follow_count'],
friend_count: mm['friend_count'],
recording_count: mm['recording_count'],

View File

@ -0,0 +1,207 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.InviteMusiciansUtil = function(app) {
var logger = context.JK.logger;
var userNames = [];
var userIds = [];
var userPhotoUrls = [];
var friendSelectorDialog = null;
var invitedFriends = [];
var existingInvites = [];
var autoComplete = null;
var rest = context.JK.Rest();
var inviteAction = 'create'; // create/update
var updateSessionID = null;
this.inviteSessionCreate = function(elemSelector) {
inviteAction = 'create';
_appendFriendSelector($(elemSelector));
};
this.inviteSessionUpdate = function(elemSelector, sessionId) {
this.clearSelections();
updateSessionID = sessionId;
friendSelectorDialog.setCallback(friendSelectorCallback);
inviteAction = 'update';
if (0 == $(elemSelector + ' .friendbox').length) {
_appendFriendSelector($(elemSelector));
$('#btn-save-invites').click(function() {
createInvitations(updateSessionID);
});
}
$.ajax({
url: "/api/invitations",
data: { session_id: sessionId, sender: context.JK.currentUserId }
}).done(function(response) {
response.map(function(item) {
var dd = item['receiver'];
existingInvites.push(dd.id);
addInvitation(dd.name, dd.id);
});
}).fail(app.ajaxError);
}
this.clearSelections = function() {
userNames = [];
userIds = [];
userPhotoUrls = [];
invitedFriends = [];
existingInvites = [];
updateSessionID = null;
$('.selected-friends').empty();
};
this.loadFriends = function() {
friendSelectorDialog.setCallback(friendSelectorCallback);
var friends = rest.getFriends({ id: context.JK.currentUserId })
.done(function(friends) {
$.each(friends, function() {
userNames.push(this.name);
userIds.push(this.id);
userPhotoUrls.push(this.photo_url);
});
var autoCompleteOptions = {
lookup: { suggestions: userNames, data: userIds },
onSelect: addInvitation
};
$('#friend-input').attr("placeholder", "Type a friend\'s name").prop('disabled', false);
if (!autoComplete) {
autoComplete = $('#friend-input').autocomplete(autoCompleteOptions);
}
else {
autoComplete.setOptions(autoCompleteOptions);
}
$(".autocomplete").width("150px");
})
.fail(function() {
$('#friend-input').attr("placeholder", "Unable to lookup friends");
app.ajaxError(arguments);
});
}
function friendSelectorCallback(newSelections) {
var keys = Object.keys(newSelections);
for (var i=0; i < keys.length; i++) {
var dd = newSelections[keys[i]];
addInvitation(dd.userName, dd.userId);
}
}
function _inviteExists(userID) {
return 0 <= existingInvites.indexOf(userID);
}
function addInvitation(value, data) {
if (0 > invitedFriends.indexOf(data)) {
var template = $('#template-added-invitation').html();
var imgStyle = _inviteExists(data) ? 'display:none' : '';
var invitationHtml = context.JK.fillTemplate(template,
{userId: data,
userName: value,
imageStyle: imgStyle});
$('.selected-friends').append(invitationHtml);
$('#friend-input').select();
invitedFriends.push(data);
} else {
$('#friend-input').select();
context.alert('Invitation already exists for this musician.');
}
}
function removeInvitation(evt) {
var idx = invitedFriends.indexOf($(evt.currentTarget).parent().attr('user-id'));
if (0 <= idx) invitedFriends.splice(idx, 1);
$(evt.currentTarget).closest('.invitation').remove();
}
function createInvitations(sessionId, onComplete) {
var callCount = 0;
var totalInvitations = invitedFriends.length - existingInvites.length;
invitedFriends.map(function(invite_id) {
if (!_inviteExists(invite_id)) {
callCount++;
var invite = {
music_session: sessionId,
receiver: invite_id
};
$.ajax({
type: "POST",
url: "/api/invitations",
data: invite
}).done(function(response) {
callCount--;
}).fail(app.ajaxError);
}
});
// TODO - this is the second time I've used this pattern.
// refactor to make a common utility for this.
function checker() {
callCount === 0 ? onComplete() : context.setTimeout(checker, 10);
}
if (onComplete) checker();
return totalInvitations;
}
this.createInvitations = createInvitations;
function searchFriends(query) {
if (query.length < 2) {
$('#friend-search-results').empty();
return;
}
var url = "/api/search?query=" + query + "&userId=" + context.JK.currentUserId;
$.ajax({
type: "GET",
url: url,
success: friendSearchComplete
});
}
function friendSearchComplete(response) {
// reset search results each time
$('#friend-search-results').empty();
// loop through each
$.each(response.friends, function() {
// only show friends who are musicians
if (this.musician === true) {
var template = $('#template-friend-search-results').html();
var searchResultHtml = context.JK.fillTemplate(template, {userId: this.id, name: this.first_name + ' ' + this.last_name});
$('#friend-search-results').append(searchResultHtml);
$('#friend-search-results').attr('style', 'display:block');
}
});
}
function _friendSelectorHTML() {
return context.JK.fillTemplate($('#template-session-invite-musicians').html(),
{choose_friends_id: 'btn-choose-friends-'+inviteAction,
selected_friends_id: 'selected-friends-'+inviteAction});
}
function _appendFriendSelector(elemSelector) {
elemSelector.append(_friendSelectorHTML());
$('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation);
$('#btn-choose-friends-'+inviteAction).click(function(){
var obj = {};
invitedFriends.map(function(uid) { obj[uid] = true; });
friendSelectorDialog.showDialog(obj);
});
};
this.initialize = function(friendSelectorDialogInstance) {
friendSelectorDialog = friendSelectorDialogInstance;
};
return this;
};
})(window,jQuery);

View File

@ -14,6 +14,8 @@
var addNewGearDialog;
var localRecordingsDialog = null;
var recordingFinishedDialog = null;
var friendSelectorDialog = null;
var inviteMusiciansUtil = null;
var screenActive = false;
var currentMixerRangeMin = null;
var currentMixerRangeMax = null;
@ -1302,12 +1304,17 @@
}
}
function inviteMusicians() {
inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId);
}
function events() {
$('#session-resync').on('click', sessionResync);
$('#session-contents').on("click", '[action="delete"]', deleteSession);
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
$('#recording-start-stop').on('click', startStopRecording);
$('#open-a-recording').on('click', openRecording);
$('#session-invite-musicians').on('click', inviteMusicians);
$('#track-settings').click(function() {
configureTrackDialog.showVoiceChatPanel(true);
configureTrackDialog.showMusicAudioPanel(true);
@ -1319,9 +1326,10 @@
.on('change-position', onChangePlayPosition);
}
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance) {
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, inviteMusiciansUtilInstance) {
localRecordingsDialog = localRecordingsDialogInstance;
recordingFinishedDialog = recordingFinishedDialogInstance;
inviteMusiciansUtil = inviteMusiciansUtilInstance;
context.jamClient.SetVURefreshRate(150);
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
events();

View File

@ -233,10 +233,95 @@
acceptBandInvitation({ "band_invitation_id": payload.band_invitation_id, "band_id": payload.band_id, "notification_id": payload.notification_id });
});
}
else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.SOURCE_UP_REQUESTED) {
var current_session_id = context.JK.CurrentSessionModel.id();
if (!current_session_id) {
// we are not in a session
var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session")
}
else {
// this means we aren't in a session, and, what's worse,
// the last session we were in does not match the specified music_session id
throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified";
}
}
else {
// we are in a session
if(current_session_id == payload.music_session) {
context.jamClient.SessionLiveBroadcastStart(payload.host, payload.port, payload.mount,
payload.source_user, payload.source_pass,
'', payload.bitrate)
}
else {
var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one")
}
else {
// this means we aren't in a session, and, what's worse,
// the last session we were in does not match the specified music_session id
throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified";
}
}
}
}
else if (type === context.JK.MessageType.SOURCE_DOWN_REQUESTED) {
var current_session_id = context.JK.CurrentSessionModel.id();
if (!current_session_id) {
// we are not in a session
var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session")
}
else {
// this means we aren't in a session, and, what's worse,
// the last session we were in does not match the specified music_session id
throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified";
}
}
else {
// we are in a session
if(current_session_id == payload.music_session) {
context.jamClient.SessionLiveBroadcastStop();
}
else {
var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one")
}
else {
// this means we aren't in a session, and, what's worse,
// the last session we were in does not match the specified music_session id
throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified";
}
}
}
}
else if (type === context.JK.MessageType.SOURCE_UP) {
log.debug("session %o is now being broadcasted", payload.music_session);
app.notify({
"title": "Now Broadcasting",
"text": "This session is now being broadcasted."
});
}
else if (type === context.JK.MessageType.SOURCE_DOWN) {
log.debug("session %o is no longer being broadcasted", payload.music_session);
app.notify({
"title": "No Longer Broadcasting",
"text": "This session is no longer being broadcasted."
});
}
}
function deleteNotificationHandler(evt) {

View File

@ -155,6 +155,9 @@
margin-bottom: 10px;
> a.smallbutton {
margin: 2px;
&.button-grey {
display:none; // @FIXME VRFS-930 / VRFS-931 per comment from David - don't show.
}
}
}
@ -204,7 +207,6 @@
.friendbox {
padding:5px;
width:100%;
height:60px;
}

View File

@ -11,7 +11,7 @@
}
}
#btn-choose-friends {
.btn-choose-friends {
margin:0;
}
#create-session-genre select, #create-session-band select {

View File

@ -1,5 +1,11 @@
@import "client/common.css.scss";
.profile-head {
}
.profile-body {
}
.profile-header {
padding:10px 20px;
// height:120px;

View File

@ -716,3 +716,6 @@ table.vu td {
}
#update-session-invite-musicians {
margin: 10px;
}

View File

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

View File

@ -14,8 +14,11 @@ class ApiInvitationsController < ApiController
if current_user.id != sender_id
raise PermissionError, "You can only ask for your own sent invitations"
end
@invitations = Invitation.where(:sender_id => current_user.id)
if session_id = params[:session_id]
@invitations = Invitation.where(:sender_id => sender_id, :music_session_id => session_id)
else
@invitations = Invitation.where(:sender_id => current_user.id)
end
elsif !receiver_id.nil?
if current_user.id != receiver_id
raise PermissionError, "You can only ask for your own received invitations"
@ -34,25 +37,33 @@ class ApiInvitationsController < ApiController
sender = current_user
join_request = JoinRequest.find(params[:join_request]) unless params[:join_request].nil?
@invitation = Invitation.new
@invitation.music_session = music_session
@invitation.sender = sender
@invitation.receiver = receiver
@invitation.join_request = join_request
@invitation.save
unless @invitation.errors.any?
User.save_session_settings(current_user, music_session)
# send notification
Notification.send_session_invitation(receiver, current_user, music_session.id)
@invitation = Invitation.limit(1)
.where(:receiver_id => params[:receiver],
:sender_id => current_user.id,
:music_session_id => params[:music_session])
.first
if @invitation
respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation)
else
# we have to do this because api_invitation_detail_url will fail with a bad @invitation
response.status = :unprocessable_entity
respond_with @invitation
@invitation = Invitation.new
@invitation.music_session = music_session
@invitation.sender = sender
@invitation.receiver = receiver
@invitation.join_request = join_request
@invitation.save
unless @invitation.errors.any?
User.save_session_settings(current_user, music_session)
# send notification
Notification.send_session_invitation(receiver, current_user, music_session.id)
respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation)
else
# we have to do this because api_invitation_detail_url will fail with a bad @invitation
response.status = :unprocessable_entity
respond_with @invitation
end
end
end

View File

@ -43,6 +43,10 @@ if @search.musicians_filter_search?
@search.is_follower?(musician)
end
node :biography do |musician|
musician.biography.nil? ? "" : musician.biography
end
child :musician_instruments => :instruments do
attributes :instrument_id, :description, :proficiency_level, :priority
end
@ -73,6 +77,10 @@ if @search.bands_filter_search?
@search.is_follower?(band)
end
node :biography do |band|
band.biography.nil? ? "" : band.biography
end
child :genres => :genres do
attributes :genre_id, :description
end
@ -81,7 +89,7 @@ if @search.bands_filter_search?
node :user_id do |uu| uu.id end
node :photo_url do |uu| uu.photo_url end
node :name do |uu| uu.name end
node :instruments do |uu| uu.instruments.map(&:id).join(',') end
node :instruments do |uu| uu.instruments.map(&:id).join(',') end
end
node :follow_count do |band| @search.follow_count(band) end

View File

@ -26,4 +26,8 @@ node :genres do |band|
attributes :id, :description
end
end
end
end
node :biography do |band|
band.biography.nil? ? "" : band.biography
end

View File

@ -22,7 +22,7 @@
<!-- Session Row Template -->
<script type="text/template" id="template-find-band-row">
<div class="profile-band-list-result band-list-result">
<div style="float:left">
<div class="left">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>

View File

@ -84,22 +84,7 @@
<h2>invite musicians</h2>
<br />
<div>
<div class="right" layout-link="select-friends">
<a href="#" id="btn-choose-friends" class="button-grey">CHOOSE FRIENDS</a>
</div>
<div style="margin-right:140px;">
Start typing friends' names or:
</div>
<div class="clearall"></div>
</div>
<br />
<!-- friend invitation box -->
<div class="friendbox">
<div id="selected-friends"></div>
<input id="friend-input" type="text" placeholder="Looking up friends..." />
</div>
<div id="create-session-invite-musicians"></div>
<div class="mt35 mb15">
Invite friends and contacts to join you on JamKazam from:
@ -171,13 +156,6 @@
</div>
</div>
<!-- Added Invitation Template -->
<script type="text/template" id="template-added-invitation">
<div user-id="{userId}" class="invitation">{userName}
<a><%= image_tag "shared/icon_delete_sm.png", :size => "13x13" %></a>
</div>
</script>
<!-- Band option template -->
<script type="text/template" id="template-band-option">
<option value="{value}">{label}</option>

View File

@ -1,18 +1,49 @@
<!-- Feed Screen -->
<div layout="screen" layout-id="feed" class="screen secondary">
<div class="content">
<div class="content-head">
<div class="content-icon">
<%= image_tag "content/icon_feed.png", {:height => 19, :width => 19} %>
</div>
<h1>feed</h1>
<%= content_tag(:div, :layout => 'screen', 'layout-id' => 'feed', :class => "screen secondary") do -%>
<%= content_tag(:div, :class => :content) do -%>
<%= content_tag(:div, :class => 'content-head') do -%>
<%= content_tag(:div, image_tag("content/icon_feed.png", {:height => 19, :width => 19}), :class => 'content-icon') %>
<%= content_tag(:h1, 'feed') %>
<%= render "screen_navigation" %>
</div>
<div class="content-body">
<div class="content-body-scroller">
<p>This feature not yet implemented</p>
</div>
</div>
<% end -%>
<%= content_tag(:div, :class => 'content-body') do -%>
<%= form_tag('', {:id => 'find-session-form', :class => 'inner-content'}) do -%>
<%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED}) %>
<%= content_tag(:div, :class => 'filter-body') do %>
<%= content_tag(:div, :class => 'content-body-scroller') do -%>
<p>This feature not yet implemented</p>
<%= content_tag(:div, content_tag(:div, '', :id => 'session-filter-results', :class => 'filter-results'), :class => 'content-wrapper') %>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<!-- Session Row Template -->
<script type="text/template" id="template-find-session-row">
<div class="profile-band-list-result band-list-result">
<div class="left">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>
<!-- name & location -->
<div style="width:220px;" class="result-name">{band_name}<br />
<span class="result-location">{band_location}</span>
<br /><br />
<div id="result_sessions" class="nowrap">{genres}</div>
<br /><br />
{follow_count} <img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" />&nbsp;&nbsp;&nbsp;{recording_count} <img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" />&nbsp;&nbsp;&nbsp;{session_count} <img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" /><br /><br />
</div>
</div>
</div>
<div class="left ml20 f11 whitespace w35"><br />
{biography}<br />
<br />
<div data-band-id={band_id}>{band_action_template}</div>
</div>
<div class="left ml10 w25 band-players">
<table class="musicians" cellpadding="0" cellspacing="5">{band_player_template}</table>
</div>
<br clear="all" />
</div>
</script>

View File

@ -0,0 +1,38 @@
<!-- Session Update Invite Musicians Dialog -->
<div class="dialog invitemusicians-overlay" layout="dialog" layout-id="select-invites">
<div class="invitemusicians-inner" id="update-session-invite-musicians">
</div>
<br clear="all" />
<div class="left">
<a id="btn-cancel-invites" layout-action="close" class="button-grey">CANCEL</a>&nbsp;
</div>
<div class="right">
<a id="btn-save-invites" layout-action="close" class="button-orange">INVITE</a>
</div>
</div>
<!-- invite musician friend selector template -->
<script type="text/template" id="template-session-invite-musicians">
<div>
<div class="right" layout-link="select-friends">
<a href="#" class="btn-choose-friends button-grey" id="{choose_friends_id}">CHOOSE FRIENDS</a>
</div>
<div style="margin-right:140px;">
Start typing friends' names or:
</div>
<div class="clearall"></div>
</div>
<br />
<!-- friend invitation box -->
<div class="friendbox">
<div class="selected-friends" id="{selected_friends_id}"></div>
<!--<input id="friend-input" type="text" placeholder="Looking up friends..." />-->
</div>
</script>
<!-- Added Invitation Template -->
<script type="text/template" id="template-added-invitation">
<div user-id="{userId}" class="invitation">{userName}
<a><%= image_tag "shared/icon_delete_sm.png", :size => "13x13", :style => "{imageStyle}" %></a>
</div>
</script>

View File

@ -22,24 +22,35 @@
<!-- Session Row Template -->
<script type="text/template" id="template-find-musician-row">
<div class="profile-band-list-result musician-list-result">
<div class="left">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>
<!-- name & location -->
<div style="width:220px;" class="result-name">{musician_name}<br />
<span class="result-location">{musician_location}
<br /><br />
<div id="result_instruments" class="nowrap">{instruments}</div>
<br clear="all" /><br />
{friend_count} <img src="../assets/content/icon_friend.png" width="14" height="12" align="absmiddle" />&nbsp;&nbsp;&nbsp;{follow_count} <img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" />&nbsp;&nbsp;&nbsp;{recording_count} <img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" />&nbsp;&nbsp;&nbsp;{session_count} <img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" /></span><br /><br />
<div class="left" data-hint="container">
<div class="left">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>
</div>
</div>
<div class="left ml35 f11 whitespace w40"><br />
{biography}<br />
<br />
<div class="result-list-button-wrapper" data-musician-id={musician_id}>
{musician_action_template}
<div class="left">
<div data-hint="top-row">
<div class="left">
<!-- name & location -->
<div style="" class="result-name">{musician_name}</div>
<div class="result-location">{musician_location}</div>
<div id="result_instruments" class="nowrap">{instruments}</div>
</div>
<div class="left ml35 f11 whitespace w40">
<div class="biography">{biography}</div>
</div>
</div>
<div data-hint="button-row">
<div class="stats">
{friend_count} <img src="../assets/content/icon_friend.png" width="14" height="12" align="absmiddle" />&nbsp;&nbsp;&nbsp;
{follow_count} <img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" />&nbsp;&nbsp;&nbsp;
{recording_count} <img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" />&nbsp;&nbsp;&nbsp;
{session_count} <img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" />
</div>
<div class="result-list-button-wrapper" data-musician-id={musician_id}>
{musician_action_template}
</div>
</div>
</div>
</div>
<div class="left ml10 w20 musician-following">
@ -54,9 +65,9 @@
<script type="text/template" id="template-musician-action-btns">
<a href="{profile_url}" class="button-orange smallbutton">PROFILE</a>
<% if current_user.musician? %>
<a href="#" class="{button_friend} smallbutton search-m-friend">CONNECT</a>
<a href="#" class="{friend_class} smallbutton search-m-friend">{friend_caption}</a>
<% end %>
<a href="#" class="{button_follow} smallbutton search-m-follow">FOLLOW</a>
<a href="#" class="{follow_class} smallbutton search-m-follow">{follow_caption}</a>
<!--<a href="#" class="{button_message} smallbutton search-m-like">MESSAGE</a>-->
<div class="clearall"></div>
</script>

View File

@ -69,8 +69,8 @@
<!-- live tracks -->
<div class="session-livetracks">
<h2>live tracks</h2>
<div class="session-add">
<a>
<div class="session-add" layout-link="select-invites">
<a href="#" id="session-invite-musicians">
<%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} %>&nbsp;&nbsp;Invite Musicians
</a>
</div>
@ -178,3 +178,4 @@
<script type="text/template" id="template-genre-option">
<option value="{value}">{label}</option>
</script>

View File

@ -40,7 +40,7 @@
<input type="button" value="Get"/>
<textarea data-what="GetFTUE"></textarea>
</div>
<div style="float:left;"
<div style="float:left;">
<h2>SetFTUE</h2>
<form>
<label>true <input name="ftue" type="radio" value="true"/></label>

View File

@ -4,36 +4,61 @@
filter_label = :band
when Search::PARAM_MUSICIAN
filter_label = :musician
when Search::PARAM_FEED
filter_label = :feed
end %>
<%= content_tag(:div, :id => defined?(id) ? id : 'session-controls', :class => "#{filter_label}-filter filter-head") do %>
<%= content_tag(:div, :class => "filter-element wrapper") do -%>
<%= content_tag(:div, 'Filter By:', :class => 'filter-element desc') %>
<!-- order by filter -->
<%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %>
<% if :feed == filter_label %>
<!-- @begin sort filter -->
<%= content_tag(:div, 'Sort Feed by:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_order_by", options_for_select(Search::F_SORT_OPTS), {:class => "#{filter_label}-order-by"} ) %>
<!-- @end sort filter -->
<% else %>
<!-- @begin order by filter -->
<%= content_tag(:div, 'Filter By:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %>
<!-- @end order by filter -->
<% end %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element wrapper') do -%>
<% if :musician == filter_label %>
<!-- instrument filter -->
<!-- @begin instrument filter -->
<%= content_tag(:div, 'Instrument:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_instrument",
options_for_select([['Any', '']].concat(JamRuby::Instrument.all.collect { |ii| [ii.description, ii.id] }))) %>
<!-- @end instrument filter -->
<% elsif :band == filter_label %>
<!-- genre filter -->
<!-- @begin genre filter -->
<%= content_tag(:div, 'Genre:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_genre",
options_for_select([['Any', '']].concat(JamRuby::Genre.all.collect { |ii| [ii.description, ii.id] }))) %>
<!-- @end genre filter -->
<% elsif :feed == filter_label %>
<!-- @begin date filter -->
<%= content_tag(:div, 'Include Dates:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_date", options_for_select(Search::DATE_OPTS)) %>
<!-- @end date filter -->
<% end %>
<% end -%>
<!-- distance filter -->
<%= content_tag(:div, :class => 'filter-element wrapper') do -%>
<%= content_tag(:div, 'Within', :class => 'filter-element desc') %>
<%= content_tag(:div, :class => 'query-distance-params') do -%>
<% default_distance = :musician == filter_label ? Search::M_MILES_DEFAULT : Search::B_MILES_DEFAULT %>
<%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance)) %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element desc') do -%>
miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %>
<% end -%>
<% if :feed == filter_label %>
<!-- @begin show filter -->
<%= content_tag(:div, 'Show:', :class => 'filter-element desc') %>
<%= select_tag("#{filter_label}_show", options_for_select(Search::SHOW_OPTS)) %>
<!-- @end show filter -->
<% else %>
<!-- @begin distance filter -->
<%= content_tag(:div, 'Within', :class => 'filter-element desc') %>
<%= content_tag(:div, :class => 'query-distance-params') do -%>
<% default_distance = :musician == filter_label ? Search::M_MILES_DEFAULT : Search::B_MILES_DEFAULT %>
<%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance)) %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element desc') do -%>
miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %>
<% end -%>
<!-- @end distance filter -->
<% end %>
<% end -%>
<% end -%>
<!-- @end web_filter -->

View File

@ -35,6 +35,7 @@
<%= render "account_profile_avatar" %>
<%= render "account_audio_profile" %>
<%= render "invitationDialog" %>
<%= render "inviteMusicians" %>
<%= render "whatsNextDialog" %>
<%= render "recordingFinishedDialog" %>
<%= render "localRecordingsDialog" %>
@ -105,6 +106,9 @@
var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app);
friendSelectorDialog.initialize();
var inviteMusiciansUtil = new JK.InviteMusiciansUtil(JK.app);
inviteMusiciansUtil.initialize(friendSelectorDialog);
var userDropdown = new JK.UserDropdown(JK.app);
JK.UserDropdown = userDropdown;
userDropdown.initialize(invitationDialog);
@ -148,7 +152,7 @@
JK.Banner.initialize();
var createSessionScreen = new JK.CreateSessionScreen(JK.app);
createSessionScreen.initialize(invitationDialog, friendSelectorDialog);
createSessionScreen.initialize(invitationDialog, inviteMusiciansUtil);
var bandSetupScreen = new JK.BandSetupScreen(JK.app);
bandSetupScreen.initialize(invitationDialog, friendSelectorDialog);
@ -170,7 +174,7 @@
findBandScreen.initialize();
var sessionScreen = new JK.SessionScreen(JK.app);
sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog);
sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog, inviteMusiciansUtil);
var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen);
sessionSettingsDialog.initialize();

View File

@ -102,7 +102,7 @@
<input type="text" /> <br clear="all" /><br />
-->
<%= f.submit "CREATE ACCOUNT", class: "right button-orange" %><br/ style="clear:both;"><br/>
<%= f.submit "CREATE ACCOUNT", class: "right button-orange" %><br style="clear:both;"/><br/>
<a href="/auth/facebook" class="right"><img src="/fb-signup-button.png"></a>
</div>

View File

@ -171,12 +171,13 @@ include JamRuby
config.audiomixer_path = "/var/lib/audiomixer/audiomixer/audiomixerapp"
# if it looks like linux, use init.d script; otherwise use kill
config.icecast_reload_cmd = ENV['ICECAST_RELOAD_CMD'] || (File.exist?('/usr/bin/icecast2') ? '/etc/init.d/icecast2 reload' : "bash -l -c #{Shellwords.escape("kill -1 `ps -f | grep /usr/local/bin/icecast | grep -v grep | awk \'{print $2}\'`")}")
config.icecast_reload_cmd = ENV['ICECAST_RELOAD_CMD'] || (File.exist?('/usr/local/bin/icecast2') ? "bash -l -c #{Shellwords.escape("sudo /etc/init.d/icecast2 reload")}" : "bash -l -c #{Shellwords.escape("kill -1 `ps -f | grep /usr/local/bin/icecast | grep -v grep | awk \'{print $2}\'`")}")
# if it looks like linux, use that path; otherwise use the brew default path
config.icecast_config_file = ENV['ICECAST_CONFIG_FILE'] || (File.exist?('/etc/icecast2/icecast.xml') ? '/etc/icecast2/icecast.xml' : '/usr/local/etc/icecast.xml')
# this will be the qualifier on the IcecastConfigWorker queue name
config.icecast_server_id = ENV['ICECAST_SERVER_ID'] || 'localhost'
config.icecast_max_missing_check = 2 * 60 # 2 minutes
config.icecast_max_sourced_changed = 15 # 15 seconds
config.email_alerts_alias = 'nobody@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'

View File

@ -8,3 +8,10 @@ IcecastConfigRetry:
cron: 0 * * * *
class: "JamRuby::IcecastConfigRetry"
description: "Finds icecast servers that have had their config_changed, but no IcecastConfigWriter check recently"
IcecastSourceCheck:
cron: "10 * * * * *"
class: "JamRuby::IcecastSourceCheck"
description: "Finds icecast mounts that need their 'sourced' state to change, but haven't in some time"

View File

@ -14,7 +14,8 @@ MusicSessionManager < BaseManager
ActiveRecord::Base.transaction do
# check if we are connected to rabbitmq
music_session = MusicSession.new()
music_session = MusicSession.new
music_session.id = SecureRandom.uuid
music_session.creator = user
music_session.description = description
music_session.musician_access = musician_access
@ -24,9 +25,6 @@ MusicSessionManager < BaseManager
music_session.band = band
music_session.legal_terms = legal_terms
#genres = genres
@log.debug "Genres class: " + genres.class.to_s
unless genres.nil?
genres.each do |genre_id|
loaded_genre = Genre.find(genre_id)
@ -34,6 +32,13 @@ MusicSessionManager < BaseManager
end
end
if fan_access
# create an icecast mount since regular users can listen in to the broadcast
music_session.mount = IcecastMount.build_session_mount(music_session)
end
music_session.save
unless music_session.errors.any?

View File

@ -53,13 +53,13 @@ describe "Musician Search API", :type => :api do
it "gets musicians for default instrument" do
get_query({:instrument => 'electric guitar'})
good_response
expect(json.count).to be >= 1
expect(json['musicians'].count).to be >= 1
end
it "gets no musicians for unused instruments" do
get_query({:instrument => 'tuba'})
good_response
expect(json.count).to eq(0)
expect(json['musicians'].count).to eq(0)
end
end

View File

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

View File

@ -79,8 +79,8 @@ include Jampb
end
config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] }
DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] })
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] }
DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] })
end
#config.after(:each) do