wavesync 1.0.0.alpha2 → 1.0.0.alpha4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +95 -59
  3. data/config/devices.yml +20 -0
  4. data/lib/wavesync/acid_chunk.rb +45 -4
  5. data/lib/wavesync/analyzer.rb +17 -4
  6. data/lib/wavesync/audio.rb +153 -90
  7. data/lib/wavesync/audio_format.rb +9 -2
  8. data/lib/wavesync/bpm_detector.rb +14 -7
  9. data/lib/wavesync/cli.rb +3 -0
  10. data/lib/wavesync/commands/analyze.rb +2 -0
  11. data/lib/wavesync/commands/clear_cache.rb +75 -0
  12. data/lib/wavesync/commands/command.rb +4 -1
  13. data/lib/wavesync/commands/help.rb +6 -0
  14. data/lib/wavesync/commands/pull.rb +43 -0
  15. data/lib/wavesync/commands/setlist.rb +66 -0
  16. data/lib/wavesync/commands/sync.rb +19 -26
  17. data/lib/wavesync/commands.rb +52 -12
  18. data/lib/wavesync/config.rb +43 -3
  19. data/lib/wavesync/cue_chunk.rb +203 -0
  20. data/lib/wavesync/device.rb +32 -7
  21. data/lib/wavesync/essentia_bpm_detector.rb +38 -0
  22. data/lib/wavesync/ffmpeg/probe.rb +74 -0
  23. data/lib/wavesync/ffmpeg.rb +144 -0
  24. data/lib/wavesync/file_converter.rb +7 -2
  25. data/lib/wavesync/libmtp.rb +333 -0
  26. data/lib/wavesync/logger.rb +84 -0
  27. data/lib/wavesync/path_resolver.rb +32 -6
  28. data/lib/wavesync/percival_bpm_detector.rb +31 -0
  29. data/lib/wavesync/python_venv.rb +25 -0
  30. data/lib/wavesync/scanner.rb +143 -27
  31. data/lib/wavesync/{set.rb → setlist.rb} +28 -12
  32. data/lib/wavesync/setlist_editor.rb +556 -0
  33. data/lib/wavesync/track_padding.rb +15 -4
  34. data/lib/wavesync/transport/filesystem.rb +36 -0
  35. data/lib/wavesync/transport/mtp.rb +285 -0
  36. data/lib/wavesync/transport.rb +21 -0
  37. data/lib/wavesync/ui.rb +67 -12
  38. data/lib/wavesync/version.rb +2 -1
  39. data/lib/wavesync.rb +7 -2
  40. metadata +17 -32
  41. data/lib/wavesync/commands/set.rb +0 -63
  42. data/lib/wavesync/set_editor.rb +0 -245
@@ -1,245 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tty-prompt'
4
- require 'io/console'
5
-
6
- module Wavesync
7
- class SetEditor
8
- KEY_MAP = {
9
- 'a' => :add,
10
- 'u' => :move_up,
11
- 'd' => :move_down,
12
- 'r' => :remove,
13
- 's' => :save,
14
- 'c' => :quit,
15
- ' ' => :toggle_play,
16
- "\e[A" => :cursor_up,
17
- "\e[B" => :cursor_down
18
- }.freeze
19
-
20
- def initialize(set, library_path)
21
- @set = set
22
- @library_path = library_path
23
- @prompt = TTY::Prompt.new(interrupt: :exit, active_color: :red)
24
- @ui = UI.new
25
- @selected = @set.tracks.empty? ? nil : 0
26
- @player_pid = nil
27
- @player_track = nil
28
- @player_state = :stopped
29
- @player_offset = 0
30
- @player_started_at = nil
31
- end
32
-
33
- def run
34
- loop do
35
- check_player
36
- render
37
- action = KEY_MAP[read_key]
38
- next unless action
39
-
40
- result = handle_action(action)
41
- break if result == :quit
42
- end
43
- ensure
44
- stop_playback
45
- end
46
-
47
- private
48
-
49
- def read_key
50
- $stdin.raw do |io|
51
- char = io.getc
52
- if char == "\e"
53
- rest = begin
54
- io.read_nonblock(3)
55
- rescue StandardError
56
- ''
57
- end
58
- char + rest
59
- else
60
- char
61
- end
62
- end
63
- end
64
-
65
- def relative_path(absolute)
66
- absolute.sub("#{@library_path}/", '')
67
- end
68
-
69
- def display_name(relative)
70
- File.basename(relative, '.*').sub(/\A\d+[\s.\-_]+/, '')
71
- end
72
-
73
- def render(title = "wavesync set #{@set.name}")
74
- @ui.clear
75
- puts @ui.color(title, :primary)
76
- puts
77
-
78
- if @set.tracks.empty?
79
- puts @ui.color(' (no tracks)', :secondary)
80
- else
81
- @set.tracks.each_with_index do |track, i|
82
- render_track(i, relative_path(track), i == @selected, track == @player_track)
83
- end
84
- end
85
-
86
- puts
87
- puts @ui.color('[↑↓] navigate [space] play/pause [a] add [u] up [d] down [r] remove [s] save [c] cancel',
88
- :secondary)
89
- end
90
-
91
- def render_track(index, relative, selected, playing)
92
- name = display_name(relative)
93
- folder = File.dirname(relative)
94
- icon = if playing
95
- @player_state == :paused ? '⏸' : '▶'
96
- else
97
- ' '
98
- end
99
- puts "#{@ui.color(icon, :surface)} #{@ui.color("#{index + 1}.", selected ? :highlight : :extra)} #{@ui.color(name, selected ? :highlight : :primary)}"
100
- puts @ui.color(" #{folder}", selected ? :highlight : :tertiary) unless folder == '.'
101
- end
102
-
103
- def handle_action(action)
104
- case action
105
- when :cursor_up
106
- @selected = [@selected - 1, 0].max unless @set.tracks.empty?
107
- when :cursor_down
108
- @selected = [@selected + 1, @set.tracks.size - 1].min unless @set.tracks.empty?
109
- when :toggle_play then toggle_playback
110
- when :add then add_track
111
- when :remove then remove_track
112
- when :move_up then move_track(:up)
113
- when :move_down then move_track(:down)
114
- when :save
115
- @set.save
116
- path = Set.set_path(@library_path, @set.name)
117
- puts @ui.color("Saved '#{@set.name}' to #{path}.", :primary)
118
- :quit
119
- when :quit
120
- :quit
121
- end
122
- end
123
-
124
- def toggle_playback
125
- return if @selected.nil?
126
-
127
- track = @set.tracks[@selected]
128
-
129
- if @player_track == track
130
- case @player_state
131
- when :playing
132
- @player_offset += Time.now - @player_started_at
133
- kill_player
134
- @player_state = :paused
135
- when :paused
136
- start_player(track, @player_offset)
137
- end
138
- else
139
- stop_playback
140
- start_player(track)
141
- end
142
- end
143
-
144
- def start_player(track, offset = 0)
145
- ffplay = FFMPEG.ffmpeg_binary.sub('ffmpeg', 'ffplay')
146
- args = [ffplay, '-nodisp', '-autoexit', '-loglevel', 'quiet', '-probesize', '32', '-analyzeduration', '0']
147
- args += ['-ss', offset.to_s] if offset.positive?
148
- args << track
149
- @player_pid = spawn(*args, out: File::NULL, err: File::NULL)
150
- @player_track = track
151
- @player_offset = offset
152
- @player_started_at = Time.now
153
- @player_state = :playing
154
- end
155
-
156
- def kill_player
157
- return unless @player_pid
158
-
159
- Process.kill('TERM', @player_pid)
160
- @player_pid = nil
161
- rescue Errno::ESRCH
162
- @player_pid = nil
163
- end
164
-
165
- def stop_playback
166
- kill_player
167
- @player_track = nil
168
- @player_state = :stopped
169
- @player_offset = 0
170
- @player_started_at = nil
171
- end
172
-
173
- def check_player
174
- return unless @player_pid
175
-
176
- result = Process.waitpid(@player_pid, Process::WNOHANG)
177
- return unless result
178
-
179
- @player_pid = nil
180
- @player_track = nil
181
- @player_state = :stopped
182
- @player_offset = 0
183
- advance_and_play
184
- rescue Errno::ECHILD
185
- @player_pid = nil
186
- @player_track = nil
187
- @player_state = :stopped
188
- @player_offset = 0
189
- advance_and_play
190
- end
191
-
192
- def advance_and_play
193
- return if @selected.nil? || @selected >= @set.tracks.size - 1
194
-
195
- @selected += 1
196
- start_player(@set.tracks[@selected])
197
- end
198
-
199
- def audio_files
200
- @audio_files ||= Audio.find_all(@library_path)
201
- end
202
-
203
- def add_track
204
- if audio_files.empty?
205
- puts @ui.color('No audio files found in library.', :highlight)
206
- sleep 1
207
- return
208
- end
209
-
210
- choices = audio_files.map { |file| { name: relative_path(file), value: file } }
211
-
212
- render("wavesync set #{@set.name} — add track")
213
- puts
214
- picked = @prompt.select('Select a track to add:', choices, cycle: true, filter: true, per_page: 20)
215
-
216
- insert_at = @selected.nil? ? 0 : @selected + 1
217
- @set.tracks.insert(insert_at, picked)
218
- @selected = insert_at
219
- end
220
-
221
- def remove_track
222
- return if @set.tracks.empty? || @selected.nil?
223
-
224
- stop_playback if @player_track == @set.tracks[@selected]
225
- @set.remove_track(@selected)
226
- @selected = if @set.tracks.empty?
227
- nil
228
- else
229
- [@selected, @set.tracks.size - 1].min
230
- end
231
- end
232
-
233
- def move_track(direction)
234
- return if @set.tracks.size < 2 || @selected.nil?
235
-
236
- if direction == :up
237
- @set.move_up(@selected)
238
- @selected = [@selected - 1, 0].max
239
- else
240
- @set.move_down(@selected)
241
- @selected = [@selected + 1, @set.tracks.size - 1].min
242
- end
243
- end
244
- end
245
- end