diff --git a/README.md b/README.md index ce8cb07..70ff557 100644 --- a/README.md +++ b/README.md @@ -111,19 +111,19 @@ wavesync sync -p When a source file's sample rate isn't supported by the target device, Wavesync selects the closest supported rate. Example: If a 96kHz file is synced to an Octatrack (which only supports 44.1kHz), it will be downsampled to 44.1kHz. -### Sets (experimental) +### Setlists (experimental) -A set is a named, ordered selection of tracks from your library. Sets are stored as YAML files inside a `.sets` folder within the library directory. Syncing sets to devices is not yet implemented. +A setlist is a named, ordered selection of tracks from your library. Setlists are stored as YAML files inside a `.setlists` folder within the library directory. Syncing setlists to devices is not yet implemented. ```bash -# Create a new set and open the interactive editor -wavesync set create NAME +# Create a new setlist and open the interactive editor +wavesync setlist create NAME -# Edit an existing set -wavesync set edit NAME +# Edit an existing setlist +wavesync setlist edit NAME -# List all sets -wavesync set list +# List all setlists +wavesync setlist list ``` ## Development diff --git a/lib/wavesync.rb b/lib/wavesync.rb index 2721211..c9cf771 100644 --- a/lib/wavesync.rb +++ b/lib/wavesync.rb @@ -19,7 +19,7 @@ module Wavesync require 'wavesync/scanner' require 'wavesync/bpm_detector' require 'wavesync/analyzer' -require 'wavesync/set' -require 'wavesync/set_editor' +require 'wavesync/setlist' +require 'wavesync/setlist_editor' require 'wavesync/commands' require 'wavesync/cli' diff --git a/lib/wavesync/commands.rb b/lib/wavesync/commands.rb index 785dd03..6fba5a2 100644 --- a/lib/wavesync/commands.rb +++ b/lib/wavesync/commands.rb @@ -20,9 +20,9 @@ def self.load_config(path) require_relative 'commands/command' require_relative 'commands/sync' require_relative 'commands/analyze' - require_relative 'commands/set' + require_relative 'commands/setlist' require_relative 'commands/help' - ALL = [Sync, Analyze, Set, Help].freeze + ALL = [Sync, Analyze, Setlist, Help].freeze end end diff --git a/lib/wavesync/commands/set.rb b/lib/wavesync/commands/set.rb deleted file mode 100644 index f5bc618..0000000 --- a/lib/wavesync/commands/set.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true -# rbs_inline: enabled - -require 'optparse' - -module Wavesync - module Commands - class Set < Command - self.name = 'set' - self.subcommands = [ - Subcommand.new(usage: 'set create NAME', description: 'Create a new track set'), - Subcommand.new(usage: 'set edit NAME', description: 'Edit an existing track set'), - Subcommand.new(usage: 'set list', description: 'List all track sets') - ].freeze - - #: () -> void - def run - subcommand = ARGV.shift - - _options, config = parse_options(banner: 'Usage: wavesync set [options]') - - case subcommand - when 'create' - name = require_name('create') - if Wavesync::Set.exists?(config.library, name) - puts "Set '#{name}' already exists. Use 'wavesync set edit #{name}' to edit it." - exit 1 - end - set = Wavesync::Set.new(config.library, name) - Wavesync::SetEditor.new(set, config.library).run - when 'edit' - name = require_name('edit') - unless Wavesync::Set.exists?(config.library, name) - puts "Set '#{name}' not found. Use 'wavesync set create #{name}' to create it." - exit 1 - end - set = Wavesync::Set.load(config.library, name) - Wavesync::SetEditor.new(set, config.library).run - when 'list' - sets = Wavesync::Set.all(config.library) - if sets.empty? - puts 'No sets found.' - else - sets.each { |set| puts "#{set.name} (#{set.tracks.size} tracks)" } - end - else - puts "Unknown subcommand: #{subcommand || '(none)'}" - puts 'Available subcommands: create, edit, list' - exit 1 - end - end - - private - - #: (String subcommand) -> String - def require_name(subcommand) - name = ARGV.shift - unless name - puts "Usage: wavesync set #{subcommand} " - exit 1 - end - name - end - end - end -end diff --git a/lib/wavesync/commands/setlist.rb b/lib/wavesync/commands/setlist.rb new file mode 100644 index 0000000..0290ef6 --- /dev/null +++ b/lib/wavesync/commands/setlist.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +# rbs_inline: enabled + +require 'optparse' + +module Wavesync + module Commands + class Setlist < Command + self.name = 'setlist' + self.subcommands = [ + Subcommand.new(usage: 'setlist create NAME', description: 'Create a new setlist'), + Subcommand.new(usage: 'setlist edit NAME', description: 'Edit an existing setlist'), + Subcommand.new(usage: 'setlist list', description: 'List all setlists') + ].freeze + + #: () -> void + def run + subcommand = ARGV.shift + + _options, config = parse_options(banner: 'Usage: wavesync setlist [options]') + + case subcommand + when 'create' + name = require_name('create') + if Wavesync::Setlist.exists?(config.library, name) + puts "Setlist '#{name}' already exists. Use 'wavesync setlist edit #{name}' to edit it." + exit 1 + end + setlist = Wavesync::Setlist.new(config.library, name) + Wavesync::SetlistEditor.new(setlist, config.library).run + when 'edit' + name = require_name('edit') + unless Wavesync::Setlist.exists?(config.library, name) + puts "Setlist '#{name}' not found. Use 'wavesync setlist create #{name}' to create it." + exit 1 + end + setlist = Wavesync::Setlist.load(config.library, name) + Wavesync::SetlistEditor.new(setlist, config.library).run + when 'list' + setlists = Wavesync::Setlist.all(config.library) + if setlists.empty? + puts 'No setlists found.' + else + setlists.each { |setlist| puts "#{setlist.name} (#{setlist.tracks.size} tracks)" } + end + else + puts "Unknown subcommand: #{subcommand || '(none)'}" + puts 'Available subcommands: create, edit, list' + exit 1 + end + end + + private + + #: (String subcommand) -> String + def require_name(subcommand) + name = ARGV.shift + unless name + puts "Usage: wavesync setlist #{subcommand} " + exit 1 + end + name + end + end + end +end diff --git a/lib/wavesync/set.rb b/lib/wavesync/setlist.rb similarity index 76% rename from lib/wavesync/set.rb rename to lib/wavesync/setlist.rb index 50c6964..6dd3a6e 100644 --- a/lib/wavesync/set.rb +++ b/lib/wavesync/setlist.rb @@ -5,32 +5,32 @@ require 'fileutils' module Wavesync - class Set - SETS_FOLDER = '.sets' + class Setlist + SETLISTS_FOLDER = '.setlists' attr_reader :name #: String attr_reader :tracks #: Array[String] attr_reader :library_path #: String #: (String library_path) -> String - def self.sets_path(library_path) - File.join(library_path, SETS_FOLDER) + def self.setlists_path(library_path) + File.join(library_path, SETLISTS_FOLDER) end #: (String library_path, String name) -> String - def self.set_path(library_path, name) - File.join(sets_path(library_path), "#{name}.yml") + def self.setlist_path(library_path, name) + File.join(setlists_path(library_path), "#{name}.yml") end - #: (String library_path, String name) -> Set + #: (String library_path, String name) -> Setlist def self.load(library_path, name) - data = YAML.load_file(set_path(library_path, name)) + data = YAML.load_file(setlist_path(library_path, name)) new(library_path, data['name'], expand_tracks(library_path, data['tracks'])) end - #: (String library_path) -> Array[Set] + #: (String library_path) -> Array[Setlist] def self.all(library_path) - path = sets_path(library_path) + path = setlists_path(library_path) return [] unless Dir.exist?(path) Dir.glob(File.join(path, '*.yml')).map do |file| @@ -41,7 +41,7 @@ def self.all(library_path) #: (String library_path, String name) -> bool def self.exists?(library_path, name) - File.exist?(set_path(library_path, name)) + File.exist?(setlist_path(library_path, name)) end #: (String library_path, String name, ?Array[String] tracks) -> void @@ -77,8 +77,8 @@ def move_down(index) #: () -> void def save - FileUtils.mkdir_p(self.class.sets_path(@library_path)) - File.write(self.class.set_path(@library_path, @name), to_yaml) + FileUtils.mkdir_p(self.class.setlists_path(@library_path)) + File.write(self.class.setlist_path(@library_path, @name), to_yaml) end private diff --git a/lib/wavesync/set_editor.rb b/lib/wavesync/setlist_editor.rb similarity index 85% rename from lib/wavesync/set_editor.rb rename to lib/wavesync/setlist_editor.rb index 20e84c3..b976291 100644 --- a/lib/wavesync/set_editor.rb +++ b/lib/wavesync/setlist_editor.rb @@ -7,7 +7,7 @@ require_relative 'logger' module Wavesync - class SetEditor + class SetlistEditor KEY_MAP = { 'a' => :add, 'u' => :move_up, @@ -21,17 +21,17 @@ class SetEditor }.freeze attr_accessor :player_state #: Symbol - attr_reader :selected, :set, :ui #: untyped + attr_reader :selected, :setlist, :ui #: untyped attr_writer :player_track, :player_index, :player_offset, :player_started_at, :player_pid - #: (Set set, String library_path) -> void - def initialize(set, library_path) - @set = set #: Set + #: (Setlist setlist, String library_path) -> void + def initialize(setlist, library_path) + @setlist = setlist #: Setlist @library_path = library_path #: String Logger.configure(@library_path) @prompt = TTY::Prompt.new(interrupt: :exit, active_color: :red) #: untyped @ui = UI.new #: UI - @selected = @set.tracks.empty? ? nil : 0 #: Integer? + @selected = @setlist.tracks.empty? ? nil : 0 #: Integer? @player_pid = nil #: Integer? @player_track = nil #: String? @player_index = nil #: Integer? @@ -67,7 +67,7 @@ def track_bpm(path) @track_bpms[path] = begin Audio.new(path).bpm rescue StandardError => e - Logger.log_error(e, call_site: 'SetEditor#track_bpm', arguments: { path: }) + Logger.log_error(e, call_site: 'SetlistEditor#track_bpm', arguments: { path: }) nil end end @@ -82,7 +82,7 @@ def track_duration(path) @track_durations[path] = begin Audio.new(path).duration rescue StandardError => e - Logger.log_error(e, call_site: 'SetEditor#track_duration', arguments: { path: }) + Logger.log_error(e, call_site: 'SetlistEditor#track_duration', arguments: { path: }) nil end end @@ -104,7 +104,7 @@ def track_cue_fractions(path) [] #: Array[Float] end rescue StandardError => e - Logger.log_error(e, call_site: 'SetEditor#track_cue_fractions', arguments: { path: }) + Logger.log_error(e, call_site: 'SetlistEditor#track_cue_fractions', arguments: { path: }) [] #: Array[Float] end end @@ -254,22 +254,22 @@ def display_name(relative) end #: (?String title) -> void - def render(title = "wavesync set #{@set.name}") + def render(title = "wavesync setlist #{@setlist.name}") buffer = StringIO.new $stdout = buffer header = @ui.color(title, :primary) - total_duration = @set.tracks.sum { |track_path| track_duration(track_path) || 0.0 } + total_duration = @setlist.tracks.sum { |track_path| track_duration(track_path) || 0.0 } - duration_widths = @set.tracks.map { |track_path| format_duration(track_duration(track_path))&.length || 0 } + duration_widths = @setlist.tracks.map { |track_path| format_duration(track_duration(track_path))&.length || 0 } duration_widths << (format_duration(total_duration)&.length || 0) - player_duration = @player_index ? track_duration(@set.tracks[@player_index]) : nil + player_duration = @player_index ? track_duration(@setlist.tracks[@player_index]) : nil duration_widths << remaining_display(0.0, player_duration).length if player_duration duration_col_width = duration_widths.max || 0 - if @set.tracks.any? && total_duration.positive? - track_label = @set.tracks.size == 1 ? 'track' : 'tracks' - track_count_part = @ui.color("#{@set.tracks.size} #{track_label}", :secondary) + if @setlist.tracks.any? && total_duration.positive? + track_label = @setlist.tracks.size == 1 ? 'track' : 'tracks' + track_count_part = @ui.color("#{@setlist.tracks.size} #{track_label}", :secondary) duration_part = @ui.color(format_duration(total_duration).to_s.rjust(duration_col_width), :secondary) summary = "#{track_count_part} #{duration_part}" gap = [terminal_width - visible_length(header) - visible_length(summary), 2].max @@ -280,13 +280,13 @@ def render(title = "wavesync set #{@set.name}") puts - if @set.tracks.empty? + if @setlist.tracks.empty? puts @ui.color(' (no tracks)', :secondary) else - @set.tracks.each_with_index do |track, i| + @setlist.tracks.each_with_index do |track, i| current_bpm = track_bpm(track) current_duration = track_duration(track) - pitch_shift = pitch_shift_semitones(current_bpm, track_bpm(@set.tracks[i + 1])) + pitch_shift = pitch_shift_semitones(current_bpm, track_bpm(@setlist.tracks[i + 1])) render_track(i, relative_path(track), i == @selected, i == @player_index, bpm: current_bpm, pitch_shift: pitch_shift, duration: current_duration, duration_col_width: duration_col_width, cue_fractions: track_cue_fractions(track)) @@ -362,10 +362,10 @@ def render_track(index, relative, selected, playing, bpm: nil, pitch_shift: nil, def handle_action(action) case action when :cursor_up - @selected = [@selected - 1, 0].max unless @set.tracks.empty? + @selected = [@selected - 1, 0].max unless @setlist.tracks.empty? nil when :cursor_down - @selected = [@selected + 1, @set.tracks.size - 1].min unless @set.tracks.empty? + @selected = [@selected + 1, @setlist.tracks.size - 1].min unless @setlist.tracks.empty? nil when :toggle_play toggle_playback @@ -386,7 +386,7 @@ def handle_action(action) jump_to_next_cue nil when :quit - @set.save + @setlist.save :quit end end @@ -395,7 +395,7 @@ def handle_action(action) def toggle_playback return if @selected.nil? - track = @set.tracks[@selected] + track = @setlist.tracks[@selected] if @player_track == track case @player_state @@ -453,7 +453,7 @@ def kill_player Process.kill('TERM', @player_pid) @player_pid = nil rescue Errno::ESRCH => e - Logger.log_error(e, call_site: 'SetEditor#kill_player', arguments: { player_pid: }) + Logger.log_error(e, call_site: 'SetlistEditor#kill_player', arguments: { player_pid: }) @player_pid = nil end @@ -482,7 +482,7 @@ def check_player @player_offset = 0 advance_and_play rescue Errno::ECHILD => e - Logger.log_error(e, call_site: 'SetEditor#check_player', arguments: { player_pid: }) + Logger.log_error(e, call_site: 'SetlistEditor#check_player', arguments: { player_pid: }) @player_pid = nil @player_track = nil @player_index = nil @@ -493,10 +493,10 @@ def check_player #: () -> void def advance_and_play - return if @selected.nil? || @selected >= @set.tracks.size - 1 + return if @selected.nil? || @selected >= @setlist.tracks.size - 1 @selected += 1 - start_player(@set.tracks[@selected]) + start_player(@setlist.tracks[@selected]) end #: () -> Array[String] @@ -514,43 +514,43 @@ def add_track choices = audio_files.map { |file| { name: relative_path(file), value: file } } - render("wavesync set #{@set.name} — add track") + render("wavesync setlist #{@setlist.name} — add track") puts picked = @prompt.select('Select a track to add:', choices, cycle: true, filter: true, per_page: 20) insert_at = @selected.nil? ? 0 : @selected + 1 - @set.tracks.insert(insert_at, picked) + @setlist.tracks.insert(insert_at, picked) @selected = insert_at end #: () -> void def remove_track - return if @set.tracks.empty? || @selected.nil? + return if @setlist.tracks.empty? || @selected.nil? - stop_playback if @player_track == @set.tracks[@selected] - @set.remove_track(@selected) - @selected = if @set.tracks.empty? + stop_playback if @player_track == @setlist.tracks[@selected] + @setlist.remove_track(@selected) + @selected = if @setlist.tracks.empty? nil else - [@selected, @set.tracks.size - 1].min + [@selected, @setlist.tracks.size - 1].min end end #: (Symbol direction) -> void def move_track(direction) - return if @set.tracks.size < 2 || @selected.nil? + return if @setlist.tracks.size < 2 || @selected.nil? if direction == :up - @set.move_up(@selected) + @setlist.move_up(@selected) @selected = [@selected - 1, 0].max else - @set.move_down(@selected) - @selected = [@selected + 1, @set.tracks.size - 1].min + @setlist.move_down(@selected) + @selected = [@selected + 1, @setlist.tracks.size - 1].min end end public :handle_action, :advance_and_play, :kill_player, :check_player, :display_name, :relative_path, :format_duration, :playback_elapsed, - :visible_length, :playback_bar, :selected, :set, :ui + :visible_length, :playback_bar, :selected, :setlist, :ui end end diff --git a/sig/generated/wavesync/commands/set.rbs b/sig/generated/wavesync/commands/setlist.rbs similarity index 68% rename from sig/generated/wavesync/commands/set.rbs rename to sig/generated/wavesync/commands/setlist.rbs index 979fb3d..2a51694 100644 --- a/sig/generated/wavesync/commands/set.rbs +++ b/sig/generated/wavesync/commands/setlist.rbs @@ -1,8 +1,8 @@ -# Generated from lib/wavesync/commands/set.rb with RBS::Inline +# Generated from lib/wavesync/commands/setlist.rb with RBS::Inline module Wavesync module Commands - class Set < Command + class Setlist < Command # : () -> void def run: () -> void diff --git a/sig/generated/wavesync/set.rbs b/sig/generated/wavesync/setlist.rbs similarity index 70% rename from sig/generated/wavesync/set.rbs rename to sig/generated/wavesync/setlist.rbs index 878ff1c..ac29256 100644 --- a/sig/generated/wavesync/set.rbs +++ b/sig/generated/wavesync/setlist.rbs @@ -1,8 +1,8 @@ -# Generated from lib/wavesync/set.rb with RBS::Inline +# Generated from lib/wavesync/setlist.rb with RBS::Inline module Wavesync - class Set - SETS_FOLDER: ::String + class Setlist + SETLISTS_FOLDER: ::String attr_reader name: String @@ -11,16 +11,16 @@ module Wavesync attr_reader library_path: String # : (String library_path) -> String - def self.sets_path: (String library_path) -> String + def self.setlists_path: (String library_path) -> String # : (String library_path, String name) -> String - def self.set_path: (String library_path, String name) -> String + def self.setlist_path: (String library_path, String name) -> String - # : (String library_path, String name) -> Set - def self.load: (String library_path, String name) -> Set + # : (String library_path, String name) -> Setlist + def self.load: (String library_path, String name) -> Setlist - # : (String library_path) -> Array[Set] - def self.all: (String library_path) -> Array[Set] + # : (String library_path) -> Array[Setlist] + def self.all: (String library_path) -> Array[Setlist] # : (String library_path, String name) -> bool def self.exists?: (String library_path, String name) -> bool diff --git a/sig/generated/wavesync/set_editor.rbs b/sig/generated/wavesync/setlist_editor.rbs similarity index 93% rename from sig/generated/wavesync/set_editor.rbs rename to sig/generated/wavesync/setlist_editor.rbs index d871c10..c93ddc8 100644 --- a/sig/generated/wavesync/set_editor.rbs +++ b/sig/generated/wavesync/setlist_editor.rbs @@ -1,14 +1,14 @@ -# Generated from lib/wavesync/set_editor.rb with RBS::Inline +# Generated from lib/wavesync/setlist_editor.rb with RBS::Inline module Wavesync - class SetEditor + class SetlistEditor KEY_MAP: untyped attr_accessor player_state: Symbol attr_reader selected: untyped - attr_reader set: untyped + attr_reader setlist: untyped attr_reader ui: untyped @@ -22,8 +22,8 @@ module Wavesync attr_writer player_pid: untyped - # : (Set set, String library_path) -> void - def initialize: (Set set, String library_path) -> void + # : (Setlist setlist, String library_path) -> void + def initialize: (Setlist setlist, String library_path) -> void # : () -> void def run: () -> void diff --git a/test/wavesync/cli_test.rb b/test/wavesync/cli_test.rb index 74da7f5..e1752fc 100644 --- a/test/wavesync/cli_test.rb +++ b/test/wavesync/cli_test.rb @@ -106,7 +106,7 @@ def teardown assert_includes output, 'sync' assert_includes output, 'analyze' - assert_includes output, 'set' + assert_includes output, 'setlist' assert_includes output, 'help' end diff --git a/test/wavesync/set_test.rb b/test/wavesync/set_test.rb deleted file mode 100644 index 7a5f944..0000000 --- a/test/wavesync/set_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -require 'tmpdir' -require 'yaml' -require_relative 'test_case' -require_relative '../../lib/wavesync/set' - -module Wavesync - class SetTest < Wavesync::TestCase - def setup - @tmp = Dir.mktmpdir - @library = File.join(@tmp, 'library') - FileUtils.mkdir_p(@library) - end - - def teardown - FileUtils.rm_rf(@tmp) - end - - test 'sets_path returns .sets inside the library' do - assert_equal File.join(@library, '.sets'), Set.sets_path(@library) - end - - test 'set_path returns yaml file inside .sets' do - assert_equal File.join(@library, '.sets', 'my_set.yml'), Set.set_path(@library, 'my_set') - end - - test 'exists? returns false when set file is absent' do - refute Set.exists?(@library, 'missing') - end - - test 'exists? returns true after a set is saved' do - Set.new(@library, 'demo').save - assert Set.exists?(@library, 'demo') - end - - test 'new set has empty tracks by default' do - assert_empty Set.new(@library, 'empty').tracks - end - - test 'new set accepts an initial track list' do - set = Set.new(@library, 'preloaded', ['/a.wav', '/b.wav']) - assert_equal ['/a.wav', '/b.wav'], set.tracks - end - - test 'tracks are independent from the array passed to initialize' do - source = ['/a.wav'] - set = Set.new(@library, 's', source) - source << '/b.wav' - assert_equal ['/a.wav'], set.tracks - end - - test 'add_track appends to the track list' do - set = Set.new(@library, 's') - set.add_track('/a.wav') - set.add_track('/b.wav') - assert_equal ['/a.wav', '/b.wav'], set.tracks - end - - test 'remove_track deletes the track at the given index' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) - set.remove_track(1) - assert_equal ['/a.wav', '/c.wav'], set.tracks - end - - test 'remove_track on the first element works' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav']) - set.remove_track(0) - assert_equal ['/b.wav'], set.tracks - end - - test 'remove_track on the last element works' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav']) - set.remove_track(1) - assert_equal ['/a.wav'], set.tracks - end - - test 'move_up swaps the track with the one above it' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) - set.move_up(1) - assert_equal ['/b.wav', '/a.wav', '/c.wav'], set.tracks - end - - test 'move_up is a no-op at index 0' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav']) - set.move_up(0) - assert_equal ['/a.wav', '/b.wav'], set.tracks - end - - test 'move_down swaps the track with the one below it' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) - set.move_down(1) - assert_equal ['/a.wav', '/c.wav', '/b.wav'], set.tracks - end - - test 'move_down is a no-op at the last index' do - set = Set.new(@library, 's', ['/a.wav', '/b.wav']) - set.move_down(1) - assert_equal ['/a.wav', '/b.wav'], set.tracks - end - - test 'save creates the .sets directory if it does not exist' do - Set.new(@library, 'fresh').save - assert Dir.exist?(Set.sets_path(@library)) - end - - test 'save writes a yaml file with name and relative tracks' do - tracks = [File.join(@library, 'a.wav'), File.join(@library, 'sub/b.wav')] - Set.new(@library, 'my_set', tracks).save - - data = YAML.load_file(Set.set_path(@library, 'my_set')) - assert_equal 'my_set', data['name'] - assert_equal %w[a.wav sub/b.wav], data['tracks'] - end - - test 'save persists an empty track list' do - Set.new(@library, 'empty').save - data = YAML.load_file(Set.set_path(@library, 'empty')) - assert_equal [], data['tracks'] - end - - test 'load returns a Set with the persisted name and absolute tracks' do - track = File.join(@library, 'x.wav') - Set.new(@library, 'persisted', [track]).save - loaded = Set.load(@library, 'persisted') - assert_equal 'persisted', loaded.name - assert_equal [track], loaded.tracks - end - - test 'load raises when the set does not exist' do - assert_raises(Errno::ENOENT) { Set.load(@library, 'ghost') } - end - - test 'all returns empty array when .sets folder is absent' do - assert_empty Set.all(@library) - end - - test 'all returns all saved sets sorted by name' do - Set.new(@library, 'zebra').save - Set.new(@library, 'alpha').save - Set.new(@library, 'mango').save - - names = Set.all(@library).map(&:name) - assert_equal %w[alpha mango zebra], names - end - - test 'all returns sets with their tracks as absolute paths' do - tracks = [File.join(@library, 'a.wav'), File.join(@library, 'b.wav')] - Set.new(@library, 'with_tracks', tracks).save - loaded = Set.all(@library).first - assert_equal tracks, loaded.tracks - end - end -end diff --git a/test/wavesync/set_editor_test.rb b/test/wavesync/setlist_editor_test.rb similarity index 92% rename from test/wavesync/set_editor_test.rb rename to test/wavesync/setlist_editor_test.rb index d548b2b..fe67815 100644 --- a/test/wavesync/set_editor_test.rb +++ b/test/wavesync/setlist_editor_test.rb @@ -4,13 +4,13 @@ require 'stringio' require_relative 'test_case' require_relative '../../lib/wavesync/ui' -require_relative '../../lib/wavesync/set' -require_relative '../../lib/wavesync/set_editor' +require_relative '../../lib/wavesync/setlist' +require_relative '../../lib/wavesync/setlist_editor' module Wavesync Audio = Class.new unless defined?(Audio) - class SetEditorTest < Wavesync::TestCase + class SetlistEditorTest < Wavesync::TestCase def setup @orig_stdout = $stdout $stdout = StringIO.new @@ -31,8 +31,8 @@ def teardown end def editor(*tracks) - set = Set.new(@library, 'test', tracks) - SetEditor.new(set, @library) + setlist = Setlist.new(@library, 'test', tracks) + SetlistEditor.new(setlist, @library) end def track(name) @@ -40,9 +40,9 @@ def track(name) end test 'initializing configures error logger with library path' do - set = Set.new(@library, 'test', []) + setlist = Setlist.new(@library, 'test', []) Logger.expects(:configure).with(@library) - SetEditor.new(set, @library) + SetlistEditor.new(setlist, @library) end test 'display_name strips leading number and space' do @@ -75,7 +75,7 @@ def track(name) assert_equal 0, e.selected end - test 'cursor starts as nil when set is empty' do + test 'cursor starts as nil when setlist is empty' do e = editor assert_nil e.selected end @@ -106,7 +106,7 @@ def track(name) assert_equal 0, e.selected end - test 'cursor navigation is a no-op on empty set' do + test 'cursor navigation is a no-op on empty setlist' do e = editor e.handle_action(:cursor_down) e.handle_action(:cursor_up) @@ -120,7 +120,7 @@ def track(name) e = editor(a, b, c) e.handle_action(:cursor_down) e.handle_action(:remove) - assert_equal [a, c], e.set.tracks + assert_equal [a, c], e.setlist.tracks end test 'remove_track keeps selection in bounds when removing last track' do @@ -130,7 +130,7 @@ def track(name) assert_equal 0, e.selected end - test 'remove_track sets selected to nil when set becomes empty' do + test 'remove_track sets selected to nil when setlist becomes empty' do e = editor(track('a.wav')) e.handle_action(:remove) assert_nil e.selected @@ -162,7 +162,7 @@ def track(name) e = editor(a, b, c) e.handle_action(:cursor_down) e.handle_action(:move_up) - assert_equal [b, a, c], e.set.tracks + assert_equal [b, a, c], e.setlist.tracks assert_equal 0, e.selected end @@ -172,7 +172,7 @@ def track(name) c = track('c.wav') e = editor(a, b, c) e.handle_action(:move_down) - assert_equal [b, a, c], e.set.tracks + assert_equal [b, a, c], e.setlist.tracks assert_equal 1, e.selected end @@ -181,7 +181,7 @@ def track(name) b = track('b.wav') e = editor(a, b) e.handle_action(:move_up) - assert_equal [a, b], e.set.tracks + assert_equal [a, b], e.setlist.tracks assert_equal 0, e.selected end @@ -191,7 +191,7 @@ def track(name) e = editor(a, b) e.handle_action(:cursor_down) e.handle_action(:move_down) - assert_equal [a, b], e.set.tracks + assert_equal [a, b], e.setlist.tracks assert_equal 1, e.selected end @@ -243,7 +243,7 @@ def track(name) e.handle_action(:toggle_play) end - test 'toggle_playback does nothing on empty set' do + test 'toggle_playback does nothing on empty setlist' do e = editor e.expects(:start_player).never e.handle_action(:toggle_play) @@ -267,7 +267,7 @@ def track(name) e.advance_and_play end - test 'advance_and_play does nothing when set is empty' do + test 'advance_and_play does nothing when setlist is empty' do e = editor e.expects(:start_player).never e.advance_and_play @@ -550,21 +550,21 @@ def track(name) test 'track_bpm logs error with path when audio raises' do error = StandardError.new('load failed') Audio.stubs(:new).raises(error) - Logger.expects(:log_error).with(error, call_site: 'SetEditor#track_bpm', arguments: { path: '/some/track.wav' }) + Logger.expects(:log_error).with(error, call_site: 'SetlistEditor#track_bpm', arguments: { path: '/some/track.wav' }) editor.track_bpm('/some/track.wav') end test 'track_duration logs error with path when audio raises' do error = StandardError.new('load failed') Audio.stubs(:new).raises(error) - Logger.expects(:log_error).with(error, call_site: 'SetEditor#track_duration', arguments: { path: '/some/track.wav' }) + Logger.expects(:log_error).with(error, call_site: 'SetlistEditor#track_duration', arguments: { path: '/some/track.wav' }) editor.track_duration('/some/track.wav') end test 'track_cue_fractions logs error with path when audio raises' do error = StandardError.new('load failed') Audio.stubs(:new).raises(error) - Logger.expects(:log_error).with(error, call_site: 'SetEditor#track_cue_fractions', arguments: { path: '/some/track.wav' }) + Logger.expects(:log_error).with(error, call_site: 'SetlistEditor#track_cue_fractions', arguments: { path: '/some/track.wav' }) editor.track_cue_fractions('/some/track.wav') end @@ -572,7 +572,7 @@ def track(name) e = editor e.player_pid = 99_999 Process.stubs(:kill).raises(Errno::ESRCH) - Logger.expects(:log_error).with(instance_of(Errno::ESRCH), call_site: 'SetEditor#kill_player', arguments: { player_pid: 99_999 }) + Logger.expects(:log_error).with(instance_of(Errno::ESRCH), call_site: 'SetlistEditor#kill_player', arguments: { player_pid: 99_999 }) e.kill_player end @@ -581,7 +581,7 @@ def track(name) e.player_pid = 99_999 Process.stubs(:waitpid).raises(Errno::ECHILD) e.stubs(:advance_and_play) - Logger.expects(:log_error).with(instance_of(Errno::ECHILD), call_site: 'SetEditor#check_player', arguments: { player_pid: 99_999 }) + Logger.expects(:log_error).with(instance_of(Errno::ECHILD), call_site: 'SetlistEditor#check_player', arguments: { player_pid: 99_999 }) e.check_player end end diff --git a/test/wavesync/setlist_test.rb b/test/wavesync/setlist_test.rb new file mode 100644 index 0000000..a7cec1e --- /dev/null +++ b/test/wavesync/setlist_test.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'yaml' +require_relative 'test_case' +require_relative '../../lib/wavesync/setlist' + +module Wavesync + class SetlistTest < Wavesync::TestCase + def setup + @tmp = Dir.mktmpdir + @library = File.join(@tmp, 'library') + FileUtils.mkdir_p(@library) + end + + def teardown + FileUtils.rm_rf(@tmp) + end + + test 'setlists_path returns .setlists inside the library' do + assert_equal File.join(@library, '.setlists'), Setlist.setlists_path(@library) + end + + test 'setlist_path returns yaml file inside .setlists' do + assert_equal File.join(@library, '.setlists', 'my_set.yml'), Setlist.setlist_path(@library, 'my_set') + end + + test 'exists? returns false when setlist file is absent' do + refute Setlist.exists?(@library, 'missing') + end + + test 'exists? returns true after a setlist is saved' do + Setlist.new(@library, 'demo').save + assert Setlist.exists?(@library, 'demo') + end + + test 'new setlist has empty tracks by default' do + assert_empty Setlist.new(@library, 'empty').tracks + end + + test 'new setlist accepts an initial track list' do + setlist = Setlist.new(@library, 'preloaded', ['/a.wav', '/b.wav']) + assert_equal ['/a.wav', '/b.wav'], setlist.tracks + end + + test 'tracks are independent from the array passed to initialize' do + source = ['/a.wav'] + setlist = Setlist.new(@library, 's', source) + source << '/b.wav' + assert_equal ['/a.wav'], setlist.tracks + end + + test 'add_track appends to the track list' do + setlist = Setlist.new(@library, 's') + setlist.add_track('/a.wav') + setlist.add_track('/b.wav') + assert_equal ['/a.wav', '/b.wav'], setlist.tracks + end + + test 'remove_track deletes the track at the given index' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) + setlist.remove_track(1) + assert_equal ['/a.wav', '/c.wav'], setlist.tracks + end + + test 'remove_track on the first element works' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav']) + setlist.remove_track(0) + assert_equal ['/b.wav'], setlist.tracks + end + + test 'remove_track on the last element works' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav']) + setlist.remove_track(1) + assert_equal ['/a.wav'], setlist.tracks + end + + test 'move_up swaps the track with the one above it' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) + setlist.move_up(1) + assert_equal ['/b.wav', '/a.wav', '/c.wav'], setlist.tracks + end + + test 'move_up is a no-op at index 0' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav']) + setlist.move_up(0) + assert_equal ['/a.wav', '/b.wav'], setlist.tracks + end + + test 'move_down swaps the track with the one below it' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav', '/c.wav']) + setlist.move_down(1) + assert_equal ['/a.wav', '/c.wav', '/b.wav'], setlist.tracks + end + + test 'move_down is a no-op at the last index' do + setlist = Setlist.new(@library, 's', ['/a.wav', '/b.wav']) + setlist.move_down(1) + assert_equal ['/a.wav', '/b.wav'], setlist.tracks + end + + test 'save creates the .setlists directory if it does not exist' do + Setlist.new(@library, 'fresh').save + assert Dir.exist?(Setlist.setlists_path(@library)) + end + + test 'save writes a yaml file with name and relative tracks' do + tracks = [File.join(@library, 'a.wav'), File.join(@library, 'sub/b.wav')] + Setlist.new(@library, 'my_set', tracks).save + + data = YAML.load_file(Setlist.setlist_path(@library, 'my_set')) + assert_equal 'my_set', data['name'] + assert_equal %w[a.wav sub/b.wav], data['tracks'] + end + + test 'save persists an empty track list' do + Setlist.new(@library, 'empty').save + data = YAML.load_file(Setlist.setlist_path(@library, 'empty')) + assert_equal [], data['tracks'] + end + + test 'load returns a Setlist with the persisted name and absolute tracks' do + track = File.join(@library, 'x.wav') + Setlist.new(@library, 'persisted', [track]).save + loaded = Setlist.load(@library, 'persisted') + assert_equal 'persisted', loaded.name + assert_equal [track], loaded.tracks + end + + test 'load raises when the setlist does not exist' do + assert_raises(Errno::ENOENT) { Setlist.load(@library, 'ghost') } + end + + test 'all returns empty array when .setlists folder is absent' do + assert_empty Setlist.all(@library) + end + + test 'all returns all saved setlists sorted by name' do + Setlist.new(@library, 'zebra').save + Setlist.new(@library, 'alpha').save + Setlist.new(@library, 'mango').save + + names = Setlist.all(@library).map(&:name) + assert_equal %w[alpha mango zebra], names + end + + test 'all returns setlists with their tracks as absolute paths' do + tracks = [File.join(@library, 'a.wav'), File.join(@library, 'b.wav')] + Setlist.new(@library, 'with_tracks', tracks).save + loaded = Setlist.all(@library).first + assert_equal tracks, loaded.tracks + end + end +end