diff --git a/admin/Gemfile b/admin/Gemfile index 048e1a87d..7fc960e3b 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -49,7 +49,7 @@ gem 'aasm', '3.0.16' gem 'postgres-copy', '0.6.0' gem 'aws-sdk', '1.29.1' gem 'bugsnag' - +gem 'resque' gem 'eventmachine', '1.0.3' gem 'amqp', '0.9.8' diff --git a/admin/jenkins b/admin/jenkins index 67733e8c2..cfc6734ab 100755 --- a/admin/jenkins +++ b/admin/jenkins @@ -9,17 +9,21 @@ if [ "$?" = "0" ]; then echo "build succeeded" if [ ! -z "$PACKAGE" ]; then - echo "publishing ubuntu package (.deb)" - DEBPATH=`find target/deb -name *.deb` - DEBNAME=`basename $DEBPATH` + if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* ]]; then + echo "publishing ubuntu package (.deb)" + DEBPATH=`find target/deb -name *.deb` + DEBNAME=`basename $DEBPATH` - curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME + curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME - if [ "$?" != "0" ]; then - echo "deb publish failed" - exit 1 + if [ "$?" != "0" ]; then + echo "deb publish failed" + exit 1 + fi + echo "done publishing deb" + else + echo "Skipping publish since branch is neither master or develop..." fi - echo "done publishing deb" fi else echo "build failed" diff --git a/db/.ruby-version b/db/.ruby-version index abf2ccea0..cb506813e 100644 --- a/db/.ruby-version +++ b/db/.ruby-version @@ -1 +1 @@ -ruby-2.0.0-p247 +2.0.0-p247 diff --git a/db/jenkins b/db/jenkins index 8d2afbc49..3c3f5b055 100755 --- a/db/jenkins +++ b/db/jenkins @@ -7,38 +7,39 @@ echo "starting build..." ./build if [ "$?" = "0" ]; then - echo "build succeeded" - echo "publishing gem" - pushd "target/ruby_package" - find . -name *.gem -exec curl -f -T {} $GEM_SERVER/{} \; - - if [ "$?" != "0" ]; then - echo "publish failed" - exit 1 - fi - popd - echo "done publishing gems" + echo "build succeeded" + if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* ]]; then + echo "publishing gem" + pushd "target/ruby_package" + find . -name *.gem -exec curl -f -T {} $GEM_SERVER/{} \; - if [ ! -z "$PACKAGE" ]; then - echo "publishing ubuntu packages (.deb)" - for f in `find target -name '*.deb'`; do - DEBNAME=`basename $f` - DEBPATH="$f" - echo "publishing $DEBPATH to deb server" - curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME - - if [ "$?" != "0" ]; then - echo "deb publish failed of $DEBPATH" - exit 1 - fi - done + if [ "$?" != "0" ]; then + echo "publish failed" + exit 1 + fi - echo "done publishing debs" + popd + echo "done publishing gems" + + if [ ! -z "$PACKAGE" ]; then + echo "publishing ubuntu packages (.deb)" + for f in `find target -name '*.deb'`; do + DEBNAME=`basename $f` + DEBPATH="$f" + echo "publishing $DEBPATH to deb server" + curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME + if [ "$?" != "0" ]; then + echo "deb publish failed of $DEBPATH" + exit 1 + fi + done + + echo "done publishing debs" + fi + else + echo "Skipping publish since branch is neither master or develop..." fi - else echo "build failed" exit 1 fi - - diff --git a/db/manifest b/db/manifest index 878961343..f480dea41 100755 --- a/db/manifest +++ b/db/manifest @@ -81,5 +81,8 @@ notification_band_invite.sql band_photo_filepicker.sql bands_geocoding.sql store_s3_filenames.sql +discardable_recorded_tracks.sql +music_sessions_have_claimed_recording.sql +discardable_recorded_tracks2.sql +icecast.sql home_page_promos.sql - diff --git a/db/up/discardable_recorded_tracks.sql b/db/up/discardable_recorded_tracks.sql new file mode 100644 index 000000000..8e30f6528 --- /dev/null +++ b/db/up/discardable_recorded_tracks.sql @@ -0,0 +1,5 @@ +-- there are no valid recordings and mixes at this time +DELETE FROM recorded_tracks; +DELETE FROM mixes; + +ALTER TABLE recorded_tracks ADD COLUMN discard BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/db/up/discardable_recorded_tracks2.sql b/db/up/discardable_recorded_tracks2.sql new file mode 100644 index 000000000..0d46581f9 --- /dev/null +++ b/db/up/discardable_recorded_tracks2.sql @@ -0,0 +1,2 @@ +ALTER TABLE recorded_tracks ALTER COLUMN discard DROP DEFAULT; +ALTER TABLE recorded_tracks ALTER COLUMN discard DROP NOT NULL; \ No newline at end of file diff --git a/db/up/icecast.sql b/db/up/icecast.sql new file mode 100644 index 000000000..b167890ac --- /dev/null +++ b/db/up/icecast.sql @@ -0,0 +1,305 @@ + +-- see http://www.icecast.org/docs/icecast-2.3.3/icecast2_config_file.html#limits +CREATE TABLE icecast_limits ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + -- number of listening clients + clients INTEGER NOT NULL DEFAULT 1000, + + --number of sources include souce clients and relays + sources INTEGER NOT NULL DEFAULT 50, + + -- maximum size (in bytes) of the stream queue + queue_size INTEGER NOT NULL DEFAULT 102400, + + -- does not appear to be used + client_timeout INTEGER DEFAULT 10, + + -- The maximum time (in seconds) to wait for a request to come in once + -- the client has made a connection to the server. + -- In general this value should not need to be tweaked. + header_timeout INTEGER DEFAULT 15, + + -- If a connected source does not send any data within this + -- timeout period (in seconds), then the source connection + -- will be removed from the server. + source_timeout INTEGER DEFAULT 10, + + -- The burst size is the amount of data (in bytes) + -- to burst to a client at connection time. + burst_size INTEGER DEFAULT 65536, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + + ); + + +create table icecast_admin_authentications ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + -- The unencrypted password used by sources to connect to icecast2. + -- The default username for all source connections is 'source' but + -- this option allows to specify a default password. This and the username + -- can be changed in the individual mount sections. + source_password VARCHAR NOT NULL DEFAULT 'icejam321', + + -- Used in the master server as part of the authentication when a slave requests + -- the list of streams to relay. The default username is 'relay' + relay_user VARCHAR NOT NULL DEFAULT 'relay', + relay_password VARCHAR NOT NULL DEFAULT 'jkrelayhack', + + --The username/password used for all administration functions. + admin_user VARCHAR NOT NULL DEFAULT 'jkadmin', + admin_password VARCHAR NOT NULL DEFAULT 'jKadmin123', + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + +--contains all the settings for listing a stream on any of the Icecast2 YP Directory servers. +-- Multiple occurances of this section can be specified in order to be listed on multiple directory servers. +create table icecast_directorys ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + yp_url_timeout integer not null default 15, + yp_url character not null UNIQUE default 'http://dir.xiph.org/cgi-bin/yp-cgi', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create table icecast_servermiscs ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + -- This is the DNS name or IP address that will be used for the stream directory lookups or possibily + -- the playlist generation if a Host header is not provided. While localhost is shown as an example, + -- in fact you will want something that your listeners can use. + hostname character not null default 'concertsvr.jamkazam.com', + --This sets the location string for this icecast instance. It will be shown e.g in the web interface. + location character not null default 'earth', + --This should contain contact details for getting in touch with the server administrator. + admin character not null default 'icemaster@localhost', + -- This flag turns on the icecast2 fileserver from which static files can be served. + -- All files are served relative to the path specified in the configuration + -- setting. By default the setting is enabled so that requests for the images + -- on the status page are retrievable. + fileserve INTEGER not null default 1, + -- This optional setting allows for the administrator of the server to override the + -- default server identification. The default is icecast followed by a version number + -- and most will not care to change it however this setting will change that. + server_id character not null default 'icecast 2.3', + + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create table icecast_listen_sockets ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + -- The TCP port that will be used to accept client connections. + port integer not null default 8001, + + -- An optional IP address that can be used to bind to a specific network card. + -- If not supplied, then it will bind to all interfaces. + bind_address character, + + shoutcast_mount character default NULL, + shoutcast_compat INTEGER not null default 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +create table icecast_mastersvr_relays ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + -- ip address of the master icecast server and port number + master_server character not null, + master_server_port integer not null, + --The interval (in seconds) that the Relay Server will poll the Master Server for any new mountpoints to relay. + master_update_interval integer not null default 120, + + -- This is the relay username on the master server. It is used to query the server for a list of + -- mountpoints to relay. If not specified then 'relay' is used + master_username character not null default 'relay', + master_password character not null, + + --Global on-demand setting for relays. Because you do not have individual relay options when + -- using a master server relay, you still may want those relays to only pull the stream when + -- there is at least one listener on the slave. The typical case here is to avoid surplus + -- bandwidth costs when no one is listening. + relays_on_demand INTEGER default 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + --make sure this combo is unique + --CONSTRAINT serverID UNIQUE KEY (master_server,master_server_port) +); + +create table icecast_relays ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + -- ip address of server we are relaying from and port number + server character not null, + port integer not null default 8001, + + -- mount at server. eg /example.ogg + mount character not null, + -- eg /different.ogg + local_mount character not null, + -- eg joe. could be null + username character default NULL , + -- user password + password character default null , + relay_shoutcast_metadata INTEGER default 0, + --- relay only if we have someone wanting to listen + on_demand INTEGER default 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create TABLE icecast_user_authentications( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + --"htpasswd or url" + type CHARACTER DEFAULT NULL , + -- these are for httpasswd + filename CHARACTER default NULL, + allow_duplicate_users INTEGER DEFAULT 0, + + -- these options are for url + -- eg value="http://myauthserver.com/stream_start.php" + mount_add CHARACTER default NULL, + --value="http://myauthserver.com/stream_end.php" + mount_remove CHARACTER default NULL, + --value="http://myauthserver.com/listener_joined.php" + listener_add CHARACTER default NULL, + --value="http://myauthserver.com/listener_left.php" + listener_remove CHARACTER default NULL, + -- value="user" + username CHARACTER default NULL, + -- value="pass" + password CHARACTER default NULL, + -- value="icecast-auth-user: 1" + auth_header CHARACTER default NULL, + -- value="icecast-auth-timelimit:" + timelimit_header CHARACTER default NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +create table icecast_mounts ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + -- eg/example-complex.ogg + mount_name CHARACTER UNIQUE NOT NULL, + username CHARACTER NOT NULL DEFAULT 'jamsource', + password CHARACTER NOT NULL DEFAULT 'jamksource', + max_listeners INTEGER NOT NULL DEFAULT 4, + max_listener_duration INTEGER NOT NULL DEFAULT 3600, + -- dump of the stream coming through on this mountpoint. + -- eg /tmp/dump-example1.ogg + dump_file CHARACTER DEFAULT NULL, + + -- intro music to play + -- This optional value specifies a mountpoint that clients are automatically moved to + -- if the source shuts down or is not streaming at the time a listener connects. + intro CHARACTER NOT NULL DEFAULT '/intro.ogg', + fallback_mount CHARACTER NOT NULL DEFAULT '/sourcedown.ogg', + -- When enabled, this allows a connecting source client or relay on this mountpoint + -- to move listening clients back from the fallback mount. + fallback_override INTEGER DEFAULT 1, + + -- When set to 1, this will cause new listeners, when the max listener count for the mountpoint + -- has been reached, to move to the fallback mount if there is one specified. + fallback_when_full INTEGER DEFAULT 1, + + --For non-Ogg streams like MP3, the metadata that is inserted into the stream often + -- has no defined character set. + charset CHARACTER NOT NULL DEFAULT 'ISO8859-1', + -- possilble values are -1, 0, 1 + public INTEGER DEFAULT 1, + + stream_name VARCHAR NOT NULL DEFAULT 'My Jamkazam Audio Stream', + stream_description VARCHAR NOT NULL DEFAULT 'My JK audio description', + -- direct to user page + stream_url CHARACTER NOT NULL DEFAULT 'http://wwww.jamakazam.com#user_id', + -- get this from the session info + genre VARCHAR NOT NULL DEFAULT 'Unknown', + bitrate integer NOT NULL default 92, + type CHARACTER NOT NULL DEFAULT 'application/ogg' , + subtype CHARACTER NOT NULL DEFAULT 'vorbis', + -- Enable this to prevent this mount from being shown on the xsl pages. + -- This is mainly for cases where a local relay is configured and you do + -- not want the source of the local relay to be shown + hidden INTEGER DEFAULT 1, + + -- This optional setting allows for providing a burst size which overrides the + -- default burst size as defined in limits. The value is in bytes. + burst_size INTEGER DEFAULT 65536, + mp3_metadata_interval INTEGER DEFAULT 4096, + + --called when the source connects or disconnects. The scripts are called with the name of the mount + on_connect CHARACTER DEFAULT '/home/icecast/bin/source-start', + on_disconnect CHARACTER DEFAULT '/home/icecast/bin/source-end', + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + auth_id VARCHAR(64) NOT NULL REFERENCES icecast_user_authentications(id) +); + + +create table icecast_paths ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + basedir CHARACTER NOT NULL DEFAULT './', + logdir CHARACTER NOT NULL DEFAULT './logs', + pidfile CHARACTER NOT NULL DEFAULT './icecast.pid', + webroot CHARACTER NOT NULL DEFAULT './web', + adminroot CHARACTER NOT NULL DEFAULT './admin', + allow_ip CHARACTER NOT NULL DEFAULT '/path/to/ip_allowlist', + deny_ip CHARACTER NOT NULL DEFAULT '/path_to_ip_denylist', + alias CHARACTER DEFAULT 'source="/foo" dest="/bar"', + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +create table icecast_loggings ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + accesslog CHARACTER NOT NULL DEFAULT 'access.log', + errorlog CHARACTER NOT NULL DEFAULT 'error.log', + playlistlog CHARACTER NOT NULL DEFAULT 'playlist.log', + -- 4 Debug, 3 Info, 2 Warn, 1 Error + loglevel INTEGER NOT NULL DEFAULT 4 , + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +create table icecast_securitys ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + + chroot INTEGER NOT NULL DEFAULT 0, + changeowner_user CHARACTER DEFAULT 'nobody', + changeowner_group CHARACTER DEFAULT 'nogroup', + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create TABLE icecast_servers( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + icecast_limit_id VARCHAR(64) REFERENCES icecast_limits(id) + + + + -- configs + -- mounts + +); + + diff --git a/db/up/music_sessions_have_claimed_recording.sql b/db/up/music_sessions_have_claimed_recording.sql new file mode 100644 index 000000000..c8365b179 --- /dev/null +++ b/db/up/music_sessions_have_claimed_recording.sql @@ -0,0 +1,3 @@ +-- let a music_session reference a claimed recording, so that the state of the session knows if someone is playing a recording back +ALTER TABLE music_sessions ADD COLUMN claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id); +ALTER TABLE music_sessions ADD COLUMN claimed_recording_initiator_id VARCHAR(64) REFERENCES users(id); \ No newline at end of file diff --git a/db/up/recordings_public_launch.sql b/db/up/recordings_public_launch.sql index be7323e75..63f12ea46 100644 --- a/db/up/recordings_public_launch.sql +++ b/db/up/recordings_public_launch.sql @@ -24,4 +24,5 @@ ALTER TABLE recorded_tracks ADD COLUMN client_id VARCHAR(64) NOT NULL; ALTER TABLE recorded_tracks ADD COLUMN track_id VARCHAR(64) NOT NULL; -- so that server can correlate to client track +DELETE FROM tracks; ALTER TABLE tracks ADD COLUMN client_track_id VARCHAR(64) NOT NULL; diff --git a/pb/.ruby-version b/pb/.ruby-version index abf2ccea0..cb506813e 100644 --- a/pb/.ruby-version +++ b/pb/.ruby-version @@ -1 +1 @@ -ruby-2.0.0-p247 +2.0.0-p247 diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index fef2995dc..9e16de1f4 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -9,43 +9,62 @@ package jampb; message ClientMessage { enum Type { - LOGIN = 100; - LOGIN_ACK = 101; + 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_MUSIC_SESSION = 102; - LOGIN_MUSIC_SESSION_ACK = 103; - FRIEND_SESSION_JOIN = 104; - LEAVE_MUSIC_SESSION = 105; - LEAVE_MUSIC_SESSION_ACK = 106; - HEARTBEAT = 107; - FRIEND_UPDATE = 108; - SESSION_INVITATION = 109; - MUSICIAN_SESSION_DEPART = 110; - JOIN_REQUEST = 111; - FRIEND_REQUEST = 112; - FRIEND_REQUEST_ACCEPTED = 113; - MUSICIAN_SESSION_JOIN = 114; - MUSICIAN_SESSION_FRESH = 115; - MUSICIAN_SESSION_STALE = 116; - HEARTBEAT_ACK = 117; - JOIN_REQUEST_APPROVED = 118; - JOIN_REQUEST_REJECTED = 119; - BAND_INVITATION = 120; - BAND_INVITATION_ACCEPTED = 121; + // friend notifications + FRIEND_UPDATE = 140; + FRIEND_REQUEST = 145; + FRIEND_REQUEST_ACCEPTED = 150; + FRIEND_SESSION_JOIN = 155; + NEW_USER_FOLLOWER = 160; + NEW_BAND_FOLLOWER = 161; - TEST_SESSION_MESSAGE = 200; + // 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; + + // recording notifications + 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; + + MUSICIAN_SESSION_FRESH = 240; + MUSICIAN_SESSION_STALE = 245; + + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; - PING_ACK = 301; - PEER_MESSAGE = 302; - TEST_CLIENT_MESSAGE = 303; + PING_ACK = 305; + PEER_MESSAGE = 310; + TEST_CLIENT_MESSAGE = 315; SERVER_BAD_STATE_RECOVERED = 900; SERVER_GENERIC_ERROR = 1000; - SERVER_REJECTION_ERROR = 1001; - SERVER_PERMISSION_ERROR = 1002; - SERVER_BAD_STATE_ERROR = 1003; + SERVER_REJECTION_ERROR = 1005; + SERVER_PERMISSION_ERROR = 1010; + SERVER_BAD_STATE_ERROR = 1015; } // Identifies which inner message is filled in @@ -59,45 +78,65 @@ message ClientMessage { // Client-Server messages (to/from) optional Login login = 100; // to server - optional LoginAck login_ack = 101; // from server - optional LoginMusicSession login_music_session = 102; // to server - optional LoginMusicSessionAck login_music_session_ack = 103; // from server - optional FriendSessionJoin friend_session_join = 104; // from server to all members - optional LeaveMusicSession leave_music_session = 105; - optional LeaveMusicSessionAck leave_music_session_ack = 106; - optional Heartbeat heartbeat = 107; - optional FriendUpdate friend_update = 108; // from server to all friends of user - optional SessionInvitation session_invitation = 109; // from server to user - optional MusicianSessionDepart musician_session_depart = 110; - optional JoinRequest join_request = 111; - optional FriendRequest friend_request = 112; - optional FriendRequestAccepted friend_request_accepted = 113; - optional MusicianSessionJoin musician_session_join = 114; - optional MusicianSessionFresh musician_session_fresh = 115; - optional MusicianSessionStale musician_session_stale = 116; - optional HeartbeatAck heartbeat_ack = 117; - optional JoinRequestApproved join_request_approved = 118; - optional JoinRequestRejected join_request_rejected = 119; - optional BandInvitation band_invitation = 120; - optional BandInvitationAccepted band_invitation_accepted = 121; + optional LoginAck login_ack = 105; // from server + optional LoginMusicSession login_music_session = 110; // to server + optional LoginMusicSessionAck login_music_session_ack = 115; // from server + optional LeaveMusicSession leave_music_session = 120; + optional LeaveMusicSessionAck leave_music_session_ack = 125; + optional Heartbeat heartbeat = 130; + optional HeartbeatAck heartbeat_ack = 135; + + // friend notifications + optional FriendUpdate friend_update = 140; // from server to all friends of user + optional FriendRequest friend_request = 145; + optional FriendRequestAccepted friend_request_accepted = 150; + optional NewUserFollower new_user_follower = 160; + optional NewBandFollower new_band_follower = 161; + + // session invitations + optional SessionInvitation session_invitation = 165; // from server to user + optional SessionEnded session_ended = 170; + optional JoinRequest join_request = 175; + optional JoinRequestApproved join_request_approved = 180; + optional JoinRequestRejected join_request_rejected = 185; + optional SessionJoin session_join = 190; + optional SessionDepart session_depart = 195; + optional MusicianSessionJoin musician_session_join = 196; + optional BandSessionJoin band_session_join = 197; + + // recording notifications + optional MusicianRecordingSaved musician_recording_saved = 200; + optional BandRecordingSaved band_recording_saved = 205; + optional RecordingStarted recording_started = 210; + optional RecordingEnded recording_ended = 215; + optional RecordingMasterMixComplete recording_master_mix_complete = 220; + optional DownloadAvailable download_available = 221; + + // band notifications + optional BandInvitation band_invitation = 225; + optional BandInvitationAccepted band_invitation_accepted = 230; + + optional MusicianSessionFresh musician_session_fresh = 240; + optional MusicianSessionStale musician_session_stale = 245; + // Client-Session messages (to/from) - optional TestSessionMessage test_session_message = 200; + optional TestSessionMessage test_session_message = 295; // Client-Client messages (to/from) optional PingRequest ping_request = 300; - optional PingAck ping_ack = 301; - optional PeerMessage peer_message = 302; - optional TestClientMessage test_client_message = 303; + optional PingAck ping_ack = 305; + optional PeerMessage peer_message = 310; + optional TestClientMessage test_client_message = 315; // Server-to-Client special messages optional ServerBadStateRecovered server_bad_state_recovered = 900; // Server-to-Client errors optional ServerGenericError server_generic_error = 1000; - optional ServerRejectionError server_rejection_error = 1001; - optional ServerPermissionError server_permission_error = 1002; - optional ServerBadStateError server_bad_state_error = 1003; + optional ServerRejectionError server_rejection_error = 1005; + optional ServerPermissionError server_permission_error = 1010; + optional ServerBadStateError server_bad_state_error = 1015; } // route_to: server @@ -157,32 +196,161 @@ message LeaveMusicSessionAck { optional string error_reason = 2; } -// route_to: client: -// sent by server to let the rest of the participants know a user has joined. -message FriendSessionJoin { - optional string session_id = 1; // the session ID - optional string user_id = 2; // this is the user_id and can be used for user unicast messages - optional string username = 3; // meant to be a display name - optional string photo_url = 4; +message FriendUpdate { + optional string user_id = 1; + optional string photo_url = 2; + optional bool online = 3; + optional string msg = 4; +} + +message FriendRequest { + optional string friend_request_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; +} + +message FriendRequestAccepted { + optional string photo_url = 1; + optional string msg = 2; + optional string notification_id = 3; + optional string created_at = 4; +} + +message NewUserFollower { + optional string photo_url = 1; + optional string msg = 2; + optional string notification_id = 3; + optional string created_at = 4; +} + +message NewBandFollower { + optional string photo_url = 1; + optional string msg = 2; + optional string notification_id = 3; + optional string created_at = 4; +} + +message SessionInvitation { + optional string session_id = 1; + optional string msg = 2; + optional string notification_id = 3; + optional string created_at = 4; +} + +message SessionEnded { + +} + +message JoinRequest { + optional string join_request_id = 1; + optional string session_id = 2; + optional string photo_url = 3; + optional string msg = 4; + optional string notification_id = 5; + optional string created_at = 6; +} + +message JoinRequestApproved { + optional string join_request_id = 1; + optional string session_id = 2; + optional string photo_url = 3; + optional string msg = 4; + optional string notification_id = 5; + optional string created_at = 6; +} + +message JoinRequestRejected { + optional string invitation_id = 1; + optional string session_id = 2; + optional string photo_url = 3; + optional string msg = 4; + optional string notification_id = 5; + optional string created_at = 6; +} + +message SessionJoin { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; +} + +message SessionDepart { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string recording_id = 4; } -// route_to: client: -// sent by server to let the rest of the participants know a user has joined. message MusicianSessionJoin { - optional string session_id = 1; // the session ID - optional string user_id = 2; // this is the user_id and can be used for user unicast messages - optional string username = 3; // meant to be a display name - optional string photo_url = 4; + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; } -// route_to: client: -// sent by server to let the rest of the participants know a user has left. -message MusicianSessionDepart { - optional string session_id = 1; // the session ID - optional string user_id = 2; // this is the user_id and can be used for user unicast messages - optional string username = 3; // meant to be a display name - optional string photo_url = 4; - optional string recordingId = 5; // if specified, the recording was stopped automatically +message BandSessionJoin { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; +} + +message MusicianRecordingSaved { + optional string recording_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; +} + +message BandRecordingSaved { + optional string recording_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; +} + +message RecordingStarted { + optional string photo_url = 1; + optional string msg = 2; +} + +message RecordingEnded { + optional string photo_url = 1; + optional string msg = 2; +} + +message RecordingMasterMixComplete { + optional string recording_id = 1; + optional string msg = 2; + optional string notification_id = 3; + optional string created_at = 4; +} + +message DownloadAvailable { + +} + +message BandInvitation { + optional string band_invitation_id = 1; + optional string band_id = 2; + optional string photo_url = 3; + optional string msg = 4; + optional string notification_id = 5; + optional string created_at = 6; +} + +message BandInvitationAccepted { + optional string band_invitation_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; } // route_to: client: @@ -203,59 +371,6 @@ message MusicianSessionStale { optional string photo_url = 4; } -message JoinRequest { - optional string join_request_id = 1; - optional string session_id = 2; - optional string username = 3; - optional string photo_url = 4; - optional string msg = 5; - optional string notification_id = 6; - optional string created_at = 7; -} - -message JoinRequestApproved { - optional string join_request_id = 1; - optional string session_id = 2; - optional string username = 3; - optional string photo_url = 4; - optional string msg = 5; - optional string notification_id = 6; - optional string created_at = 7; -} - -message JoinRequestRejected { - optional string invitation_id = 1; - optional string session_id = 2; - optional string username = 3; - optional string photo_url = 4; - optional string msg = 5; - optional string notification_id = 6; - optional string created_at = 7; -} - -message BandInvitation { - optional string band_invitation_id = 1; - optional string band_id = 2; - optional string user_id = 3; - optional string username = 4; - optional string photo_url = 5; - optional string band_name = 6; - optional string msg = 7; - optional string notification_id = 8; - optional string created_at = 9; -} - -message BandInvitationAccepted { - optional string band_invitation_id = 1; - optional string user_id = 2; - optional string username = 3; - optional string photo_url = 4; - optional string band_name = 5; - optional string msg = 6; - optional string notification_id = 7; - optional string created_at = 8; -} - // route_to: session // a test message used by ruby-client currently. just gives way to send out to rest of session message TestSessionMessage { @@ -298,57 +413,12 @@ message Heartbeat { message HeartbeatAck { } -// target: client -// send from server to client when user sends a friend request -message FriendRequest { - optional string friend_request_id = 1; - optional string user_id = 2; - optional string name = 3; - optional string photo_url = 4; - optional string friend_id = 5; - optional string msg = 6; - optional string notification_id = 7; - optional string created_at = 8; -} - -// target: client -message FriendRequestAccepted { - optional string friend_id = 1; // accepter - optional string name = 2; - optional string photo_url = 3; - optional string user_id = 4; // original requester - optional string msg = 5; - optional string notification_id = 6; - optional string created_at = 7; -} - -// target: client -// send from server to client when a user logs in -message FriendUpdate { - optional string user_id = 1; - optional string name = 2; - optional string photo_url = 3; - optional bool online = 4; - optional string msg = 5; -} - -// route_to: user:[USER_ID] -// let a user know they've been invited to a session -message SessionInvitation { - optional string sender_name = 1; - optional string session_id = 2; - optional string notification_id = 3; - optional string created_at = 4; -} - // route_to: client // this should follow a ServerBadStateError in the case that the // websocket gateway recovers from whatever ailed it message ServerBadStateRecovered { } - - // route_to: client // this indicates unhandled error on server // if you receive this, your connection will close after. diff --git a/ruby/.ruby-version b/ruby/.ruby-version index abf2ccea0..cb506813e 100644 --- a/ruby/.ruby-version +++ b/ruby/.ruby-version @@ -1 +1 @@ -ruby-2.0.0-p247 +2.0.0-p247 diff --git a/ruby/Gemfile b/ruby/Gemfile index 19c5f3743..a8b11947f 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -10,6 +10,7 @@ gem 'pg', '0.15.1', :platform => [:mri, :mswin, :mingw] gem 'jdbc_postgres', :platform => [:jruby] gem 'activerecord', '3.2.13' +gem "activerecord-import", "~> 0.4.1" gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' @@ -23,7 +24,7 @@ gem 'carrierwave' gem 'aasm', '3.0.16' gem 'devise', '>= 1.1.2' gem 'postgres-copy' - +gem 'resque' gem 'geokit-rails' gem 'postgres_ext' diff --git a/ruby/config/resque.yml b/ruby/config/resque.yml new file mode 100644 index 000000000..408f57713 --- /dev/null +++ b/ruby/config/resque.yml @@ -0,0 +1 @@ +test: localhost:6379 diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index a2235ad27..ab8eb59a6 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -2,7 +2,6 @@ require "pg" require "active_record" require "carrierwave" require "carrierwave/orm/activerecord" -require "carrierwave_direct" require "jampb" require "uuidtools" require "logging" @@ -14,6 +13,7 @@ require "sendgrid" require "postgres-copy" require "geokit-rails" require "postgres_ext" +require 'builder' require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" @@ -27,6 +27,7 @@ require "jam_ruby/lib/module_overrides" require "jam_ruby/lib/s3_util" require "jam_ruby/lib/s3_manager" require "jam_ruby/lib/profanity" +require "jam_ruby/resque/audiomixer" require "jam_ruby/mq_router" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" @@ -84,7 +85,23 @@ require "jam_ruby/models/mix" require "jam_ruby/models/claimed_recording" require "jam_ruby/models/crash_dump" require "jam_ruby/models/isp_score_batch" +<<<<<<< HEAD require "jam_ruby/models/promotional" +======= +require "jam_ruby/models/icecast_admin_authentication" +require "jam_ruby/models/icecast_directory" +require "jam_ruby/models/icecast_limit" +require "jam_ruby/models/icecast_listen_socket" +require "jam_ruby/models/icecast_logging" +require "jam_ruby/models/icecast_mastersvr_relay" +require "jam_ruby/models/icecast_mount" +require "jam_ruby/models/icecast_path" +require "jam_ruby/models/icecast_relay" +require "jam_ruby/models/icecast_sercurity" +require "jam_ruby/models/icecast_server" +require "jam_ruby/models/icecast_servermisc" +require "jam_ruby/models/icecast_user_authentication" +>>>>>>> develop include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index c399f4e24..9fbc5bf66 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -88,5 +88,83 @@ end end + #################################### NOTIFICATION EMAILS #################################### + def friend_request(email, msg) + subject = "You have a new friend request on JamKazam" + unique_args = {:type => "friend_request"} + send_notification(email, subject, msg, unique_args) + end + + def friend_request_accepted(email, msg) + subject = "You have a new friend on JamKazam" + unique_args = {:type => "friend_request_accepted"} + send_notification(email, subject, msg, unique_args) + end + + def new_user_follower(email, msg) + subject = "You have a new follower on JamKazam" + unique_args = {:type => "new_user_follower"} + send_notification(email, subject, msg, unique_args) + end + + def new_band_follower(email, msg) + subject = "Your band has a new follower on JamKazam" + unique_args = {:type => "new_band_follower"} + send_notification(email, subject, msg, unique_args) + end + + def session_invitation(email, msg) + subject = "You have been invited to a session on JamKazam" + unique_args = {:type => "session_invitation"} + send_notification(email, subject, msg, unique_args) + end + + def musician_session_join(email, msg) + subject = "Someone you know is in a session on JamKazam" + unique_args = {:type => "musician_session_join"} + send_notification(email, subject, msg, unique_args) + end + + def band_session_join(email, msg) + subject = "A band that you follow has joined a session" + unique_args = {:type => "band_session_join"} + send_notification(email, subject, msg, unique_args) + end + + def musician_recording_saved(email, msg) + subject = msg + unique_args = {:type => "musician_recording_saved"} + send_notification(email, subject, msg, unique_args) + end + + def band_recording_saved(email, msg) + subject = msg + unique_args = {:type => "band_recording_saved"} + send_notification(email, subject, msg, unique_args) + end + + def band_invitation(email, msg) + subject = "You have been invited to join a band on JamKazam" + unique_args = {:type => "band_invitation"} + send_notification(email, subject, msg, unique_args) + end + + def band_invitation_accepted(email, msg) + subject = "Your band invitation was accepted" + unique_args = {:type => "band_invitation_accepted"} + send_notification(email, subject, msg, unique_args) + end + + def send_notification(email, subject, msg, unique_args) + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + mail(:bcc => email, :subject => subject) do |format| + format.text + format.html + end + end + ############################################################################################# + end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.html.erb new file mode 100644 index 000000000..daac81671 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Band Invitation') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.html.erb new file mode 100644 index 000000000..91eb88c0b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'Band Invitation Accepted') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_invitation_accepted.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.html.erb new file mode 100644 index 000000000..e0a1a0008 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Band Recording') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_recording_saved.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.html.erb new file mode 100644 index 000000000..74a84c56b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Band Session') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/band_session_join.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.html.erb new file mode 100644 index 000000000..ccfdaf529 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New JamKazam Friend Request') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.html.erb new file mode 100644 index 000000000..00a7d3e08 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'Friend Request Accepted') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/friend_request_accepted.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.html.erb new file mode 100644 index 000000000..8a0ff0a7a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Musician Recording') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_recording_saved.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.html.erb new file mode 100644 index 000000000..ae36c8a1e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'Musician in Session') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/musician_session_join.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.html.erb new file mode 100644 index 000000000..9fd8ef321 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Band Follower on JamKazam') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_band_follower.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.html.erb new file mode 100644 index 000000000..cdb0c6622 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'New Follower on JamKazam') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_user_follower.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.html.erb new file mode 100644 index 000000000..7d74c64b8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'Recording Master Mix Completed') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/recording_master_mix_complete.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.html.erb new file mode 100644 index 000000000..6c99a1d33 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, 'Session Invitation') %> + +

<%= @body %>

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.text.erb new file mode 100644 index 000000000..2f21cf84a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/session_invitation.text.erb @@ -0,0 +1 @@ +<%= @body %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 869906da9..a16ee93b8 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -270,7 +270,14 @@ SQL raise Exception, msg end end + else + # there are still people in the session + + #ensure that there is no active claimed recording if the owner of that recording left the session + conn.exec("UPDATE music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2", + [user_id, previous_music_session_id]) end + end end diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index 272095f59..a6091f5a0 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -5,23 +5,29 @@ module NotificationTypes FRIEND_REQUEST = "FRIEND_REQUEST" FRIEND_REQUEST_ACCEPTED = "FRIEND_REQUEST_ACCEPTED" FRIEND_SESSION_JOIN = "FRIEND_SESSION_JOIN" + NEW_USER_FOLLOWER = "NEW_USER_FOLLOWER" + NEW_BAND_FOLLOWER = "NEW_BAND_FOLLOWER" # session notifications SESSION_INVITATION = "SESSION_INVITATION" - SESSION_ENDED = "SESSION_ENDED" # used to remove session-related notification from sidebar + SESSION_ENDED = "SESSION_ENDED" # used to remove session-related notifications from sidebar JOIN_REQUEST = "JOIN_REQUEST" JOIN_REQUEST_APPROVED = "JOIN_REQUEST_APPROVED" JOIN_REQUEST_REJECTED = "JOIN_REQUEST_REJECTED" - - # musician notifications + SESSION_JOIN = "SESSION_JOIN" + SESSION_DEPART = "SESSION_DEPART" MUSICIAN_SESSION_JOIN = "MUSICIAN_SESSION_JOIN" - MUSICIAN_SESSION_DEPART = "MUSICIAN_SESSION_DEPART" # recording notifications - RECORDING_CREATED = "RECORDING_CREATED" + MUSICIAN_RECORDING_SAVED = "MUSICIAN_RECORDING_SAVED" + BAND_RECORDING_SAVED = "BAND_RECORDING_SAVED" + RECORDING_STARTED = "RECORDING_STARTED" + RECORDING_ENDED = "RECORDING_ENDED" + RECORDING_MASTER_MIX_COMPLETE = "RECORDING_MASTER_MIX_COMPLETE" # band notifications BAND_INVITATION = "BAND_INVITATION" BAND_INVITATION_ACCEPTED = "BAND_INVITATION_ACCEPTED" + BAND_SESSION_JOIN = "BAND_SESSION_JOIN" # cleared using SESSION_ENDED notification end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 5e8bbc1c0..c46d08ab3 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -46,8 +46,10 @@ module ValidationMessages # recordings ALREADY_BEING_RECORDED = "already being recorded" + ALREADY_PLAYBACK_RECORDING = "already playing a recording" NO_LONGER_RECORDING = "no longer recording" NOT_IN_SESSION = "not in session" + PREVIOUS_RECORDING_STILL_BEING_FINALIZED = "still has previous recording being finalized" # recorded tracks ALREADY_UPLOADED = "already set" @@ -59,6 +61,10 @@ module ValidationMessages PART_NOT_STARTED = "not started" UPLOAD_FAILURES_EXCEEDED = "exceeded" + # music sessions + MUST_BE_A_MUSICIAN = "must be a musician" + CLAIMED_RECORDING_ALREADY_IN_PROGRESS = "already started by someone else" + # takes either a string/string hash, or a string/array-of-strings|symbols hash, # and creates a ActiveRecord.errors style object diff --git a/ruby/lib/jam_ruby/lib/audiomixer.rb b/ruby/lib/jam_ruby/lib/audiomixer.rb deleted file mode 100644 index 2efc0c955..000000000 --- a/ruby/lib/jam_ruby/lib/audiomixer.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'json' -require 'resque' - -module JamRuby - - @queue = :audiomixer - - class AudioMixer - - def self.perform(manifest) - tmp = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{manifest['recordingId']}", nil - File.open(tmp,"w") do |f| - f.write(manifest.to_json) - end - - system("tar zxvf some_big_tarball.tar.gz")) - end - - end - -end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index 66a882ebd..a5c543755 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -19,8 +19,8 @@ module JamRuby "s3://#{@aws_bucket}/#{filename}" end - def url(filename) - "https://s3.amazonaws.com/#{@aws_bucket}/#{filename}" + def url(filename, options = @@def_opts) + "http#{options[:secure] ? "s" : ""}://s3.amazonaws.com/#{@aws_bucket}/#{filename}" end def upload_sign(filename, content_md5, part_number, upload_id) diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 51df0e0d5..24b8ddcff 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -1,4 +1,4 @@ - module JamRuby +module JamRuby # creates messages (implementation: protocol buffer) objects cleanly class MessageFactory @@ -9,18 +9,18 @@ CLIENT_TARGET_PREFIX = "client:" def initialize() - @type_values = {} + @type_values = {} - Jampb::ClientMessage::Type.constants.each do |constant| - @type_values[Jampb::ClientMessage::Type.const_get(constant)] = constant - end + Jampb::ClientMessage::Type.constants.each do |constant| + @type_values[Jampb::ClientMessage::Type.const_get(constant)] = constant + end end - # given a string (bytes) payload, return a client message - def parse_client_msg(payload) - return Jampb::ClientMessage.parse(payload) - end + # given a string (bytes) payload, return a client message + def parse_client_msg(payload) + return Jampb::ClientMessage.parse(payload) + end # create a login message using user/pass def login_with_user_pass(username, password, options = {}) @@ -70,6 +70,16 @@ ) end + def download_available + download_available = Jampb::DownloadAvailable.new + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::DOWNLOAD_AVAILABLE, + :route_to => CLIENT_TARGET, + :download_available => download_available + ) + end + # create a music session login message def login_music_session(music_session) login_music_session = Jampb::LoginMusicSession.new(:music_session => music_session) @@ -114,6 +124,28 @@ ) end + # create a heartbeat + def heartbeat() + heartbeat = Jampb::Heartbeat.new + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::HEARTBEAT, + :route_to => SERVER_TARGET, + :heartbeat => heartbeat + ) + end + + # create a heartbeat ack + def heartbeat_ack() + heartbeat_ack = Jampb::HeartbeatAck.new + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::HEARTBEAT_ACK, + :route_to => CLIENT_TARGET, + :heartbeat_ack => heartbeat_ack + ) + end + # create a server bad state recovered msg def server_bad_state_recovered(original_message_id) recovered = Jampb::ServerBadStateRecovered.new() @@ -126,7 +158,7 @@ ) end - # create a server error + # create a server error def server_generic_error(error_msg) error = Jampb::ServerGenericError.new(:error_msg => error_msg) @@ -137,7 +169,7 @@ ) end - # create a server rejection error + # create a server rejection error def server_rejection_error(error_msg) error = Jampb::ServerRejectionError.new(:error_msg => error_msg) @@ -172,52 +204,310 @@ ) end - # create a friend joined session message - def friend_session_join(session_id, user_id, username, photo_url) - join = Jampb::FriendSessionJoin.new( - :session_id => session_id, + ###################################### NOTIFICATIONS ###################################### + + # create a friend update message + def friend_update(user_id, photo_url, online, msg) + friend = Jampb::FriendUpdate.new( :user_id => user_id, - :username => username, - :photo_url => photo_url + :photo_url => photo_url, + :online => online, + :msg => msg ) return Jampb::ClientMessage.new( - :type => ClientMessage::Type::FRIEND_SESSION_JOIN, - :route_to => CLIENT_TARGET, - :friend_session_join => join + :type => ClientMessage::Type::FRIEND_UPDATE, + :route_to => USER_TARGET_PREFIX + user_id, + :friend_update => friend ) end - # create a musician joined session message - def musician_session_join(session_id, user_id, username, photo_url) - join = Jampb::MusicianSessionJoin.new( + # create a friend request message + def friend_request(receiver_id, friend_request_id, photo_url, msg, notification_id, created_at) + friend_request = Jampb::FriendRequest.new( + :friend_request_id => friend_request_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::FRIEND_REQUEST, + :route_to => USER_TARGET_PREFIX + receiver_id, + :friend_request => friend_request + ) + end + + # create a friend request acceptance message + def friend_request_accepted(receiver_id, photo_url, msg, notification_id, created_at) + friend_request_accepted = Jampb::FriendRequestAccepted.new( + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::FRIEND_REQUEST_ACCEPTED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :friend_request_accepted => friend_request_accepted + ) + end + + def new_user_follower(receiver_id, photo_url, msg, notification_id, created_at) + new_user_follower = Jampb::NewUserFollower.new( + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::NEW_USER_FOLLOWER, + :route_to => USER_TARGET_PREFIX + receiver_id, + :new_user_follower => new_user_follower + ) + end + + def new_band_follower(receiver_id, photo_url, msg, notification_id, created_at) + new_band_follower = Jampb::NewBandFollower.new( + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::NEW_BAND_FOLLOWER, + :route_to => USER_TARGET_PREFIX + receiver_id, + :new_band_follower => new_band_follower + ) + end + + def session_invitation(receiver_id, session_id, msg, notification_id, created_at) + session_invitation = Jampb::SessionInvitation.new( :session_id => session_id, - :user_id => user_id, - :username => username, - :photo_url => photo_url + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::SESSION_INVITATION, + :route_to => USER_TARGET_PREFIX + receiver_id, + :session_invitation => session_invitation + ) + end + + # create a join request session message + def join_request(join_request_id, session_id, photo_url, msg, notification_id, created_at) + req = Jampb::JoinRequest.new( + :join_request_id => join_request_id, + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::JOIN_REQUEST, + :route_to => SESSION_TARGET_PREFIX + session_id, + :join_request => req + ) + end + + # create a join request approved session message + def join_request_approved(join_request_id, session_id, photo_url, msg, notification_id, created_at) + req_approved = Jampb::JoinRequestApproved.new( + :join_request_id => join_request_id, + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::JOIN_REQUEST_APPROVED, + :route_to => SESSION_TARGET_PREFIX + session_id, + :join_request_approved => req_approved + ) + end + + # create a join request rejected session message + def join_request_rejected(join_request_id, session_id, photo_url, msg, notification_id, created_at) + req_rejected = Jampb::JoinRequestRejected.new( + :join_request_id => join_request_id, + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::JOIN_REQUEST_REJECTED, + :route_to => SESSION_TARGET_PREFIX + session_id, + :join_request_rejected => req_rejected + ) + end + + def session_join(session_id, photo_url, msg) + join = Jampb::SessionJoin.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::SESSION_JOIN, + :route_to => CLIENT_TARGET, + :session_join => join + ) + end + + def session_depart(session_id, photo_url, msg, recording_id = nil) + left = Jampb::SessionDepart.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :recording_id => recording_id + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::SESSION_DEPART, + :route_to => CLIENT_TARGET, + :session_depart => left + ) + end + + def musician_session_join(receiver_id, session_id, photo_url, msg, notification_id, created_at) + musician_session_join = Jampb::MusicianSessionJoin.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at ) return Jampb::ClientMessage.new( :type => ClientMessage::Type::MUSICIAN_SESSION_JOIN, - :route_to => CLIENT_TARGET, - :musician_session_join => join + :route_to => USER_TARGET_PREFIX + receiver_id, + :musician_session_join => musician_session_join ) end - # create a musician left session message - def musician_session_depart(session_id, user_id, username, photo_url, recordingId = nil) - left = Jampb::MusicianSessionDepart.new( - :session_id => session_id, - :user_id => user_id, - :username => username, + def band_session_join(receiver_id, session_id, photo_url, msg, notification_id, created_at) + band_session_join = Jampb::BandSessionJoin.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::BAND_SESSION_JOIN, + :route_to => USER_TARGET_PREFIX + receiver_id, + :band_session_join => band_session_join + ) + end + + def musician_recording_saved(receiver_id, recording_id, photo_url, msg, notification_id, created_at) + musician_recording_saved = Jampb::MusicianRecordingSaved.new( + :recording_id => recording_id, :photo_url => photo_url, - :recordingId => recordingId + :msg => msg, + :notification_id => notification_id, + :created_at => created_at ) return Jampb::ClientMessage.new( - :type => ClientMessage::Type::MUSICIAN_SESSION_DEPART, - :route_to => CLIENT_TARGET, - :musician_session_depart => left + :type => ClientMessage::Type::MUSICIAN_RECORDING_SAVED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :musician_recording_saved => musician_recording_saved + ) + end + + def band_recording_saved(receiver_id, recording_id, photo_url, msg, notification_id, created_at) + band_recording_saved = Jampb::BandRecordingSaved.new( + :recording_id => recording_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::BAND_RECORDING_SAVED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :band_recording_saved => band_recording_saved + ) + end + + def recording_started(receiver_id, photo_url, msg) + recording_started = Jampb::RecordingStarted.new( + :photo_url => photo_url, + :msg => msg + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::RECORDING_STARTED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :recording_started => recording_started + ) + end + + def recording_ended(receiver_id, photo_url, msg) + recording_ended = Jampb::RecordingEnded.new( + :photo_url => photo_url, + :msg => msg + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::RECORDING_ENDED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :recording_ended => recording_ended + ) + end + + def recording_master_mix_complete + end + + # create a band invitation message + def band_invitation(receiver_id, invitation_id, band_id, photo_url, msg, notification_id, created_at) + band_invitation = Jampb::BandInvitation.new( + :band_invitation_id => invitation_id, + :band_id => band_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::BAND_INVITATION, + :route_to => USER_TARGET_PREFIX + receiver_id, + :band_invitation => band_invitation + ) + end + + # create a band invitation acceptance message + def band_invitation_accepted(receiver_id, invitation_id, photo_url, msg, notification_id, created_at) + band_invitation_accepted = Jampb::BandInvitationAccepted.new( + :band_invitation_id => invitation_id, + :photo_url => photo_url, + :msg => msg, + :notification_id => notification_id, + :created_at => created_at + ) + + return Jampb::ClientMessage.new( + :type => ClientMessage::Type::BAND_INVITATION_ACCEPTED, + :route_to => USER_TARGET_PREFIX + receiver_id, + :band_invitation_accepted => band_invitation_accepted ) end @@ -253,183 +543,14 @@ ) end - # create a join request session message - def join_request(join_request_id, session_id, username, photo_url, msg, notification_id, created_at) - req = Jampb::JoinRequest.new( - :join_request_id => join_request_id, - :session_id => session_id, - :username => username, - :photo_url => photo_url, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::JOIN_REQUEST, - :route_to => SESSION_TARGET_PREFIX + session_id, - :join_request => req - ) - end - - # create a join request approved session message - def join_request_approved(join_request_id, session_id, username, photo_url, msg, notification_id, created_at) - req_approved = Jampb::JoinRequestApproved.new( - :join_request_id => join_request_id, - :session_id => session_id, - :username => username, - :photo_url => photo_url, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::JOIN_REQUEST_APPROVED, - :route_to => SESSION_TARGET_PREFIX + session_id, - :join_request_approved => req_approved - ) - end - - # create a join request rejected session message - def join_request_rejected(join_request_id, session_id, username, photo_url, msg, notification_id, created_at) - req_rejected = Jampb::JoinRequestRejected.new( - :join_request_id => join_request_id, - :session_id => session_id, - :username => username, - :photo_url => photo_url, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::JOIN_REQUEST_REJECTED, - :route_to => SESSION_TARGET_PREFIX + session_id, - :join_request_rejected => req_rejected - ) - end - - # create a band invitation message - def band_invitation(invitation_id, band_id, receiver_id, username, photo_url, band_name, msg, notification_id, created_at) - band_invitation = Jampb::BandInvitation.new( - :band_invitation_id => invitation_id, - :band_id => band_id, - :user_id => receiver_id, - :username => username, - :photo_url => photo_url, - :band_name => band_name, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::BAND_INVITATION, - :route_to => USER_TARGET_PREFIX + receiver_id, - :band_invitation => band_invitation - ) - end - - # create a band invitation acceptance message - def band_invitation_accepted(invitation_id, receiver_id, username, photo_url, band_name, msg, notification_id, created_at) - band_invitation_accepted = Jampb::BandInvitationAccepted.new( - :band_invitation_id => invitation_id, - :user_id => receiver_id, - :username => username, - :photo_url => photo_url, - :band_name => band_name, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::BAND_INVITATION_ACCEPTED, - :route_to => USER_TARGET_PREFIX + receiver_id, - :band_invitation_accepted => band_invitation_accepted - ) - end - - # create a test message to send in session - def test_session_message(session_id, msg) - test = Jampb::TestSessionMessage.new(:msg => msg) + # create a test message to send in session + def test_session_message(session_id, msg) + test = Jampb::TestSessionMessage.new(:msg => msg) return Jampb::ClientMessage.new( :type => ClientMessage::Type::TEST_SESSION_MESSAGE, :route_to => SESSION_TARGET_PREFIX + session_id, :test_session_message => test - ) - end - - def session_invitation(receiver_id, sender_name, session_id, notification_id, created_at) - session_invitation = Jampb::SessionInvitation.new( - :sender_name => sender_name, - :session_id => session_id, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::SESSION_INVITATION, - :route_to => USER_TARGET_PREFIX + receiver_id, - :session_invitation => session_invitation - ) - end - - # create a friend update message - def friend_update(user_id, name, photo_url, online, msg) - friend = Jampb::FriendUpdate.new( - :user_id => user_id, - :name => name, - :photo_url => photo_url, - :online => online, - :msg => msg - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::FRIEND_UPDATE, - :route_to => USER_TARGET_PREFIX + user_id, - :friend_update => friend - ) - end - - # create a friend request message - def friend_request(friend_request_id, user_id, name, photo_url, friend_id, msg, notification_id, created_at) - friend_request = Jampb::FriendRequest.new( - :friend_request_id => friend_request_id, - :user_id => user_id, - :name => name, - :photo_url => photo_url, - :friend_id => friend_id, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::FRIEND_REQUEST, - :route_to => USER_TARGET_PREFIX + friend_id, - :friend_request => friend_request - ) - end - - # create a friend request acceptance message - def friend_request_accepted(friend_id, name, photo_url, user_id, msg, notification_id, created_at) - friend_request_accepted = Jampb::FriendRequestAccepted.new( - :friend_id => friend_id, - :name => name, - :photo_url => photo_url, - :user_id => user_id, - :msg => msg, - :notification_id => notification_id, - :created_at => created_at - ) - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::FRIEND_REQUEST_ACCEPTED, - :route_to => USER_TARGET_PREFIX + user_id, - :friend_request_accepted => friend_request_accepted ) end @@ -473,28 +594,6 @@ #################################################### - # create a heartbeat - def heartbeat() - heartbeat = Jampb::Heartbeat.new - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::HEARTBEAT, - :route_to => SERVER_TARGET, - :heartbeat => heartbeat - ) - end - - # create a heartbeat ack - def heartbeat_ack() - heartbeat_ack = Jampb::HeartbeatAck.new - - return Jampb::ClientMessage.new( - :type => ClientMessage::Type::HEARTBEAT_ACK, - :route_to => CLIENT_TARGET, - :heartbeat_ack => heartbeat_ack - ) - end - # is this message directed to the server? def server_directed? msg return msg.route_to == MessageFactory::SERVER_TARGET @@ -519,8 +618,8 @@ return msg.route_to[MessageFactory::SESSION_TARGET_PREFIX..-1] end - def get_message_type msg - return @type_values[msg.type] - end + def get_message_type msg + return @type_values[msg.type] + end end end diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index aa8d0764d..6764150b5 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -1,13 +1,19 @@ module JamRuby class ClaimedRecording < ActiveRecord::Base - validates :name, no_profanity: true - validates :description, no_profanity: true + validates :name, no_profanity: true, length: {minimum: 3, maximum: 64}, presence: true + validates :description, no_profanity: true, length: {maximum: 8000} + validates :is_public, :inclusion => {:in => [true, false]} + validates :is_downloadable, :inclusion => {:in => [true, false]} + validates :genre, presence: true + validates_uniqueness_of :recording_id, :scope => :user_id + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :claimed_recordings belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings belongs_to :genre, :class_name => "JamRuby::Genre" has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack" + has_many :playing_sessions, :class_name => "JamRuby::MusicSession" # user must own this object # params is a hash, and everything is optional @@ -31,7 +37,7 @@ module JamRuby # If this is the only copy, destroy the entire recording. Otherwise, just destroy this claimed_recording if recording.claimed_recordings.count == 1 - recording.discard + recording.destroy else self.destroy end diff --git a/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb new file mode 100644 index 000000000..3abd1696c --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastAdminAuthentication < ActiveRecord::Base + + self.primary_key = 'id' + + validates :source_password, length: {minimum: 5} + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_directory.rb b/ruby/lib/jam_ruby/models/icecast_directory.rb new file mode 100644 index 000000000..7d5fd4b45 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_directory.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastDirectory < ActiveRecord::Base + + self.primary_key = 'id' + + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_limit.rb b/ruby/lib/jam_ruby/models/icecast_limit.rb new file mode 100644 index 000000000..006ee6e69 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_limit.rb @@ -0,0 +1,13 @@ +module JamRuby + class IcecastLimit < ActiveRecord::Base + + self.primary_key = 'id' + + validates :clients, numericality: {only_integer: true} + + def dumpXml() + + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_listen_socket.rb b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb new file mode 100644 index 000000000..f6d13f7c0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb @@ -0,0 +1,8 @@ +module JamRuby + class IcecastListenSocket < ActiveRecord::Base + + self.primary_key = 'id' + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_logging.rb b/ruby/lib/jam_ruby/models/icecast_logging.rb new file mode 100644 index 000000000..457afc2a2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_logging.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastLogging < ActiveRecord::Base + + self.primary_key = 'id' + + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_mastersvr_relay.rb b/ruby/lib/jam_ruby/models/icecast_mastersvr_relay.rb new file mode 100644 index 000000000..2ff4239f0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_mastersvr_relay.rb @@ -0,0 +1,7 @@ +module JamRuby + class IcecastMastersvrRelay < ActiveRecord::Base + + self.primary_key = 'id' + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_mount.rb b/ruby/lib/jam_ruby/models/icecast_mount.rb new file mode 100644 index 000000000..6627f3ddd --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_mount.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastMount < ActiveRecord::Base + + self.primary_key = 'id' + + has_one :authentication, :class_name => "IcecastUserAuthentication" + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_path.rb b/ruby/lib/jam_ruby/models/icecast_path.rb new file mode 100644 index 000000000..434d7839c --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_path.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastPath < ActiveRecord::Base + + self.primary_key = 'id' + + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_relay.rb b/ruby/lib/jam_ruby/models/icecast_relay.rb new file mode 100644 index 000000000..e0228df0f --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_relay.rb @@ -0,0 +1,8 @@ +module JamRuby + class IcecastRelay < ActiveRecord::Base + + self.primary_key = 'id' + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_sercurity.rb b/ruby/lib/jam_ruby/models/icecast_sercurity.rb new file mode 100644 index 000000000..510fd95c3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_sercurity.rb @@ -0,0 +1,9 @@ +module JamRuby + class IcecastSecurity < ActiveRecord::Base + + self.primary_key = 'id' + + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_server.rb b/ruby/lib/jam_ruby/models/icecast_server.rb new file mode 100644 index 000000000..f015f35ac --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_server.rb @@ -0,0 +1,18 @@ +module JamRuby + class IcecastServer < ActiveRecord::Base + + self.primary_key = 'id' + + has_one :limit, :class_name => "JamRuby::IcecastLimit" + has_one :adminauth, :class_name => "JamRuby::IcecastAdminAuthentication" + has_one :directory, :class_name => "JamRuby::IcecastDirectory" + has_one :misc, :class_name => "JamRuby::IcecastServermisc" + has_many :listen_sockets, :class_name => "JamRuby::IcecastListenSocket" + has_one :master_relay, :class_name => "JamRuby::IcecastMastersvrRelay" + has_one :relay, :class_name => "JamRuby::IcecastRelay" + has_many :mounts, :class_name => "JamRuby::IcecastMount" + has_one :path, :class_name => "JamRuby::IcecastPath" + has_one :logging, :class_name => "JamRuby::IcecastLogging" + has_one :security, :class_name => "JamRuby::IceCastSecurity" + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_servermisc.rb b/ruby/lib/jam_ruby/models/icecast_servermisc.rb new file mode 100644 index 000000000..d620f032e --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_servermisc.rb @@ -0,0 +1,7 @@ +module JamRuby + class IcecastServermisc < ActiveRecord::Base + + self.primary_key = 'id' + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_user_authentication.rb b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb new file mode 100644 index 000000000..ec6974093 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb @@ -0,0 +1,8 @@ +module JamRuby + class IcecastUserAuthentication < ActiveRecord::Base + + self.primary_key = 'id' + + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index ddb3be628..e419d39ba 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -6,6 +6,8 @@ module JamRuby attr_accessible :creator, :description, :musician_access, :approval_required, :fan_chat, :fan_access, :genres belongs_to :creator, :inverse_of => :music_sessions, :class_name => "JamRuby::User", :foreign_key => "user_id" + 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_many :connections, :class_name => "JamRuby::Connection" has_many :users, :through => :connections, :class_name => "JamRuby::User" @@ -33,10 +35,20 @@ module JamRuby validates :legal_terms, :inclusion => {:in => [true]}, :on => :create validates :creator, :presence => true validate :creator_is_musician + validate :no_new_playback_while_playing def creator_is_musician unless creator.musician? - errors.add(:creator, "must be a musician") + errors.add(:creator, ValidationMessages::MUST_BE_A_MUSICIAN) + end + end + + def no_new_playback_while_playing + # if we previous had a claimed recording and are trying to set one + # and if also the previous initiator is different than the current one... it's a no go + if !claimed_recording_id_was.nil? && !claimed_recording_id.nil? && + claimed_recording_initiator_id_was != claimed_recording_initiator_id + errors.add(:claimed_recording, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) end end @@ -167,11 +179,19 @@ module JamRuby return self.users.exists? user end + def most_recent_recording + recordings.where(:music_session_id => self.id).order('created_at desc').limit(1).first + end + # is this music session currently recording? def is_recording? recordings.where(:duration => nil).count > 0 end + def is_playing_recording? + !self.claimed_recording.nil? + end + def recording recordings.where(:duration => nil).first end @@ -182,8 +202,20 @@ module JamRuby current_recording.stop unless current_recording.nil? end + def claimed_recording_start(owner, claimed_recording) + self.claimed_recording = claimed_recording + self.claimed_recording_initiator = owner + self.save + end + + def claimed_recording_stop + self.claimed_recording = nil + self.claimed_recording_initiator = nil + self.save + end + def to_s - return description + description end private diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 5c54cdd00..127b761ac 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -12,8 +12,7 @@ module JamRuby belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" def index(user_id) - results = Notification.where(:target_user_id => user_id).limit(50) - return results + Notification.where(:target_user_id => user_id).limit(50) end def photo_url @@ -25,37 +24,17 @@ module JamRuby # used for persisted notifications def formatted_msg # target_user, band, session, recording, invitation, join_request = nil - source_user = nil - - # unless self.target_user_id.nil? - # target_user = User.find(self.target_user_id) - # end + source_user, band = nil unless self.source_user_id.nil? source_user = User.find(self.source_user_id) end - # unless self.band_id.nil? - # band = Band.find(self.band_id) - # end + unless self.band_id.nil? + band = Band.find(self.band_id) + end - # unless self.session_id.nil? - # session = MusicSession.find(self.session_id) - # end - - # unless self.recording_id.nil? - # recording = Recording.find(self.recording_id) - # end - - # unless self.invitation_id.nil? - # invitation = Invitation.find(self.invitation_id) - # end - - # unless self.join_request_id.nil? - # join_request = JoinRequest.find(self.join_request_id) - # end - - return self.class.format_msg(self.description, source_user) + return self.class.format_msg(self.description, source_user, band) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -81,7 +60,7 @@ module JamRuby return friend_ids end - def retrieve_followers(connection, user_id) + def retrieve_user_followers(connection, user_id) follower_ids = [] connection.exec("SELECT uf.follower_id as friend_id FROM users_followers uf WHERE uf.user_id = $1", [user_id]) do |follower_results| follower_results.each do |follower_result| @@ -91,9 +70,20 @@ module JamRuby return follower_ids end + def retrieve_friends_not_in_session(connection, user_id, session_id) + ids = retrieve_friends(connection, user_id) + connection.exec("SELECT c.user_id as musician_id FROM connections c WHERE c.music_session_id = $1", [session_id]) do |musicians| + musicians.each do |musician_result| + # remove users who are in the session + ids.reject! {|item| item == musician_result['musician_id']} + end + end + return ids + end + def retrieve_friends_and_followers(connection, user_id) ids = retrieve_friends(connection, user_id) - ids.concat(retrieve_followers(connection, user_id)) + ids.concat(retrieve_user_followers(connection, user_id)) ids.uniq! {|id| id} return ids end @@ -109,15 +99,21 @@ module JamRuby return ids end - def format_msg(description, user = nil) - name = "" + def format_msg(description, user = nil, band = nil) + name, band_name = "" unless user.nil? name = user.name else name = "Someone" end + if !band.nil? + band_name = band.name + end + case description + + # friend notifications when NotificationTypes::FRIEND_UPDATE return "#{name} is now " @@ -127,15 +123,13 @@ module JamRuby when NotificationTypes::FRIEND_REQUEST_ACCEPTED return "#{name} has accepted your friend request." - when NotificationTypes::FRIEND_SESSION_JOIN - return "#{name} has joined the session." + when NotificationTypes::NEW_USER_FOLLOWER + return "#{name} is now following you on JamKazam." - when NotificationTypes::MUSICIAN_SESSION_JOIN - return "#{name} has joined the session." - - when NotificationTypes::MUSICIAN_SESSION_DEPART - return "#{name} has left the session." + when NotificationTypes::NEW_BAND_FOLLOWER + return "#{name} is now following your band #{band.name} on JamKazam." + # session notifications when NotificationTypes::SESSION_INVITATION return "#{name} has invited you to a session." @@ -148,46 +142,72 @@ module JamRuby when NotificationTypes::JOIN_REQUEST_REJECTED return "We're sorry, but you cannot join the session at this time." + when NotificationTypes::SESSION_JOIN + return "#{name} has joined the session." + + when NotificationTypes::SESSION_DEPART + return "#{name} has left the session." + + when NotificationTypes::MUSICIAN_SESSION_JOIN + return "#{name} is now in a session." + + when NotificationTypes::BAND_SESSION_JOIN + return "#{band_name} is now in a session." + + + # recording notifications + when NotificationTypes::MUSICIAN_RECORDING_SAVED + return "#{name} has made a new recording." + + when NotificationTypes::BAND_RECORDING_SAVED + return "#{band.name} has made a new recording." + + when NotificationTypes::RECORDING_STARTED + return "#{name} has started a recording." + + when NotificationTypes::RECORDING_ENDED + return "#{name} has stopped recording." + + when NotificationTypes::RECORDING_MASTER_MIX_COMPLETE + return "This recording has been mastered and mixed and is ready to share." + + + # band notifications when NotificationTypes::BAND_INVITATION - return "You have been invited to join the band #{name}." + return "You have been invited to join the band #{band_name}." when NotificationTypes::BAND_INVITATION_ACCEPTED - return "#{name} has accepted your band invitation." + return "#{name} has accepted your band invitation to join #{band_name}." else return "" end end - ################### FRIEND UPDATE ################### def send_friend_update(user_id, online, connection) - # (1) get all of this user's friends friend_ids = retrieve_friends(connection, user_id) - unless friend_ids.blank? + unless friend_ids.empty? user = User.find(user_id) - # (2) create notification online_msg = online ? "online." : "offline." notification_msg = format_msg(NotificationTypes::FRIEND_UPDATE, user) + online_msg msg = @@message_factory.friend_update( - user_id, user.name, + user.id, user.photo_url, online, notification_msg ) - # (3) send notification @@mq_router.publish_to_friends(friend_ids, msg, user_id) end end - ################### FRIEND REQUEST ################### def send_friend_request(friend_request_id, user_id, friend_id) user = User.find(user_id) + friend = User.find(friend_id) - # (1) save to database notification = Notification.new notification.description = NotificationTypes::FRIEND_REQUEST notification.source_user_id = user_id @@ -195,157 +215,151 @@ module JamRuby notification.friend_request_id = friend_request_id notification.save - # (2) create notification notification_msg = format_msg(notification.description, user) - msg = @@message_factory.friend_request( - friend_request_id, - user_id, - user.name, - user.photo_url, - friend_id, - notification_msg, - notification.id, - notification.created_at.to_s - ) - # (3) send notification - @@mq_router.publish_to_user(friend_id, msg) + if friend.online + msg = @@message_factory.friend_request( + friend.id, + friend_request_id, + user.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(friend_id, msg) + + else + UserMailer.friend_request(friend.email, notification_msg) + end end - ############### FRIEND REQUEST ACCEPTED ############### def send_friend_request_accepted(user_id, friend_id) friend = User.find(friend_id) + user = User.find(user_id) - # (1) save to database notification = Notification.new notification.description = NotificationTypes::FRIEND_REQUEST_ACCEPTED notification.source_user_id = friend_id notification.target_user_id = user_id notification.save - # (2) create notification notification_msg = format_msg(notification.description, friend) - msg = @@message_factory.friend_request_accepted( - friend_id, - friend.name, - friend.photo_url, - user_id, notification_msg, - notification.id, - notification.created_at.to_s - ) - # (3) send notification - @@mq_router.publish_to_user(user_id, msg) - end - - ################## SESSION INVITATION ################## - def send_session_invitation(receiver_id, sender, session_id) - - # (1) save to database - notification = Notification.new - notification.description = NotificationTypes::SESSION_INVITATION - notification.source_user_id = sender.id - notification.target_user_id = receiver_id - notification.session_id = session_id - notification.save - - # (2) create notification - msg = @@message_factory.session_invitation( - receiver_id, - sender.name, - session_id, - notification.id, - notification.created_at.to_s - ) - - # (3) send notification - @@mq_router.publish_to_user(receiver_id, msg) - end - - ################## SESSION INVITATION ################## - def send_musician_session_join(music_session, connection, user) - - # (1) create notification - msg = @@message_factory.musician_session_join( - music_session.id, - user.id, - user.name, - user.photo_url - ) - - # (2) send notification - @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id}) - end - - ################## MUSICIAN SESSION DEPART ################## - def send_musician_session_depart(music_session, client_id, user, recordingId = nil) - - # (1) create notification - msg = @@message_factory.musician_session_depart( - music_session.id, - user.id, - user.name, - user.photo_url, - recordingId - ) - - # (2) send notification - @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) - end - - ################## MUSICIAN SESSION FRESH ################## - def send_musician_session_fresh(music_session, client_id, user) - - # (1) create notification - msg = @@message_factory.musician_session_fresh( - music_session.id, - user.id, - user.name, - user.photo_url - ) - - # (2) send notification - @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) - end - - ################## MUSICIAN SESSION STALE ################## - def send_musician_session_stale(music_session, client_id, user) - - # (1) create notification - msg = @@message_factory.musician_session_stale( - music_session.id, - user.id, - user.name, - user.photo_url - ) - - # (2) send notification - @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) - end - - ################## FRIEND SESSION JOIN ################## - def send_friend_session_join(db_conn, connection, user) - ids = retrieve_friends_and_followers_not_in_session(db_conn, user.id, connection.music_session.id) - - if ids.length > 0 - # (1) save to database - - # (2) create notification - msg = @@message_factory.friend_session_join( - connection.music_session.id, + if user.online + msg = @@message_factory.friend_request_accepted( user.id, - user.name, - user.photo_url) + friend.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) - # (3) send notification - @@mq_router.publish_to_friends(ids, msg, sender = {:client_id => connection.client_id}) + @@mq_router.publish_to_user(user.id, msg) + + else + UserMailer.friend_request_accepted(user.email, notification_msg) end end - ################## JOIN REQUEST ################## + def send_new_user_follower(follower, user) + + notification = Notification.new + notification.description = NotificationTypes::NEW_USER_FOLLOWER + notification.source_user_id = follower.id + notification.target_user_id = user.id + notification.save + + notification_msg = format_msg(notification.description, follower) + + if follower.id != user.id + if user.online + msg = @@message_factory.new_user_follower( + user.id, + follower.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(user.id, msg) + + else + UserMailer.new_user_follower(user.email, notification_msg) + end + end + end + + def send_new_band_follower(follower, band) + + notifications = [] + + band.band_musicians.each.each do |bm| + + notification = Notification.new + notification.description = NotificationTypes::NEW_BAND_FOLLOWER + notification.source_user_id = follower.id + notification.target_user_id = bm.user_id + notification.band_id = band.id + notification.save + + notification_msg = format_msg(notification.description, follower, band) + + # this protects against sending the notification to a band member who decides to follow the band + if follower.id != bm.user.id + if bm.user.online + msg = @@message_factory.new_user_follower( + bm.user_id, + follower.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(bm.user_id, msg) + + else + UserMailer.new_band_follower(bm.user.email, notification_msg) + end + end + end + end + + def send_session_invitation(receiver, sender, session_id) + + notification = Notification.new + notification.description = NotificationTypes::SESSION_INVITATION + notification.source_user_id = sender.id + notification.target_user_id = receiver.id + notification.session_id = session_id + notification.save + + notification_msg = format_msg(NotificationTypes::SESSION_INVITATION, sender) + + if receiver.online + msg = @@message_factory.session_invitation( + receiver.id, + session_id, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(receiver.id, msg) + + else + UserMailer.session_invitation(receiver.email, notification_msg) + end + end + + def send_session_ended(music_session, connection) + + # TODO: this should actually publish to all users who have a notification for this session + @@mq_router.server_publish_to_session(music_session, nil, sender = {:client_id => connection.client_id}) + end + def send_join_request(music_session, join_request, text) - # (1) save to database notification = Notification.new notification.description = NotificationTypes::JOIN_REQUEST notification.source_user_id = join_request.user.id @@ -353,26 +367,22 @@ module JamRuby notification.session_id = music_session.id notification.save - # (2) create notification notification_msg = format_msg(notification.description, join_request.user) + msg = @@message_factory.join_request( join_request.id, music_session.id, - join_request.user.name, join_request.user.photo_url, notification_msg, notification.id, notification.created_at.to_s ) - # (3) send notification @@mq_router.publish_to_user(music_session.creator.id, msg) end - ################## JOIN REQUEST APPROVED ################## def send_join_request_approved(music_session, join_request) - # (1) save to database notification = Notification.new notification.description = NotificationTypes::JOIN_REQUEST_APPROVED notification.source_user_id = music_session.creator.id @@ -380,26 +390,22 @@ module JamRuby notification.session_id = music_session.id notification.save - # (2) create notification notification_msg = format_msg(notification.description, music_session.creator) + msg = @@message_factory.join_request_approved( join_request.id, music_session.id, - music_session.creator.name, music_session.creator.photo_url, notification_msg, notification.id, notification.created_at.to_s ) - # (3) send notification @@mq_router.publish_to_user(join_request.user.id, msg) end - ################## JOIN REQUEST REJECTED ################## def send_join_request_rejected(music_session, join_request) - # (1) save to database notification = Notification.new notification.description = NotificationTypes::JOIN_REQUEST_REJECTED notification.source_user_id = music_session.creator.id @@ -407,26 +413,255 @@ module JamRuby notification.session_id = music_session.id notification.save - # (2) create notification notification_msg = format_msg(notification.description, music_session.creator) + msg = @@message_factory.join_request_rejected( join_request.id, music_session.id, - music_session.creator.name, music_session.creator.photo_url, - notification_msg, notification.id, notification.created_at.to_s ) - # (3) send notification @@mq_router.publish_to_user(join_request.user.id, msg) end - ################## BAND INVITATION ################## + def send_session_join(music_session, connection, user) + + notification_msg = format_msg(NotificationTypes::SESSION_JOIN, user) + + msg = @@message_factory.session_join( + music_session.id, + user.photo_url, + notification_msg + ) + + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id}) + end + + def send_session_depart(music_session, client_id, user, recordingId = nil) + + notification_msg = format_msg(NotificationTypes::SESSION_DEPART, user) + + msg = @@message_factory.session_depart( + music_session.id, + user.photo_url, + notification_msg, + recordingId + ) + + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) + end + + def send_musician_session_join(music_session, connection, user) + + if music_session.musician_access || music_session.fan_access + + friends = Friendship.where(:friend_id => user.id) + user_followers = UserFollower.where(:user_id => user.id) + + # construct an array of User objects representing friends and followers + friend_users = friends.map { |fu| fu.user } + follower_users = user_followers.map { |uf| uf.follower } + friends_and_followers = friend_users.concat(follower_users).uniq + + # remove anyone in the session + friends_and_followers = friends_and_followers - music_session.users + notifications, online_ff, offline_ff = [], [], [] + notification_msg = format_msg(NotificationTypes::MUSICIAN_SESSION_JOIN, user) + + friends_and_followers.each do |ff| + if (ff.musician && music_session.musician_access) || (!ff.musician && music_session.fan_access) + notification = Notification.new + notification.description = NotificationTypes::MUSICIAN_SESSION_JOIN + notification.source_user_id = user.id + notification.target_user_id = ff.id + notification.save + + if ff.online + msg = @@message_factory.musician_session_join( + ff.id, + music_session.id, + user.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(ff.id, msg) + else + offline_ff << ff + end + end + end + + # send email notifications + unless offline_ff.empty? + UserMailer.musician_session_join(offline_ff.map! {|f| f.email}, notification_msg) + end + end + end + + def send_band_session_join(music_session, band) + + # if the session is private, don't send any notifications + if music_session.musician_access || music_session.fan_access + + band_followers = BandFollower.where(:band_id => band.id) + + notifications, online_followers, offline_followers = [], [], [] + notification_msg = format_msg(NotificationTypes::BAND_SESSION_JOIN, nil, band) + + band_followers.each do |bf| + if (bf.follower.musician && music_session.musician_access) || (!bf.follower.musician && music_session.fan_access) + notification = Notification.new + notification.band_id = band.id + notification.description = NotificationTypes::BAND_SESSION_JOIN + notification.target_user_id = bf.follower.id + notification.save + + if bf.follower.online + msg = @@message_factory.band_session_join( + bf.follower.id, + music_session.id, + band.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(bf.follower.id, msg) + else + offline_followers << bf.follower + end + end + end + + # send email notifications + unless offline_followers.empty? + UserMailer.band_session_join(offline_followers.map! {|f| f.email}, notification_msg) + end + end + end + + def send_musician_recording_saved(recording) + + user = recording.owner + + friends = Friendship.where(:friend_id => user.id) + user_followers = UserFollower.where(:user_id => user.id) + + # construct an array of User objects representing friends and followers + friend_users = friends.map { |fu| fu.friend } + follower_users = user_followers.map { |uf| uf.follower } + friends_and_followers = friend_users.concat(follower_users).uniq + + notifications, online_ff, offline_ff = [], [], [] + notification_msg = format_msg(NotificationTypes::MUSICIAN_RECORDING_SAVED, user) + + friends_and_followers.each do |ff| + notification = Notification.new + notification.description = NotificationTypes::MUSICIAN_SESSION_JOIN + notification.source_user_id = user.id + notification.target_user_id = ff.id + notification.save + + if ff.online + msg = @@message_factory.musician_recording_saved( + ff.id, + recording.id, + user.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(ff.id, notification_msg) + else + offline_ff << ff + end + end + + # send email notifications + unless offline_ff.empty? + UserMailer.musician_recording_saved(offline_ff.map! {|f| f.email}, notification_msg) + end + end + + def send_band_recording_saved(recording) + + band_followers = BandFollower.where(:band_id => band.id) + notification_msg = format_msg(NotificationTypes::BAND_RECORDING_SAVED, nil, recording.band) + + band_followers.each do |bf| + notification = Notification.new + notification.description = NotificationTypes::BAND_RECORDING_SAVED + notification.band_id = band.id + notification.target_user_id = bf.follower.id + notification.recording_id = recording.id + notification.save + + if bf.follower.online + msg = @@message_factory.band_recording_saved( + bf.follower.id, + recording.id, + band.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(of.id, notification_msg) + else + offline_followers << bf.follower + end + end + + # send email notifications + unless offline_followers.empty? + UserMailer.band_recording_saved(offline_followers.map! {|f| f.email}, notification_msg) + end + end + + def send_recording_started(music_session, connection, user) + + notification_msg = format_msg(NotificationTypes::RECORDING_STARTED, user) + + music_session.users.each do |musician| + if musician.id != user.id + msg = @@message_factory.recording_started( + musician.id, + user.photo_url, + notification_msg + ) + + @@mq_router.publish_to_user(musician.id, msg) + end + end + end + + def send_recording_ended(music_session, connection, user) + + notification_msg = format_msg(NotificationTypes::RECORDING_ENDED, user) + + music_session.users.each do |musician| + if musician.id != user.id + msg = @@message_factory.recording_ended( + musician.id, + user.photo_url, + notification_msg + ) + + @@mq_router.publish_to_user(musician.id, msg) + end + end + end + + def send_recording_master_mix_complete(recording) + end + def send_band_invitation(band, band_invitation, sender, receiver) - # (1) save to database notification = Notification.new notification.band_id = band.id notification.band_invitation_id = band_invitation.id @@ -435,28 +670,28 @@ module JamRuby notification.target_user_id = receiver.id notification.save - # (2) create notification - notification_msg = format_msg(notification.description, band) - msg = @@message_factory.band_invitation( - band_invitation.id, - band.id, - receiver.id, - sender.name, - sender.photo_url, - band.name, - notification_msg, - notification.id, - notification.created_at.to_s - ) + notification_msg = format_msg(notification.description, nil, band) - # (3) send notification - @@mq_router.publish_to_user(receiver.id, msg) + if receiver.online + msg = @@message_factory.band_invitation( + receiver.id, + band_invitation.id, + band.id, + sender.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + + @@mq_router.publish_to_user(receiver.id, msg) + + else + UserMailer.band_invitation(receiver.email, notification_msg) + end end - ################## BAND INVITATION ACCEPTED ################## def send_band_invitation_accepted(band, band_invitation, sender, receiver) - # (1) save to database notification = Notification.new notification.band_id = band.id notification.description = NotificationTypes::BAND_INVITATION_ACCEPTED @@ -464,23 +699,54 @@ module JamRuby notification.target_user_id = receiver.id notification.save - # (2) create notification - notification_msg = format_msg(notification.description, sender) - msg = @@message_factory.band_invitation_accepted( - band_invitation.id, - receiver.id, - sender.name, - sender.photo_url, - band.name, - notification_msg, - notification.id, - notification.created_at.to_s - ) + notification_msg = format_msg(notification.description, sender, band) - # (3) send notification - @@mq_router.publish_to_user(receiver.id, msg) + if receiver.online + msg = @@message_factory.band_invitation_accepted( + receiver.id, + band_invitation.id, + sender.photo_url, + notification_msg, + notification.id, + notification.created_at.to_s + ) + @@mq_router.publish_to_user(receiver.id, msg) + + else + UserMailer.band_invitation_accepted(receiver.email, notification_msg) + end end + def send_musician_session_fresh(music_session, client_id, user) + + msg = @@message_factory.musician_session_fresh( + music_session.id, + user.id, + user.name, + user.photo_url + ) + + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) + end + + def send_musician_session_stale(music_session, client_id, user) + + msg = @@message_factory.musician_session_stale( + music_session.id, + user.id, + user.name, + user.photo_url + ) + + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) + end + + + def send_download_available(user_id) + msg = @@message_factory.download_available + + @@mq_router.publish_to_user(user_id, msg) + end end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index 8b22e86a2..53302a141 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -8,6 +8,8 @@ module JamRuby self.table_name = "recorded_tracks" self.primary_key = 'id' + attr_accessible :discard + SOUND = %w(mono stereo) MAX_PART_FAILURES = 3 MAX_UPLOAD_FAILURES = 10 @@ -28,6 +30,10 @@ module JamRuby validate :validate_too_many_upload_failures + def can_download?(some_user) + !ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id).nil? + end + def upload_starting? next_part_to_upload_was == 0 && next_part_to_upload == 1 end @@ -80,7 +86,7 @@ module JamRuby recorded_track.next_part_to_upload = 0 recorded_track.file_offset = 0 recorded_track.save - recorded_track.url = construct_filename(recording.id, track.id) + recorded_track.url = construct_filename(recording.id, track.client_track_id) recorded_track.save recorded_track end @@ -154,12 +160,12 @@ module JamRuby def filename # construct a path for s3 - RecordedTrack.construct_filename(self.recording.id, self.track_id) + RecordedTrack.construct_filename(self.recording.id, self.client_track_id) end - def self.construct_filename(recording_id, track_id) - raise "unknown ID" unless track_id - "recordings/#{recording_id}/track-#{track_id}.ogg" + def self.construct_filename(recording_id, client_track_id) + raise "unknown ID" unless client_track_id + "recordings/#{recording_id}/track-#{client_track_id}.ogg" end end end diff --git a/ruby/lib/jam_ruby/models/recorded_track_observer.rb b/ruby/lib/jam_ruby/models/recorded_track_observer.rb index c8392f4cb..8ad806704 100644 --- a/ruby/lib/jam_ruby/models/recorded_track_observer.rb +++ b/ruby/lib/jam_ruby/models/recorded_track_observer.rb @@ -5,6 +5,8 @@ module JamRuby observe JamRuby::RecordedTrack def before_validation(recorded_track) + + # if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded if recorded_track.is_part_uploading_was && !recorded_track.is_part_uploading begin aws_part = recorded_track.s3_manager.multiple_upload_find_part(recorded_track.url, recorded_track.upload_id, recorded_track.next_part_to_upload - 1) @@ -22,10 +24,13 @@ module JamRuby end - # if we detect that this just became fully uploaded + # if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together if !recorded_track.fully_uploaded_was && recorded_track.fully_uploaded + + multipart_success = false begin recorded_track.s3_manager.multipart_upload_complete(recorded_track.url, recorded_track.upload_id) + multipart_success = true rescue SocketError => e raise # this should cause a 500 error, which is what we want. The client will retry later. rescue Exception => e @@ -34,6 +39,11 @@ module JamRuby recorded_track.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD) end + # tell all users that a download is available, except for the user who just uploaded + recorded_track.recording.users.each do |user| + Notification.send_download_available(recorded_track.user_id) unless user == recorded_track.user + end + end end @@ -41,6 +51,7 @@ module JamRuby end + # here we tick upload failure counts, or revert the state of the model, as needed def after_rollback(recorded_track) # if fully uploaded, don't increment failures if recorded_track.fully_uploaded diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 18b2054de..f96d4e23d 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -14,6 +14,8 @@ module JamRuby has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id validates :music_session, :presence => true validate :not_already_recording, :on => :create + validate :not_still_finalizing_previous, :on => :create + validate :not_playback_recording, :on => :create validate :already_stopped_recording def not_already_recording @@ -22,12 +24,51 @@ module JamRuby end end + def not_still_finalizing_previous + # after a recording is done, users need to keep or discard it. + # this checks if the previous recording is still being finalized + + unless music_session.is_recording? + previous_recording = music_session.most_recent_recording + if previous_recording + previous_recording.recorded_tracks.each do |recorded_track| + # if at least one user hasn't taken any action yet... + if recorded_track.discard.nil? + # and they are still around and in this music session still... + connection = Connection.find_by_client_id(recorded_track.client_id) + if !connection.nil? && connection.music_session == music_session + errors.add(:music_session, ValidationMessages::PREVIOUS_RECORDING_STILL_BEING_FINALIZED) + break + end + end + end + end + end + end + + def not_playback_recording + if music_session.is_playing_recording? + errors.add(:music_session, ValidationMessages::ALREADY_PLAYBACK_RECORDING) + end + end + def already_stopped_recording if is_done && is_done_was errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING) end end + def recorded_tracks_for_user(user) + unless self.users.exists?(user) + raise PermissionError, "user was not in this session" + end + recorded_tracks.where(:user_id=> user.id) + end + + def has_access?(user) + return users.exists?(user) + end + # Start recording a session. def self.start(music_session, owner) recording = nil @@ -46,16 +87,9 @@ module JamRuby end end end - - # FIXME: - # NEED TO SEND NOTIFICATION TO ALL USERS IN THE SESSION THAT RECORDING HAS STARTED HERE. - # I'LL STUB IT A BIT. NOTE THAT I REDO THE FIND HERE BECAUSE I DON'T WANT TO SEND THESE - # NOTIFICATIONS WHILE THE DB ROW IS LOCKED - #music_session = MusicSession.find(music_session_id) - #music_session.connections.each do |connection| - # # connection.notify_recording_has_started - #end + connection = Connection.where(:user_id => owner.id).where(:music_session_id => music_session.id).first + Notification.send_recording_started(music_session, connection, owner) recording end @@ -70,27 +104,21 @@ module JamRuby self.is_done = true self.save end + + connection = Connection.where(:user_id => self.owner.id).where(:music_session_id => music_session.id).first + Notification.send_recording_ended(music_session, connection, self.owner) + self end + # Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording. def claim(user, name, description, genre, is_public, is_downloadable) - # if self.users.include?(user) - # raise PermissionError, "user already claimed this recording" - # end unless self.users.exists?(user) raise PermissionError, "user was not in this session" - end - - if self.music_session.is_recording? - raise PermissionError, "recording cannot be claimed while it is being recorded" end - if name.nil? || genre.nil? || is_public.nil? || is_downloadable.nil? - raise PermissionError, "recording must have name, genre and flags" - end - claimed_recording = ClaimedRecording.new claimed_recording.user = user claimed_recording.recording = self @@ -101,8 +129,23 @@ module JamRuby claimed_recording.is_downloadable = is_downloadable self.claimed_recordings << claimed_recording + if claimed_recording.save + keep(user) + end + claimed_recording end + + # the user votes to keep their tracks for this recording + def keep(user) + recorded_tracks_for_user(user).update_all(:discard => false) + end + + + # the user votes to discard their tracks for this recording + def discard(user) + recorded_tracks_for_user(user).update_all(:discard => true) + end # Find out if all the tracks for this recording have been uploaded def uploaded? @@ -112,85 +155,53 @@ module JamRuby return true end - # Discards this recording and schedules deletion of all files associated with it. - def discard - self.destroy - end - - # Returns the list of files the user needs to upload. This will only ever be recordings - def self.upload_file_list(user) - files = [] - User.joins(:recordings).joins(:recordings => :recorded_tracks) - .where(%Q{ recordings.duration IS NOT NULL }) - .where("recorded_tracks.user_id = '#{user.id}'") - .where(%Q{ recorded_tracks.fully_uploaded = FALSE }).each do |user| - user.recordings.each.do |recording| - recording.recorded_tracks.each do |recorded_track| - files.push( - { - :type => "recorded_track", - :id => recorded_track.client_track_id, - :url => recorded_track.url # FIXME IS THIS RIGHT? - } - ) - end - end - files - end - def self.list_downloads(user, limit = 100, since = 0) since = 0 unless since || since == '' # guard against nil downloads = [] # That second join is important. It's saying join off of recordings, NOT user. If you take out the # ":recordings =>" part, you'll just get the recorded_tracks that I played. Very different! - User.joins(:recordings).joins(:recordings => :recorded_tracks) - .order(%Q{ recorded_tracks.id }) - .where(%Q{ recorded_tracks.fully_uploaded = TRUE }) + + # we also only allow you to be told about downloads if you have claimed the recording + #User.joins(:recordings).joins(:recordings => :recorded_tracks).joins(:recordings => :claimed_recordings) + RecordedTrack.joins(:recording).joins(:recording => :claimed_recordings) + .order('recorded_tracks.id') + .where('recorded_tracks.fully_uploaded = TRUE') .where('recorded_tracks.id > ?', since) - .where(:id => user.id).limit(limit).each do |theuser| - theuser.recordings.each do |recording| - recording.recorded_tracks.each do |recorded_track| - # recorded_track = user.claimed_recordings.first.recording.recorded_tracks.first - downloads.push( - { - :type => "recorded_track", - :id => recorded_track.client_track_id, - :recording_id => recording.id, - :length => recorded_track.length, - :md5 => recorded_track.md5, - :url => recorded_track.filename, - :next => recorded_track.id - } - ) - end - end + .where('claimed_recordings.user_id = ?', user).limit(limit).each do |recorded_track| + downloads.push( + { + :type => "recorded_track", + :id => recorded_track.client_track_id, + :recording_id => recorded_track.recording_id, + :length => recorded_track.length, + :md5 => recorded_track.md5, + :url => recorded_track.url, + :next => recorded_track.id + } + ) end latest_recorded_track = downloads[-1][:next] if downloads.length > 0 - # HOW TO LIMIT ON USER - User.joins(:recordings).joins(:recordings => :mixes) + Mix.joins(:recording).joins(:recording => :claimed_recordings) .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) - .limit(limit).each do |theuser| - theuser.recordings.each do |recording| - recording.mixes.each do |mix| - downloads.push( - { - :type => "mix", - :id => mix.id, - :recording_id => recording.id, - :length => mix.length, - :md5 => mix.md5, - :url => mix.filename, - :created_at => mix.created_at, - :next => mix.id - } - ) - end - end + .where('claimed_recordings.user_id = ?', user) + .limit(limit).each do |mix| + downloads.push( + { + :type => "mix", + :id => mix.id, + :recording_id => mix.recording_id, + :length => mix.length, + :md5 => mix.md5, + :url => mix.url, + :created_at => mix.created_at, + :next => mix.id + } + ) end latest_mix = downloads[-1][:next] if downloads.length > 0 diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index c2b138aed..60a3b4dd6 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -39,6 +39,7 @@ module JamRuby has_many :owned_recordings, :class_name => "JamRuby::Recording" has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording" has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user + has_many :playing_claimed_recordings, :class_name => "JamRuby::MusicSession", :inverse_of => :claimed_recording_initiator # user likers (a musician has likers and may have likes too; fans do not have likers) has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user @@ -548,6 +549,28 @@ module JamRuby self.save end + def create_user_following(user_id) + follower = UserFollower.new + follower.user_id = user_id + follower.follower_id = self.id + follower.save + + # TODO: make this async + user = User.find(user_id) + Notification.send_new_user_follower(self, user) + end + + def create_band_following(band_id) + follower = BandFollower.new + follower.band_id = band_id + follower.follower_id = self.id + follower.save + + # TODO: make this async + band = Band.find(band_id) + Notification.send_new_band_follower(self, band) + end + def self.finalize_update_email(update_email_token) # updates the user model to have a new email address user = User.find_by_update_email_token!(update_email_token) @@ -560,7 +583,6 @@ module JamRuby return user end - def self.create_user_like(user_id, liker_id) liker = UserLiker.new() liker.user_id = user_id @@ -578,7 +600,7 @@ module JamRuby end def self.create_band_like(band_id, liker_id) - liker = BandLiker.new() + liker = BandLiker.new liker.band_id = band_id liker.liker_id = liker_id liker.save @@ -588,13 +610,6 @@ module JamRuby JamRuby::BandLiker.delete_all "(band_id = '#{band_id}' AND liker_id = '#{liker_id}')" end - def self.create_user_following(user_id, follower_id) - follower = UserFollower.new() - follower.user_id = user_id - follower.follower_id = follower_id - follower.save - end - def self.delete_following(user_id, band_id, follower_id) if !user_id.nil? JamRuby::UserFollower.delete_all "(user_id = '#{user_id}' AND follower_id = '#{follower_id}')" @@ -604,19 +619,12 @@ module JamRuby end end - def self.create_band_following(band_id, follower_id) - follower = BandFollower.new() - follower.band_id = band_id - follower.follower_id = follower_id - follower.save - end - def self.delete_band_following(band_id, follower_id) JamRuby::BandFollower.delete_all "(band_id = '#{band_id}' AND follower_id = '#{follower_id}')" end def self.create_favorite(user_id, recording_id) - favorite = UserFavorite.new() + favorite = UserFavorite.new favorite.user_id = user_id favorite.recording_id = recording_id favorite.save diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index 8fb3b30a6..087bace44 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -89,7 +89,7 @@ class MQRouter def publish_to_friends(friend_ids, user_msg, from_user_id) EM.schedule do friend_ids.each do |friend_id| - @@log.debug "publishing to friend:#{friend_id} from user #{from_user_id}" + @@log.debug "publishing to friend:#{friend_id} from user/band #{from_user_id}" # put it on the topic exchange for users self.class.user_exchange.publish(user_msg, :routing_key => "user.#{friend_id}") end diff --git a/ruby/lib/jam_ruby/resque/audiomixer.rb b/ruby/lib/jam_ruby/resque/audiomixer.rb new file mode 100644 index 000000000..8bb0f5aa3 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/audiomixer.rb @@ -0,0 +1,57 @@ +require 'json' +require 'resque' + +module JamRuby + + @queue = :audiomixer + + class AudioMixer + + @@log = Logging.logger[AudioMixer] + + def self.perform(manifest) + audiomixer = AudioMixer.new + audiomixer.run(manifest) + + end + + def initialize + + end + + def validate + raise "no files specified" if !@manifest[:files] || @manifest[:files].length == 0 + end + + def fetch_audio_files + @manifest[:files].each do |file| + filename = file[:filename] + end + end + + def run(manifest) + + @manifest = manifest.symbolize_keys + + validate + + fetch_audio_files + + manifest_file = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{@manifest['recordingId']}", nil + File.open(manifest_file,"w") do |f| + f.write(@manifest.to_json) + end + + #{"files": [{"codec": "vorbis", "offset": 0, "filename": "TPD - bass.flac-stereo.ogg"}, {"codec": "vorbis", "offset": 0, "filename": "TPD - bg vox.flac-stereo.ogg"}, {"codec": "vorbis", "offset": 0, "filename": "TPD - drums.flac-stereo.ogg"}, {"codec": "vorbis", "offset": 0, "filename": "TPD - guitars.flac-stereo.ogg"}, {"codec": "vorbis", "offset": 0, "filename": "TPD - lead vox.flac-stereo.ogg"}], "output": {"codec": "vorbis", "filename": "mix.ogg"}, "timeline": [{"timestamp": 0, "mix": [{"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}]}]} + + + audiomixer_cmd = "#{APP_CONFIG.audiomixer_path} #{manifest_file}" + + @@log.debug("executing #{audiomixer_cmd}") + + system(audiomixer_cmd) + end + + end + +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 285dd5ad5..23043fcdf 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -32,6 +32,7 @@ FactoryGirl.define do approval_required false musician_access true legal_terms true + genres [JamRuby::Genre.first] association :creator, :factory => :user end diff --git a/ruby/spec/jam_ruby/lib/s3_util_spec.rb b/ruby/spec/jam_ruby/lib/s3_util_spec.rb deleted file mode 100644 index d0dbb07d9..000000000 --- a/ruby/spec/jam_ruby/lib/s3_util_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe S3Util do - - describe "sign_url" do - pending - it "returns something" do - S3Util.sign_url("jamkazam-dev", "avatar-tmp/user/image.png").should_not be_nil - end - end - -end - diff --git a/ruby/spec/jam_ruby/models/claimed_recording_spec.rb b/ruby/spec/jam_ruby/models/claimed_recording_spec.rb new file mode 100644 index 000000000..bca0b4b0e --- /dev/null +++ b/ruby/spec/jam_ruby/models/claimed_recording_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +def valid_claimed_recording + @name = "hello" + @description = "description" + @genre = Genre.first + @is_public = true + @is_downloadable = true +end + +def make_claim + @claimed_recording = @recording.claim(@user, @name, @description, @genre, @is_public, @is_downloadable) +end + +describe ClaimedRecording do + + before(:each) do + @user = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) + @music_session.connections << @connection + @music_session.save + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + + end + + describe "sucessful save" do + it "with default case" do + valid_claimed_recording + make_claim + @claimed_recording.errors.any?.should be_false + @recording.reload + @recording.recorded_tracks.first.discard.should be_false + end + end + + describe "update name" do + it "with nil" do + valid_claimed_recording + @name = nil + make_claim + @claimed_recording.errors.any?.should be_true + @claimed_recording.errors[:name].length.should == 2 + @claimed_recording.errors[:name].select { |value| value.include?("can't be blank") }.length.should == 1 + @claimed_recording.errors[:name].select { |value| value.include?("is too short") }.length.should == 1 + @recording.reload + @recording.recorded_tracks.first.discard.should be_nil + end + + it "too short" do + valid_claimed_recording + @name = "a" + make_claim + @claimed_recording.errors.any?.should be_true + @claimed_recording.errors[:name].length.should == 1 + @claimed_recording.errors[:name].select { |value| value.include?("is too short") }.length.should == 1 + end + end + + describe "update description" do + it "with nil" do + valid_claimed_recording + @description = nil + make_claim + @claimed_recording.errors.any?.should be_false + end + end + + describe "update is_public" do + it "with nil" do + valid_claimed_recording + @is_public = nil + make_claim + @claimed_recording.errors.any?.should be_true + @claimed_recording.errors[:is_public].length.should == 1 + @claimed_recording.errors[:is_public].should == ["is not included in the list"] + end + end + + describe "update is_downloadable" do + it "with nil" do + valid_claimed_recording + @is_downloadable = nil + make_claim + @claimed_recording.errors.any?.should be_true + @claimed_recording.errors[:is_downloadable].length.should == 1 + @claimed_recording.errors[:is_downloadable].should == ["is not included in the list"] + end + end + + describe "update genre" do + it "with nil" do + valid_claimed_recording + @genre = nil + make_claim + @claimed_recording.errors.any?.should be_true + @claimed_recording.errors[:genre].length.should == 1 + @claimed_recording.errors[:genre].should == ["can't be blank"] + end + end + + describe "multiple claims" do + it "not valid" do + valid_claimed_recording + make_claim + duplicate = @recording.claim(@user, "name", "description", @genre, true, true) + duplicate.valid?.should be_false + duplicate.errors[:recording_id].should == ['has already been taken'] + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb new file mode 100644 index 000000000..d5cd83588 --- /dev/null +++ b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe IcecastAdminAuthentication do + + let(:admin) { IcecastAdminAuthentication.new } + + before(:all) do + + end + + it "save" do + admin.save.should be_true + end + +end diff --git a/ruby/spec/jam_ruby/models/icecast_directory_spec.rb b/ruby/spec/jam_ruby/models/icecast_directory_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_limit_spec.rb b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb new file mode 100644 index 000000000..aa3c34bdf --- /dev/null +++ b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe IcecastLimit do + + let(:limit) { IcecastLimit.new } + + before(:all) do + + end + + it "save" do + limit.save.should be_true + end + + it "non-integer clients should be checked" do + limit.clients = "a" + limit.save.should be_false + limit.errors[:clients].should == ['is not a number'] + end +end diff --git a/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb b/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_logging_spec.rb b/ruby/spec/jam_ruby/models/icecast_logging_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_mastersvr_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_mastersvr_relay_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_mount_spec.rb b/ruby/spec/jam_ruby/models/icecast_mount_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_path_spec.rb b/ruby/spec/jam_ruby/models/icecast_path_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_relay_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_security_spec.rb b/ruby/spec/jam_ruby/models/icecast_security_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_server_spec.rb b/ruby/spec/jam_ruby/models/icecast_server_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_servermisc_spec.rb b/ruby/spec/jam_ruby/models/icecast_servermisc_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/icecast_user_authentication_spec.rb b/ruby/spec/jam_ruby/models/icecast_user_authentication_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb index c0d58e398..2053bf1c8 100755 --- a/ruby/spec/jam_ruby/models/mix_spec.rb +++ b/ruby/spec/jam_ruby/models/mix_spec.rb @@ -11,6 +11,8 @@ describe Mix do @music_session.save @recording = Recording.start(@music_session, @user) @recording.stop + @recording.claim(@user, "name", "description", Genre.first, true, true) + @recording.errors.any?.should be_false @mix = Mix.schedule(@recording, "{}") end @@ -62,6 +64,23 @@ describe Mix do @mix.sign_url.should_not be_nil end + it "mixes are restricted by user" do + + @mix.finish(1, "abc") + @mix.reload + @mix.errors.any?.should be_false + + @user2 = FactoryGirl.create(:user) + + recordings = Recording.list_downloads(@user)["downloads"] + recordings.length.should == 1 + recordings[0][:type].should == "mix" + recordings[0][:id].should == @mix.id + + recordings = Recording.list_downloads(@user2)["downloads"] + recordings.length.should == 0 + end + end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index a8c671208..32909739e 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -430,7 +430,54 @@ describe MusicSession do it "stop_recording should return recording object if recording" do @music_session.stop_recording.should == @recording end + end + describe "claim a recording" do + + before(:each) do + @recording = Recording.start(@music_session, @user1) + @recording.errors.any?.should be_false + @recording.stop + @recording.reload + @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true, true) + @claimed_recording.errors.any?.should be_false + end + + it "allow a claimed recording to be associated" do + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.claimed_recording.should == @claimed_recording + @music_session.claimed_recording_initiator.should == @user1 + end + + it "allow a claimed recording to be removed" do + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + @music_session.claimed_recording_stop + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.claimed_recording.should be_nil + @music_session.claimed_recording_initiator.should be_nil + end + + it "disallow a claimed recording to be started when already started by someone else" do + @user2 = FactoryGirl.create(:user) + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + @music_session.claimed_recording_start(@user2, @claimed_recording) + @music_session.errors.any?.should be_true + @music_session.errors[:claimed_recording] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS] + end + + it "allow a claimed recording to be started when already started by self" do + @user2 = FactoryGirl.create(:user) + @claimed_recording2 = @recording.claim(@user1, "name", "description", Genre.first, true, true) + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + @music_session.claimed_recording_start(@user1, @claimed_recording2) + @music_session.errors.any?.should be_false + end end end end diff --git a/ruby/spec/jam_ruby/models/recorded_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_track_spec.rb index 8a0e5ae12..ed2b74701 100644 --- a/ruby/spec/jam_ruby/models/recorded_track_spec.rb +++ b/ruby/spec/jam_ruby/models/recorded_track_spec.rb @@ -42,7 +42,7 @@ describe RecordedTrack do it "gets a url for the track" do @recorded_track = RecordedTrack.create_from_track(@track, @recording) @recorded_track.save.should be_true - @recorded_track.url.should == "recordings/#{@recording.id}/track-#{@track.id}.ogg" + @recorded_track.url.should == "recordings/#{@recording.id}/track-#{@track.client_track_id}.ogg" end it "signs url" do @@ -51,6 +51,18 @@ describe RecordedTrack do @recorded_track.sign_url.should_not be_nil end + it "can not be downloaded if no claimed recording" do + user2 = FactoryGirl.create(:user) + @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recorded_track.can_download?(user2).should be_false + @recorded_track.can_download?(@user).should be_false + end + + it "can be downloaded if there is a claimed recording" do + @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recording.claim(@user, "my recording", "my description", Genre.first, true, true).errors.any?.should be_false + @recorded_track.can_download?(@user).should be_true + end describe "aws-based operations", :aws => true do diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index b77c72492..7f34bb2c3 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -8,7 +8,26 @@ describe Recording do @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) - end + end + + + it "should allow finding of recorded tracks" do + user2 = FactoryGirl.create(:user) + connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session) + track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument) + + @recording = Recording.start(@music_session, @user) + user1_recorded_tracks = @recording.recorded_tracks_for_user(@user) + user1_recorded_tracks[0].should == @user.recorded_tracks[0] + user1_recorded_tracks.length.should == 1 + user2_recorded_tracks = @recording.recorded_tracks_for_user(user2) + user2_recorded_tracks.length.should == 1 + user2_recorded_tracks[0].should == user2.recorded_tracks[0] + + RecordedTrack.update(user1_recorded_tracks, :discard => true) + user1_recorded_tracks[0].reload + user1_recorded_tracks[0].discard.should be_true + end it "should set up the recording properly when recording is started with 1 user in the session" do @music_session.is_recording?.should be_false @@ -50,6 +69,7 @@ describe Recording do it "should be able to start, stop then start a recording again for the same music session" do @recording = Recording.start(@music_session, @user) @recording.stop + @recording.keep(@user) @recording2 = Recording.start(@music_session, @user) @music_session.recordings.exists?(@recording2).should be_true end @@ -118,16 +138,6 @@ describe Recording do expect { @recording.claim(user2, "name", "description", @genre, true, true) }.to raise_error end - it "should fail if a user tries to claim a recording twice" do - @recording = Recording.start(@music_session, @user) - @recording.stop - @recording.reload - @genre = FactoryGirl.create(:genre) - @recording.claim(@user, "name", "description", @genre, true, true) - @recording.reload - expect { @recording.claim(@user, "name", "description", @genre, true, true) }.to raise_error - end - it "should allow editing metadata for claimed recordings" do @recording = Recording.start(@music_session, @user) @recording.stop @@ -206,6 +216,21 @@ describe Recording do Recording.list_uploads(@user, 10, uploads["next"])["uploads"].length.should == 0 end + it "should return a download only if claimed" do + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "Recording", "Recording Description", @genre, true, true) + downloads = Recording.list_downloads(@user) + downloads["downloads"].length.should == 0 + @recorded_track = RecordedTrack.where(:recording_id => @recording.id)[0] + @recorded_track.update_attribute(:fully_uploaded, true) + downloads = Recording.list_downloads(@user) + downloads["downloads"].length.should == 1 + end + + it "should return a file list for a user properly" do pending stub_const("APP_CONFIG", app_config) @@ -292,6 +317,45 @@ describe Recording do timeline.last["timestamp"].should == @recording.duration timeline.last["end"].should == true end + + describe "chance for everyone to keep or discard" do + before(:each) do + @user2 = FactoryGirl.create(:user) + @connection2 = FactoryGirl.create(:connection, :user => @user2, :music_session => @music_session) + @track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument) + + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + end + + it "no one votes" do + @recording2 = Recording.start(@music_session, @user) + @recording2.errors.any?.should be_true + @recording2.errors[:music_session].should == [ValidationMessages::PREVIOUS_RECORDING_STILL_BEING_FINALIZED] + end + + it "only one discards" do + @recording.discard(@user) + @recording2 = Recording.start(@music_session, @user) + @recording2.errors.any?.should be_true + @recording2.errors[:music_session].should == [ValidationMessages::PREVIOUS_RECORDING_STILL_BEING_FINALIZED] + end + + it "everyone discards" do + @recording.discard(@user) + @recording.discard(@user2) + @recording2 = Recording.start(@music_session, @user) + @recording2.errors.any?.should be_false + end + + it "one discards, the other leaves the session" do + @recording.discard(@user) + @connection2.delete + @recording2 = Recording.start(@music_session, @user2) + @recording2.errors.any?.should be_false + end + end end diff --git a/ruby/spec/jam_ruby/mq_router_spec.rb b/ruby/spec/jam_ruby/mq_router_spec.rb index 2e1b36750..4de2f124c 100644 --- a/ruby/spec/jam_ruby/mq_router_spec.rb +++ b/ruby/spec/jam_ruby/mq_router_spec.rb @@ -31,12 +31,25 @@ describe MQRouter do music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1") music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :ip_address => "2.2.2.2", :client_id => "2") + + # this is necessary because other tests will call EM.schedule indirectly as they fiddle with AR models, since some of our + # notifications are tied to model activity. So, the issue here is that you'll have an unknown known amount of + # queued up messages ready to be sent to MQRouter (because EM.schedule will put deferred blocks onto @next_tick_queue), + # resulting in messages from other tests being sent to client_exchange or user_exchange + + # there is no API I can see to clear out the EM queue. so just open up the EM module and do it manually + module EM + @next_tick_queue = [] + end + EM.run do # mock up exchange MQRouter.client_exchange = double("client_exchange") + MQRouter.user_exchange = double("user_exchange") MQRouter.client_exchange.should_receive(:publish).with("a message", :routing_key => "client.#{music_session_member2.client_id}") + MQRouter.user_exchange.should_not_receive(:publish) @mq_router.user_publish_to_session(music_session, user1, "a message", :client_id => music_session_member1.client_id) diff --git a/ruby/spec/jam_ruby/resque/audiomixer_spec.rb b/ruby/spec/jam_ruby/resque/audiomixer_spec.rb new file mode 100644 index 000000000..f01519dbe --- /dev/null +++ b/ruby/spec/jam_ruby/resque/audiomixer_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests +describe AudioMixer do + + let(:audiomixer) { AudioMixer.new } + + describe "validate" do + it "no files specified" do + expect{ audiomixer.run({}) }.to raise_error("no files specified") + end + + it "no codec specified" do + expect{ audiomixer.run({ "files" => [ "offset" => 0, "filename" => "/some/path"] }) } + end + end + +end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 9a43ca42f..77cdf2ddd 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -78,6 +78,10 @@ Spork.prefork do DatabaseCleaner.clean end + config.after(:suite) do + wipe_s3_test_bucket + end + # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 822d07c51..53de28feb 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -1,8 +1,9 @@ +JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws_bucket accessor directly def app_config klass = Class.new do def aws_bucket - 'jamkazam-testing' + JAMKAZAM_TESTING_BUCKET end def aws_access_key_id @@ -12,7 +13,38 @@ def app_config def aws_secret_access_key 'h0V0ffr3JOp/UtgaGrRfAk25KHNiO9gm8Pj9m6v3' end + + def audiomixer_path + # you can specify full path to audiomixer with AUDIOMIXER_PATH env variable... + # or we check for audiomixer path in the user's workspace + # and finally the debian install path + ENV['AUDIOMIXER_PATH'] || audiomixer_workspace_path || "/var/lib/audiomixer/audiomixer/audiomixerapp" + end + + private + + def audiomixer_workspace_path + if ENV['WORKSPACE'] + dev_path = ENV['WORKSPACE'] + else + dev_path = ENV['HOME'] + end + + dev_path = "#{dev_path}/audiomixer/audiomixer/audiomixerapp" + dev_path if File.exist? dev_path + end + end return klass.new +end + +def wipe_s3_test_bucket + test_config = app_config + s3 = AWS::S3.new(:access_key_id => test_config.aws_access_key_id, + :secret_access_key => test_config.aws_secret_access_key) + test_bucket = s3.buckets[JAMKAZAM_TESTING_BUCKET] + if test_bucket.name == JAMKAZAM_TESTING_BUCKET + #test_bucket.clear! + end end \ No newline at end of file diff --git a/runweb b/runweb index 459c28606..8bb4a626d 100755 --- a/runweb +++ b/runweb @@ -1,5 +1,6 @@ #!/bin/bash pushd web +# run jam-web rails server bundle exec rails s popd diff --git a/update b/update index 50b3c776b..a5495fcb3 100755 --- a/update +++ b/update @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash set -e diff --git a/web/.ruby-version b/web/.ruby-version index abf2ccea0..cb506813e 100644 --- a/web/.ruby-version +++ b/web/.ruby-version @@ -1 +1 @@ -ruby-2.0.0-p247 +2.0.0-p247 diff --git a/web/Gemfile b/web/Gemfile index 0f5f3066e..8d5ca4f50 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -18,9 +18,10 @@ else gem 'jam_ruby', "0.1.#{ENV["BUILD_NUMBER"]}" gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" end - +gem 'builder' gem 'rails', '>=3.2.11' gem 'jquery-rails', '2.0.2' +gem 'jquery-ui-rails' gem 'bootstrap-sass', '2.0.4' gem 'bcrypt-ruby', '3.0.1' gem 'faker', '1.0.1' @@ -32,7 +33,7 @@ gem 'ruby-protocol-buffers', '1.2.2' gem 'pg', '0.15.1' gem 'compass-rails' gem 'rabl' # for JSON API development -gem 'gon' # for passthrough of Ruby variables to Javascript variables +gem 'gon', '~>4.1.0' # for passthrough of Ruby variables to Javascript variables gem 'eventmachine', '1.0.3' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' @@ -57,6 +58,7 @@ gem 'postgres-copy' gem 'geokit-rails' gem 'postgres_ext' gem 'haml-rails' +gem 'resque' gem 'quiet_assets', :group => :development @@ -64,6 +66,7 @@ gem "bugsnag" group :development, :test do gem 'rspec-rails' + gem "activerecord-import", "~> 0.4.1" gem 'guard-rspec', '0.5.5' gem 'jasmine', '1.3.1' gem 'pry' diff --git a/web/app/assets/images/content/icon_pausebutton.png b/web/app/assets/images/content/icon_pausebutton.png new file mode 100644 index 000000000..0df0af051 Binary files /dev/null and b/web/app/assets/images/content/icon_pausebutton.png differ diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index b648b7ec9..b6820a8b1 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -12,22 +12,42 @@ LOGIN_ACK : "LOGIN_ACK", LOGIN_MUSIC_SESSION : "LOGIN_MUSIC_SESSION", LOGIN_MUSIC_SESSION_ACK : "LOGIN_MUSIC_SESSION_ACK", - FRIEND_SESSION_JOIN : "FRIEND_SESSION_JOIN", - MUSICIAN_SESSION_JOIN : "MUSICIAN_SESSION_JOIN", - MUSICIAN_SESSION_DEPART : "MUSICIAN_SESSION_DEPART", LEAVE_MUSIC_SESSION : "LEAVE_MUSIC_SESSION", LEAVE_MUSIC_SESSION_ACK : "LEAVE_MUSIC_SESSION_ACK", HEARTBEAT : "HEARTBEAT", HEARTBEAT_ACK : "HEARTBEAT_ACK", + + // friend notifications FRIEND_UPDATE : "FRIEND_UPDATE", + FRIEND_REQUEST : "FRIEND_REQUEST", + FRIEND_REQUEST_ACCEPTED : "FRIEND_REQUEST_ACCEPTED", + FRIEND_SESSION_JOIN : "FRIEND_SESSION_JOIN", + NEW_USER_FOLLOWER : "NEW_USER_FOLLOWER", + NEW_BAND_FOLLOWER : "NEW_BAND_FOLLOWER", + + // session notifications SESSION_INVITATION : "SESSION_INVITATION", + SESSION_ENDED : "SESSION_ENDED", JOIN_REQUEST : "JOIN_REQUEST", JOIN_REQUEST_APPROVED : "JOIN_REQUEST_APPROVED", JOIN_REQUEST_REJECTED : "JOIN_REQUEST_REJECTED", - FRIEND_REQUEST : "FRIEND_REQUEST", - FRIEND_REQUEST_ACCEPTED : "FRIEND_REQUEST_ACCEPTED", + SESSION_JOIN : "SESSION_JOIN", + SESSION_DEPART : "SESSION_DEPART", + MUSICIAN_SESSION_JOIN : "MUSICIAN_SESSION_JOIN", + BAND_SESSION_JOIN : "BAND_SESSION_JOIN", + + // recording notifications + MUSICIAN_RECORDING_SAVED : "MUSICIAN_RECORDING_SAVED", + BAND_RECORDING_SAVED : "BAND_RECORDING_SAVED", + RECORDING_STARTED : "RECORDING_STARTED", + RECORDING_ENDED : "RECORDING_ENDED", + RECORDING_MASTER_MIX_COMPLETE : "RECORDING_MASTER_MIX_COMPLETE", + DOWNLOAD_AVAILABLE : "DOWNLOAD_AVAILABLE", + + // band notifications BAND_INVITATION : "BAND_INVITATION", BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED", + TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE", PING_REQUEST : "PING_REQUEST", PING_ACK : "PING_ACK", @@ -84,10 +104,10 @@ // reconnect_music_session_id is an optional argument that allows the session to be immediately associated // with a music session. factory.login_with_token = function(token, reconnect_music_session_id) { - //context.JK.logger.debug("*** login_with_token: client_id = "+$.cookie("client_id")); + //context.JK.logger.debug("*** login_with_token: client_id = "+$.cookie("client_id")); var login = { token : token, - client_id : $.cookie("client_id") - }; + client_id : $.cookie("client_id") + }; return client_container(msg.LOGIN, route_to.SERVER, login); }; diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 56d992936..21d78399b 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -21,7 +21,7 @@ server.connected = false; - // handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect + // handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect function closedCleanup(in_error) { if(server.connected) { server.connected = false; @@ -61,10 +61,10 @@ break; } } - } - if (server.dispatchTable[messageType].length === 0) { - delete server.dispatchTable[messageType]; + if (server.dispatchTable[messageType].length === 0) { + delete server.dispatchTable[messageType]; + } } }; diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 85491c39c..615bdf708 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -36,6 +36,7 @@ city: userDetail.city, first_name: userDetail.first_name, last_name: userDetail.last_name, + photoUrl: context.JK.resolveAvatarUrl(userDetail.photo_url), user_instruments: userDetail.instruments, birth_date : userDetail.birth_date, gender: userDetail.gender, diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 581b7306a..8f7f4b573 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -12,11 +12,14 @@ // //= require jquery //= require jquery_ujs +//= require jquery.ui.draggable +//= require jquery.bt //= require jquery.icheck //= require jquery.color //= require jquery.cookie //= require jquery.Jcrop //= require jquery.naturalsize //= require jquery.queryparams +//= require jquery.timeago //= require globals //= require_directory . diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 5752a35e4..8bb64150e 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -62,6 +62,11 @@ $("#band-setup-step-1").show(); $("#band-setup-step-2").hide(); + + $('#band-invitee-input') + .unbind('blur') + .attr("placeholder", "Looking up friends...") + .prop('disabled', true) } function resetGenres() { @@ -290,25 +295,34 @@ // TODO: this is repeated in createSession.js function loadFriends() { - var friends = rest.getFriends({ id: context.JK.currentUserId }); - $.each(friends, function() { - userNames.push(this.name); - userIds.push(this.id); - userPhotoUrls.push(this.photo_url); - }); + 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 - }; - if (!autoComplete) { - autoComplete = $('#band-invitee-input').autocomplete(autoCompleteOptions); - } - else { - autoComplete.setOptions(autoCompleteOptions); - } + var autoCompleteOptions = { + lookup: { suggestions: userNames, data: userIds }, + onSelect: addInvitation + }; - $(".autocomplete").width("150px"); + $('#band-invitee-input').attr("placeholder", "Type a friend\'s name").prop('disabled', false); + + if (!autoComplete) { + autoComplete = $('#band-invitee-input').autocomplete(autoCompleteOptions); + } + else { + autoComplete.setOptions(autoCompleteOptions); + } + + $(".autocomplete").width("150px"); + }) + .fail(function() { + $('#band-invitee-input').attr("placeholder", "Unable to lookup friends"); + app.ajaxError(arguments) + }); } function loadGenres(selectedGenres) { @@ -455,14 +469,7 @@ $('#selected-band-invitees').on("click", ".invitation a", removeInvitation); // friend input focus - $('#band-invitee-input').focus(function() { - $(this).val(''); - }); - - // friend input blur - $('#band-invitee-input').blur(function() { - $(this).val('Type a friend\'s name'); - }); + $('#band-invitee-input').focus(function() { $(this).val(''); }); $('#btn-band-setup-cancel').click(function() { resetForm(); diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 3856af5d1..6a6f1a9dc 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -28,25 +28,34 @@ function afterShow(data) { friendSelectorDialog.setCallback(friendSelectorCallback); - var friends = rest.getFriends({ id: context.JK.currentUserId }); - $.each(friends, function() { - userNames.push(this.name); - userIds.push(this.id); - userPhotoUrls.push(this.photo_url); - }); + 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 - }; - if (!autoComplete) { - autoComplete = $('#friend-input').autocomplete(autoCompleteOptions); - } - else { - autoComplete.setOptions(autoCompleteOptions); - } + var autoCompleteOptions = { + lookup: { suggestions: userNames, data: userIds }, + onSelect: addInvitation + }; - $(".autocomplete").width("150px"); + $('#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) { @@ -101,6 +110,11 @@ var fan_chat = sessionSettings.hasOwnProperty('fan_chat') ? sessionSettings.fan_chat : false; $('#fan-chat-option-' + fan_chat).iCheck('check').attr('checked', 'checked'); } + + $('#friend-input') + .unbind('blur') + .attr("placeholder", "Looking up friends...") + .prop('disabled', true) // Should easily be able to grab other items out of sessionSettings and put them into the appropriate ui elements. } @@ -267,6 +281,7 @@ } 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); @@ -288,15 +303,7 @@ invitationDialog.showFacebookDialog(); }); - // friend input focus - $('#friend-input').focus(function() { - $(this).val(''); - }); - - // friend input blur - $('#friend-input').blur(function() { - $(this).val('Type a friend\'s name'); - }); + $('#friend-input').focus(function() { $(this).val(''); }) } function toggleMusicianAccess() { diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index 951ee5e4c..4fce20c60 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -11,21 +11,25 @@ var $draggingFaderHandle = null; var $draggingFader = null; + var draggingOrientation = null; var subscribers = {}; var logger = g.JK.logger; - var MAX_VISUAL_FADER = 95; - function faderClick(evt) { - evt.stopPropagation(); - if (g.JK.$draggingFaderHandle) { - return; + function faderClick(e) { + e.stopPropagation(); + + var $fader = $(this); + draggingOrientation = $fader.attr('orientation'); + var faderId = $fader.attr("fader-id"); + var offset = $fader.offset(); + var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} + + var faderPct = faderValue($fader, e, position); + + if (faderPct < 0 || faderPct > 100) { + return false; } - var $fader = $(evt.currentTarget); - var faderId = $fader.closest('[fader-id]').attr("fader-id"); - var $handle = $fader.find('div[control="fader-handle"]'); - - var faderPct = faderValue($fader, evt); // Notify subscribers of value change g._.each(subscribers, function(changeFunc, index, list) { @@ -39,75 +43,35 @@ } function setHandlePosition($fader, value) { - if (value > MAX_VISUAL_FADER) { value = MAX_VISUAL_FADER; } // Visual limit + var ratio, position; var $handle = $fader.find('div[control="fader-handle"]'); var handleCssAttribute = getHandleCssAttribute($fader); - $handle.css(handleCssAttribute, value + '%'); - } - - function faderHandleDown(evt) { - evt.stopPropagation(); - $draggingFaderHandle = $(evt.currentTarget); - $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); - return false; - } - - function faderMouseUp(evt) { - evt.stopPropagation(); - if ($draggingFaderHandle) { - var $fader = $draggingFaderHandle.closest('div[control="fader"]'); - var faderId = $fader.closest('[fader-id]').attr("fader-id"); - var faderPct = faderValue($fader, evt); - // Notify subscribers of value change - g._.each(subscribers, function(changeFunc, index, list) { - if (faderId === index) { - changeFunc(faderId, faderPct, false); - } - }); - $draggingFaderHandle = null; - $draggingFader = null; + if(draggingOrientation === "horizontal") { + ratio = value / 100; + position = ((ratio * $fader.width()) - (ratio * handleWidth(draggingOrientation))) + 'px'; } - return false; + else { + ratio = (100 - value) / 100; + position = ((ratio * $fader.height()) - (ratio * handleWidth(draggingOrientation))) + 'px'; + } + $handle.css(handleCssAttribute, position); } - function faderValue($fader, evt) { + function faderValue($fader, e, offset) { var orientation = $fader.attr('orientation'); var getPercentFunction = getVerticalFaderPercent; - var absolutePosition = evt.clientY; + var relativePosition = offset.top; if (orientation && orientation == 'horizontal') { getPercentFunction = getHorizontalFaderPercent; - absolutePosition = evt.clientX; + relativePosition = offset.left; } - return getPercentFunction(absolutePosition, $fader); + return getPercentFunction(relativePosition, $fader); } function getHandleCssAttribute($fader) { var orientation = $fader.attr('orientation'); - return (orientation === 'horizontal') ? 'left' : 'bottom'; - } - - function faderMouseMove(evt) { - // bail out early if there's no in-process drag - if (!($draggingFaderHandle)) { - return false; - } - var $fader = $draggingFader; - var faderId = $fader.closest('[fader-id]').attr("fader-id"); var $handle = $draggingFaderHandle; - evt.stopPropagation(); - var faderPct = faderValue($fader, evt); - - // Notify subscribers of value change - g._.each(subscribers, function(changeFunc, index, list) { - if (faderId === index) { - changeFunc(faderId, faderPct, true); - } - }); - - if (faderPct > MAX_VISUAL_FADER) { faderPct = MAX_VISUAL_FADER; } // Visual limit - var handleCssAttribute = getHandleCssAttribute($fader); - $handle.css(handleCssAttribute, faderPct + '%'); - return false; + return (orientation === 'horizontal') ? 'left' : 'top'; } function getVerticalFaderPercent(eventY, $fader) { @@ -119,28 +83,75 @@ } /** - * Returns the current value of the fader as int percent 0-100 - */ + * Returns the current value of the fader as int percent 0-100 + */ function getFaderPercent(value, $fader, orientation) { - var faderPosition = $fader.offset(); - var faderMin = faderPosition.top; - var faderSize = $fader.height(); - var handleValue = (faderSize - (value-faderMin)); + var faderSize, faderPct; + + // the handle takes up room, and all calculations use top. So when the + // handle *looks* like it's at the bottom by the user, it won't give a 0% value. + // so, we subtract handleWidth from the size of it's parent + if (orientation === "horizontal") { - faderMin = faderPosition.left; faderSize = $fader.width(); - handleValue = (value - faderMin); + faderPct = Math.round( ( value + (value / faderSize * handleWidth(orientation))) / faderSize * 100); } - var faderPct = Math.round(handleValue/faderSize * 100); - if (faderPct < 0) { - faderPct = 0; - } - if (faderPct > 100) { - faderPct = 100; + else { + faderSize = $fader.height(); + faderPct = Math.round((faderSize - handleWidth(orientation) - value)/(faderSize - handleWidth(orientation)) * 100); } + return faderPct; } + function onFaderDrag(e, ui) { + var faderId = $draggingFader.attr("fader-id"); + var faderPct = faderValue($draggingFader, e, ui.position); + + // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows + if (faderPct < 0 || faderPct > 100) { + return false; + } + + // Notify subscribers of value change + g._.each(subscribers, function(changeFunc, index, list) { + if (faderId === index) { + changeFunc(faderId, faderPct, true); + } + }); + } + + function onFaderDragStart(e, ui) { + $draggingFaderHandle = $(this); + $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); + draggingOrientation = $draggingFader.attr('orientation'); + } + + function onFaderDragStop(e, ui) { + var faderId = $draggingFader.attr("fader-id"); + var faderPct = faderValue($draggingFader, e, ui.position); + + // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows + // do not return 'false' though, because that stops future drags from working, for some reason + if (faderPct < 0 || faderPct > 100) { + return; + } + + // Notify subscribers of value change + g._.each(subscribers, function(changeFunc, index, list) { + if (faderId === index) { + changeFunc(faderId, faderPct, false); + } + }); + $draggingFaderHandle = null; + $draggingFader = null; + draggingOrientation = null; + } + + function handleWidth(orientation) { + return orientation === "horizontal" ? 8 : 11; + } + g.JK.FaderHelpers = { /** @@ -174,6 +185,15 @@ var templateSource = $(templateSelector).html(); $(selector).html(g._.template(templateSource, options)); + + $('div[control="fader-handle"]', $(selector)).draggable({ + drag: onFaderDrag, + start: onFaderDragStart, + stop: onFaderDragStop, + containment: "parent", + axis: options.faderType === 'horizontal' ? 'x' : 'y' + }) + // Embed any custom styles, applied to the .fader below selector if ("style" in options) { for (var key in options.style) { @@ -213,11 +233,6 @@ initialize: function() { $('body').on('click', 'div[control="fader"]', faderClick); - $('body').on('mousedown', 'div[control="fader-handle"]', faderHandleDown); - $('body').on('mousemove', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseMove); - $('body').on('mouseup', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseUp); - //$('body').on('mousemove', faderMouseMove); - //$('body').on('mouseup', faderMouseUp); } }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 5e304714a..96ee3781c 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -283,6 +283,10 @@ ]; } + function RegisterRecordingManagerCallbacks(commandStart, commandProgress, commandStop, commandsChanged) { + + } + function RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) { fakeJamClientRecordings.RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, abortedRecordingCallbackName); } @@ -308,7 +312,9 @@ function SessionStartRecording() {} function SessionStopPlay() {} function SessionStopRecording() {} - + function isSessionTrackPlaying() { return false; } + function SessionCurrrentPlayPosMs() { return 0; } + function SessionGetTracksPlayDurationMs() { return 0; } function SessionGetDeviceLatency() { return 10.0; } function SessionGetMasterLocalMix() { @@ -542,6 +548,29 @@ } + // passed an array of recording objects from the server + function GetLocalRecordingState(recordings) { + var result = { recordings:[]}; + var recordingResults = result.recordings; + + var possibleAnswers = ['HQ', 'RT', 'MISSING', 'PARTIALLY_MISSING']; + + $.each(recordings, function(i, recording) { + // just make up a random yes-hq/yes-rt/missing answer + var recordingResult = {}; + recordingResult['aggregate_state'] = possibleAnswers[Math.floor((Math.random()*4))]; + recordingResults.push(recordingResult); + }) + + return result; + } + + function OpenRecording(claimedRecording) { + return {success: true} + } + function CloseRecording() {} + function OnDownloadAvailable() {} + // Javascript Bridge seems to camel-case // Set the instance functions: @@ -609,6 +638,7 @@ this.SessionAddTrack = SessionAddTrack; this.SessionGetControlState = SessionGetControlState; this.SessionGetIDs = SessionGetIDs; + this.RegisterRecordingManagerCallbacks = RegisterRecordingManagerCallbacks; this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; this.SessionRegisterCallback = SessionRegisterCallback; this.SessionSetAlertCallback = SessionSetAlertCallback; @@ -619,6 +649,10 @@ this.SessionStartRecording = SessionStartRecording; this.SessionStopPlay = SessionStopPlay; this.SessionStopRecording = SessionStopRecording; + this.isSessionTrackPlaying = isSessionTrackPlaying; + this.SessionCurrrentPlayPosMs = SessionCurrrentPlayPosMs; + this.SessionGetTracksPlayDurationMs = SessionGetTracksPlayDurationMs; + this.SetVURefreshRate = SetVURefreshRate; this.SessionGetMasterLocalMix = SessionGetMasterLocalMix; this.SessionSetMasterLocalMix = SessionSetMasterLocalMix; @@ -663,6 +697,12 @@ this.OnLoggedIn = OnLoggedIn; this.OnLoggedOut = OnLoggedOut; + // Recording Playback + this.GetLocalRecordingState = GetLocalRecordingState; + this.OpenRecording = OpenRecording; + this.CloseRecording = CloseRecording; + this.OnDownloadAvailable = OnDownloadAvailable; + // fake calls; not a part of the actual jam client this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; diff --git a/web/app/assets/javascripts/findBand.js b/web/app/assets/javascripts/findBand.js index 2475521d6..9b138d09d 100644 --- a/web/app/assets/javascripts/findBand.js +++ b/web/app/assets/javascripts/findBand.js @@ -18,7 +18,6 @@ $.ajax({ type: "GET", url: "/api/search.json?" + queryString, - async: true, success: afterLoadBands, error: app.ajaxError }); diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index 7363ed50f..cdd36acb0 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -1,15 +1,15 @@ (function(context,$) { - "use strict"; + "use strict"; - context.JK = context.JK || {}; - context.JK.FindMusicianScreen = function(app) { + context.JK = context.JK || {}; + context.JK.FindMusicianScreen = function(app) { var logger = context.JK.logger; var musicians = {}; var musicianList; var instrument_logo_map = context.JK.getInstrumentIconMap24(); - var did_show_musician_page = false; - var page_num=1, page_count=0; + var did_show_musician_page = false; + var page_num=1, page_count=0; function loadMusicians(queryString) { // squelch nulls and undefines @@ -18,164 +18,171 @@ $.ajax({ type: "GET", url: "/api/search.json?" + queryString, - async: true, success: afterLoadMusicians, error: app.ajaxError }); } function search() { - did_show_musician_page = true; + did_show_musician_page = true; var queryString = 'srch_m=1&page='+page_num+'&'; // order by - var orderby = $('#musician_order_by').val(); + var orderby = $('#musician_order_by').val(); if (typeof orderby != 'undefined' && orderby.length > 0) { queryString += "orderby=" + orderby + '&'; } + // instrument filter - var instrument = $('#musician_instrument').val(); + var instrument = $('#musician_instrument').val(); if (typeof instrument != 'undefined' && !(instrument === '')) { queryString += "instrument=" + instrument + '&'; } + // distance filter var query_param = $('#musician_query_distance').val(); if (query_param !== null && query_param.length > 0) { - var matches = query_param.match(/(\d+)/); - if (0 < matches.length) { - var distance = matches[0]; - queryString += "distance=" + distance + '&'; - } + var matches = query_param.match(/(\d+)/); + if (0 < matches.length) { + var distance = matches[0]; + queryString += "distance=" + distance + '&'; + } } loadMusicians(queryString); } function refreshDisplay() { - clearResults(); - search(); + clearResults(); + search(); } function afterLoadMusicians(mList) { // display the 'no musicians' banner if appropriate var $noMusiciansFound = $('#musicians-none-found'); - musicianList = mList; + musicianList = mList; if(musicianList.length == 0) { $noMusiciansFound.show(); - musicians = []; - } else { + musicians = []; + } + else { $noMusiciansFound.hide(); - musicians = musicianList['musicians']; - if (!(typeof musicians === 'undefined')) { - $('#musician-filter-city').text(musicianList['city']); - if (0 == page_count) { - page_count = musicianList['page_count']; - } - renderMusicians(); - } + musicians = musicianList['musicians']; + if (!(typeof musicians === 'undefined')) { + $('#musician-filter-city').text(musicianList['city']); + if (0 == page_count) { + page_count = musicianList['page_count']; + } + renderMusicians(); + } } } - /** - * Render a list of musicians - */ function renderMusicians() { var ii, len; - var mTemplate = $('#template-find-musician-row').html(); - var fTemplate = $('#template-musician-follow-info').html(); - var aTemplate = $('#template-musician-action-btns').html(); - var mVals, mm, renderings=''; - var instr_logos, instr; - var follows, followVals, aFollow; - - for (ii=0, len=musicians.length; ii < len; ii++) { - mm = musicians[ii]; - instr_logos = ''; - for (var jj=0, ilen=mm['instruments'].length; jj '; - } - follows = ''; - followVals = {}; - for (var jj=0, ilen=mm['followings'].length; jj= $(this)[0].scrollHeight) { - if (page_num < page_count) { - page_num += 1; - search(); - } + if (page_num < page_count) { + page_num += 1; + search(); + } } }); } - /** - * Initialize, - */ function initialize() { var screenBindings = { 'beforeShow': beforeShow, @@ -232,5 +236,4 @@ return this; }; - })(window,jQuery); diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index bc063cc73..9c96cbe80 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -45,7 +45,6 @@ $.ajax({ type: "GET", url: "/api/sessions?" + queryString, - async: true, success: afterLoadSessions, complete: removeSpinner, error: app.ajaxError diff --git a/web/app/assets/javascripts/friendSelector.js b/web/app/assets/javascripts/friendSelector.js index a2ac8c721..ce3aac5eb 100644 --- a/web/app/assets/javascripts/friendSelector.js +++ b/web/app/assets/javascripts/friendSelector.js @@ -18,31 +18,33 @@ $('#friend-selector-list').empty(); var template = $('#template-friend-selection').html(); - var friends = rest.getFriends({ id: context.JK.currentUserId }); - $.each(friends, function(index, val) { - var id = val.id; - var isSelected = selectedIds[id]; + var friends = rest.getFriends({ id: context.JK.currentUserId }) + .done(function(friends) { + $.each(friends, function(index, val) { + var id = val.id; + var isSelected = selectedIds[id]; - var html = context.JK.fillTemplate(template, { - userId: id, - css_class: isSelected ? 'selected' : '', - userName: val.name, - avatar_url: context.JK.resolveAvatarUrl(val.photo_url), - status: "", - status_img_url: "", - check_mark_display: isSelected ? "block" : "none", - status_img_display: "none" - }); + var html = context.JK.fillTemplate(template, { + userId: id, + css_class: isSelected ? 'selected' : '', + userName: val.name, + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + status: "", + status_img_url: "", + check_mark_display: isSelected ? "block" : "none", + status_img_display: "none" + }); - $('#friend-selector-list').append(html); + $('#friend-selector-list').append(html); - // disable row click if it was chosen on parent screen - if (!isSelected) { - $('#friend-selector-list tr[user-id="' + id + '"]').click(function() { - updateSelectionList(id, val.name, $(this), $(this).find('img[user-id="' + id + '"]')); + // disable row click if it was chosen on parent screen + if (!isSelected) { + $('#friend-selector-list tr[user-id="' + id + '"]').click(function() { + updateSelectionList(id, val.name, $(this), $(this).find('img[user-id="' + id + '"]')); + }); + } }); - } - }); + }).fail(app.ajaxError); } function updateSelectionList(id, name, tr, img) { diff --git a/web/app/assets/javascripts/genreSelector.js b/web/app/assets/javascripts/genreSelector.js index 0c75e8575..7022cee5a 100644 --- a/web/app/assets/javascripts/genreSelector.js +++ b/web/app/assets/javascripts/genreSelector.js @@ -65,6 +65,7 @@ $.each(genreList, function(index, value) { values.push(value.toLowerCase()); }); + console.log("OH HAI O") var selectedVal = $('select', parentSelector).val(values); } diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index f0ad75ebc..556512a40 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -93,7 +93,7 @@ band_id: bandId, user_id: userId }; - + return $.ajax({ type: "POST", dataType: "json", @@ -313,25 +313,113 @@ } function getFriends(options) { - var friends = []; var id = getId(options); - $.ajax({ + return $.ajax({ type: "GET", - async: false, url: '/api/users/' + id + '/friends', dataType: 'json' - }).done(function(response) { - friends = response; }); - return friends; } + function removeFriend(options) { + var id = getId(options); + var friendId = options["friend_id"]; + + return $.ajax({ + type: "DELETE", + dataType: "json", + url: "/api/users/" + id + "/friends/" + friendId, + processData: false + }); + } + + function addFollowing(options) { + var id = getId(options); + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/" + id + "/followings", + data: JSON.stringify(options), + processData: false + }); + } + + function removeFollowing(options) { + var id = getId(options); + + return $.ajax({ + type: "DELETE", + dataType: "json", + contentType: 'application/json', + url: "/api/users/" + id + "/followings", + data: JSON.stringify(options), + processData: false + }); + } + + function getFollowings(options) { + var userId = getId(options); + + // FOLLOWINGS (USERS) + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + userId + "/followings", + processData:false + }); + } + + function getFollowers(options) { + var userId = getId(options); + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + userId + "/followers", + processData:false + }); + } + + function getBandFollowings(options) { + var userId = getId(options); + + // FOLLOWINGS (BANDS) + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + userId + "/band_followings", + processData:false + }); + } + + function getBandFollowing(options) { + var id = getId(options); + var bandId = options["band_id"]; + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + id + "/band_followings/" + bandId, + processData: false + }); + } + + function getBands(options) { + var userId = getId(options); + + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + userId + "/bands", + processData:false + }); + } function getMusicianFollowers(userId) { } function getBandFollowers(bandId) { - + } function getClientDownloads(options) { @@ -358,6 +446,10 @@ if(!id) { id = context.JK.currentUserId; } + else { + delete options["id"]; + } + return id; } @@ -504,6 +596,71 @@ dataType: "json", contentType: 'application/json', url: "/api/recordings/" + recordingId + }); + } + + function getClaimedRecordings(options) { + + return $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: "/api/claimed_recordings", + data: options + }); + } + + function claimRecording(options) { + var recordingId = options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + "/claim", + data: JSON.stringify(options) + }) + } + + function startPlayClaimedRecording(options) { + var musicSessionId = options["id"]; + var claimedRecordingId = options["claimed_recording_id"]; + delete options["id"]; + delete options["claimed_recording_id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/claimed_recording/" + claimedRecordingId + "/start", + data: JSON.stringify(options) + }) + } + + function stopPlayClaimedRecording(options) { + var musicSessionId = options["id"]; + var claimedRecordingId = options["claimed_recording_id"]; + delete options["id"]; + delete options["claimed_recording_id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/claimed_recording/" + claimedRecordingId + "/stop", + data: JSON.stringify(options) + }) + } + + function discardRecording(options) { + var recordingId = options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + "/discard", + data: JSON.stringify(options) }) } @@ -539,9 +696,17 @@ this.deleteAvatar = deleteAvatar; this.getFilepickerPolicy = getFilepickerPolicy; this.getFriends = getFriends; + this.removeFriend = removeFriend; + this.addFollowing = addFollowing; + this.removeFollowing = removeFollowing; + this.getFollowings = getFollowings; + this.getFollowers = getFollowers; + this.getBandFollowings = getBandFollowings; + this.getBandFollowing = getBandFollowing; + this.getBands = getBands; this.updateSession = updateSession; this.getSession = getSession; - this.getClientDownloads = getClientDownloads + this.getClientDownloads = getClientDownloads; this.createInvitation = createInvitation; this.postFeedback = postFeedback; this.serverHealthCheck = serverHealthCheck; @@ -556,6 +721,11 @@ this.startRecording = startRecording; this.stopRecording = stopRecording; this.getRecording = getRecording; + this.getClaimedRecordings = getClaimedRecordings; + this.claimRecording = claimRecording; + this.startPlayClaimedRecording = startPlayClaimedRecording; + this.stopPlayClaimedRecording = stopPlayClaimedRecording; + this.discardRecording = discardRecording; this.putTrackSyncChange = putTrackSyncChange; this.createBand = createBand; this.updateBand = updateBand; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 92673c57f..d9ec02fba 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -130,6 +130,13 @@ } } + /** + * This occurs when a new download from a recording has become available + */ + function downloadAvailable() { + context.jamClient.OnDownloadAvailable(); + } + /** * Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared * @param in_error did the socket close abnormally? @@ -172,6 +179,12 @@ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered); } + function registerDownloadAvailable() { + logger.debug("register for download_available"); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable); + } + + function registerSocketClosed() { logger.debug("register for socket closed"); context.JK.JamServer.registerOnSocketClosed(socketClosed); @@ -264,6 +277,23 @@ this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"}); } + /** Using the standard rails style error object, shows an alert with all seen errors */ + this.notifyServerError = function(jqXHR, title) { + if(!title) { + title = "Server Error"; + } + if(jqXHR.status == 422) { + var errors = JSON.parse(jqXHR.responseText); + var $errors = context.JK.format_all_errors(errors); + this.notify({title:title, text:$errors, icon_url: "/assets/content/icon_alert_big.png"}) + } + else + { + // we need to cehck more status codes and make tailored messages at this point + this.notify({title:title, text:"status=" + jqXHR.status + ", message=" + jqXHR.responseText, icon_url: "/assets/content/icon_alert_big.png"}) + } + } + /** * Initialize any common events. */ @@ -329,6 +359,7 @@ registerBadStateRecovered(); registerBadStateError(); registerSocketClosed(); + registerDownloadAvailable(); context.JK.FaderHelpers.initialize(); context.window.onunload = this.unloadFunction; } diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 3335f55ac..53bac88fa 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -638,7 +638,7 @@ function setNotificationInfo(message, descriptor) { var $notify = $('[layout="notify"]'); $('h2', $notify).text(message.title); - $('p', $notify).html(message.text); + $('p', $notify).html(message.text instanceof jQuery ? message.text.html() : message.text); if (message.icon_url) { $('#avatar', $notify).attr('src', message.icon_url); diff --git a/web/app/assets/javascripts/localRecordingsDialog.js b/web/app/assets/javascripts/localRecordingsDialog.js new file mode 100644 index 000000000..27e1bc3e8 --- /dev/null +++ b/web/app/assets/javascripts/localRecordingsDialog.js @@ -0,0 +1,169 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.LocalRecordingsDialog = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var showing = false; + var perPage = 10; + + function tbody() { + return $('#local-recordings-dialog table.local-recordings tbody'); + } + + function emptyList() { + tbody().empty(); + } + + function resetPagination() { + $('#local-recordings-dialog .paginator').remove(); + } + + + function beforeShow() { + emptyList(); + resetPagination(); + showing = true; + getRecordings(0) + .done(function(data, textStatus, jqXHR) { + // initialize pagination + var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected) + $('#local-recordings-dialog .paginator-holder').append($paginator); + }); + } + + function afterHide() { + showing = false; + } + + + function onPageSelected(targetPage) { + return getRecordings(targetPage); + } + + function getRecordings(page) { + return rest.getClaimedRecordings({page:page + 1, per_page:10}) + .done(function(claimedRecordings) { + + emptyList(); + + var $tbody = tbody(); + + var recordings = $.map(claimedRecordings, function(val, i) { return val.recording; }); + var localResults = context.jamClient.GetLocalRecordingState({recordings: recordings}); + + if(localResults['error']) { + app.notify({ + title : "Get Recording State Failure", + text : localResults['error'], + "icon_url": "/assets/content/icon_alert_big.png" + }); + app.layout.closeDialog('localRecordings'); + return; + } + + $.each(claimedRecordings, function(index, claimedRecording) { + + var options = { + recordingId: claimedRecording.recording.id, + //date: context.JK.formatDate(claimedRecording.recording.created_at), + //time: context.JK.formatTime(claimedRecording.recording.created_at), + timeago: $.timeago(claimedRecording.recording.created_at), + name: claimedRecording.name, + aggregate_state: localResults.recordings[index]['aggregate_state'], + duration: context.JK.prettyPrintSeconds(claimedRecording.recording.duration) + }; + + var tr = $(context._.template($('#template-claimed-recording-row').html(), options, { variable: 'data' })); + + tr.data('server-model', claimedRecording); + $tbody.append(tr); + }); + }) + .fail(function(jqXHR, textStatus, errorMessage) { + app.ajaxError(jqXHR, textStatus, errorMessage); + }); + } + + function registerStaticEvents() { + $('#local-recordings-dialog table.local-recordings tbody').on('click', 'tr', function(e) { + + var localState = $(this).attr('data-local-state'); + + if(localState == 'MISSING') { + app.notify({ + title : "Can't Open Recording", + text : "The recording is missing all tracks", + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + else if(localState == 'PARTIALLY_MISSING') { + app.notify({ + title : "Can't Open Recording", + text : "The recording is missing some tracks", + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + else + { + var claimedRecording = $(this).data('server-model'); + + // tell the server we are about to start a recording + rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + .done(function(response) { + var recordingId = $(this).attr('data-recording-id'); + var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); + + logger.debug("OpenRecording response: %o", openRecordingResult); + + if(openRecordingResult.error) { + app.notify({ + "title": "Can't Open Recording", + "text": openRecordingResult.error, + "icon_url": "/assets/content/icon_alert_big.png" + }); + + rest.stopPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Stop Recording Playback", + "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }) + } + else { + app.layout.closeDialog('localRecordings'); + $(this).triggerHandler('openedSession', {}); + } + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open Recording For Playback"); + + }) + + + } + return false; + }) + } + + function initialize(){ + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('localRecordings', dialogBindings); + + registerStaticEvents(); + }; + + + this.initialize = initialize; + this.isShowing = function isShowing() { return showing; } + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/paginator.js b/web/app/assets/javascripts/paginator.js new file mode 100644 index 000000000..b43785b0f --- /dev/null +++ b/web/app/assets/javascripts/paginator.js @@ -0,0 +1,100 @@ +/** + * Static functions for creating pagination + */ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + context.JK.Paginator = { + + /** returns a jquery object that encapsulates pagination markup. + * It's left to the caller to append it to the page as they like. + * @param pages the number of pages + * @param currentPage the current page + * @param onPageSelected when a new page is selected. receives one argument; the page number. + * the function should return a deferred object (whats returned by $.ajax), and that response has to have a 'total-entries' header set + */ + create:function(totalEntries, perPage, currentPage, onPageSelected) { + + function calculatePages(total, perPageValue) { + return Math.ceil(total / perPageValue); + } + + + function attemptToMoveToTargetPage(targetPage) { + + // 'working' == click guard + var working = paginator.data('working'); + if(!working) { + paginator.data('working', true); + + onPageSelected(targetPage) + .done(function(data, textStatus, jqXHR) { + totalEntries = parseInt(jqXHR.getResponseHeader('total-entries')); + pages = calculatePages(totalEntries, perPage); + options = { pages: pages, + currentPage: targetPage }; + + // recreate the pagination, and + var newPaginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' })); + registerEvents(newPaginator); + paginator.replaceWith(newPaginator); + paginator = newPaginator; + }) + .always(function() { + paginator.data('working', false); + }); + } + else { + console.log("workin fool: %o", working) + } + } + + function registerEvents(paginator) { + $('a.page-less', paginator).click(function(e) { + var currentPage = parseInt($(this).attr('data-current-page')); + if (currentPage > 0) { + var targetPage = currentPage - 1; + attemptToMoveToTargetPage(targetPage); + } + else { + // do nothing + } + return false; + }); + + $('a.page-more', paginator).click(function(e) { + var currentPage = parseInt($(this).attr('data-current-page')); + if (currentPage < pages - 1) { + var targetPage = currentPage + 1; + attemptToMoveToTargetPage(targetPage); + } + else { + // do nothing + } + return false; + }); + + $('a.page-link', paginator).click(function(e) { + var targetPage = parseInt($(this).attr('data-page')); + attemptToMoveToTargetPage(targetPage); + return false; + }); + } + + + var pages = calculatePages(totalEntries, perPage); + + var options = { pages: pages, + currentPage: currentPage }; + + var paginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' })); + + registerEvents(paginator); + + return paginator; + } + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js new file mode 100644 index 000000000..cf1df1323 --- /dev/null +++ b/web/app/assets/javascripts/playbackControls.js @@ -0,0 +1,268 @@ +/** + * Playback widget (play, pause , etc) + */ +(function(context, $) { + + "use strict"; + + var PlaybackMode = { + NoPlayback: 0, + EveryWhere: 1, + PrivatePreview: 2, + PreviewToAll: 3, + LastPbMode: 4 + }; + + + + context.JK = context.JK || {}; + context.JK.PlaybackControls = function($parentElement, options){ + + options = $.extend(false, {playmodeControlsVisible:false}, options); + + var logger = context.JK.logger; + if($parentElement.length == 0) { + logger.debug("no $parentElement specified in PlaybackControls"); + } + + var $playButton = $('.play-button img.playbutton', $parentElement); + var $pauseButton = $('.play-button img.pausebutton', $parentElement); + var $currentTime = $('.recording-current', $parentElement); + var $duration = $('.duration-time', $parentElement); + var $sliderBar = $('.recording-playback', $parentElement); + var $slider = $('.recording-slider', $parentElement); + var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement); + + var $self = $(this); + + var playbackPlaying = false; + var playbackDurationMs = 0; + var playbackPositionMs = 0; + var durationChanged = false; + + var endReached = false; + var dragging = false; + var playingWhenDragStart = false; + var draggingUpdateTimer = null; + var canUpdateBackend = false; + var playbackMode = PlaybackMode.EveryWhere; + var monitorPlaybackTimeout = null; + + function startPlay() { + updateIsPlaying(true); + if(endReached) { + update(0, playbackDurationMs, playbackPlaying); + } + $self.triggerHandler('play', {playbackMode: playbackMode}); + } + + function stopPlay() { + updateIsPlaying(false); + $self.triggerHandler('pause'); + } + + function updateOffsetBasedOnPosition(offsetLeft) { + var sliderBarWidth = $sliderBar.width(); + + playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); + updateCurrentTimeText(playbackPositionMs); + if(canUpdateBackend) { + $self.triggerHandler('change-position', {positionMs: playbackPositionMs}); + canUpdateBackend = false; + } + } + + function startDrag(e, ui) { + dragging = true; + playingWhenDragStart = playbackPlaying; + draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging + if(playingWhenDragStart) { + stopPlay(); + } + } + + function stopDrag(e, ui) { + dragging = false; + + clearInterval(draggingUpdateTimer); + + canUpdateBackend = true; + updateOffsetBasedOnPosition(ui.position.left); + + if(playingWhenDragStart) { + playingWhenDragStart = false; + startPlay(); + } + } + + function onDrag(e, ui) { + updateOffsetBasedOnPosition(ui.position.left); + } + + $playButton.on('click', function(e) { + startPlay(); + return false; + }); + + $pauseButton.on('click', function(e) { + stopPlay(); + return false; + }); + + $sliderBar.on('click', function(e) { + var offset = e.pageX - $(this).offset().left; + canUpdateBackend = true; + updateOffsetBasedOnPosition(offset); + updateSliderPosition(playbackPositionMs); + return false; + }); + + $slider.draggable({ + axis: 'x', + containment: $sliderBar, + start: startDrag, + stop: stopDrag, + drag: onDrag + }); + + + if(options.playmodeControlsVisible) { + $('.playback-mode-buttons.icheckbuttons', $parentElement).show(); + } + + $playmodeButton.iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); + + + $playmodeButton.on('ifChecked', function(e) { + var playmode = $(this).val(); + console.log("set new playmode", playmode); + setPlaybackMode(playmode); + }); + + function monitorRecordingPlayback() { + var isPlaying = context.jamClient.isSessionTrackPlaying(); + var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + + update(positionMs, durationMs, isPlaying); + + monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + } + + function update(currentTimeMs, durationTimeMs, isPlaying) { + + if(dragging) { + return; + } + + // at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to + console.log("currentTimeMs, durationTimeMs", currentTimeMs, durationTimeMs); + if(currentTimeMs == 0 && durationTimeMs == 0) { + if(isPlaying) { + isPlaying = false; + durationTimeMs = playbackDurationMs; + currentTimeMs = playbackDurationMs; + stopPlay(); + endReached = true; + } + else { + return; + } + } + + updateDurationTime(durationTimeMs); + updateCurrentTime(currentTimeMs); + updateIsPlaying(isPlaying); + + durationChanged = false; + } + + function updateDurationTime(timeMs) { + if(timeMs != playbackDurationMs) { + $duration.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); + playbackDurationMs = timeMs; + durationChanged = true; + } + } + + function updateCurrentTimeText(timeMs) { + $currentTime.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); + } + + function updateSliderPosition(timeMs) { + + var slideWidthPx = $sliderBar.width(); + var xPos = Math.ceil(timeMs / playbackDurationMs * slideWidthPx); + $slider.css('left', xPos); + } + + function updateCurrentTime(timeMs) { + if(timeMs != playbackPositionMs || durationChanged) { + + updateCurrentTimeText(timeMs); + updateSliderPosition(timeMs); + + playbackPositionMs = timeMs; + } + } + + function updateIsPlaying(isPlaying) { + if(isPlaying != playbackPlaying) { + if(isPlaying) { + $playButton.hide(); + $pauseButton.show(); + } + else { + $playButton.show(); + $pauseButton.hide(); + } + + playbackPlaying = isPlaying; + } + } + + function setPlaybackMode(mode) { + if(mode == 'preview-to-all') { + playbackMode = PlaybackMode.PreviewToAll; + } + else if(mode == 'preview-to-me') { + playbackMode = PlaybackMode.PrivatePreview; + } + else if(mode == 'eveywhere') { + playbackMode = PlaybackMode.EveryWhere; + } + else { + logger.error("unable to set playback mode", mode); + } + + // let the mode change immediately affect the behavior of the stream + if(playbackPlaying) { + stopPlay(); + startPlay(); + } + } + + function startMonitor() { + monitorRecordingPlayback(); + } + + function stopMonitor() { + if(monitorPlaybackTimeout!= null) { + clearTimeout(monitorPlaybackTimeout); + monitorPlaybackTimeout = null; + } + } + + + this.update = update; + this.setPlaybackMode = setPlaybackMode; + this.startMonitor = startMonitor; + this.stopMonitor = stopMonitor; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index fb9bab119..00273a333 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -7,6 +7,10 @@ var logger = context.JK.logger; var userId; var user = null; + var userDefer = null; + var rest = context.JK.Rest(); + var decrementedFriendCountOnce = false; + var sentFriendRequest = false; var instrument_logo_map = context.JK.getInstrumentIconMap24(); @@ -24,13 +28,11 @@ function beforeShow(data) { userId = data.id; - user = null; } function afterShow(data) { + initUser(); resetForm(); - events(); - renderActive(); } function resetForm() { @@ -46,166 +48,167 @@ $('.profile-nav a.#profile-about-link').addClass('active'); } - function getUser() { - if (user === null) { - var url = "/api/users/" + userId; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { - user = response; - }, - error: function(jqXHR, textStatus, errorMessage) { - user = null; - app.ajaxError(jqXHR, textStatus, errorMessage); - } - }); - } - return user; - } + function initUser() { + user = null; + decrementedFriendCountOnce = false; + sentFriendRequest = false; + userDefer = rest.getUserDetail({id: userId}) + .done(function(response) { + user = response; + configureUserType(); + renderActive(); + }) + .fail(function(jqXHR) { + if(jqXHR.status >= 500) { + fetchUserNetworkOrServerFailure(); + } + else if(jqXHR.status == 404) { + fetchUserGone(); + } + else { + app.ajaxError(arguments); + } + }); + } - function isMusician() { - if (getUser()) { - return user.musician === true; - } - return false; - } + function isMusician() { + return user.musician; + } - function isCurrentUser() { - return userId === context.JK.currentUserId; - } + function isCurrentUser() { + return userId === context.JK.currentUserId; + } - function configureUserType() { - if (isMusician()) { - $('#profile-history-link').show(); - $('#profile-bands-link').show(); - $('#profile-instruments').show(); - $('#profile-session-stats').show(); - $('#profile-recording-stats').show(); - - // $('#profile-following-stats').hide(); - // $('#profile-favorites-stats').hide(); - - $('#btn-add-friend').show(); + function configureUserType() { + if (isMusician()) { + $('#profile-history-link').show(); + $('#profile-bands-link').show(); + $('#profile-instruments').show(); + $('#profile-session-stats').show(); + $('#profile-recording-stats').show(); + // $('#profile-following-stats').hide(); + // $('#profile-favorites-stats').hide(); $('.profile-social-left').show(); - - $('#profile-type-label').text('musician'); - $('#profile-location-label').text('Location'); - - } else { - $('#profile-history-link').hide(); - $('#profile-bands-link').hide(); - $('#profile-instruments').hide(); - $('#profile-session-stats').hide(); - $('#profile-recording-stats').hide(); - - // $('#profile-following-stats').show(); - // $('#profile-favorites-stats').show(); - - $('#btn-add-friend').hide(); + $('#profile-type-label').text('musician'); + $('#profile-location-label').text('Location'); + } + else { + $('#profile-history-link').hide(); + $('#profile-bands-link').hide(); + $('#profile-instruments').hide(); + $('#profile-session-stats').hide(); + $('#profile-recording-stats').hide(); + // $('#profile-following-stats').show(); + // $('#profile-favorites-stats').show(); $('.profile-social-left').hide(); + $('#profile-type-label').text('fan'); + $('#profile-location-label').text('Presence'); + } - $('#profile-type-label').text('fan'); - $('#profile-location-label').text('Presence'); - } - - if (isCurrentUser()) { + if (isCurrentUser()) { $('#btn-profile-edit').show(); - } else { + $('#btn-add-friend').hide(); + $('#btn-follow-user').hide(); + } + else { + configureFriendFollowersControls(); + $('#btn-profile-edit').hide(); - } - } + $('#btn-add-friend').show(); + $('#btn-follow-user').show(); + } + } + + function configureFriendFollowersControls() { + // wire up Add Friend click + configureFriendButton(); + + // wire up Follow click + configureFollowingButton(); + } /****************** MAIN PORTION OF SCREEN *****************/ // events for main screen function events() { - configureUserType(); + // wire up panel clicks -- these need to check deferred because they can't be hidden when in an invalid state + $('#profile-about-link').click(function(){ renderTabDeferred(renderAbout)}); + $('#profile-history-link').click(function(){ renderTabDeferred(renderHistory)}); + $('#profile-bands-link').click(function(){ renderTabDeferred(renderBands)}); + $('#profile-social-link').click(function(){ renderTabDeferred(renderSocial)}); + $('#profile-favorites-link').click(function(){ renderTabDeferred(renderFavorites)}); - // wire up panel clicks - $('#profile-about-link').click(renderAbout); - $('#profile-history-link').click(renderHistory); - $('#profile-bands-link').click(renderBands); - $('#profile-social-link').click(renderSocial); - $('#profile-favorites-link').click(renderFavorites); + // this doesn't need deferred because it's only shown when valid + $('#btn-add-friend').click(handleFriendChange); + $('#btn-follow-user').click(handleFollowingChange); + } - // wire up buttons if you're not viewing your own profile - if (!isCurrentUser()) { - // wire up Add Friend click - configureFriendButton(isFriend()); + function handleFriendChange(evt) { + if(isFriend()) { + removeFriend(evt); + } + else { + sendFriendRequest(evt); + } + } - // wire up Follow click - configureFollowingButton(isFollowing()); + function handleFollowingChange(evt) { + if (isFollowing()) { + removeFollowing(false, userId); } else { - $('#btn-add-friend').hide(); - $('#btn-follow-user').hide(); + addFollowing(); } } function sendFriendRequest(evt) { evt.stopPropagation(); + setFriend(true); // TODO: you aren't a friend yet. just a request to be one really there are 3 states here. + sentFriendRequest = true; context.JK.sendFriendRequest(app, userId, friendRequestCallback); } function removeFriend(evt) { evt.stopPropagation(); - var url = "/api/users/" + context.JK.currentUserId + "/friends/" + userId; - $.ajax({ - type: "DELETE", - dataType: "json", - url: url, - processData: false, - success: function(response) { - renderActive(); // refresh stats - configureFriendButton(false); - }, - error: app.ajaxError - }); + rest.removeFriend({friend_id: userId}) + .done(function() { + updateFriendCount(-1); + setFriend(false); + configureFriendButton(); + }) + .fail(app.ajaxError); } function isFriend() { - return getUser() ? user.is_friend : false; + return user.is_friend; + } + + function setFriend(isFriend) { + user.is_friend = isFriend; } function friendRequestCallback() { - configureFriendButton(true); + configureFriendButton(); } - function configureFriendButton(friend) { - $('#btn-add-friend').unbind("click"); - - if (friend) { + function configureFriendButton() { + if (isFriend()) { $('#btn-add-friend').text('REMOVE FRIEND'); - $('#btn-add-friend').click(removeFriend); } else { $('#btn-add-friend').text('ADD FRIEND'); - $('#btn-add-friend').click(sendFriendRequest); } } function addFollowing() { - var newFollowing = {}; - newFollowing.user_id = userId; - var url = "/api/users/" + context.JK.currentUserId + "/followings"; - $.ajax({ - type: "POST", - dataType: "json", - contentType: 'application/json', - url: url, - data: JSON.stringify(newFollowing), - processData: false, - success: function(response) { - renderActive(); // refresh stats - configureFollowingButton(true); - }, - error: app.ajaxError - }); + rest.addFollowing({user_id: userId}) + .done(function() { + updateFollowingCount(1); + setFollowing(true); + configureFollowingButton(); + }) + .fail(app.ajaxError); } function removeFollowing(isBand, id) { @@ -218,43 +221,36 @@ following.band_id = id; } - var url = "/api/users/" + context.JK.currentUserId + "/followings"; - $.ajax({ - type: "DELETE", - dataType: "json", - contentType: 'application/json', - url: url, - data: JSON.stringify(following), - processData: false, - success: function(response) { - renderActive(); // refresh stats + rest.removeFollowing(following) + .done(function() { if (!isBand) { - configureFollowingButton(false); + updateFollowingCount(-1); + setFollowing(false); + configureFollowingButton(); } else { + updateBandFollowingCount(id, -1); // refresh stats configureBandFollowingButton(false, id); } - }, - error: app.ajaxError - }); + }) + .fail(app.ajaxError); } function isFollowing() { - return getUser() ? user.is_following : false; + return user.is_following; } - function configureFollowingButton(following) { - $('#btn-follow-user').unbind("click"); + function setFollowing(isFollowing) { + user.is_following = isFollowing; + } - if (following) { + function configureFollowingButton() { + + if (isFollowing()) { $('#btn-follow-user').text('STOP FOLLOWING'); - $('#btn-follow-user').click(function() { - removeFollowing(false, userId); - }); } else { $('#btn-follow-user').text('FOLLOW'); - $('#btn-follow-user').click(addFollowing); } } @@ -281,6 +277,17 @@ } } + function renderTabDeferred(tabRenderer) { + userDefer + .done(function() { + tabRenderer(); + }) + .fail(function() { + // try again + initUser(); + }) + } + /****************** ABOUT TAB *****************/ function renderAbout() { $('#profile-instruments').empty(); @@ -300,62 +307,57 @@ function bindAbout() { $('#profile-instruments').empty(); - if (getUser()) { - // name - $('#profile-username').html(user.name); + // name + $('#profile-username').html(user.name); - // avatar - $('#profile-avatar').attr('src', context.JK.resolveAvatarUrl(user.photo_url)); + // avatar + $('#profile-avatar').attr('src', context.JK.resolveAvatarUrl(user.photo_url)); - // instruments - if (user.instruments) { - for (var i=0; i < user.instruments.length; i++) { - var instrument = user.instruments[i]; - var description = instrument.instrument_id; - var proficiency = instrument.proficiency_level; - var instrument_icon_url = context.JK.getInstrumentIcon45(description); + // instruments + if (user.instruments) { + for (var i=0; i < user.instruments.length; i++) { + var instrument = user.instruments[i]; + var description = instrument.instrument_id; + var proficiency = instrument.proficiency_level; + var instrument_icon_url = context.JK.getInstrumentIcon45(description); - // add instrument info to layout - var template = $('#template-profile-instruments').html(); - var instrumentHtml = context.JK.fillTemplate(template, { - instrument_logo_url: instrument_icon_url, - instrument_description: description, - proficiency_level: proficiencyDescriptionMap[proficiency], - proficiency_level_css: proficiencyCssMap[proficiency] - }); + // add instrument info to layout + var template = $('#template-profile-instruments').html(); + var instrumentHtml = context.JK.fillTemplate(template, { + instrument_logo_url: instrument_icon_url, + instrument_description: description, + proficiency_level: proficiencyDescriptionMap[proficiency], + proficiency_level_css: proficiencyCssMap[proficiency] + }); - $('#profile-instruments').append(instrumentHtml); - } + $('#profile-instruments').append(instrumentHtml); } - - // location - $('#profile-location').html(user.location); - - // stats - var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend"; - $('#profile-friend-stats').html(user.friend_count + text); - - text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower"; - $('#profile-follower-stats').html(user.follower_count + text); - - if (isMusician()) { - text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session"; - $('#profile-session-stats').html(user.session_count + text); - - text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording"; - $('#profile-recording-stats').html(user.recording_count + text); - } else { - text = " Following"; - $('#profile-following-stats').html(user.following_count + text); - text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite"; - $('#profile-favorite-stats').html(user.favorite_count + text); - } - - $('#profile-biography').html(user.biography); } - else { - logger.debug("No user found with userId = " + userId); + + // location + $('#profile-location').html(user.location); + + // stats + var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend"; + $('#profile-friend-stats').html('' + user.friend_count + '' + text); + + text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower"; + $('#profile-follower-stats').html('' + user.follower_count + '' + text); + + if (isMusician()) { + text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session"; + $('#profile-session-stats').html(user.session_count + text); + + text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording"; + $('#profile-recording-stats').html(user.recording_count + text); + } else { + text = " Following"; + $('#profile-following-stats').html(user.following_count + text); + text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite"; + $('#profile-favorite-stats').html(user.favorite_count + text); } + + $('#profile-biography').html(user.biography); } /****************** SOCIAL TAB *****************/ @@ -373,51 +375,31 @@ $('.profile-nav a.active').removeClass('active'); $('.profile-nav a.#profile-social-link').addClass('active'); - /*if (isMusician()) { - $('.profile-social-left').show(); - } else { - $('.profile-social-left').hide(); - }*/ - bindSocial(); } function bindSocial() { - if (isMusician()) { - // FRIENDS - var url = "/api/users/" + userId + "/friends"; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { - $.each(response, function(index, val) { + if (isMusician()) { + // FRIENDS + rest.getFriends({id:userId}) + .done(function(response) { + $.each(response, function(index, val) { var template = $('#template-profile-social').html(); var friendHtml = context.JK.fillTemplate(template, { - avatar_url: context.JK.resolveAvatarUrl(val.photo_url), - userName: val.name, - location: val.location, - type: "Friends" + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + location: val.location, + type: "Friends" }); $('#profile-social-friends').append(friendHtml); - }); - }, - error: app.ajaxError - }); - } + }); + }) + .fail(app.ajaxError) + } - // FOLLOWINGS (USERS) - url = "/api/users/" + userId + "/followings"; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { + rest.getFollowings({id: userId}) + .done(function(response) { $.each(response, function(index, val) { var template = $('#template-profile-social').html(); var followingHtml = context.JK.fillTemplate(template, { @@ -428,19 +410,11 @@ $('#profile-social-followings').append(followingHtml); }); - }, - error: app.ajaxError - }); + }) + .fail(app.ajaxError); - // FOLLOWINGS (BANDS) - url = "/api/users/" + userId + "/band_followings"; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { + rest.getBandFollowings({id: userId}) + .done(function(response) { $.each(response, function(index, val) { var template = $('#template-profile-social').html(); var followingHtml = context.JK.fillTemplate(template, { @@ -451,19 +425,11 @@ $('#profile-social-followings').append(followingHtml); }); - }, - error: app.ajaxError - }); + }) + .fail(app.ajaxError); - // FOLLOWERS - url = "/api/users/" + userId + "/followers"; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { + rest.getFollowers({id: userId}) + .done(function(response) { $.each(response, function(index, val) { var template = $('#template-profile-social').html(); var followerHtml = context.JK.fillTemplate(template, { @@ -474,9 +440,9 @@ $('#profile-social-followers').append(followerHtml); }); - }, - error: app.ajaxError - }); + }) + .fail(app.ajaxError); + } /****************** HISTORY TAB *****************/ @@ -514,34 +480,28 @@ } function bindBands() { - var url = "/api/users/" + userId + "/bands"; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData:false, - success: function(response) { - if ( (!response || response.length === 0) && isCurrentUser()) { + + rest.getBands({id:userId}) + .done(function(response) { + if ( (!response || response.length === 0) && isCurrentUser()) { var noBandHtml = $('#template-no-bands').html(); - $("#profile-bands").append(noBandHtml); - } + $("#profile-bands").html(noBandHtml); + } + else { + addMoreBandsLink(); - else { - addMoreBandsLink(); - - $.each(response, function(index, val) { + $.each(response, function(index, val) { // build band member HTML var musicianHtml = ''; - if ("musicians" in val) { + if (val.musicians) { for (var i=0; i < val.musicians.length; i++) { var musician = val.musicians[i]; var instrumentLogoHtml = ''; - if ("instruments" in musician) { + if (musician.instruments) { for (var j=0; j < musician.instruments.length; j++) { var instrument = musician.instruments[j]; - var inst = '../assets/content/icon_instrument_default24.png'; + var inst = '/assets/content/icon_instrument_default24.png'; if (instrument.instrument_id in instrument_logo_map) { inst = instrument_logo_map[instrument.instrument_id]; } @@ -552,7 +512,7 @@ var musicianTemplate = $('#template-musician-info').html(); musicianHtml += context.JK.fillTemplate(musicianTemplate, { avatar_url: context.JK.resolveAvatarUrl(musician.photo_url), - profile_url: "/#/profile/" + musician.id, + profile_url: "/client#/profile/" + musician.id, musician_name: musician.name, instruments: instrumentLogoHtml }); @@ -562,7 +522,7 @@ var bandHtml = context.JK.fillTemplate(template, { bandId: val.id, biography: val.biography, - band_url: "/#/bandProfile/" + val.id, + band_url: "/client#/bandProfile/" + val.id, avatar_url: context.JK.resolveBandAvatarUrl(val.logo_url), name: val.name, location: val.location, @@ -576,15 +536,22 @@ $('#profile-bands').append(bandHtml); // wire up Band Follow button click handler - var following = isFollowingBand(val.id); - configureBandFollowingButton(following, val.id); - }); + // XXX; we should return if you are following the band in the rabl, so we don't + // have to hit the server per band shown + rest.getBandFollowing({band_id: val.id}) + .done(function(response) { + var alreadyFollowing = response.id !== undefined; + configureBandFollowingButton(alreadyFollowing, val.id); + }); + }); + + if(response.length >= 3) { + addMoreBandsLink(); + } + } + }) + .fail(app.ajaxError); - addMoreBandsLink(); - } - }, - error: app.ajaxError - }); } function addMoreBandsLink() { @@ -608,6 +575,24 @@ return formattedGenres; } + function updateFriendCount(value) { + if(!decrementedFriendCountOnce && !sentFriendRequest) { + decrementedFriendCountOnce = true; + var friendCount = $('#profile-friend-stats span.friend-count'); + friendCount.text(value + parseInt(friendCount.text())); + } + } + + function updateFollowingCount(value) { + var followingCount = $('#profile-follower-stats span.follower-count'); + followingCount.text(value + parseInt(followingCount.text())); + } + + function updateBandFollowingCount(bandId, value) { + var bandFollowing = $('div[band-id="' + bandId + '"].profile-bands span.follower-count'); + bandFollowing.text(value + parseInt(bandFollowing.text())); + } + function addBandFollowing(evt) { evt.stopPropagation(); var bandId = $(this).parent().parent().attr('band-id'); @@ -615,44 +600,13 @@ var newFollowing = {}; newFollowing.band_id = bandId; - var url = "/api/users/" + context.JK.currentUserId + "/followings"; - $.ajax({ - type: "POST", - dataType: "json", - contentType: 'application/json', - url: url, - data: JSON.stringify(newFollowing), - processData: false, - success: function(response) { - renderBands(); // refresh stats + rest.addFollowing(newFollowing) + .done(function(response) { + logger.debug("following band " + bandId); + updateBandFollowingCount(bandId, 1); // increase counter configureBandFollowingButton(true, bandId); - }, - error: app.ajaxError - }); - } - - function isFollowingBand(bandId) { - var alreadyFollowing = false; - - var url = "/api/users/" + context.JK.currentUserId + "/band_followings/" + bandId; - $.ajax({ - type: "GET", - dataType: "json", - url: url, - async: false, - processData: false, - success: function(response) { - if (response.id !== undefined) { - alreadyFollowing = true; - } - else { - alreadyFollowing = false; - } - }, - error: app.ajaxError - }); - - return alreadyFollowing; + }) + .fail(app.ajaxError); } function configureBandFollowingButton(following, bandId) { @@ -661,8 +615,10 @@ if (following) { $btnFollowBand.text('UN-FOLLOW'); - $btnFollowBand.click(function() { + $btnFollowBand.click(function(evt) { removeFollowing(true, bandId); + evt.stopPropagation(); + return false; }); } else { @@ -685,6 +641,23 @@ bindFavorites(); } + + function fetchUserGone() { + app.notify({ + title: "User Deleted", + text: "The user you are looking for is gone", + icon_url: "/assets/content/icon_alert_big.png" + }) + } + + function fetchUserNetworkOrServerFailure() { + app.notify({ + title: "Unable to communicate with server", + text: "Please try again later", + icon_url: "/assets/content/icon_alert_big.png" + }) + } + function bindFavorites() { } @@ -694,6 +667,8 @@ 'afterShow': afterShow }; app.bindScreen('profile', screenBindings); + + events(); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/recordingFinishedDialog.js b/web/app/assets/javascripts/recordingFinishedDialog.js new file mode 100644 index 000000000..e7200038b --- /dev/null +++ b/web/app/assets/javascripts/recordingFinishedDialog.js @@ -0,0 +1,240 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RecordingFinishedDialog = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var playbackControls = null; + var recording = null; // deferred object + + function resetForm() { + + // remove all display errors + $('#recording-finished-dialog form .error-text').remove() + $('#recording-finished-dialog form .error').removeClass("error") + } + + function beforeShow() { + if(recording == null) { + alert("recording data should not be null"); + app.layout.closeDialog('recordingFinished'); + return false; + } + + resetForm(); + + var parentSelector = '#recording-finished-dialog div.genre-selector'; + context.JK.GenreSelectorHelper.render(parentSelector); + + // preset genre from 1st genre in current session + var currentOrLastSession = JK.CurrentSessionModel.getCurrentOrLastSession(); + if(currentOrLastSession && currentOrLastSession.genres.length > 0) { + var genreDescription = currentOrLastSession.genres[0]; + context.JK.GenreSelectorHelper.setSelectedGenres(parentSelector, [genreDescription]); + } + + var localResults = context.jamClient.GetLocalRecordingState({recordings: [recording]}); + + + if(localResults['error']) { + app.notify({ + title : "Unable to Open Recording for Playback", + text : localResults['error'], + icon_url: "/assets/content/icon_alert_big.png" + }); + } + else { + var localResult = localResults.recordings[0]; + if(localResult.aggregate_state == 'MISSING') { + app.notify({ + title : "Unable to Open Recording for Playback", + text : "All tracks associated with the recording are missing", + icon_url: "/assets/content/icon_alert_big.png" + }); + } + else if(localResult.aggregate_state == 'PARTIALLY_MISSING') { + app.notify({ + title : "Unable to Open Recording for Playback", + text: "Some tracks associated with the recording are missing", + icon_url: "/assets/content/icon_alert_big.png" + }) + } + else { + + // load recording + var openRecordingResult = context.jamClient.OpenRecording(recording); + + logger.debug("OpenRecording response: %o", openRecordingResult); + + if(openRecordingResult.error) { + app.notify({ + "title": "Can't Open Recording", + "text": openRecordingResult.error, + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + + playbackControls.startMonitor(); + } + } + + } + + function afterHide() { + recording = null; + playbackControls.stopMonitor(); + context.jamClient.CloseRecording(); + } + + function discardRecording(e) { + + resetForm(); + registerDiscardRecordingHandlers(false); + + rest.discardRecording({ + id: recording.id + }) + .done(function() { + console.error("recording discarded by user. recordingId=%o", recording.id); + }) + .fail(function(jqXHR){ + console.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText); + }) + .always(function() { + app.layout.closeDialog('recordingFinished') + registerDiscardRecordingHandlers(true); + }) + return false; + } + function claimRecording(e){ + + resetForm(); + registerClaimRecordingHandlers(false); + + var name = $('#recording-finished-dialog form input[name=name]').val(); + var description = $('#recording-finished-dialog form textarea[name=description]').val(); + var genre = $('#recording-finished-dialog form select[name=genre]').val(); + var is_public = $('#recording-finished-dialog form input[name=is_public]').is(':checked') + var is_downloadable = $('#recording-finished-dialog form input[name=is_downloadable]').is(':checked'); + + rest.claimRecording({ + id : recording.id, + name: name, + description: description, + genre: genre, + is_public: is_public, + is_downloadable: is_downloadable + }) + .done(function() { + app.layout.closeDialog('recordingFinished') + }) + .fail(function(jqXHR) { + if(jqXHR.status == 422) { + var errors = JSON.parse(jqXHR.responseText); + + var $name_errors = context.JK.format_errors('name', errors); + if($name_errors) $('#recording-finished-dialog form input[name=name]').closest('div.field').addClass('error').end().after($name_errors); + + var $description_errors = context.JK.format_errors('description', errors); + if($description_errors) $('#recording-finished-dialog form input[name=description]').closest('div.field').addClass('error').end().after($description_errors); + + var $genre_errors = context.JK.format_errors('genre', errors); + if($genre_errors) $('#recording-finished-dialog form select[name=genre]').closest('div.field').addClass('error').end().after($genre_errors); + + var $is_public_errors = context.JK.format_errors('is_public', errors); + if($is_public_errors) $('#recording-finished-dialog form input[name=is_public]').closest('div.field').addClass('error').end().after($is_public_errors); + + var $is_downloadable_errors = context.JK.format_errors('is_downloadable', errors); + if($is_downloadable_errors) $('#recording-finished-dialog form input[name=is_downloadable]').closest('div.field').addClass('error').end().after($is_downloadable_errors); + + var recording_error = context.JK.get_first_error('recording_id', errors); + + if (recording_error) context.JK.showErrorDialog(app, "Unable to claim recording.", recording_error); + } + else { + logger.error("unable to claim recording %o", arguments); + + context.JK.showErrorDialog(app, "Unable to claim recording.", jqXHR.responseText); + } + }) + .always(function() { + registerClaimRecordingHandlers(true); + }); + return false; + } + + function registerClaimRecordingHandlers(onOff) { + if(onOff) { + $('#keep-session-recording').on('click', claimRecording); + $('#recording-finished-dialog form').on('submit', claimRecording); + } + else { + $('#keep-session-recording').off('click', claimRecording) + $('#recording-finished-dialog form').off('submit', claimRecording); + } + + } + + function registerDiscardRecordingHandlers(onOff) { + if(onOff) { + $('#discard-session-recording').on('click', discardRecording); + } + else { + $('#discard-session-recording').off('click', discardRecording); + } + } + + function onPause() { + logger.debug("calling jamClient.SessionStopPlay"); + context.jamClient.SessionStopPlay(); + } + + function onPlay(e, data) { + logger.debug("calling jamClient.SessionStartPlay"); + context.jamClient.SessionStartPlay(data.playbackMode); + } + + function onChangePlayPosition(e, data) { + logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")"); + context.jamClient.SessionTrackSeekMs(data.positionMs); + } + + function registerStaticEvents() { + registerClaimRecordingHandlers(true); + registerDiscardRecordingHandlers(true); + $(playbackControls) + .on('pause', onPause) + .on('play', onPlay) + .on('change-position', onChangePlayPosition); + } + + function setRecording(recordingData) { + if(recording != null) { + //XXX - prevent start/stop recording mashing; protect this dialog + logger.error("unable to set recording data over existing recording data. this coudld be due to start/stop recording mashing"); + return; + } + recording = recordingData; + } + + function initialize(){ + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('recordingFinished', dialogBindings); + + playbackControls = new context.JK.PlaybackControls($('#recording-finished-dialog .recording-controls')); + + registerStaticEvents(); + }; + + + this.initialize = initialize; + this.setRecording = setRecording; + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/recordingManager.js b/web/app/assets/javascripts/recordingManager.js new file mode 100644 index 000000000..54c8bc6e7 --- /dev/null +++ b/web/app/assets/javascripts/recordingManager.js @@ -0,0 +1,123 @@ +/** + * Recording Manager viewer + * Although multiple instances could be made, only one should be + */ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + context.JK.RecordingManager = function(){ + + var $parentElement = $('#recording-manager-viewer'); + + var logger = context.JK.logger; + if($parentElement.length == 0) { + logger.debug("no $parentElement specified in RecordingManager"); + } + + var $downloadCommand = $('#recording-manager-download', $parentElement); + var $downloadPercent = $('#recording-manager-download .percent', $parentElement); + var $uploadCommand = $('#recording-manager-upload', $parentElement); + var $uploadPercent = $('#recording-manager-upload .percent', $parentElement); + var $convertCommand = $('#recording-manager-convert', $parentElement); + var $convertPercent = $('#recording-manager-convert .percent', $parentElement); + + // keys come from backend + var lookup = { + SyncDownload: { command: $downloadCommand, percent: $downloadPercent }, + SyncUpload: { command: $uploadCommand, percent: $uploadPercent }, + SyncConvert: { command: $convertCommand, percent: $convertPercent } + } + + var $self = $(this); + + + function renderStartCommand($command) { + $command.css('visibility', 'visible'); + } + + function renderEndCommand($command) { + $command.css('visibility', 'hidden'); + } + + function renderPercentage($percent, value) { + $percent.text(Math.round(value * 100)); + } + + function onStartCommand(id, type) { + var command = lookup[type]; + if(!command) { return } + + var existingCommandId = command.command.data('command-id'); + + if(existingCommandId && existingCommandId != id) { + renderEndCommand(command.command); + } + + command.command.data('command-id', id); + renderStartCommand(command.command); + renderPercentage(command.percent, 0); + } + + function onStopCommand(id, type, success, reason, detail) { + var command = lookup[type]; + if(!command) { return } + + var existingCommandId = command.command.data('command-id'); + + if(!existingCommandId) { + command.command.data('command-id', id); + renderStartCommand(command.command); + } + else if(existingCommandId && existingCommandId != id) { + renderEndCommand(command.command); + command.command.data('command-id', id); + renderStartCommand(command.command); + } + + renderPercentage(command.percent, 1); + renderEndCommand(command.command); + command.command.data('command-id', null); + } + + function onCommandProgress(id, type, progress) { + var command = lookup[type]; + if(!command) { return } + + var existingCommandId = command.command.data('command-id'); + + if(!existingCommandId) { + command.command.data('command-id', id); + renderStartCommand(command.command); + } + else if(existingCommandId && existingCommandId != id) { + renderEndCommand(command.command); + command.command.data('command-id', id); + renderStartCommand(command.command); + } + + renderPercentage(command.percent, progress); + } + + function onCommandsChanged(id, type) { + + } + + context.JK.RecordingManagerCommandStart = onStartCommand; + context.JK.RecordingManagerCommandStop = onStopCommand; + context.JK.RecordingManagerCommandProgress = onCommandProgress; + context.JK.RecordingManagerCommandsChanged = onCommandsChanged; + + context.jamClient.RegisterRecordingManagerCallbacks( + "JK.RecordingManagerCommandStart", + "JK.RecordingManagerCommandProgress", + "JK.RecordingManagerCommandStop", + "JK.RecordingManagerCommandsChanged" + + ) + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index 19bf02fe2..078a41c4d 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -20,6 +20,7 @@ context.JK.RecordingModel = function(app, sessionModel, _rest, _jamClient) { var currentRecording = null; // the JSON response from the server for a recording + var currentOrLastRecordingId = null; var currentRecordingId = null; var rest = _rest; var currentlyRecording = false; @@ -57,6 +58,7 @@ currentRecordingId = null; } + function groupTracksToClient(recording) { // group N tracks to the same client Id var groupedTracks = {}; @@ -83,12 +85,13 @@ currentRecording = rest.startRecording({"music_session_id": sessionModel.id()}) .done(function(recording) { currentRecordingId = recording.id; + currentOrLastRecordingId = recording.id; // ask the backend to start the session. var groupedTracks = groupTracksToClient(recording); jamClient.StartRecording(recording["id"], groupedTracks); }) - .fail(function() { + .fail(function(jqXHR) { $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments }); currentlyRecording = false; }) @@ -220,6 +223,7 @@ }) .done(function(recording) { currentRecordingId = recording.id; + currentOrLastRecordingId = recording.id; }); $self.triggerHandler('startingRecording', {recordingId: recordingId}); @@ -320,6 +324,7 @@ this.isRecording = isRecording; this.reset = reset; this.stopRecordingIfNeeded = stopRecordingIfNeeded; + this.currentOrLastRecordingId = function () { return currentOrLastRecordingId; }; context.JK.HandleRecordingStartResult = handleRecordingStartResult; context.JK.HandleRecordingStopResult = handleRecordingStopResult; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index db0ea888c..4245dbee8 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -12,6 +12,8 @@ var mixers = []; var configureTrackDialog; var addNewGearDialog; + var localRecordingsDialog = null; + var recordingFinishedDialog = null; var screenActive = false; var currentMixerRangeMin = null; var currentMixerRangeMax = null; @@ -22,6 +24,8 @@ var recordingTimerInterval = null; var startTimeDate = null; var startingRecording = false; // double-click guard + var claimedRecording = null; + var playbackControls = null; var rest = JK.Rest(); @@ -242,6 +246,10 @@ else if(data.reason == 'recording-engine-sample-rate') { notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); } + else if(data.reason == 'rest') { + var jqXHR = detail[0]; + app.notifyServerError(jqXHR); + } else { notifyWithUserInfo(title, 'Error Reason: ' + reason); } @@ -356,8 +364,26 @@ .fail(app.ajaxError); } + function handleTransitionsInRecordingPlayback() { + // let's see if we detect a transition to start playback or stop playback + + var currentSession = sessionModel.getCurrentSession(); + + if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { + // this is a 'started with a claimed_recording' transition. + // we need to start a timer to watch for the state of the play session + playbackControls.startMonitor(); + } + else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { + playbackControls.stopMonitor(); + } + + claimedRecording = currentSession == null ? null : currentSession.claimed_recording; + + } function sessionChanged() { + handleTransitionsInRecordingPlayback(); // TODO - in the specific case of a user changing their tracks using the configureTrack dialog, // this event appears to fire before the underlying mixers have updated. I have no event to // know definitively when the underlying mixers are up to date, so for now, we just delay slightly. @@ -387,6 +413,7 @@ $voiceChat.hide(); _updateMixers(); _renderTracks(); + _renderLocalMediaTracks(); _wireTopVolume(); _wireTopMix(); _addVoiceChat(); @@ -394,6 +421,11 @@ if ($('.session-livetracks .track').length === 0) { $('.session-livetracks .when-empty').show(); } + if ($('.session-recordings .track').length === 0) { + $('.session-recordings .when-empty').show(); + $('.session-recording-name-wrapper').hide(); + $('.session-recordings .recording-controls').hide(); + } } function _initDialogs() { @@ -406,6 +438,7 @@ var mixerIds = context.jamClient.SessionGetIDs(); var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)}); mixers = holder.mixers; + // Always add a hard-coded simplified 'mixer' for the L2M mix var l2m_mixer = { id: '__L2M__', @@ -416,6 +449,17 @@ mixers.push(l2m_mixer); } + function _mixersForGroupId(groupId) { + var foundMixers = []; + $.each(mixers, function(index, mixer) { + if (mixer.group_id === groupId) { + foundMixers.push(mixer); + } + + }); + return foundMixers; + } + // TODO FIXME - This needs to support multiple tracks for an individual // client id and group. function _mixerForClientId(clientId, groupIds, usedMixers) { @@ -538,6 +582,113 @@ }); } + function _renderLocalMediaTracks() { + var localMediaMixers = _mixersForGroupId(ChannelGroupIds.MediaTrackGroup); + if(localMediaMixers.length == 0) { + localMediaMixers = _mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup); + } + + var recordedTracks = sessionModel.recordedTracks(); + + console.log("recorded tracks=%o local_media=%o", recordedTracks, localMediaMixers); + + if(recordedTracks && localMediaMixers.length == 0) { + // if we are the creator, then rather than raise an error, tell the server the recording is over. + // this shoudl only happen if we get temporarily disconnected by forced reload, which isn't a very normal scenario + if(sessionModel.getCurrentSession().claimed_recording_initiator_id == context.JK.userMe.id) { + closeRecording(); + return; + } + } + + if(recordedTracks) { + + $('.session-recording-name').text(sessionModel.getCurrentSession().claimed_recording.name); + + var noCorrespondingTracks = false; + $.each(localMediaMixers, function(index, mixer) { + var preMasteredClass = ""; + // find the track or tracks that correspond to the mixer + var correspondingTracks = [] + $.each(recordedTracks, function(i, recordedTrack) { + if(mixer.id.indexOf("L") == 0) { + if(mixer.id.substring(1) == recordedTrack.client_track_id) { + correspondingTracks.push(recordedTrack); + } + } + else if(mixer.id.indexOf("C") == 0) { + if(mixer.id.substring(1) == recordedTrack.client_id) { + correspondingTracks.push(recordedTrack); + preMasteredClass = "pre-mastered-track"; + } + } + else { + // this should not be possible + alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id"); + } + }); + + if(correspondingTracks.length == 0) { + noCorrespondingTracks = true; + app.notify({ + title: "Unable to Open Recording", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png"}); + return false; + } + + // prune found recorded tracks + recordedTracks = $.grep(recordedTracks, function(value) { + return $.inArray(value, correspondingTracks) < 0; + }); + + var oneOfTheTracks = correspondingTracks[0]; + var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); + var photoUrl = "/assets/content/icon_recording.png"; + + var name = oneOfTheTracks.user.name; + if (!(name)) { + name = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name; + } + + + // Default trackData to participant + no Mixer state. + var trackData = { + trackId: oneOfTheTracks.id, + clientId: oneOfTheTracks.client_id, + name: name, + instrumentIcon: instrumentIcon, + avatar: photoUrl, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + mixerId: "", + avatarClass : 'avatar-recording', + preMasteredClass: preMasteredClass + }; + + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; + } + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixer.id; + + _addMediaTrack(index, trackData); + }); + + if(!noCorrespondingTracks && recordedTracks.length > 0) { + logger.error("unable to find all recorded tracks against client tracks"); + app.notify({title:"All tracks not found", + text: "Some tracks in the recording are not present in the playback", + icon_url: "/assets/content/icon_alert_big.png"}) + } + } + } + function _renderTracks() { myTracks = []; @@ -572,7 +723,9 @@ latency: "good", gainPercent: 0, muteClass: 'muted', - mixerId: "" + mixerId: "", + avatarClass: 'avatar-med', + preMasteredClass: "" }; // This is the likely cause of multi-track problems. @@ -728,7 +881,7 @@ $('.session-livetracks .when-empty').hide(); } var template = $('#template-session-track').html(); - var newTrack = context.JK.fillTemplate(template, trackData); + var newTrack = $(context.JK.fillTemplate(template, trackData)); $destination.append(newTrack); // Render VU meters and gain fader @@ -747,6 +900,31 @@ tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } + + + function _addMediaTrack(index, trackData) { + var parentSelector = '#session-recordedtracks-container'; + var $destination = $(parentSelector); + $('.session-recordings .when-empty').hide(); + $('.session-recording-name-wrapper').show(); + $('.session-recordings .recording-controls').show(); + + var template = $('#template-session-track').html(); + var newTrack = $(context.JK.fillTemplate(template, trackData)); + $destination.append(newTrack); + if(trackData.preMasteredClass) { + context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')}); + } + + // Render VU meters and gain fader + var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]'; + var gainPercent = trackData.gainPercent || 0; + connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent); + + tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); + } + + /** * Will be called when fader changes. The fader id (provided at subscribe time), * the new value (0-100) and whether the fader is still being dragged are passed. @@ -1058,11 +1236,63 @@ function promptUserToSave(recordingId) { rest.getRecording( {id: recordingId} ) .done(function(recording) { + recordingFinishedDialog.setRecording(recording); app.layout.showDialog('recordingFinished'); }) .fail(app.ajaxError); } + function openRecording(e) { + // just ignore the click if they are currently recording for now + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a recording while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } + + if(!localRecordingsDialog.isShowing()) { + app.layout.showDialog('localRecordings'); + } + + return false; + } + + function closeRecording() { + rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) + .done(function() { + sessionModel.refreshCurrentSession(); + }) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Stop Recording Playback", + "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + + context.jamClient.CloseRecording(); + + return false; + } + + function onPause() { + logger.debug("calling jamClient.SessionStopPlay"); + context.jamClient.SessionStopPlay(); + } + + function onPlay(e, data) { + logger.debug("calling jamClient.SessionStartPlay"); + context.jamClient.SessionStartPlay(data.playbackMode); + } + + function onChangePlayPosition(e, data){ + logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")"); + context.jamClient.SessionTrackSeekMs(data.positionMs); + } + function startStopRecording() { if(sessionModel.recordingModel.isRecording()) { sessionModel.recordingModel.stopRecording(); @@ -1076,17 +1306,26 @@ $('#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) - + $('#recording-start-stop').on('click', startStopRecording); + $('#open-a-recording').on('click', openRecording); $('#track-settings').click(function() { configureTrackDialog.showVoiceChatPanel(true); configureTrackDialog.showMusicAudioPanel(true); }); + $('#close-playback-recording').on('click', closeRecording); + $(playbackControls) + .on('pause', onPause) + .on('play', onPlay) + .on('change-position', onChangePlayPosition); } - this.initialize = function() { + this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance) { + localRecordingsDialog = localRecordingsDialogInstance; + recordingFinishedDialog = recordingFinishedDialogInstance; context.jamClient.SetVURefreshRate(150); + playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); events(); + var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow, diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index c4b5148c6..91ec7d1c9 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -11,6 +11,7 @@ var clientId = client.clientID; var currentSessionId = null; // Set on join, prior to setting currentSession. var currentSession = null; + var currentOrLastSession = null; var subscribers = {}; var users = {}; // User info for session participants var rest = context.JK.Rest(); @@ -29,6 +30,21 @@ } } + function isPlayingRecording() { + // this is the server's state; there is no guarantee that the local tracks + // requested from the backend will have corresponding track information + return currentSession && currentSession.claimed_recording; + } + + function recordedTracks() { + if(currentSession && currentSession.claimed_recording) { + return currentSession.claimed_recording.recording.recorded_tracks + } + else { + return null; + } + } + function creatorId() { if(!currentSession) { throw "creator is not known" @@ -68,8 +84,8 @@ recordingModel.reset(); client.JoinSession({ sessionID: sessionId }); refreshCurrentSession(); - server.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession); - server.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession); + server.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, refreshCurrentSession); + server.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, refreshCurrentSession); }) @@ -91,10 +107,14 @@ // this may be bad if someone decides to badmouth others in the left-session during this time logger.debug("calling jamClient.LeaveSession for clientId=" + clientId); client.LeaveSession({ sessionID: currentSessionId }); - deferred = leaveSessionRest(currentSessionId); - deferred.done(function() { - sessionChanged(); - }); + leaveSessionRest(currentSessionId) + .done(function() { + sessionChanged(); + deferred.resolve(arguments); + }) + .fail(function() { + deferred.reject(arguments); + }); // 'unregister' for callbacks context.jamClient.SessionRegisterCallback(""); @@ -146,7 +166,11 @@ } } + // you should only update currentSession with this function function updateCurrentSession(sessionData) { + if(sessionData != null) { + currentOrLastSession = sessionData; + } currentSession = sessionData; } @@ -449,12 +473,14 @@ // Public interface this.id = id; + this.recordedTracks = recordedTracks; this.participants = participants; this.joinSession = joinSession; this.leaveCurrentSession = leaveCurrentSession; this.refreshCurrentSession = refreshCurrentSession; this.subscribe = subscribe; this.participantForClientId = participantForClientId; + this.isPlayingRecording = isPlayingRecording; this.addTrack = addTrack; this.updateTrack = updateTrack; this.deleteTrack = deleteTrack; @@ -464,6 +490,9 @@ this.getCurrentSession = function() { return currentSession; }; + this.getCurrentOrLastSession = function() { + return currentOrLastSession; + }; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index f78fda21e..618e88d39 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -131,7 +131,7 @@ notificationId: val.notification_id, avatar_url: context.JK.resolveAvatarUrl(val.photo_url), text: val.formatted_msg, - date: context.JK.formatDate(val.created_at) + date: context.JK.formatDateTime(val.created_at) }); $('#sidebar-notification-list').append(notificationHtml); @@ -156,9 +156,15 @@ acceptFriendRequest({ "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }); }); } + else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) { $notification.find('#div-actions').hide(); } + + else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) { + $notification.find('#div-actions').hide(); + } + else if (type === context.JK.MessageType.SESSION_INVITATION) { var $action_btn = $notification.find('#btn-notification-action'); $action_btn.text('JOIN'); @@ -166,6 +172,7 @@ openTerms({ "session_id": payload.session_id, "notification_id": payload.notification_id }); }); } + else if (type === context.JK.MessageType.JOIN_REQUEST) { var $action_btn = $notification.find('#btn-notification-action'); $action_btn.text('APPROVE'); @@ -173,6 +180,7 @@ approveJoinRequest({ "join_request_id": payload.join_request_id, "notification_id": payload.notification_id }); }); } + else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) { var $action_btn = $notification.find('#btn-notification-action'); $action_btn.text('JOIN'); @@ -180,9 +188,31 @@ openTerms({ "session_id": payload.session_id, "notification_id": payload.notification_id }); }); } + else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) { $notification.find('#div-actions').hide(); } + + else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) { + var $action_btn = $notification.find('#btn-notification-action'); + $action_btn.text('LISTEN'); + $action_btn.click(function() { + listenToSession({ "session_id": payload.session_id, "notification_id": payload.notification_id }); + }); + } + + else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) { + var $action_btn = $notification.find('#btn-notification-action'); + $action_btn.text('LISTEN'); + $action_btn.click(function() { + listenToRecording({ "recording_id": payload.recording_id, "notification_id": payload.notification_id }); + }); + } + + else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) { + $notification.find('#div-actions').hide(); + } + else if (type === context.JK.MessageType.BAND_INVITATION) { var $action_btn = $notification.find('#btn-notification-action'); $action_btn.text('ACCEPT'); @@ -190,6 +220,7 @@ 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(); } @@ -346,12 +377,7 @@ // default handler for incoming notification function handleNotification(payload, type) { var sidebarText; - if (type === context.JK.MessageType.SESSION_INVITATION) { - sidebarText = payload.sender_name + " has invited you to a session."; - } - else { - sidebarText = payload.msg; - } + sidebarText = payload.msg; // increment displayed notification count incrementNotificationCount(); @@ -362,7 +388,7 @@ notificationId: payload.notification_id, avatar_url: context.JK.resolveAvatarUrl(payload.photo_url), text: sidebarText, - date: context.JK.formatDate(payload.created_at) + date: context.JK.formatDateTime(payload.created_at) }); $('#sidebar-notification-list').prepend(notificationHtml); @@ -421,16 +447,32 @@ $('.sidebar .invite-friend-row').hoverIntent(inviteHoverIn, inviteHoverOut); + // friend notifications registerFriendUpdate(); registerFriendRequest(); registerFriendRequestAccepted(); - registerMusicianSessionJoin(); - registerMusicianSessionDepart(); - registerFriendSessionJoin(); + registerNewUserFollower(); + registerNewBandFollower(); + + // session invitations registerSessionInvitation(); + registerSessionEnded(); registerJoinRequest(); registerJoinRequestApproved(); registerJoinRequestRejected(); + registerSessionJoin(); + registerSessionDepart(); + registerMusicianSessionJoin(); + registerBandSessionJoin(); + + // recording notifications + registerMusicianRecordingSaved(); + registerBandRecordingSaved(); + registerRecordingStarted(); + registerRecordingEnded(); + registerRecordingMasterMixComplete(); + + // band notifications registerBandInvitation(); registerBandInvitationAccepted(); @@ -450,11 +492,9 @@ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_UPDATE, function(header, payload) { logger.debug("Handling FRIEND_UPDATE msg " + JSON.stringify(payload)); - // update friends panel in sidebar friends[payload.user_id].online = payload.online; updateFriendList(friends); - // display notification var online_text = payload.online ? "online" : "offline"; app.notify({ "title": "Friend is now " + online_text, @@ -470,7 +510,6 @@ handleNotification(payload, header.type); - // display notification app.notify({ "title": "New Friend Request", "text": payload.msg, @@ -500,10 +539,8 @@ handleNotification(payload, header.type); - // refresh friends panel initializeFriendsPanel(); - // display notification app.notify({ "title": "Friend Request Accepted", "text": payload.msg, @@ -512,52 +549,35 @@ }); } - function registerMusicianSessionJoin() { - context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) { - logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload)); + function registerNewUserFollower() { + + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) { + logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); - // display notification app.notify({ - "title": "Musician Joined Session", - "text": payload.username + " has joined the session.", + "title": "New Follower", + "text": payload.msg, "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) }); }); } - function registerMusicianSessionDepart() { - context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, function(header, payload) { - logger.debug("Handling MUSICIAN_SESSION_DEPART msg " + JSON.stringify(payload)); + function registerNewBandFollower() { + + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) { + logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload)); - if(payload.recordingId && context.JK.CurrentSessionModel.recordingModel.isRecording(payload.recordingId)) { - context.JK.CurrentSessionModel.recordingModel.onServerStopRecording(payload.recordingId); - /**app.notify({ - "title": "Recording Stopped", - "text": payload.username + " has left the session.", - "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) - }); */ - } - else { - app.notify({ - "title": "Musician Left Session", - "text": payload.username + " has left the session.", - "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) - }); - } - }); - } + handleNotification(payload, header.type); - function registerFriendSessionJoin() { - context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_SESSION_JOIN, function(header, payload) { - logger.debug("Handling FRIEND_SESSION_JOIN msg " + JSON.stringify(payload)); - - // display notification app.notify({ - "title": "Friend Joined Session", - "text": payload.username + " has joined a session.", + "title": "New Band Follower", + "text": payload.msg, "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) }); }); + } function registerSessionInvitation() { @@ -585,7 +605,6 @@ participantHtml += ""; - // display notification app.notify({ "title": "Session Invitation", "text": participantHtml @@ -608,13 +627,16 @@ context.location = '#/session/' + args.session_id; } + function registerSessionEnded() { + // TODO: this should clean up all notifications related to this session + } + function registerJoinRequest() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) { logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload)); handleNotification(payload, header.type); - // display notification app.notify({ "title": "New Join Request", "text": payload.msg, @@ -650,7 +672,6 @@ handleNotification(payload, header.type); - // display notification app.notify({ "title": "Join Request Approved", "text": payload.msg, @@ -669,7 +690,6 @@ handleNotification(payload, header.type); - // display notification app.notify({ "title": "Join Request Rejected", "text": payload.msg, @@ -678,13 +698,189 @@ }); } + function registerSessionJoin() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, function(header, payload) { + logger.debug("Handling SESSION_JOIN msg " + JSON.stringify(payload)); + + // display notification + app.notify({ + "title": "New Session Participant", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }); + }); + } + + function registerSessionDepart() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, function(header, payload) { + logger.debug("Handling SESSION_DEPART msg " + JSON.stringify(payload)); + + var recordingId = payload.recording_id; + + if(recordingId&& context.JK.CurrentSessionModel.recordingModel.isRecording(recordingId)) { + context.JK.CurrentSessionModel.recordingModel.onServerStopRecording(recordingId); + /**app.notify({ + "title": "Recording Stopped", + "text": payload.username + " has left the session.", + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }); */ + } + else { + app.notify({ + "title": "Musician Left Session", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }); + } + }); + } + + function registerMusicianSessionJoin() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) { + logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + app.notify({ + "title": "Musician Joined Session", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, { + "ok_text": "LISTEN", + "ok_callback": listenToSession, + "ok_callback_args": { + "session_id": payload.session_id + } + }); + }); + } + + function registerBandSessionJoin() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) { + logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + // TODO: add LISTEN button linking to session + app.notify({ + "title": "Band Joined Session", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, { + "ok_text": "LISTEN", + "ok_callback": listenToSession, + "ok_callback_args": { + "session_id": payload.session_id + } + }); + }); + } + + function listenToSession(args) { + deleteNotification(args.notification_id); + context.location = '#/session/' + args.session_id; + } + + function registerMusicianRecordingSaved() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) { + logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + app.notify({ + "title": "Musician Recording Saved", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, { + "ok_text": "LISTEN", + "ok_callback": listenToRecording, + "ok_callback_args": { + "recording_id": payload.recording_id + } + }); + }); + + } + + function registerBandRecordingSaved() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) { + logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + app.notify({ + "title": "Band Recording Saved", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, { + "ok_text": "LISTEN", + "ok_callback": listenToRecording, + "ok_callback_args": { + "recording_id": payload.recording_id + } + }); + }); + } + + function listenToRecording(args) { + deleteNotification(args.notification_id); + context.location = '#/recording/' + args.recording_id; + } + + function registerRecordingStarted() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STARTED, function(header, payload) { + logger.debug("Handling RECORDING_STARTED msg " + JSON.stringify(payload)); + + app.notify({ + "title": "Recording Started", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }); + }); + } + + function registerRecordingEnded() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_ENDED, function(header, payload) { + logger.debug("Handling RECORDING_ENDED msg " + JSON.stringify(payload)); + + app.notify({ + "title": "Recording Ended", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }); + }); + } + + function registerRecordingMasterMixComplete() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) { + logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + app.notify({ + "title": "Recording Master Mix Complete", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, { + "ok_text": "SHARE", + "ok_callback": shareRecording, + "ok_callback_args": { + "recording_id": payload.recording_id + } + }); + }); + } + + function shareRecording(args) { + var recordingId = args.recording_id; + } + function registerBandInvitation() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) { logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload)); handleNotification(payload, header.type); - // display notification app.notify({ "title": "Band Invitation", "text": payload.msg, @@ -717,7 +913,6 @@ handleNotification(payload, header.type); - // display notification app.notify({ "title": "Band Invitation Accepted", "text": payload.msg, diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 8f676b221..2d9ec36a1 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -8,6 +8,14 @@ context.JK = context.JK || {}; var logger = context.JK.logger; + var days = new Array("Sun", "Mon", "Tue", + "Wed", "Thu", "Fri", "Sat"); + + var months = new Array("January", "February", "March", + "April", "May", "June", "July", "August", "September", + "October", "November", "December"); + + context.JK.stringToBool = function(s) { switch(s.toLowerCase()){ case "true": case "yes": case "1": return true; @@ -70,6 +78,69 @@ instrumentIconMap45[instrumentId] = "../assets/content/icon_instrument_" + icon + "45.png"; }); + /** + * Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) + * @param $element The element that should show the help when hovered + * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb + * @param data (optional) data for your template, if applicable + * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips + * + */ + context.JK.helpBubble = function($element, templateName, data, options) { + if(!data) { + data = {} + } + var helpText = context._.template($('#template-help-' + templateName).html(), data, { variable: 'data' }); + + var holder = $('
'); + holder.append(helpText); + + context.JK.hoverBubble($element, helpText, options); + } + + /** + * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) + * @param $element The element that should show the bubble when hovered + * @param text the text or jquery element that should be shown as contents of the bubble + * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips + */ + context.JK.hoverBubble = function($element, text, options) { + if(!text) { + logger.error("hoverBubble: no text to attach to $element %o", $element); + return; + } + + if($element instanceof jQuery) { + if ($element.length == 0) { + logger.error("hoverBubble: no element specified with text %o", text); + return; + } + } + + var defaultOpts = { + fill: '#333', + strokeStyle: '#ED3618', + spikeLength: 10, + spikeGirth: 10, + padding: 8, + cornerRadius: 0, + cssStyles: { + fontFamily: 'Raleway, Arial, Helvetica, sans-serif', + fontSize: '11px', + color:'white', + whiteSpace:'normal' + } + }; + + if(options) { + options = $.extend(false, defaultOpts, options); + } + else { + options = defaultOpts; + } + + $element.bt(text, options); + } // Uber-simple templating // var template = "Hey {name}"; // var vals = { name: "Jon" }; @@ -175,11 +246,46 @@ return retVal; } - context.JK.formatDate = function(dateString) { + context.JK.formatDateTime = function(dateString) { var date = new Date(dateString); return date.getFullYear() + "-" + context.JK.padString(date.getMonth()+1, 2) + "-" + context.JK.padString(date.getDate(), 2) + " @ " + date.toLocaleTimeString(); } + // returns Fri May 20, 2013 + context.JK.formatDate = function(dateString) { + var date = new Date(dateString); + + return days[date.getDay()] + ' ' + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); + } + + context.JK.formatTime = function(dateString) { + var date = new Date(dateString); + return date.toLocaleTimeString(); + } + + context.JK.prettyPrintSeconds = function(seconds) { + // from: http://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds + + // Minutes and seconds + var mins = ~~(seconds / 60); + var secs = seconds % 60; + + // Hours, minutes and seconds + var hrs = ~~(seconds / 3600); + var mins = ~~((seconds % 3600) / 60); + var secs = seconds % 60; + + // Output like "1:01" or "4:03:59" or "123:03:59" + var ret = ""; + + if (hrs > 0) + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; + } + context.JK.search = function(query, app, callback) { $.ajax({ type: "GET", @@ -270,7 +376,7 @@ return null; } - var ul = $('
    ') + var ul = $('
      '); $.each(field_errors, function(index, item) { ul.append(context.JK.fillTemplate("
    • {message}
    • ", {message: item})) @@ -279,6 +385,22 @@ return ul; } + context.JK.format_all_errors = function(errors_data) { + var errors = errors_data["errors"]; + if(errors == null) return $('
      • unknown error
      '); + + var ul = $('
        '); + + $.each(errors, function(fieldName, field_errors) { + + $.each(field_errors, function(index, item) { + ul.append(context.JK.fillTemplate("
      • {field} {message}
      • ", {field: fieldName, message: item})) + }); + }); + + return ul; + } + /** * Way to verify that a number of parallel tasks have all completed. diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index a5981de62..c6410266f 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -1,9 +1,10 @@ @import 'common.css.scss'; -.account-content-scroller { +.account-content-scroller, +#account-identity-content-scroller { .content-wrapper { - padding:10px 35px; + padding:10px 30px; } .content-wrapper.account { @@ -16,6 +17,7 @@ } h4 { + margin-top:8px; margin-bottom: 10px; font-weight:bold; } @@ -57,7 +59,15 @@ } .account-profile { - padding-top:25px; + padding-top:20px; + + .avatar_large { + position:absolute; + float:none; + top: -36px; + right:0; + margin-right:20%; + } h2 { margin-bottom:15px; @@ -65,17 +75,16 @@ .location { position:relative; + padding-top:25px; } #account-change-avatar { - position:absolute; - top:-27px; - right:72px; height:13px; + float:right; + margin-right:17%; } .birth_date { - margin-left:40px; } } @@ -114,6 +123,7 @@ .button-orange, .button-grey { line-height:14px; + margin-top:6px; margin-right:0; } @@ -122,7 +132,7 @@ } div.field { - margin-bottom:27px; + margin-bottom:21px; } div.profile-instrumentlist table { diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index 63152401a..814fed92f 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -273,9 +273,6 @@ #band-filter-results { margin: 0 10px 5px 10px; - overflow: auto; - height: 100%; - width: 100%; } .band-list-result { diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 2fb0e0a17..a852d46bf 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -10,12 +10,15 @@ * *= require_self *= require ./ie + *= require jquery.bt *= require ./jamkazam *= require ./content + *= require ./paginator *= require ./faders *= require ./header #= require ./user_dropdown *= require ./footer + *= require ./recordingManager *= require ./screen_common *= require ./notify *= require ./dialog @@ -29,6 +32,8 @@ *= require ./search *= require ./ftue *= require ./invitationDialog + *= require ./recordingFinishedDialog + *= require ./localRecordingsDialog *= require ./createSession *= require ./genreSelector *= require ./sessionList diff --git a/web/app/assets/stylesheets/client/clientUpdate.css.scss b/web/app/assets/stylesheets/client/clientUpdate.css.scss index 82f037af4..2b799b8da 100644 --- a/web/app/assets/stylesheets/client/clientUpdate.css.scss +++ b/web/app/assets/stylesheets/client/clientUpdate.css.scss @@ -1,34 +1,35 @@ #client_update { display:none; + + .progress-bar { + width:100%; + background-color:#000; + border: solid 1px #ED3618; + height:22px; + } + + #progress-bar { + width:0%; + } + + .progress-bar-progress { + background-color:#ED3618; + border:solid 1px #000; + height:20px; + display:block; + } + + h2 { + font-weight:bold; + font-size:x-large; + } + + #client-updater-updating #update-steps { + margin-top:20px; + } + + #client-updater-updating span.status { + color:white; + } } -.progress-bar { - width:100%; - background-color:#000; - border: solid 1px #ED3618; - height:22px; -} - -#progress-bar { - width:0%; -} - -.progress-bar-progress { - background-color:#ED3618; - border:solid 1px #000; - height:20px; - display:block; -} - -#client_update h2 { - font-weight:bold; - font-size:x-large; -} - -#client-updater-updating #update-steps { - margin-top:20px; -} - -#client-updater-updating span.status { - color:white; -} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index 168845843..88550ab29 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -88,6 +88,66 @@ } } } + + .filter-head { + position: absolute; + padding:11px 0; + } + .filter-body { + height:100%; + width:100%; + box-sizing: border-box; + padding-top: 49px; + } + + .profile-head { + position: absolute; + box-sizing: border-box; + width:100%; + } + .profile-body { + height:100%; + width:100%; + box-sizing: border-box; + padding-top: 157px; + > * { + box-sizing:border-box; + } + } + .profile-body-content { + height: inherit; + overflow:auto; + &.outer { + overflow:hidden; + } + &.padded { + padding:10px 25px; + } + } + + .profile-social-head { + position: absolute; + width:100%; + padding:0 25px; + line-height:25px; + box-sizing:border-box; + + +.profile-wrapper { + padding-top: 0; + } + } + .profile-social-body { + height:100%; + width:100%; + box-sizing:border-box; + padding-top:25px; + .profile-social-body-wrapper { + height:inherit; + } + .profile-social-content { + padding:0 25px; + } + } } .result-list-button-wrapper { @@ -248,7 +308,7 @@ a.arrow-down { #session-controls { width:100%; - padding:11px 0px 11px 0px; + padding:6px 0px 11px 0px; background-color:#4c4c4c; min-height:20px; overflow-x:scroll; @@ -341,6 +401,8 @@ ul.shortcuts { border-color:#ED3618; } + + span.arrow-right { display:inline-block; width: 0; diff --git a/web/app/assets/stylesheets/client/createSession.css.scss b/web/app/assets/stylesheets/client/createSession.css.scss index f345b2ac4..c9fe9d001 100644 --- a/web/app/assets/stylesheets/client/createSession.css.scss +++ b/web/app/assets/stylesheets/client/createSession.css.scss @@ -111,20 +111,4 @@ padding:10px; border-bottom:solid 1px #999; cursor:pointer; -} - - - -div[layout-id="createSession"] .icheckbuttons { - - margin-top:5px; - - div.iradio_minimal { - float:left; - } - - label { - float:left; - margin:0 10px 0 3px; - } -} +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/hover.css.scss b/web/app/assets/stylesheets/client/hover.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 407f0230c..819e8e9ef 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -461,3 +461,18 @@ div[layout-id=session], div[layout-id=ftue], .no-selection-range { -ms-user-select: none; user-select: none; } + + +.icheckbuttons { + + margin-top:5px; + + div.iradio_minimal { + float:left; + } + + label { + float:left; + margin:0 10px 0 3px; + } +} diff --git a/web/app/assets/stylesheets/client/localRecordingsDialog.css.scss b/web/app/assets/stylesheets/client/localRecordingsDialog.css.scss new file mode 100644 index 000000000..c1e5b9225 --- /dev/null +++ b/web/app/assets/stylesheets/client/localRecordingsDialog.css.scss @@ -0,0 +1,16 @@ +#local-recordings-dialog { + table.local-recordings { + tbody { + tr:hover { + background-color: #400606; + cursor:pointer; + } + + tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] { + background-color:#777; + color:#aaa; + } + } + } +} + diff --git a/web/app/assets/stylesheets/client/musician.css.scss b/web/app/assets/stylesheets/client/musician.css.scss index fc78eefa3..b9e47e290 100644 --- a/web/app/assets/stylesheets/client/musician.css.scss +++ b/web/app/assets/stylesheets/client/musician.css.scss @@ -22,14 +22,11 @@ #musician-filter-results { margin: 0 10px 0px 10px; - overflow: auto; - height: 100%; -// width: 100%; } .musician-wrapper { -// overflow: auto; -// height: 480px; + overflow: initial; + height: initial; width: 100%; } @@ -37,12 +34,8 @@ padding-top: 5px; padding-right: 5px; padding-left: 5px; - box-sizing:border-box; } -#session-controls.musician-filter { - padding-top: 6px; -} .musician-following { overflow: auto; } diff --git a/web/app/assets/stylesheets/client/paginator.css.scss b/web/app/assets/stylesheets/client/paginator.css.scss new file mode 100644 index 000000000..16fb9f9b8 --- /dev/null +++ b/web/app/assets/stylesheets/client/paginator.css.scss @@ -0,0 +1,30 @@ + +div.paginator { + .arrow-right { + display:inline-block; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid #FFCC00; + padding-left:5px; + } + + span.arrow-right { + border-left: 4px solid #aaa; + } + + .arrow-left { + display:inline-block; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-right: 4px solid #FFCC00; + padding-right:5px; + } + + span.arrow-left { + border-right: 4px solid #aaa; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index ef0a772dd..5e766429d 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -196,6 +196,7 @@ position:relative; margin:10px 0px 10px 0px; padding-bottom:5px; + box-sizing:border-box; } .profile-social-left { @@ -253,3 +254,7 @@ padding:3px; vertical-align:middle; } + +#btn-add-friend { + display:none; +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss b/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss new file mode 100644 index 000000000..986d140a8 --- /dev/null +++ b/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss @@ -0,0 +1,20 @@ +#recording-finished-dialog { + width:1000px; + height:auto; + div[purpose=description], div[purpose=is_public], div[purpose=is_downloadable] { + margin-top:20px; + } + + label[for=is_downloadable], label[for=is_public], label[for=playback-mode-preview-all], label[for=playback-mode-preview-me] { + display:inline; + } + + .recording-controls { + position:relative; + } + + .icheckbuttons { + margin-top:20px; + } +} + diff --git a/web/app/assets/stylesheets/client/recordingManager.css.scss b/web/app/assets/stylesheets/client/recordingManager.css.scss new file mode 100644 index 000000000..8ed92305e --- /dev/null +++ b/web/app/assets/stylesheets/client/recordingManager.css.scss @@ -0,0 +1,42 @@ +#recording-manager-viewer { + + color: #CCCCCC; + font-size: 11px; + margin: 0 auto; + position: absolute; + text-align: center; + left: 25%; + width: 50%; + + .recording-manager-command { + box-sizing: border-box; + width:33%; + margin:5px 10px; + visibility: hidden; + + .percent { + margin-left:3px; + } + + .percent:after { + content:"%"; + } + + .progress-bar { + width:100%; + background-color:#000; + border: solid 1px #ED3618; + height:22px; + display:inline; + } + + .progress-bar-progress { + background-color:#ED3618; + border:solid 1px #000; + height:20px; + display:block; + width:0%; + display:inline; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/screen_common.css.scss b/web/app/assets/stylesheets/client/screen_common.css.scss index 0edcc5c43..98fd15a4b 100644 --- a/web/app/assets/stylesheets/client/screen_common.css.scss +++ b/web/app/assets/stylesheets/client/screen_common.css.scss @@ -42,7 +42,8 @@ } } -.content { +.content, +.inner-content { clear: both; float: left; height: 100%; diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index bf23a1479..ad415a5da 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -232,14 +232,28 @@ table.vu td { .session-recording-name-wrapper { position:relative; - white-space:nowrap; + white-space:nowrap; + display:none; + + .session-add { + margin-top:9px; + } + .session-add a { + vertical-align:top; + outline:none; + + img { + margin-top:-3px; + } + } + } .session-recording-name { width:60%; overflow:hidden; - margin-top:6px; + margin-top:9px; margin-bottom:8px; font-size:16px; } @@ -477,6 +491,38 @@ table.vu td { background-color:#666; } +.session-mytracks { + .track-connection { + display:none; + } +} + +.session-recordings { + .track-connection { + display:none; + } + + .track-close { + display:none; + } +} + +.recording-controls { + display:none; + + .play-button { + outline:none; + } + + .play-button img.pausebutton { + display:none; + } +} + +#recording-finished-dialog .recording-controls { + display:block; +} + .voicechat { margin-top:10px; @@ -571,7 +617,7 @@ table.vu td { width:93%; min-width:200px; background-color:#242323; - position:relative; + position:absolute; font-size:13px; text-align:center; } @@ -593,6 +639,10 @@ table.vu td { margin-top:4px; } +.recording-time.duration-time { + padding-left:2px; +} + .recording-playback { display:inline-block; background-image:url(/assets/content/bkg_playcontrols.png); @@ -601,12 +651,17 @@ table.vu td { width:65%; height:16px; margin-top:2px; + cursor:pointer; } .recording-slider { position:absolute; - left:40px; + left:0px; top:0px; + + img { + position:absolute; + } } .recording-current { diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index 869353633..aae78f5e4 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -1,49 +1,49 @@ -table.findsession-table { - margin-top:6px; - width:98%; - font-size:11px; - color:#fff; - background-color:#262626; - border:solid 1px #4d4d4d; -} +table.findsession-table, table.local-recordings { + margin-top:6px; + width:98%; + font-size:11px; + color:#fff; + background-color:#262626; + border:solid 1px #4d4d4d; -.findsession-table th { + th { font-weight:300; background-color:#4d4d4d; padding:6px; border-right:solid 1px #333; -} + } -.findsession-table td { + td { padding:9px 5px 5px 5px; border-right:solid 1px #333; border-top:solid 1px #333; vertical-align:top; white-space:normal; -} + } -.findsession-table .noborder { + .noborder { border-right:none; -} + } -.findsession-table .musicians { + .musicians { margin-top:-3px; -} + } -.findsession-table .musicians td { + .musicians td { border-right:none; border-top:none; padding:3px; vertical-align:middle; -} + } -.findsession-table a { + a { color:#fff; text-decoration:none; -} + } -.findsession-table a:hover { + a:hover { color:#227985; + } } .latency-grey { diff --git a/web/app/assets/stylesheets/landing/footer.css.scss b/web/app/assets/stylesheets/landing/footer.css.scss index ad1bc8b7f..63fcbbaac 100644 --- a/web/app/assets/stylesheets/landing/footer.css.scss +++ b/web/app/assets/stylesheets/landing/footer.css.scss @@ -12,6 +12,10 @@ padding-top: 10px; margin: 30px 30px 0; border-top:solid 1px #444; + + #recording-manager-viewer { + display:none; + } } #copyright { diff --git a/web/app/assets/stylesheets/playbackControls.css.scss b/web/app/assets/stylesheets/playbackControls.css.scss new file mode 100644 index 000000000..f0a0ab29a --- /dev/null +++ b/web/app/assets/stylesheets/playbackControls.css.scss @@ -0,0 +1,5 @@ +.recording-controls { + .playback-mode-buttons { + display:none; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/footer.css.scss b/web/app/assets/stylesheets/web/footer.css.scss index df599b11b..f29ff4261 100644 --- a/web/app/assets/stylesheets/web/footer.css.scss +++ b/web/app/assets/stylesheets/web/footer.css.scss @@ -10,6 +10,11 @@ #footer { padding-top: 10px; border-top:solid 1px #444; + + #recording-manager-viewer { + display:none; + } + } #copyright { diff --git a/web/app/assets/stylesheets/web/main.css.scss b/web/app/assets/stylesheets/web/main.css.scss index b9622093b..fb3b8f142 100644 --- a/web/app/assets/stylesheets/web/main.css.scss +++ b/web/app/assets/stylesheets/web/main.css.scss @@ -363,4 +363,4 @@ strong { fieldset.login-error .login-error-msg { display:block; } -} +} \ No newline at end of file diff --git a/web/app/controllers/api_claimed_recordings_controller.rb b/web/app/controllers/api_claimed_recordings_controller.rb index f19fe5c88..a7c7dc013 100644 --- a/web/app/controllers/api_claimed_recordings_controller.rb +++ b/web/app/controllers/api_claimed_recordings_controller.rb @@ -6,7 +6,8 @@ class ApiClaimedRecordingsController < ApiController respond_to :json def index - @claimed_recordings = ClaimedRecording.where(:user_id => current_user.id).order("created_at DESC").paginate(page: params[:page]) + @claimed_recordings = ClaimedRecording.where(:user_id => current_user.id).order("created_at DESC").paginate(page: params[:page], per_page: params[:per_page]) + response.headers['total-entries'] = @claimed_recordings.total_entries.to_s end def show @@ -23,13 +24,13 @@ class ApiClaimedRecordingsController < ApiController end def delete - begin - @claimed_recording.discard(current_user) - render :json => {}, :status => 204 + #begin + #@claimed_recording.discard(current_user) + #render :json => {}, :status => 204 # respond_with responder: ApiResponder, :status => 204 - rescue - render :json => { :message => "claimed_recording could not be deleted" }, :status => 403 - end + #rescue + #render :json => { :message => "claimed_recording could not be deleted" }, :status => 403 + #end end private diff --git a/web/app/controllers/api_invitations_controller.rb b/web/app/controllers/api_invitations_controller.rb index 48faa4fb5..266f97d6d 100644 --- a/web/app/controllers/api_invitations_controller.rb +++ b/web/app/controllers/api_invitations_controller.rb @@ -46,7 +46,7 @@ class ApiInvitationsController < ApiController User.save_session_settings(current_user, music_session) # send notification - Notification.send_session_invitation(receiver.id, current_user, music_session.id) + Notification.send_session_invitation(receiver, current_user, music_session.id) respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation) else diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index f4a2411c8..ab93d3c29 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController # have to be signed in currently to see this screen before_filter :api_signed_in_user - before_filter :lookup_session, only: [:show, :update, :delete] + before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop] skip_before_filter :api_signed_in_user, only: [:perf_upload] respond_to :json @@ -122,10 +122,6 @@ class ApiMusicSessionsController < ApiController end end - def lookup_session - @music_session = MusicSession.find(params[:id]) - end - def track_index @tracks = Track.index(current_user, params[:id]) end @@ -235,8 +231,38 @@ class ApiMusicSessionsController < ApiController # so... just return 200 render :json => { :id => @perfdata.id }, :status => 200 end - end - end + + + def claimed_recording_start + @music_session.claimed_recording_start(current_user, ClaimedRecording.find(params[:claimed_recording_id])) + + if @music_session.errors.any? + # we have to do this because api_session_detail_url will fail with a bad @music_session + response.status = :unprocessable_entity + respond_with @music_session + else + respond_with @music_session, responder: ApiResponder + end + end + + def claimed_recording_stop + @music_session.claimed_recording_stop + + if @music_session.errors.any? + # we have to do this because api_session_detail_url will fail with a bad @music_session + response.status = :unprocessable_entity + respond_with @music_session + else + respond_with @music_session, responder: ApiResponder + end + end + + private + + def lookup_session + @music_session = MusicSession.find(params[:id]) + end + end diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 40d2aae14..d87f1f9c3 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -1,18 +1,22 @@ class ApiRecordingsController < ApiController before_filter :api_signed_in_user - before_filter :look_up_recording, :only => [ :show, :stop, :claim, :keep ] + before_filter :look_up_recording, :only => [ :show, :stop, :claim, :discard, :keep ] before_filter :parse_filename, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] respond_to :json @@log = Logging.logger[ApiRecordingsController] + def index + # lists recordings created by for the current user + @recordings = Recording.list_recordings(current_user, params[:created_by]) + end + # Returns all files this user should be uploading from his client def list_uploads begin result = Recording.list_uploads(current_user, params[:limit], params[:since]) - puts result.inspect render :json => result, :status => 200 rescue render :json => { :message => "could not produce list of files" }, :status => 403 @@ -32,15 +36,15 @@ class ApiRecordingsController < ApiController end def download + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user) + redirect_to @recorded_track.sign_url end def start music_session = MusicSession.find(params[:music_session_id]) - unless music_session.users.exists?(current_user) - raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR - end + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless music_session.users.exists?(current_user) @recording = Recording.start(music_session, current_user) @@ -54,10 +58,6 @@ class ApiRecordingsController < ApiController def stop - unless @recording.users.exists?(current_user) - raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR - end - @recording.stop if @recording.errors.any? @@ -68,10 +68,9 @@ class ApiRecordingsController < ApiController end end - # keep will kick off a mix, as well as create a claimed recording for the creator + # claim will create a claimed recording for the creator def claim - claim = @recording.claim(current_user, params[:name], params[:description], Genre.find(params[:genre_id]), params[:is_public], params[:is_downloadable]) - claim.save + claim = @recording.claim(current_user, params[:name], params[:description], Genre.find_by_id(params[:genre]), params[:is_public], params[:is_downloadable]) if claim.errors.any? response.status = :unprocessable_entity @@ -81,6 +80,12 @@ class ApiRecordingsController < ApiController end end + # discard will tell the server the user has no interest in the recording + def discard + @recording.discard(current_user) + render :json => {}, :status => 200 + end + def upload_next_part length = params[:length] md5 = params[:md5] @@ -141,14 +146,13 @@ class ApiRecordingsController < ApiController private def parse_filename - @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) - unless @recorded_track - render :json => { :message => ValidationMessages::RECORDING_NOT_FOUND }, :status => 404 - end + @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user) end def look_up_recording @recording = Recording.find(params[:id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recording.has_access?(current_user) end end diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 1fa1c4271..0565d95b5 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -23,6 +23,7 @@ class ApiUsersController < ApiController {:band_musicians => :user}, :bands, :instruments]) .find(params[:id]) + respond_with @user, responder: ApiResponder, :status => 200 end @@ -55,8 +56,6 @@ class ApiUsersController < ApiController def update @user = User.find(params[:id]) - - @user.first_name = params[:first_name] if params.has_key?(:first_name) @user.last_name = params[:last_name] if params.has_key?(:last_name) @user.gender = params[:gender] if params.has_key?(:gender) @@ -269,11 +268,11 @@ class ApiUsersController < ApiController id = params[:id] if !params[:user_id].nil? - User.create_user_following(params[:user_id], id) + @user.create_user_following(params[:user_id]) respond_with @user, responder: ApiResponder, :location => api_user_following_index_url(@user) elsif !params[:band_id].nil? - User.create_band_following(params[:band_id], id) + @user.create_band_following(params[:band_id]) respond_with @user, responder: ApiResponder, :location => api_band_following_index_url(@user) end end diff --git a/web/app/responders/api_responder.rb b/web/app/responders/api_responder.rb index 64df5fd69..0520f6d88 100644 --- a/web/app/responders/api_responder.rb +++ b/web/app/responders/api_responder.rb @@ -5,7 +5,6 @@ class ApiResponder < ActionController::Responder logger.debug("REST API entity has error: #{resource.errors.inspect}") controller.response.status = :unprocessable_entity when post? - logger.debug("REST API post") controller.response.status = :created end diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index 9668f029a..cb46530cd 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -4,7 +4,7 @@ object @claimed_recording -attributes :id, :name, :is_public, :is_downloadable +attributes :id, :name, :description, :is_public, :is_downloadable child(:recording => :recording) { attributes :id, :created_at, :duration @@ -15,15 +15,13 @@ child(:recording => :recording) { child(:mixes => :mixes) { attributes :id, :url, :is_completed } -} -child(:recorded_tracks => :recorded_tracks) { - attributes :id, :fully_uploaded, :url - child(:instrument => :instrument) { - attributes :id, :description - } - child(:user => :user) { - attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + child(:recorded_tracks => :recorded_tracks) { + attributes :id, :fully_uploaded, :url, :client_track_id, :client_id, :instrument_id + + child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + } } } diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index adc061e44..5170503ad 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -1,6 +1,6 @@ object @music_session -attributes :id, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id +attributes :id, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id node :genres do |item| item.genres.map(&:description) @@ -38,3 +38,30 @@ node(:join_requests, :if => lambda { |music_session| music_session.users.exists? } } end + +# only show currently playing recording data if the current_user is in the session +node(:claimed_recording, :if => lambda { |music_session| music_session.users.exists?(current_user) } ) do |music_session| + + child(:claimed_recording => :claimed_recording) { + attributes :id, :name, :description, :is_public, :is_downloadable + + child(:recording => :recording) { + attributes :id, :created_at, :duration + child(:band => :band) { + attributes :id, :name + } + + child(:mixes => :mixes) { + attributes :id, :url, :is_completed + } + + child(:recorded_tracks => :recorded_tracks) { + attributes :id, :fully_uploaded, :url, :client_track_id, :client_id, :instrument_id + + child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + } + } + } + } +end diff --git a/web/app/views/api_recordings/index.rabl b/web/app/views/api_recordings/index.rabl new file mode 100644 index 000000000..524fe1792 --- /dev/null +++ b/web/app/views/api_recordings/index.rabl @@ -0,0 +1,3 @@ +object @recordings + +extends "api_recordings_sessions/show" diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index f0f15de2b..9fd83dfcf 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -3,12 +3,9 @@ object @recording attributes :id, :band, :created_at, :duration child(:recorded_tracks => :recorded_tracks) { - attributes :id, :client_id, :track_id, :user_id, :fully_uploaded, :url - child(:instrument => :instrument) { - attributes :id, :description - } + attributes :id, :fully_uploaded, :url, :client_track_id, :client_id, :instrument_id + child(:user => :user) { attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url } } - diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index 13360ac19..fb3e14d28 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -13,10 +13,10 @@ if @user == current_user attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email elsif current_user node :is_friend do |uu| - @user.friends?(current_user) + current_user.friends?(@user) end node :is_following do |uu| - @user.following?(current_user) + current_user.following?(@user) end end diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index e301ae4e5..982493710 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -30,7 +30,9 @@

        profile:

        + Change Avatar +
        @@ -54,19 +56,19 @@
        -
        +
        -
        +
        - +

        -
        +
        -
        +
        <%= date_select("user", "birth_date", :use_short_month => true, :start_year => 1900, :end_year => Time.now.year - 18, :order => [:month, :day, :year], :default => -25.years.from_now) %>
        @@ -100,9 +102,9 @@ CANCEL  UPDATE PROFILE
        - -
        +
        +
        diff --git a/web/app/views/clients/_account_profile_avatar.html.erb b/web/app/views/clients/_account_profile_avatar.html.erb index 49bf17f20..010ee663e 100644 --- a/web/app/views/clients/_account_profile_avatar.html.erb +++ b/web/app/views/clients/_account_profile_avatar.html.erb @@ -12,11 +12,13 @@
        - - \ No newline at end of file diff --git a/web/app/views/clients/_hoverBubble.html.erb b/web/app/views/clients/_hoverBubble.html.erb new file mode 100644 index 000000000..6fb92a38e --- /dev/null +++ b/web/app/views/clients/_hoverBubble.html.erb @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/web/app/views/clients/_localRecordingsDialog.html.erb b/web/app/views/clients/_localRecordingsDialog.html.erb new file mode 100644 index 000000000..f1234de60 --- /dev/null +++ b/web/app/views/clients/_localRecordingsDialog.html.erb @@ -0,0 +1,48 @@ + +
        + +
        + <%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %> +

        open a recording

        +
        + +
        + +
        + + + + + + + + + + + +
        WHENNAMEDURATION
        +
        + +
        + +
        + +
        +
        + CANCEL +
        + +
        +
        + + + +
        + + diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index 115aba151..0df6f6e06 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -7,11 +7,11 @@ <%= render "screen_navigation" %> <% end -%> <%= content_tag(:div, :class => 'content-body') do -%> - <%= content_tag(:div, :class => 'content-body-scroller') do -%> - <%= form_tag('', :id => 'find-musician-form') do -%> - <%= content_tag(:div, render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_MUSICIAN}), :class => 'musician-filter', :id => 'session-controls') %> - <%= content_tag(:div, :class => 'content-scroller') do -%> - <%= content_tag(:div, content_tag(:div, '', :id => 'musician-filter-results'), :class => 'content-wrapper musician-wrapper') %> + <%= form_tag('', {:id => 'find-musician-form', :class => 'inner-content'}) do -%> + <%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_MUSICIAN}) %> + <%= content_tag(:div, :class => 'filter-body') do %> + <%= content_tag(:div, :class => 'content-body-scroller') do -%> + <%= content_tag(:div, content_tag(:div, '', :id => 'musician-filter-results', :class => 'filter-results'), :class => 'content-wrapper musician-wrapper') %> <% end -%> <% end -%> <% end -%> @@ -22,7 +22,7 @@ diff --git a/web/app/views/clients/_play_controls.html.erb b/web/app/views/clients/_play_controls.html.erb new file mode 100644 index 000000000..7ee2042d4 --- /dev/null +++ b/web/app/views/clients/_play_controls.html.erb @@ -0,0 +1,35 @@ + +
        + + + + <%= image_tag "content/icon_playbutton.png", {:height => 20, :width => 20, :class=> "playbutton"} %> + <%= image_tag "content/icon_pausebutton.png", {:height => 20, :width => 20, :class=> "pausebutton"} %> + + + +
        + + +
        0:00
        + + +
        +
        <%= image_tag "content/slider_playcontrols.png", {:height => 16, :width => 5} %>
        +
        + + +
        0:00
        +
        + + + +
        0:00
        + +
        + + +
        + +
        + \ No newline at end of file diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb index 238d2cdd8..029292e0c 100644 --- a/web/app/views/clients/_profile.html.erb +++ b/web/app/views/clients/_profile.html.erb @@ -10,9 +10,8 @@ <%= render "screen_navigation" %>
        -
        -
        -
        + +

        @@ -45,57 +44,83 @@
        -
        -
        + +
        +
        -
        -

        Location:


        -


        -

        Stats:


        -
        -
        -
        -
        -
        -
        -
        -
        -


        -
        +
        +
        +

        Location:


        +


        +

        Stats:


        +
        +
        +
        +
        +
        +
        +
        +


        +
        +
        +
        -
        +
        +
        +
        +
        +

        -
        -
        -
        -
        -
        -

        Friends

        -
        +
        +
        +
        +

        Friends

        +
        +
        +

        Following

        +
        +
        +

        Followers

        +
        +
        +
        +
        +
        +
        + +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        + +
        -
        -

        Following

        -
        +
        +
        +
        +
        -
        -

        Followers

        -
        -
        -
        -
        -
        -
        -
        +
        -
        @@ -130,9 +155,9 @@ {location}

        {genres}


        - {follower_count}   - {recording_count}   - {session_count}  + {follower_count}   + {recording_count}   + {session_count} 

        {biography}

        diff --git a/web/app/views/clients/_recordingFinishedDialog.html.erb b/web/app/views/clients/_recordingFinishedDialog.html.erb index 4193782a2..ea1cd0e6b 100644 --- a/web/app/views/clients/_recordingFinishedDialog.html.erb +++ b/web/app/views/clients/_recordingFinishedDialog.html.erb @@ -7,89 +7,56 @@
        - Fill out the fields below and click the "SAVE" button to save this recording to your library. If you do not want to keep the recording, click the "DISCARD" button. -
        -
        + Fill out the fields below and click the "SAVE" button to save this recording to your library. If you do not want to + keep the recording, click the "DISCARD" button. +
        +
        -
        -
        - Recording name:
        - -
        -
        - - Genre:
        - - - -
        -
        -
        - Description:
        - -
        - -
        - Preview Recording:
        - -
        - - - <%= image_tag "content/icon_playbutton.png", {:height => 20, :width => 20} %> - - -
        - - -
        0:00
        - - -
        -
        <%= image_tag "content/slider_playcontrols.png", {:height => 16, :width => 5} %>
        -
        - - -
        4:59
        +
        +
        +
        +
        +
        - - - -
        - 1:23 +
        +
        +
        + +
        + +
        +
        +
        + + +
        +
        +
        +
        + +
        + + +
        + Preview Recording: + + <%= render "play_controls" %> + +
        +
        - -
        -
        - Make this Recording Public
        - Make this Recording Downloadable
        -
        - -

        +

        -
        +
        - - - - DISCARD
        diff --git a/web/app/views/clients/_recordingManager.html.erb b/web/app/views/clients/_recordingManager.html.erb new file mode 100644 index 000000000..6a1f7f2b4 --- /dev/null +++ b/web/app/views/clients/_recordingManager.html.erb @@ -0,0 +1,11 @@ + + + converting0 + + + uploading0 + + + downloading0 + + \ No newline at end of file diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb index 4c71c8349..d044ff38c 100644 --- a/web/app/views/clients/_session.html.erb +++ b/web/app/views/clients/_session.html.erb @@ -7,116 +7,125 @@

        session

        +
        -
        - - <%= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} %> - RESYNC - - - <%= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} %> - SETTINGS - - - <%= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} %> - SHARE - +
        + + <%= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} %> + RESYNC + + + <%= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} %> + SETTINGS + + + <%= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} %> + SHARE + - -
        -
        VOLUME:
        -
        + +
        +
        VOLUME:
        +
        +
        + + +
        +
        MIX:
        +
        others
        +
        +
        me
        +
        + + + X  LEAVE
        + - -
        -
        MIX:
        -
        others
        -
        -
        me
        -
        + +
        +
        - - X  LEAVE -
        - + +
        - -
        -
        + +
        +

        my tracks

        +
        + <%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %>  Settings +
        - -
        - - -
        -

        my tracks

        -
        - <%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %>  Settings -
        - -
        -
        - -
        - -
        @@ -138,10 +147,10 @@
        <%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
        -
        +
        -
        +
        diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb index ae0be5889..cfad92687 100644 --- a/web/app/views/clients/_web_filter.html.erb +++ b/web/app/views/clients/_web_filter.html.erb @@ -1,30 +1,39 @@ -<% filter_label = defined?(search_type) && search_type == Search::PARAM_BAND ? :band : :musician %> -<%= content_tag(:div, :class => "filter-element wrapper") do -%> - <%= 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 -%> -<%= content_tag(:div, :class => 'filter-element wrapper') do -%> - <% if :musician == filter_label %> - - <%= 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] }))) %> - <% elsif :band == filter_label %> - - <%= 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 %> -<% end -%> - -<%= 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)) %> + +<% case search_type + when Search::PARAM_BAND + filter_label = :band + when Search::PARAM_MUSICIAN + filter_label = :musician + 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') %> + + <%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %> <% 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") %> + <%= content_tag(:div, :class => 'filter-element wrapper') do -%> + <% if :musician == filter_label %> + + <%= 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] }))) %> + <% elsif :band == filter_label %> + + <%= 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 %> + <% end -%> + + <%= 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 -%> <% end -%> <% end -%> + \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 1e8a95051..bbd434dbf 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -9,6 +9,7 @@ <%= render "header" %> <%= render "home" %> <%= render "footer" %> +<%= render "paginator" %> <%= render "searchResults" %> <%= render "faders" %> <%= render "vu_meters" %> @@ -36,11 +37,13 @@ <%= render "invitationDialog" %> <%= render "whatsNextDialog" %> <%= render "recordingFinishedDialog" %> +<%= render "localRecordingsDialog" %> <%= render "notify" %> <%= render "client_update" %> <%= render "banner" %> <%= render "clients/banners/disconnected" %> <%= render "overlay_small" %> +<%= render "help" %>