From 164fe8db61ae084c4b1f186f43787f5ac12da1a9 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 6 Nov 2014 11:26:13 -0600 Subject: [PATCH] * recording cleanup logic VRFS-2393 --- db/manifest | 1 + db/up/deletable_recordings.sql | 35 ++ ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/models/feed.rb | 2 +- ruby/lib/jam_ruby/models/mix.rb | 8 +- ruby/lib/jam_ruby/models/recorded_track.rb | 7 +- ruby/lib/jam_ruby/models/recording.rb | 80 +++- ruby/lib/jam_ruby/models/user_sync.rb | 25 ++ .../resque/scheduled/recordings_cleaner.rb | 32 ++ ruby/spec/factories.rb | 39 +- ruby/spec/jam_ruby/models/feed_spec.rb | 14 + ruby/spec/jam_ruby/models/recording_spec.rb | 410 ++++++++++++++++++ ruby/spec/jam_ruby/models/user_sync_spec.rb | 206 ++++++++- ruby/spec/support/utilities.rb | 4 + web/app/assets/javascripts/feedHelper.js | 11 + web/app/assets/javascripts/jam_rest.js | 10 + .../assets/javascripts/recordingManager.js | 6 +- .../assets/javascripts/sync_viewer.js.coffee | 86 +++- web/app/assets/javascripts/utils.js | 2 +- .../client/recordingManager.css.scss | 3 + .../dialogs/editRecordingDialog.css.scss | 18 + .../stylesheets/users/syncViewer.css.scss | 12 +- .../api_claimed_recordings_controller.rb | 2 +- .../controllers/api_recordings_controller.rb | 16 +- .../controllers/api_user_syncs_controller.rb | 5 + web/app/controllers/recordings_controller.rb | 4 +- .../views/api_claimed_recordings/show.rabl | 2 +- web/app/views/api_feeds/show.rabl | 2 +- web/app/views/api_recordings/show.rabl | 2 +- web/app/views/clients/_help.html.erb | 14 +- .../views/clients/_recordingManager.html.erb | 3 + .../clients/_sync_viewer_templates.html.slim | 6 +- .../dialogs/_edit_recording_dialog.html.slim | 34 +- web/app/views/recordings/show.html.erb | 4 +- .../users/_feed_recording_ajax.html.haml | 2 +- web/config/routes.rb | 2 + web/config/scheduler.yml | 7 +- .../api_user_syncs_controller_spec.rb | 6 + 38 files changed, 1055 insertions(+), 68 deletions(-) diff --git a/db/manifest b/db/manifest index c8652bebf..3625cb735 100755 --- a/db/manifest +++ b/db/manifest @@ -225,3 +225,4 @@ add_youtube_flag_to_claimed_recordings.sql add_session_create_type.sql user_syncs_and_quick_mix.sql user_syncs_fix_dup_tracks_2408.sql +deletable_recordings.sql diff --git a/db/up/deletable_recordings.sql b/db/up/deletable_recordings.sql index 842c4d4d8..648b45e19 100644 --- a/db/up/deletable_recordings.sql +++ b/db/up/deletable_recordings.sql @@ -1,2 +1,37 @@ -- this is to make sure we don't delete any recordings for 7 days UPDATE recordings SET updated_at = NOW(); + +ALTER TABLE recordings ADD COLUMN deleted BOOLEAN DEFAULT FALSE NOT NULL; + +DROP VIEW user_syncs; + +CREATE VIEW user_syncs AS + SELECT DISTINCT b.id AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) as quick_mix_id, + b.id AS unified_id, + a.user_id AS user_id, + b.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + mixes.id AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + mixes.id AS unified_id, + claimed_recordings.user_id AS user_id, + NULL as fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + quick_mixes.id AS quick_mix_id, + quick_mixes.id AS unified_id, + quick_mixes.user_id, + quick_mixes.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE; diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 649dc40e6..22f97a24f 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -54,6 +54,7 @@ require "jam_ruby/resque/scheduled/music_session_scheduler" require "jam_ruby/resque/scheduled/active_music_session_cleaner" require "jam_ruby/resque/scheduled/score_history_sweeper" require "jam_ruby/resque/scheduled/scheduled_music_session_cleaner" +require "jam_ruby/resque/scheduled/recordings_cleaner" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/mq_router" diff --git a/ruby/lib/jam_ruby/models/feed.rb b/ruby/lib/jam_ruby/models/feed.rb index 7fba1e705..71b10d054 100644 --- a/ruby/lib/jam_ruby/models/feed.rb +++ b/ruby/lib/jam_ruby/models/feed.rb @@ -35,7 +35,7 @@ module JamRuby query = Feed.joins("LEFT OUTER JOIN recordings ON recordings.id = feeds.recording_id") .joins("LEFT OUTER JOIN music_sessions ON music_sessions.id = feeds.music_session_id") .limit(limit) - .where('recordings is NULL OR recordings.all_discarded = false') # remove any 'all_discarded recordings from the search results' + .where('recordings is NULL OR (recordings.all_discarded = false AND recordings.deleted = false)') # remove any 'all_discarded recordings from the search results' or 'deleted' # handle sort if sort == 'date' diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 957e46ba4..8e8ab2ace 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -198,13 +198,15 @@ module JamRuby self.last_downloaded_at = Time.now end - private def delete_s3_files - s3_manager.delete(filename(type='ogg')) if self[:ogg_url] - s3_manager.delete(filename(type='mp3')) if self[:mp3_url] + s3_manager.delete(filename(type='ogg')) if self[:ogg_url] && s3_manager.exists?(filename(type='ogg')) + s3_manager.delete(filename(type='mp3')) if self[:mp3_url] && s3_manager.exists?(filename(type='mp3')) end + private + + def self.construct_filename(created_at, recording_id, id, type='ogg') raise "unknown ID" unless id diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index ce3299d8b..ccd5a2a3c 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -218,11 +218,12 @@ module JamRuby self.last_downloaded_at = Time.now end + def delete_s3_files + s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url]) + end + private - def delete_s3_files - s3_manager.delete(self[:url]) if self[:url] - end def self.construct_filename(created_at, recording_id, client_track_id) raise "unknown ID" unless client_track_id diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 663622fff..34ccf78a7 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -1,12 +1,8 @@ module JamRuby class Recording < ActiveRecord::Base - self.primary_key = 'id' - - @@log = Logging.logger[Recording] - attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" @@ -225,8 +221,6 @@ module JamRuby 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, upload_to_youtube=false) upload_to_youtube = !!upload_to_youtube # Correct where nil is borking save @@ -255,6 +249,8 @@ module JamRuby def keep(user) recorded_tracks_for_user(user).update_all(:discard => false) + Recording.where(:id => id).update_all(:updated_at => Time.now) # updated updated_at for benefit of RecordingsCleaner + User.where(:id => user.id).update_all(:first_recording_at => Time.now ) unless user.first_recording_at end @@ -263,6 +259,8 @@ module JamRuby def discard(user) recorded_tracks_for_user(user).update_all(:discard => true) + Recording.where(:id => id).update_all(:updated_at => Time.now) # updated updated_at for benefit of RecordingsCleaner + # check if all recorded_tracks for this recording are discarded if recorded_tracks.where('discard = false or discard is NULL').length == 0 self.all_discarded = true # the feed won't pick this up; also background cleanup will find these and whack them later @@ -299,6 +297,8 @@ module JamRuby .order('recorded_tracks.id') .where('recorded_tracks.fully_uploaded = TRUE') .where('recorded_tracks.id > ?', since) + .where('all_discarded = false') + .where('deleted = false') .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_track| downloads.push( { @@ -319,6 +319,8 @@ module JamRuby .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) + .where('all_discarded = false') + .where('deleted = false') .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user) .limit(limit).each do |mix| downloads.push( @@ -454,6 +456,7 @@ module JamRuby .where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") \ .where("duration IS NOT NULL") \ .where('all_discarded = false') \ + .where('deleted = false') \ .order('recorded_items_all.id') \ .limit(limit) @@ -529,10 +532,71 @@ module JamRuby end # returns a ClaimedRecording that the user did not discard - def claim_for_user(user) + def claim_for_user(user, ignore_discarded = false) return nil unless user claim = claimed_recordings.find{|claimed_recording| claimed_recording.user == user } - claim unless claim && claim.discarded + if ignore_discarded + claim + else + claim unless claim && claim.discarded + end + end + + def self.when_will_be_discarded? + + recorded_track_votes = recorded_tracks.map(&:discard) + + discarded = 0 + recorded_track_votes.each do |discard_vote| + if discard_vote == nil || discard_vote == true + discarded = discarded + 1 + end + end + + if recorded_track_votes.length == discarded + # all tracks are discarded, figure out due time for deletion + # 3 days in seconds - amount of seconds since last updated + ((APP_CONFIG.recordings_stale_time * 3600 * 24) - (Time.now - updated_at).to_i).seconds.from_now + else + return nil + end + end + + # finds all discarded recordings that are sufficiently stale (i.e., abandoned by all those involved, and hasn't been mucked with in a while) + def self.discarded_and_stale + + # we count up all tracks for the Recording, and count up all discarded/not-voted-on tracks + # if they are equal, and if the recording is stale, let's return it. + Recording + .joins("INNER JOIN recorded_tracks ON recordings.id = recorded_tracks.recording_id") + .joins(%Q{ + LEFT OUTER JOIN + (SELECT id + FROM recorded_tracks WHERE discard IS NULL OR discard = TRUE) AS discard_info + ON recorded_tracks.id = discard_info.id + }) + .group("recordings.id") + .having('COUNT(recorded_tracks.id) = COUNT(discard_info.id)') + .where("NOW() - recordings.updated_at > '#{APP_CONFIG.recordings_stale_time} day'::INTERVAL") + .limit(1000) + end + + def mark_delete + + mixes.each do |mix| + mix.delete_s3_files + end + + quick_mixes.each do |quick_mix| + quick_mix.delete_s3_files + end + + recorded_tracks.each do |recorded_track| + recorded_track.delete_s3_files + end + + self.deleted = true + self.save(:validate => false) end private diff --git a/ruby/lib/jam_ruby/models/user_sync.rb b/ruby/lib/jam_ruby/models/user_sync.rb index 17925bcdf..03e4b938a 100644 --- a/ruby/lib/jam_ruby/models/user_sync.rb +++ b/ruby/lib/jam_ruby/models/user_sync.rb @@ -41,6 +41,31 @@ module JamRuby { query:query, next: offset + limit} end end + + + def self.deletables(params) + user_id = params[:user_id] + recording_ids = params[:recording_ids] + + limit = 1000 + + recording_ids = recording_ids.uniq + + raise "too many recording_ids" if recording_ids.length > limit + + found_recording_ids = + UserSync + .select('user_syncs.recording_id') + .joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id") + .where(%Q{ + ((claimed_recordings IS NULL OR claimed_recordings.discarded = TRUE) AND fully_uploaded = FALSE) OR (claimed_recordings IS NOT NULL AND claimed_recordings.discarded = FALSE) + }) + .where(user_id: user_id) + .paginate(:page => 1, :per_page => limit) + .group('user_syncs.recording_id').map(&:recording_id) + + recording_ids - found_recording_ids + end end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/recordings_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/recordings_cleaner.rb index e69de29bb..1e4b41769 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/recordings_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/recordings_cleaner.rb @@ -0,0 +1,32 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # periodically scheduled to find recordings to cleanup + class RecordingsCleaner + extend Resque::Plugins::LonelyJob + + @queue = :recordings_cleaner + + @@log = Logging.logger[RecordingsCleaner] + + def self.lock_timeout + # this should be enough time to make sure the job has finished, but not so long that the system isn't recovering from a abandoned job + 1200 + end + + def self.perform + discarded_recordings = Recording.discarded_and_stale + + discarded_recordings.each do |recording| + @@log.debug("deleting recording #{recording.id}") + recording.mark_delete + end + end + end + +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index b91b9dee1..264b44323 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -250,8 +250,6 @@ FactoryGirl.define do end factory :recorded_video, :class => JamRuby::RecordedVideo do - sequence(:client_id) { |n| "client_id-#{n}"} - sequence(:recording_id) { |n| "recording_id-#{n}"} sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} fully_uploaded true length 1 @@ -283,14 +281,17 @@ FactoryGirl.define do association :genre, factory: :genre association :user, factory: :user - before(:create) { |claimed_recording| - claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user) unless claimed_recording.recording + before(:create) { |claimed_recording, evaluator| + claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user) unless evaluator.recording } end factory :mix, :class => JamRuby::Mix do + ignore do + autowire true + end started_at Time.now completed_at Time.now ogg_md5 'abc' @@ -301,10 +302,12 @@ FactoryGirl.define do sequence(:mp3_url) { |n| "recordings/mp3/#{n}" } completed true - before(:create) {|mix| - user = FactoryGirl.create(:user) - mix.recording = FactoryGirl.create(:recording_with_track, owner: user) - mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording) + before(:create) {|mix, evaluator| + if evaluator.autowire + user = FactoryGirl.create(:user) + mix.recording = FactoryGirl.create(:recording_with_track, owner: user) + mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording) + end } end @@ -330,6 +333,19 @@ FactoryGirl.define do mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording) end } + + + factory :quick_mix_completed do + started_at 1.minute.ago + completed_at Time.now + ogg_md5 'a' + ogg_length 1 + ogg_url 'recordings/ogg' + mp3_md5 'a' + mp3_length 1 + mp3_url 'recordings/mp3' + completed true + end end factory :invited_user, :class => JamRuby::InvitedUser do @@ -526,6 +542,13 @@ FactoryGirl.define do token_expires_at Time.now end + + factory :recording_comment, :class => JamRuby::RecordingComment do + sequence(:comment) { |n| "comment-#{n}" } + association :recording, factory: :recording + association :user, factory: :recording + end + factory :playable_play, :class => JamRuby::PlayablePlay do end diff --git a/ruby/spec/jam_ruby/models/feed_spec.rb b/ruby/spec/jam_ruby/models/feed_spec.rb index 16ae1b73a..550802c8b 100644 --- a/ruby/spec/jam_ruby/models/feed_spec.rb +++ b/ruby/spec/jam_ruby/models/feed_spec.rb @@ -464,5 +464,19 @@ describe Feed do end end + describe "deleted recording" do + it "should not show" do + claimed_recording = FactoryGirl.create(:claimed_recording) + MusicSessionUserHistory.delete_all # the factory makes a music_session while making the recording/claimed_recording + MusicSession.delete_all # the factory makes a music_session while making the recording/claimed_recording + recording = claimed_recording.recording + recording.mark_delete + recording.reload + recording.deleted.should == true + feeds, next_page = Feed.index(user1) + feeds.length.should == 0 + end + end + end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index c093d5950..01b1290e1 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -430,6 +430,19 @@ describe Recording do Recording.list_uploads(@user, 10, uploads["next"])["uploads"].length.should == 0 end + + it "should not consider deleted recordings" do + stub_const("APP_CONFIG", app_config) + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "Recording", "Recording Description", @genre, true) + @recording.mark_delete + uploads = Recording.list_uploads(@user) + uploads["uploads"].length.should == 0 + end + it "should return a download only if claimed" do @recording = Recording.start(@music_session, @user) @recording.stop @@ -444,6 +457,21 @@ describe Recording do downloads["downloads"].length.should == 1 end + it "should not return a download if recording is deleted" do + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "Recording", "Recording Description", @genre, 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) + @recording.mark_delete + downloads = Recording.list_downloads(@user) + downloads["downloads"].length.should == 0 + end + it "should mark first_recording_at" do @recording = Recording.start(@music_session, @user) @recording.stop @@ -638,6 +666,388 @@ describe Recording do uploads["uploads"].should have(0).items end + + describe "discarded_and_stale" do + let(:recording1) {FactoryGirl.create(:recording_with_track)} + let(:track1) {recording1.recorded_tracks[0]} + + it "no results if no recordings" do + Recording.discarded_and_stale.length.should == 0 + end + + describe "with one recording" do + before(:each) do + track1.discard.should be_nil + end + + describe "that has no votes" do + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + # and we should find it now... + stale = Recording.discarded_and_stale + stale.first.should eq(recording1) + end + end + + describe "that has discard vote" do + + before(:each) do + track1.discard = true + track1.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + # and we should find it now... + stale = Recording.discarded_and_stale + stale.first.should eq(recording1) + end + end + + describe "that has keep vote" do + + before(:each) do + track1.discard = false + track1.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "still not found it has an old updated_at time, because it was kept" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + # and still not find it + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + end + + describe "that has two tracks, no votes" do + + let(:track2) { FactoryGirl.create(:recorded_track, recording: recording1, user: recording1.owner) } + + before(:each) do + track1.discard = nil + track1.save! + + recording1.recorded_tracks << track2 + recording1.save! + + track2.discard = nil + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + # and we should find it now... + stale = Recording.discarded_and_stale + stale.first.should eq(recording1) + end + end + end + + describe "that has two tracks, two discard" do + + let(:track2) { FactoryGirl.create(:recorded_track, recording: recording1, user: recording1.owner) } + + before(:each) do + track1.discard = true + track1.save! + + recording1.recorded_tracks << track2 + recording1.save! + + track2.discard = true + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + # and we should find it now... + stale = Recording.discarded_and_stale + stale.first.should eq(recording1) + end + end + + describe "that has two tracks, one discard, one keep" do + + let(:track2) { FactoryGirl.create(:recorded_track, recording: recording1, user: recording1.owner) } + + before(:each) do + track1.discard = true + track1.save! + + recording1.recorded_tracks << track2 + recording1.save! + + track2.discard = false + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + end + + describe "that has two tracks, two keeps" do + + let(:track2) { FactoryGirl.create(:recorded_track, recording: recording1, user: recording1.owner) } + + before(:each) do + track1.discard = false + track1.save! + + recording1.recorded_tracks << track2 + recording1.save! + + track2.discard = false + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + end + + describe "two recordings" do + let(:recording2) {FactoryGirl.create(:recording_with_track)} + let(:track2) {recording2.recorded_tracks[0]} + + describe "both discard" do + + before(:each) do + track1.discard = true + track1.save! + + track2.discard = true + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 1 + stale.first.should eq(recording1) + + Recording.where(:id => recording2.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 2 + end + end + + describe "both keep" do + + before(:each) do + track1.discard = false + track1.save! + + track2.discard = false + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + + Recording.where(:id => recording2.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + end + + describe "one keep, one discard" do + + before(:each) do + track1.discard = false + track1.save! + + track2.discard = true + track2.save! + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + + Recording.where(:id => recording2.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 1 + stale.first.should eq(recording2) + end + end + + describe "two keeps, two discards" do + + let(:track1_2) { FactoryGirl.create(:recorded_track, recording: recording1, user: recording1.owner) } + let(:track2_2) { FactoryGirl.create(:recorded_track, recording: recording2, user: recording1.owner) } + + before(:each) do + track1.discard = false + track1.save! + recording1.recorded_tracks << track1_2 + recording1.save! + track1_2.discard = false + track1_2.save! + + track2.discard = true + track2.save! + recording2.recorded_tracks << track2_2 + recording2.save! + track2_2.discard = true + track2_2.save! + + end + + it "not found if it has recent updated_at time" do + stale = Recording.discarded_and_stale + stale.length.should == 0 + end + + it "found if it has an old updated_at time" do + # now age the recording + Recording.where(:id => recording1.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 0 + + Recording.where(:id => recording2.id).update_all(:updated_at => (APP_CONFIG.recordings_stale_time + 1).days.ago) + + stale = Recording.discarded_and_stale + stale.length.should == 1 + stale.first.should eq(recording2) + end + end + end + end + + describe "delete" do + let(:mix) {FactoryGirl.create(:mix)} + let(:recording) {mix.recording} + + before(:each) do + + end + + it "success" do + FactoryGirl.create(:quick_mix, user: recording.owner, recording:recording, autowire: false) + FactoryGirl.create(:recording_comment, recording: recording, user: recording.owner) + FactoryGirl.create(:recording_like, recording: recording, claimed_recording: recording.claimed_recordings.first, favorite:true) + FactoryGirl.create(:playable_play, playable_id: recording.id, playable_type: 'JamRuby::Recording') + FactoryGirl.create(:recorded_video, user: recording.owner, recording: recording) + recording.reload + recording.claimed_recordings.length.should > 0 + recording.mixes.length.should > 0 + recording.quick_mixes.length.should > 0 + recording.recorded_tracks.length.should > 0 + recording.comments.length.should > 0 + recording.likes.length.should > 0 + recording.plays.length.should > 0 + recording.recorded_videos.length.should > 0 + recording.feed.should_not be_nil + + claimed_recording = recording.claimed_recordings.first + quick_mix = recording.quick_mixes.first + track = recording.recorded_tracks.first + comment = recording.comments.first + like = recording.likes.first + play = recording.plays.first + feed = recording.feed + video = recording.recorded_videos.first + + recording.mark_delete + + ClaimedRecording.find_by_id(claimed_recording.id).should_not be_nil + Mix.find_by_id(mix.id).should_not be_nil + QuickMix.find_by_id(quick_mix.id).should_not be_nil + RecordedTrack.find_by_id(track.id).should_not be_nil + RecordingComment.find_by_id(comment.id).should_not be_nil + PlayablePlay.find_by_id(play.id).should_not be_nil + RecordingLiker.find_by_id(like.id).should_not be_nil + Feed.find_by_id(feed.id).should_not be_nil + RecordedVideo.find_by_id(video.id).should_not be_nil + end + end end diff --git a/ruby/spec/jam_ruby/models/user_sync_spec.rb b/ruby/spec/jam_ruby/models/user_sync_spec.rb index 349cf706e..83b9e4465 100644 --- a/ruby/spec/jam_ruby/models/user_sync_spec.rb +++ b/ruby/spec/jam_ruby/models/user_sync_spec.rb @@ -20,19 +20,25 @@ describe UserSync do data[:next].should be_nil end - it "one mix" do + it "one mix and quick mix" do mix = FactoryGirl.create(:mix) mix.recording.duration = 1 mix.recording.save! + quick_mix = FactoryGirl.create(:quick_mix_completed, recording:mix.recording, user: mix.recording.recorded_tracks[0].user, autowire:false) data = UserSync.index({user_id: mix.recording.recorded_tracks[0].user.id}) data[:next].should be_nil user_syncs = data[:query] - user_syncs.length.should == 2 + user_syncs.length.should == 3 user_syncs[0].recorded_track.should == mix.recording.recorded_tracks[0] user_syncs[0].mix.should be_nil + user_syncs[0].quick_mix.should be_nil user_syncs[1].mix.should == mix user_syncs[1].recorded_track.should be_nil + user_syncs[1].quick_mix.should be_nil + user_syncs[2].mix.should be_nil + user_syncs[2].recorded_track.should be_nil + user_syncs[2].quick_mix.should eq(quick_mix) end it "two mixes, one not belonging to querier" do @@ -267,8 +273,204 @@ describe UserSync do user_syncs = data[:query] user_syncs.length.should == 0 data[:query].total_entries.should == 2 + end + end + it "does not return deleted recordings" do + mix = FactoryGirl.create(:mix) + mix.recording.duration = 1 + mix.recording.save! + quick_mix = FactoryGirl.create(:quick_mix_completed, recording:mix.recording, user: mix.recording.recorded_tracks[0].user, autowire:false) + mix.recording.mark_delete + data = UserSync.index({user_id: mix.recording.recorded_tracks[0].user.id}) + data[:next].should be_nil + user_syncs = data[:query] + user_syncs.length.should == 0 + end + + describe "deletable" do + describe "one mix and one quick mix" do + + let!(:mix) { m = FactoryGirl.create(:mix); m.recording.duration = 1; m.recording.save!; m} + let!(:quick_mix) { FactoryGirl.create(:quick_mix_completed, recording:mix.recording, user: mix.recording.recorded_tracks[0].user, autowire:false) } + + it "unknown id" do + recording_ids = ['1'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(recording_ids) + end + + it "unknown ids" do + recording_ids = ['1', '2', '3'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(recording_ids) + end + + it "valid recording id" do + recording_ids = [mix.recording.id] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq([]) + end + + it "valid recording id" do + recording_ids = [mix.recording.id] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq([]) + end + + it "valid recording_id mixed with unknown ids" do + recording_ids = [mix.recording.id, '1'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(['1']) + end + + end + + describe "two recordings" do + + let!(:mix) { m = FactoryGirl.create(:mix); m.recording.duration = 1; m.recording.save!; m} + let!(:quick_mix) { FactoryGirl.create(:quick_mix_completed, recording:mix.recording, user: mix.recording.recorded_tracks[0].user, autowire:false) } + + let!(:mix2) { m = FactoryGirl.create(:mix); m.recording.duration = 1; m.recording.save!; m} + let!(:quick_mix2) { FactoryGirl.create(:quick_mix_completed, recording:mix2.recording, user: mix2.recording.recorded_tracks[0].user, autowire:false) } + + before(:each) do + # fix up the user associated with the second mix/recording to be same as 1st + mix2.recording.owner = mix.recording.owner + mix2.recording.save! + mix2.recording.recorded_tracks[0].user = mix.recording.owner + mix2.recording.recorded_tracks[0].save! + end + + it "unknown id" do + recording_ids = ['1'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(recording_ids) + end + + it "unknown ids" do + recording_ids = ['1', '2', '3'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(recording_ids) + end + + it "valid recording id" do + recording_ids = [mix.recording.id, mix2.recording.id] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq([]) + end + + it "valid recording id" do + recording_ids = [mix.recording.id] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq([]) + end + + it "valid recording_id mixed with unknown ids" do + recording_ids = [mix.recording.id, '1'] + + result = UserSync.deletables(user_id: mix.recording.recorded_tracks[0].user.id, recording_ids: recording_ids) + + result.should eq(['1']) + end + end + + it "resolved recordings" do + + # start with a recording with a fully uploaded recorded_track, and no claim + recording = FactoryGirl.create(:recording_with_track, owner: user1) + recording.duration = 1 + recording.save! + + recording_ids = [recording.id] + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq(recording_ids) + + # set the recorded_track to not fully uploaded, which should make it not deletable + recording.recorded_tracks[0].fully_uploaded = false + recording.recorded_tracks[0].save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq([]) + + # mark recording as deleted, which should make it deletable + recording.deleted = true + recording.save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq(recording_ids) + + # mark recording as fully discarded, which should make it deletable + recording.all_discarded = true + recording.deleted = false + recording.save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq(recording_ids) + + # claim the recording, and make the track not fully uploaded + recording.recorded_tracks[0].fully_uploaded = false + recording.all_discarded = false + claim = FactoryGirl.create(:claimed_recording, user: user1, recording: recording) + recording.save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq([]) + + # create a mix while still claiming the recording + mix = FactoryGirl.create(:mix, autowire:false, recording:recording) + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq([]) + + # now take away the claim, and make sure the track is fully uploaded + claim.discarded = true + claim.save! + # without a claimed recording, make the track fully uploaded, so as to not trigger 'need to upload' logic + recording.recorded_tracks[0].fully_uploaded = true + recording.recorded_tracks[0].save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq(recording_ids) + + # if we make a quick mix, but still have no claim, we still should still need to delete the recording + quick_mix = FactoryGirl.create(:quick_mix, autowire:false, user: user1, recording: recording, fully_uploaded: true) + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq(recording_ids) + + # make the quick_mix be not fully_uploaded, which should make it not be marked for deleting because we need to upload it + quick_mix.fully_uploaded = false + quick_mix.save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq([]) + + quick_mix.fully_uploaded = true + quick_mix.save! + claim.discarded = false + claim.save! + + result = UserSync.deletables(user_id: user1.id, recording_ids: recording_ids) + result.should eq([]) end end end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 0ed42f4d9..48a525fe0 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -138,6 +138,10 @@ def app_config 2 # don't put to 1; it'll break tests end + def recordings_stale_time + 7 + end + private def audiomixer_workspace_path diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js index 328dcdace..9ae0b54db 100644 --- a/web/app/assets/javascripts/feedHelper.js +++ b/web/app/assets/javascripts/feedHelper.js @@ -23,6 +23,7 @@ var nextPage = 1; var $includeType = null; var didLoadAllFeeds = false, isLoading = false; + var $templateRecordingDiscardedSoon = null; function defaultQuery() { var query = { limit: feedBatchSize }; @@ -441,6 +442,15 @@ var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); var $controls = $feedItem.find('.recording-controls'); + var $titleText = $feedItem.find('.title .title-text'); + + // if this item will be discarded, tack on a * to the RECORDING NAME + var discardTime = feed['when_will_be_discarded?']; + if(discardTime) { + context.JK.helpBubble($titleText, 'recording-discarded-soon', {discardTime: discardTime}, {}); + $titleText.text($titleText.text() + '*'); + } + $controls.data('mix-state', feed.mix_info) // for recordingUtils helper methods $controls.data('server-info', feed.mix) // for recordingUtils helper methods $controls.data('view-context', 'feed') @@ -589,6 +599,7 @@ $includeDate.val(defaults.time_range) $includeType.val(defaults.type) + $templateRecordingDiscardedSoon = $('#template-help-recording-discarded-soon'); events(); } diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 5c714bde5..47368278c 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1064,6 +1064,15 @@ }); } + function deleteRecordingClaim(id) { + return $.ajax({ + type: "DELETE", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + id + "/claim" + }); + } + function claimRecording(options) { var recordingId = options["id"]; @@ -1337,6 +1346,7 @@ this.getClaimedRecording = getClaimedRecording; this.updateClaimedRecording = updateClaimedRecording; this.deleteClaimedRecording = deleteClaimedRecording; + this.deleteRecordingClaim = deleteRecordingClaim; this.claimRecording = claimRecording; this.startPlayClaimedRecording = startPlayClaimedRecording; this.stopPlayClaimedRecording = stopPlayClaimedRecording; diff --git a/web/app/assets/javascripts/recordingManager.js b/web/app/assets/javascripts/recordingManager.js index 30b10da4c..be2dacca6 100644 --- a/web/app/assets/javascripts/recordingManager.js +++ b/web/app/assets/javascripts/recordingManager.js @@ -25,6 +25,8 @@ var $uploadPercent = $('#recording-manager-upload .percent', $parentElement); var $convertCommand = $('#recording-manager-convert', $parentElement); var $convertPercent = $('#recording-manager-convert .percent', $parentElement); + var $deleteCommand = $('#recording-manager-delete', $parentElement); + var $deletePercent = $('#recording-manager-delete .percent', $parentElement); var $fileManager = $('#recording-manager-launcher', $parentElement); if($fileManager.length == 0) {throw "no file manager element"; } @@ -32,12 +34,14 @@ $downloadCommand.data('command-type', 'download') $uploadCommand.data('command-type', 'upload') $convertCommand.data('command-type', 'convert') + $deleteCommand.data('command-type', 'delete') // keys come from backend var lookup = { SyncDownload: { command: $downloadCommand, percent: $downloadPercent}, SyncUpload: { command: $uploadCommand, percent: $uploadPercent}, - SyncConvert: { command: $convertCommand, percent: $convertPercent} + SyncConvert: { command: $convertCommand, percent: $convertPercent}, + SyncDelete: { command: $deleteCommand, percent: $deletePercent} } var $self = $(this); diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 4be93ba13..98f43ec18 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -14,7 +14,10 @@ context.JK.SyncViewer = class SyncViewer @downloadCommandId = null @downloadMetadata = null @uploadCommandId = null - @uploadMetadata = null; + @uploadMetadata = null + @cleanupCommandId = null + @cleanupMetadata = null + init: () => @root = $($('#template-sync-viewer').html()) @@ -330,12 +333,15 @@ context.JK.SyncViewer = class SyncViewer for clientInfo in recording.local_tracks $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") $track.data('client-info', clientInfo) + $track.data('total-size', recording.size) $track = @list.find(".mix[data-recording-id='#{recording.recording_id}']") $track.data('client-info', recording.mix) + $track.data('total-size', recording.size) $track = @list.find(".stream-mix[data-recording-id='#{recording.recording_id}']") $track.data('client-info', recording.stream_mix) + $track.data('total-size', recording.size) displayStreamMixHover: ($streamMix) => $clientState = $streamMix.find('.client-state') @@ -470,10 +476,10 @@ context.JK.SyncViewer = class SyncViewer sendCommand: ($retry, cmd) => if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() - context.JK.confirmBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')}) + context.JK.ackBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')}) else context.jamClient.OnTrySyncCommand(cmd) - context.JK.confirmBubble($retry, 'sync-viewer-retry', {}, {offsetParent: $retry.closest('.dialog')}) + context.JK.ackBubble($retry, 'sync-viewer-retry', {}, {offsetParent: $retry.closest('.dialog')}) retryDownloadRecordedTrack: (e) => @@ -564,7 +570,7 @@ context.JK.SyncViewer = class SyncViewer exportRecording: (e) => $export = $(e.target) if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() - context.JK.confirmBubble($export, 'sync-viewer-paused', {}, {offsetParent: $export.closest('.dialog')}) + context.JK.ackBubble($export, 'sync-viewer-paused', {}, {offsetParent: $export.closest('.dialog')}) return recordingId = $export.closest('.details').attr('data-recording-id') @@ -573,7 +579,7 @@ context.JK.SyncViewer = class SyncViewer cmd = { type: 'export_recording', - action: 'export' + action: 'export', queue: 'upload', recording_id: recordingId} @@ -581,9 +587,50 @@ context.JK.SyncViewer = class SyncViewer context.jamClient.OnTrySyncCommand(cmd) return false; + deleteRecording: (e) => + $delete = $(e.target) + if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() + context.JK.ackBubble($delete, 'sync-viewer-paused', {}, {offsetParent: $delete.closest('.dialog')}) + return + + $details = $delete.closest('.details') + recordingId = $details.attr('data-recording-id') + + if !recordingId? or recordingId == "" + throw "deleteRecording can't find data-recording-id" + + context.JK.Banner.showYesNo({ + title: "Confirm Deletion", + html: "Are you sure you want to delete this recording?", + yes: => + @rest.deleteRecordingClaim(recordingId).done((response)-> + cmd = + { type: 'recording_directory', + action: 'delete', + queue: 'cleanup', + recording_id: recordingId} + + context.JK.ackBubble($delete, 'command-enqueued', {}, {offsetParent: $delete.closest('.dialog')}) + + logger.debug("enqueueing delete #{recordingId}") + + context.jamClient.OnTrySyncCommand(cmd) + ) + .fail(@app.ajaxError) + }) + + return false; + + displaySize: (size) => + # size is in bytes. divide by million, and round to one decimal place + megs = Math.round(size * 10 / (1024 * 1024) ) / 10 + "#{megs}M" createRecordingWrapper: ($toWrap, recordingInfo) => + totalSize = $($toWrap.get(0)).data('total-size') recordingInfo.recording_landing_url = "/recordings/#{recordingInfo.id}" + recordingInfo.totalSize = this.displaySize(totalSize) + recordingInfo.claimedRecordingId = recordingInfo.my?.id $wrapperDetails = $(context._.template(@templateRecordingWrapperDetails.html(), recordingInfo, {variable: 'data'})) $wrapper = $('
') $toWrap.wrapAll($wrapper) @@ -595,6 +642,7 @@ context.JK.SyncViewer = class SyncViewer $wrapper.append(this.createMix('fake', recordingInfo)) $wrapper.find('a.export').click(this.exportRecording) + $wrapper.find('a.delete').click(this.deleteRecording) separateByRecording: () => $recordedTracks = @list.find('.sync') @@ -747,6 +795,10 @@ context.JK.SyncViewer = class SyncViewer this.renderUploadRecordedTrack(commandId, commandMetadata) else this.renderGeneric(commandId, 'upload', commandMetadata) + else if commandMetadata.queue == 'cleanup' + @cleanupCommandId = commandId + @cleanupMetadata = commandMetadata + renderSingleRecording: (userSyncs) => return if userSyncs.entries.length == 0 @@ -813,6 +865,9 @@ context.JK.SyncViewer = class SyncViewer recordingId = @uploadMetadata['recording_id'] this.updateSingleRecording(recordingId) if recordingId? + else if commandId == @cleanupCommandId + this.logResult(@cleanupMetadata, success, reason, false) + else @logger.error("unknown commandId in renderFinishCommand") @@ -849,7 +904,8 @@ context.JK.SyncViewer = class SyncViewer $matchingStreamMix = @list.find(".stream-mix.sync[data-recording-id='#{recordingId}']") if $matchingStreamMix.length > 0 this.updateProgressOnSync($matchingStreamMix, 'upload', percentage) - + else if commandId == @cleanupCommandId + # ignore else @logger.error("unknown commandId in renderFinishCommand") @@ -860,7 +916,7 @@ context.JK.SyncViewer = class SyncViewer commandType = data['commandType'] commandMetadata = data['commandMetadata'] - category = commandType == 'download' ? 'download' : 'upload' + category = commandMetadata.queue if category == 'download' && (@downloadCommandId != null && @downloadCommandId != commandId) @logger.warn("received command-start for download but previous command did not send stop") @@ -876,17 +932,20 @@ context.JK.SyncViewer = class SyncViewer commandId = data['commandId'] if commandId == @downloadCommandId - category = 'download' this.renderFinishCommand(commandId, data) @downloadCommandId = null @downloadMetadata = null; else if commandId == @uploadCommandId - category = 'upload' this.renderFinishCommand(commandId, data) @uploadCommandId = null - @uploadMetadata = null; + @uploadMetadata = null + else if commandId == @cleanupCommandId + this.renderFinishCommand(commandId, data) + @cleanupCommandId = null + @cleanupMetadata = null + else - @logger.warn("received command-stop for unknown command: #{commandId} #{@downloadCommandId} #{@uploadCommandId}" ) + @logger.warn("received command-stop for unknown command: #{commandId} #{@downloadCommandId} #{@uploadCommandId} #{@cleanupCommandId}" ) fileManagerCmdProgress: (e, data) => #console.log("fileManagerCmdProgress", data) @@ -899,6 +958,8 @@ context.JK.SyncViewer = class SyncViewer else if commandId == @uploadCommandId category = 'upload' this.renderPercentage(commandId, category, data.percentage) + else if commandId == @cleanupCommandId + # do nothing else @logger.warn("received command-percentage for unknown command") @@ -918,6 +979,8 @@ context.JK.SyncViewer = class SyncViewer return 'CLEANUP TRACK' else if metadata.type == 'stream_mix' && metadata.action == 'upload' return 'UPLOADING STREAM MIX' + else if metadata.type == 'recording_directory' && metadata.action == 'delete' + return 'DELETE RECORDING' else return "#{metadata.action} #{metadata.type}".toUpperCase() @@ -938,6 +1001,7 @@ context.JK.SyncViewer = class SyncViewer when 'no-match-in-queue' then 'restart JamKazam' when 'already-done' then 'ignored, already done' when 'failed-convert' then 'failed previously' + when 'minimum-protection-time' then 'too soon to delete' else reason displaySuccess = if success then 'yes' else 'no' diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index b7b7e1b26..b7641dbbc 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -149,7 +149,7 @@ * @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.confirmBubble = function($element, templateName, data, options) { + context.JK.ackBubble = function($element, templateName, data, options) { if(!options) options = {}; options.spikeGirth = 0; options.spikeLength = 0; diff --git a/web/app/assets/stylesheets/client/recordingManager.css.scss b/web/app/assets/stylesheets/client/recordingManager.css.scss index 729e19b1e..d51d2001f 100644 --- a/web/app/assets/stylesheets/client/recordingManager.css.scss +++ b/web/app/assets/stylesheets/client/recordingManager.css.scss @@ -8,6 +8,9 @@ left: 17%; width: 50%; + #recording-manager-delete { + display:none; + } // if it's the native client, then show the File Manager span. if it's not (normal browser) hide it. // even if it's the native client, once a command is running, hide File Manager &.native-client { diff --git a/web/app/assets/stylesheets/dialogs/editRecordingDialog.css.scss b/web/app/assets/stylesheets/dialogs/editRecordingDialog.css.scss index 5a38282a4..b4d55c549 100644 --- a/web/app/assets/stylesheets/dialogs/editRecordingDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/editRecordingDialog.css.scss @@ -73,6 +73,24 @@ margin-top:4px; } } + label[for="keep"] { + display: inline; + float:right; + line-height: 26px; + padding-right: 5px; + vertical-align: middle; + } + select[name="keep"] { + float:right; + } + div[purpose="keep"] { + clear:both; + float:right; + .icheckbox_minimal { + float:right; + margin-top:4px; + } + } diff --git a/web/app/assets/stylesheets/users/syncViewer.css.scss b/web/app/assets/stylesheets/users/syncViewer.css.scss index 5ed5c471f..dcdae64e4 100644 --- a/web/app/assets/stylesheets/users/syncViewer.css.scss +++ b/web/app/assets/stylesheets/users/syncViewer.css.scss @@ -194,13 +194,23 @@ .export { float:right; - margin-right:3px; + margin-right:10px; font-size:12px; } .timeago { float:right; font-size:12px; } + .totalsize { + float:right; + margin-right:10px; + font-size:12px; + } + .delete { + float:right; + margin-right:10px; + font-size:12px; + } } .log-list { diff --git a/web/app/controllers/api_claimed_recordings_controller.rb b/web/app/controllers/api_claimed_recordings_controller.rb index 725209843..60acb0eb4 100644 --- a/web/app/controllers/api_claimed_recordings_controller.rb +++ b/web/app/controllers/api_claimed_recordings_controller.rb @@ -30,7 +30,7 @@ class ApiClaimedRecordingsController < ApiController raise PermissionError, 'only owner of claimed_recording can update it' end @claimed_recording.discard(current_user) - render :json => {}, :status => 200 + respond_with @claimed_recording end def download diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index ce6cf9d42..f08789905 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -1,7 +1,7 @@ class ApiRecordingsController < ApiController before_filter :api_signed_in_user, :except => [ :add_like ] - before_filter :look_up_recording, :only => [ :show, :stop, :claim, :discard, :keep ] + before_filter :look_up_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ] before_filter :parse_filename, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ] @@ -94,6 +94,20 @@ class ApiRecordingsController < ApiController end end + def delete_claim + claim = @recording.claim_for_user(current_user) + + if claim + claim.discard(current_user) + if claim.errors.any? + response.status = :unprocessable_entity + respond_with claim + end + end + + respond_with @recording + end + def add_comment if params[:id].blank? render :json => { :message => "Recording ID is required" }, :status => 400 diff --git a/web/app/controllers/api_user_syncs_controller.rb b/web/app/controllers/api_user_syncs_controller.rb index 32fbbe395..d511b34af 100644 --- a/web/app/controllers/api_user_syncs_controller.rb +++ b/web/app/controllers/api_user_syncs_controller.rb @@ -38,4 +38,9 @@ class ApiUserSyncsController < ApiController @next = data[:next] render "api_user_syncs/index", :layout => nil end + + def deletables + data = UserSync.deletables({user_id:current_user.id, recording_ids: params[:recording_ids]}) + render json: {recording_ids: data}, status: 200 + end end diff --git a/web/app/controllers/recordings_controller.rb b/web/app/controllers/recordings_controller.rb index 2f92c64c0..96bf1e1a1 100644 --- a/web/app/controllers/recordings_controller.rb +++ b/web/app/controllers/recordings_controller.rb @@ -5,8 +5,8 @@ class RecordingsController < ApplicationController def show @claimed_recording = ClaimedRecording.find_by_id(params[:id]) if @claimed_recording.nil? - recording = Recording.find(params[:id]) - @claimed_recording = recording.candidate_claimed_recording + recording = Recording.find_by_id(params[:id]) + @claimed_recording = recording.candidate_claimed_recording if recording end render :layout => "web" end diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index 6be19dee3..dbc1c7fd9 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, :description, :is_public, :genre_id +attributes :id, :name, :description, :is_public, :genre_id, :discarded node :share_url do |claimed_recording| unless claimed_recording.share_token.nil? diff --git a/web/app/views/api_feeds/show.rabl b/web/app/views/api_feeds/show.rabl index 06a6df897..31edd3497 100644 --- a/web/app/views/api_feeds/show.rabl +++ b/web/app/views/api_feeds/show.rabl @@ -78,7 +78,7 @@ glue :recording do 'recording' end - attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state + attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state, :when_will_be_discarded? node do |recording| { diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index b1e52f16c..1fe574026 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -1,6 +1,6 @@ object @recording -attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count +attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded? node :mix do |recording| if recording.mix diff --git a/web/app/views/clients/_help.html.erb b/web/app/views/clients/_help.html.erb index f857c3f08..38409096c 100644 --- a/web/app/views/clients/_help.html.erb +++ b/web/app/views/clients/_help.html.erb @@ -129,7 +129,7 @@ @@ -139,5 +139,17 @@ + + + + diff --git a/web/app/views/clients/_recordingManager.html.erb b/web/app/views/clients/_recordingManager.html.erb index 8553d47b7..b95c2f38b 100644 --- a/web/app/views/clients/_recordingManager.html.erb +++ b/web/app/views/clients/_recordingManager.html.erb @@ -11,4 +11,7 @@ downloading0 + + delete0 + \ No newline at end of file diff --git a/web/app/views/clients/_sync_viewer_templates.html.slim b/web/app/views/clients/_sync_viewer_templates.html.slim index 464e4007d..4c08f9ab9 100644 --- a/web/app/views/clients/_sync_viewer_templates.html.slim +++ b/web/app/views/clients/_sync_viewer_templates.html.slim @@ -88,12 +88,16 @@ script type="text/template" id='template-sync-viewer-no-syncs' | You have no recordings. script type="text/template" id="template-sync-viewer-recording-wrapper-details" - .details data-recording-id="{{data.id}}" + .details data-recording-id="{{data.id}}" data-claimed-recording-id="{{data.claimedRecordingId}}" a.session-detail-page href="{{data.recording_landing_url}}" rel="external" span.name | {{data.my ? data.my.name : 'Unknown Name'}} span.timeago | {{$.timeago(data.created_at)}} + span.totalsize + | SIZE: {{data.totalSize}} + a.delete href="#" + | DELETE a.export href="#" | EXPORT diff --git a/web/app/views/dialogs/_edit_recording_dialog.html.slim b/web/app/views/dialogs/_edit_recording_dialog.html.slim index a34457f2d..1b505af8e 100644 --- a/web/app/views/dialogs/_edit_recording_dialog.html.slim +++ b/web/app/views/dialogs/_edit_recording_dialog.html.slim @@ -1,24 +1,26 @@ -.dialog.configure-tracks{ layout: 'dialog', 'layout-id' => 'edit-recording', id: 'edit-recording-dialog'} +#edit-recording-dialog.dialog.configure-tracks layout='dialog' layout-id='edit-recording' .content-head = image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } - %h1 Edit Recording + h1 Edit Recording .dialog-inner - %form + form .field - %label{for: 'name'} Recording name: - %input{type: 'text', name: 'name'} + label for='name' Recording name: + input type='text' name='name' .field - %label{for: 'description'} Description: - %textarea{name: 'description', rows: '4'} + label for='description' Description: + textarea name='description' rows='4' .field.genre-selector - %label{for: 'genre'} Genre: - %select{name:'genre'} - .field{purpose: 'is_public'} - %input{type: 'checkbox', name: 'is_public'} - %label{for: 'is_public'} Public Recording + label for='genre' Genre: + select name='genre' + .field purpose='is_public' + input type='checkbox' name='is_public' + label for='is_public' Public Recording .buttons - %a.button-grey.cancel-btn {'layout-action' => 'cancel'} CANCEL - %a.button-orange.delete-btn DELETE - %a.button-orange.save-btn UPDATE - %br{clear: 'all'} \ No newline at end of file + .left + a.button-grey.cancel-btn layout-action='cancel' CANCEL + .right + a.button-orange.delete-btn DELETE + a.button-orange.save-btn UPDATE + br clear='all' \ No newline at end of file diff --git a/web/app/views/recordings/show.html.erb b/web/app/views/recordings/show.html.erb index b8e2bf74a..6df8f601b 100644 --- a/web/app/views/recordings/show.html.erb +++ b/web/app/views/recordings/show.html.erb @@ -18,7 +18,7 @@ <% end %>
-<% if @claimed_recording.is_public || @claimed_recording.recording.has_access?(current_user) %> +<% if !@claimed_recording.recording.deleted && (@claimed_recording.is_public || @claimed_recording.recording.has_access?(current_user)) %>
<% unless @claimed_recording.recording.band.blank? %>
@@ -97,7 +97,7 @@ <% end %>
-<% if @claimed_recording.is_public || @claimed_recording.recording.has_access?(current_user) %> +<% if !@claimed_recording.recording.deleted && (@claimed_recording.is_public || @claimed_recording.recording.has_access?(current_user)) %> <% if signed_in? %> <% unless @claimed_recording.recording.band.nil? %> <%= render :partial => "shared/landing_sidebar", :locals => {:user => @claimed_recording.recording.band, :recent_history => @claimed_recording.recording.band.recent_history} %> diff --git a/web/app/views/users/_feed_recording_ajax.html.haml b/web/app/views/users/_feed_recording_ajax.html.haml index 9e45f8575..76c40a723 100644 --- a/web/app/views/users/_feed_recording_ajax.html.haml +++ b/web/app/views/users/_feed_recording_ajax.html.haml @@ -7,7 +7,7 @@ / type and artist .left.ml20.w15 .title - %a{:href => "/recordings/{{data.candidate_claimed_recording.id}}", :rel => "external", :hoveraction => "recording", :'recording-id' => '{{data.candidate_claimed_recording.id}}'} RECORDING + %a.title-text{:href => "/recordings/{{data.candidate_claimed_recording.id}}", :rel => "external", :hoveraction => "recording", :'recording-id' => '{{data.candidate_claimed_recording.id}}'} RECORDING %a.edit-recording-dialog{href: "#"} (edit) .artist %a.artist{:hoveraction => '{{data.feed_item.helpers.artist_hoveraction}}', :profileaction => "{{data.feed_item.helpers.artist_hoveraction}}", :'{{data.feed_item.helpers.artist_datakey}}' => '{{data.feed_item.helpers.artist_id}}'} diff --git a/web/config/routes.rb b/web/config/routes.rb index 5c4bf42c7..cea8bdf29 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -320,6 +320,7 @@ SampleApp::Application.routes.draw do # downloads/uploads match '/users/:id/syncs' => 'api_user_syncs#index', :via => :get match '/users/:id/syncs/:user_sync_id' => 'api_user_syncs#show', :via => :get + match '/users/:id/syncs/deletables' => 'api_user_syncs#deletables', :via => :post # bands @@ -398,6 +399,7 @@ SampleApp::Application.routes.draw do match '/recordings/:id' => 'api_recordings#show', :via => :get, :as => 'api_recordings_detail' match '/recordings/:id/stop' => 'api_recordings#stop', :via => :post, :as => 'api_recordings_stop' match '/recordings/:id/claim' => 'api_recordings#claim', :via => :post, :as => 'api_recordings_claim' + match '/recordings/:id/claim' => 'api_recordings#delete_claim', :via => :delete, :as => 'api_recordings_delete_claim' match '/recordings/:id/comments' => 'api_recordings#add_comment', :via => :post, :as => 'api_recordings_add_comment' match '/recordings/:id/likes' => 'api_recordings#add_like', :via => :post, :as => 'api_recordings_add_like' match '/recordings/:id/discard' => 'api_recordings#discard', :via => :post, :as => 'api_recordings_discard' diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 441fe269d..4997a834e 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -57,4 +57,9 @@ ActiveMusicSessionCleaner: ScoreHistorySweeper: cron: 0 * * * * class: "JamRuby::ScoreHistorySweeper" - description: "Creates 'ScoreHistory' tables from Scores" \ No newline at end of file + description: "Creates 'ScoreHistory' tables from Scores" + +RecordingsCleaner: + cron: 0 * * * * + class: "JamRuby::RecordingsCleaner" + description: "Cleans up recordings that no one wants after 7 days" \ No newline at end of file diff --git a/web/spec/controllers/api_user_syncs_controller_spec.rb b/web/spec/controllers/api_user_syncs_controller_spec.rb index 5e88703a3..fb7c0a423 100644 --- a/web/spec/controllers/api_user_syncs_controller_spec.rb +++ b/web/spec/controllers/api_user_syncs_controller_spec.rb @@ -25,6 +25,12 @@ describe ApiUserSyncsController do json[:entries].length.should == 0 end + it "deletables" do + post :deletables, { :format => 'json', :id => user1.id, recording_ids: ['1'] } + json = JSON.parse(response.body, :symbolize_names => true) + json[:recording_ids].should eq(['1']) + end + describe "one recording with two users" do let!(:recording1) { recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1)