wavesync 1.0.0.alpha3 → 1.0.0.beta1
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.
- checksums.yaml +4 -4
- data/README.md +64 -14
- data/config/devices.yml +17 -0
- data/lib/wavesync/acid_chunk.rb +36 -0
- data/lib/wavesync/analyzer.rb +8 -0
- data/lib/wavesync/audio.rb +118 -94
- data/lib/wavesync/audio_format.rb +8 -2
- data/lib/wavesync/cli.rb +1 -0
- data/lib/wavesync/commands/clear_cache.rb +75 -0
- data/lib/wavesync/commands/command.rb +2 -0
- data/lib/wavesync/commands/pull.rb +43 -0
- data/lib/wavesync/commands/setlist.rb +66 -0
- data/lib/wavesync/commands/sync.rb +15 -26
- data/lib/wavesync/commands.rb +44 -2
- data/lib/wavesync/config.rb +37 -3
- data/lib/wavesync/cue_chunk.rb +24 -0
- data/lib/wavesync/device.rb +14 -5
- data/lib/wavesync/essentia_bpm_detector.rb +3 -1
- data/lib/wavesync/ffmpeg/probe.rb +74 -0
- data/lib/wavesync/ffmpeg.rb +144 -0
- data/lib/wavesync/file_converter.rb +6 -3
- data/lib/wavesync/libmtp.rb +333 -0
- data/lib/wavesync/logger.rb +84 -0
- data/lib/wavesync/path_resolver.rb +19 -2
- data/lib/wavesync/percival_bpm_detector.rb +3 -1
- data/lib/wavesync/scanner.rb +117 -50
- data/lib/wavesync/{set.rb → setlist.rb} +13 -13
- data/lib/wavesync/{set_editor.rb → setlist_editor.rb} +52 -43
- data/lib/wavesync/track_padding.rb +10 -2
- data/lib/wavesync/transport/filesystem.rb +36 -0
- data/lib/wavesync/transport/mtp.rb +285 -0
- data/lib/wavesync/transport.rb +21 -0
- data/lib/wavesync/ui.rb +43 -12
- data/lib/wavesync/version.rb +1 -1
- data/lib/wavesync.rb +6 -2
- metadata +13 -32
- data/lib/wavesync/commands/set.rb +0 -66
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
require_relative '../libmtp'
|
|
8
|
+
require_relative '../cue_chunk'
|
|
9
|
+
require_relative '../logger'
|
|
10
|
+
|
|
11
|
+
module Wavesync
|
|
12
|
+
module Transport
|
|
13
|
+
class Mtp
|
|
14
|
+
DEFAULT_CACHE_ROOT = File.join(Dir.home, '.cache', 'wavesync').freeze
|
|
15
|
+
MTP_ILLEGAL_CHARACTERS = %r{[<>:"/\\|?*\x00-\x1f]} #: Regexp
|
|
16
|
+
CACHE_DIR_ILLEGAL_CHARACTERS = /[^a-z0-9._-]/ #: Regexp
|
|
17
|
+
MANIFEST_FILENAME = '.manifest.yml'
|
|
18
|
+
|
|
19
|
+
attr_reader :working_directory #: String
|
|
20
|
+
attr_reader :device_path #: String
|
|
21
|
+
attr_reader :name #: String
|
|
22
|
+
|
|
23
|
+
#: (String name, ?cache_root: String) -> String
|
|
24
|
+
def self.cache_path(name, cache_root: DEFAULT_CACHE_ROOT)
|
|
25
|
+
File.join(cache_root, sanitize_dir_name(name))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (String name) -> String
|
|
29
|
+
def self.sanitize_dir_name(name)
|
|
30
|
+
sanitized = name.downcase.gsub(CACHE_DIR_ILLEGAL_CHARACTERS, '_')
|
|
31
|
+
sanitized.empty? ? 'device' : sanitized
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config, ?libmtp: Libmtp, ?cache_root: String) -> void
|
|
35
|
+
def initialize(device_config, libmtp: Libmtp.new, cache_root: DEFAULT_CACHE_ROOT)
|
|
36
|
+
@name = device_config[:name]
|
|
37
|
+
@device_path = device_config[:path]
|
|
38
|
+
@libmtp = libmtp
|
|
39
|
+
@working_directory = self.class.cache_path(@name, cache_root: cache_root)
|
|
40
|
+
FileUtils.mkdir_p(@working_directory)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#: () ?{ (Integer, Integer, String) -> void } -> void
|
|
44
|
+
def prepare!(&progress)
|
|
45
|
+
device_files = @libmtp.files
|
|
46
|
+
folder_paths = build_folder_paths(@libmtp.folders)
|
|
47
|
+
|
|
48
|
+
candidates = device_files.filter_map do |device_file|
|
|
49
|
+
next unless wav?(device_file.filename)
|
|
50
|
+
|
|
51
|
+
relative_path = relative_path_for(device_file, folder_paths)
|
|
52
|
+
next unless relative_path
|
|
53
|
+
|
|
54
|
+
local_path = File.join(@working_directory, relative_path)
|
|
55
|
+
{ device_file: device_file, relative_path: relative_path, local_path: local_path }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Dir.mktmpdir('wavesync_mtp_pull') do |tmpdir|
|
|
59
|
+
candidates.each_with_index do |candidate, index|
|
|
60
|
+
progress&.call(index, candidates.size, candidate[:relative_path])
|
|
61
|
+
pull_if_cues_differ(candidate, tmpdir)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: () -> void
|
|
67
|
+
def begin_push!
|
|
68
|
+
@push_files_by_parent = @libmtp.files.group_by(&:parent_id)
|
|
69
|
+
@push_folders_by_parent = @libmtp.folders.group_by(&:parent_id)
|
|
70
|
+
@push_storage_id = primary_storage_id(@push_folders_by_parent)
|
|
71
|
+
@push_manifest = load_manifest
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#: (String relative_path) -> void
|
|
75
|
+
def push_file!(relative_path)
|
|
76
|
+
files_by_parent = @push_files_by_parent
|
|
77
|
+
folders_by_parent = @push_folders_by_parent
|
|
78
|
+
storage_id = @push_storage_id
|
|
79
|
+
raise Libmtp::Error, 'push_file! called before begin_push!' unless files_by_parent && folders_by_parent && storage_id
|
|
80
|
+
|
|
81
|
+
local_path = File.join(@working_directory, relative_path)
|
|
82
|
+
return unless File.file?(local_path)
|
|
83
|
+
|
|
84
|
+
remote_path = join_remote_paths(@device_path, relative_path)
|
|
85
|
+
entry = { local_path: local_path, relative_path: relative_path, remote_path: remote_path } #: { local_path: String, relative_path: String, remote_path: String }
|
|
86
|
+
push_one(entry, files_by_parent: files_by_parent, folders_by_parent: folders_by_parent, storage_id: storage_id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: () -> void
|
|
90
|
+
def finish_push!
|
|
91
|
+
save_manifest if @push_manifest
|
|
92
|
+
@push_files_by_parent = nil
|
|
93
|
+
@push_folders_by_parent = nil
|
|
94
|
+
@push_storage_id = nil
|
|
95
|
+
@push_manifest = nil
|
|
96
|
+
@libmtp.close!
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: () ?{ (Integer, Integer, String) -> void } -> void
|
|
100
|
+
def commit!(&progress)
|
|
101
|
+
begin_push!
|
|
102
|
+
local_files = enumerate_local_files
|
|
103
|
+
local_files.each_with_index do |entry, index|
|
|
104
|
+
progress&.call(index, local_files.size, entry[:relative_path])
|
|
105
|
+
push_file!(entry[:relative_path])
|
|
106
|
+
end
|
|
107
|
+
finish_push!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
#: () -> Array[{ local_path: String, relative_path: String, remote_path: String }]
|
|
113
|
+
def enumerate_local_files
|
|
114
|
+
Dir.glob(File.join(@working_directory, '**', '*'))
|
|
115
|
+
.reject { |path| File.directory?(path) }
|
|
116
|
+
.sort
|
|
117
|
+
.map do |local_path|
|
|
118
|
+
relative_path = local_path.sub(%r{\A#{Regexp.escape(@working_directory)}/?}, '')
|
|
119
|
+
remote_path = join_remote_paths(@device_path, relative_path)
|
|
120
|
+
{ local_path: local_path, relative_path: relative_path, remote_path: remote_path }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#: ({ local_path: String, relative_path: String, remote_path: String } entry, files_by_parent: Hash[Integer, Array[Libmtp::DeviceFile]], folders_by_parent: Hash[Integer, Array[Libmtp::DeviceFolder]], storage_id: Integer) -> void
|
|
125
|
+
def push_one(entry, files_by_parent:, folders_by_parent:, storage_id:)
|
|
126
|
+
target_dir = File.dirname(entry[:remote_path])
|
|
127
|
+
parent_id = ensure_folder(target_dir, folders_by_parent: folders_by_parent, storage_id: storage_id)
|
|
128
|
+
|
|
129
|
+
filename = sanitize_for_mtp(File.basename(entry[:remote_path]))
|
|
130
|
+
existing = (files_by_parent[parent_id] || []).find { |file| sanitize_for_mtp(file.filename) == filename }
|
|
131
|
+
|
|
132
|
+
local_size = File.size(entry[:local_path])
|
|
133
|
+
manifest = @push_manifest || {} #: Hash[String, Integer]
|
|
134
|
+
manifest_size = manifest[entry[:relative_path]]
|
|
135
|
+
|
|
136
|
+
if existing && (manifest_size == local_size || (manifest_size.nil? && existing.size == local_size))
|
|
137
|
+
manifest[entry[:relative_path]] = local_size
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if existing
|
|
142
|
+
Logger.log_event("MTP replacing (size mismatch device=#{existing.size} local=#{local_size}): #{entry[:relative_path]}")
|
|
143
|
+
@libmtp.delete_file(id: existing.id)
|
|
144
|
+
files_by_parent[parent_id]&.delete(existing)
|
|
145
|
+
else
|
|
146
|
+
Logger.log_event("MTP uploading new file (#{local_size} bytes): #{entry[:relative_path]}")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@libmtp.send_file(
|
|
150
|
+
local_path: entry[:local_path],
|
|
151
|
+
remote_filename: filename,
|
|
152
|
+
parent_id: parent_id,
|
|
153
|
+
storage_id: storage_id
|
|
154
|
+
)
|
|
155
|
+
manifest[entry[:relative_path]] = local_size
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#: (String remote_path, folders_by_parent: Hash[Integer, Array[Libmtp::DeviceFolder]], storage_id: Integer) -> Integer
|
|
159
|
+
def ensure_folder(remote_path, folders_by_parent:, storage_id:)
|
|
160
|
+
parent_id = 0
|
|
161
|
+
path_components(remote_path).each do |raw_name|
|
|
162
|
+
name = sanitize_for_mtp(raw_name)
|
|
163
|
+
children = folders_by_parent[parent_id] || []
|
|
164
|
+
existing = children.find { |folder| sanitize_for_mtp(folder.name) == name }
|
|
165
|
+
if existing
|
|
166
|
+
parent_id = existing.folder_id
|
|
167
|
+
else
|
|
168
|
+
new_id = @libmtp.create_folder(name: name, parent_id: parent_id, storage_id: storage_id)
|
|
169
|
+
new_folder = Libmtp::DeviceFolder.new(folder_id: new_id, name: name, parent_id: parent_id, storage_id: storage_id)
|
|
170
|
+
(folders_by_parent[parent_id] ||= []) << new_folder
|
|
171
|
+
parent_id = new_id
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
parent_id
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
#: (String name) -> String
|
|
178
|
+
def normalize_unicode(name)
|
|
179
|
+
name.unicode_normalize(:nfc)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#: () -> String
|
|
183
|
+
def manifest_path
|
|
184
|
+
File.join(@working_directory, MANIFEST_FILENAME)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
#: () -> Hash[String, Integer]
|
|
188
|
+
def load_manifest
|
|
189
|
+
return {} unless File.exist?(manifest_path)
|
|
190
|
+
|
|
191
|
+
data = YAML.safe_load_file(manifest_path)
|
|
192
|
+
data.is_a?(Hash) ? data : {}
|
|
193
|
+
rescue Psych::SyntaxError
|
|
194
|
+
{}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
#: () -> void
|
|
198
|
+
def save_manifest
|
|
199
|
+
manifest = @push_manifest
|
|
200
|
+
return unless manifest
|
|
201
|
+
|
|
202
|
+
File.write(manifest_path, manifest.to_yaml)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (String name) -> String
|
|
206
|
+
def sanitize_for_mtp(name)
|
|
207
|
+
normalize_unicode(name).gsub(MTP_ILLEGAL_CHARACTERS, '')
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
#: (Hash[Integer, Array[Libmtp::DeviceFolder]] folders_by_parent) -> Integer
|
|
211
|
+
def primary_storage_id(folders_by_parent)
|
|
212
|
+
first_storage = folders_by_parent.values.flatten.map(&:storage_id).compact.first
|
|
213
|
+
raise Libmtp::DeviceNotFound, 'No MTP device detected. Connect the device and unmount it from any other app.' unless first_storage
|
|
214
|
+
|
|
215
|
+
first_storage
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
#: (String path) -> Array[String]
|
|
219
|
+
def path_components(path)
|
|
220
|
+
path.split('/').reject { |component| component.empty? || component == '.' }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
#: (String left, String right) -> String
|
|
224
|
+
def join_remote_paths(left, right)
|
|
225
|
+
"#{left}/#{right}".split('/').reject(&:empty?).join('/')
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
#: (Array[Libmtp::DeviceFolder] folders) -> Hash[Integer, String]
|
|
229
|
+
def build_folder_paths(folders)
|
|
230
|
+
by_id = {} #: Hash[Integer, Libmtp::DeviceFolder]
|
|
231
|
+
folders.each { |folder| by_id[folder.folder_id] = folder }
|
|
232
|
+
paths = { 0 => '' } #: Hash[Integer, String]
|
|
233
|
+
folders.each { |folder| resolve_folder_path(folder.folder_id, by_id, paths) }
|
|
234
|
+
paths
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
#: (Integer folder_id, Hash[Integer, Libmtp::DeviceFolder] by_id, Hash[Integer, String] paths) -> String?
|
|
238
|
+
def resolve_folder_path(folder_id, by_id, paths)
|
|
239
|
+
return paths[folder_id] if paths.key?(folder_id)
|
|
240
|
+
|
|
241
|
+
folder = by_id[folder_id]
|
|
242
|
+
return nil unless folder
|
|
243
|
+
|
|
244
|
+
parent_path = resolve_folder_path(folder.parent_id, by_id, paths) || ''
|
|
245
|
+
full_path = parent_path.empty? ? folder.name : "#{parent_path}/#{folder.name}"
|
|
246
|
+
paths[folder_id] = full_path
|
|
247
|
+
full_path
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#: (Libmtp::DeviceFile device_file, Hash[Integer, String] folder_paths) -> String?
|
|
251
|
+
def relative_path_for(device_file, folder_paths)
|
|
252
|
+
folder_path = folder_paths[device_file.parent_id]
|
|
253
|
+
return nil unless folder_path
|
|
254
|
+
|
|
255
|
+
full_path = folder_path.empty? ? device_file.filename : "#{folder_path}/#{device_file.filename}"
|
|
256
|
+
device_root = path_components(@device_path).join('/')
|
|
257
|
+
return full_path if device_root.empty?
|
|
258
|
+
return full_path[(device_root.length + 1)..] if full_path.start_with?("#{device_root}/")
|
|
259
|
+
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
#: (String filename) -> bool
|
|
264
|
+
def wav?(filename)
|
|
265
|
+
File.extname(filename).downcase == '.wav'
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
#: ({ device_file: Libmtp::DeviceFile, relative_path: String, local_path: String } candidate, String tmpdir) -> void
|
|
269
|
+
def pull_if_cues_differ(candidate, tmpdir)
|
|
270
|
+
device_file = candidate[:device_file]
|
|
271
|
+
local_path = candidate[:local_path]
|
|
272
|
+
tmp_path = File.join(tmpdir, "#{device_file.id}.wav")
|
|
273
|
+
|
|
274
|
+
@libmtp.get_file(id: device_file.id, local_path: tmp_path)
|
|
275
|
+
|
|
276
|
+
device_cues = CueChunk.read(tmp_path)
|
|
277
|
+
local_cues = File.exist?(local_path) ? CueChunk.read(local_path) : [] #: Array[{identifier: Integer, sample_offset: Integer, label: String?}]
|
|
278
|
+
return if CueChunk.same?(device_cues, local_cues)
|
|
279
|
+
|
|
280
|
+
FileUtils.mkdir_p(File.dirname(local_path))
|
|
281
|
+
FileUtils.mv(tmp_path, local_path)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Wavesync
|
|
5
|
+
module Transport
|
|
6
|
+
SUPPORTED_KINDS = %w[filesystem mtp].freeze
|
|
7
|
+
|
|
8
|
+
#: ({ name: String, model: String, path: String, transport: String?, mp3_bitrate: Integer } device_config) -> (Filesystem | Mtp)
|
|
9
|
+
def self.for(device_config)
|
|
10
|
+
kind = device_config[:transport] || 'filesystem'
|
|
11
|
+
case kind
|
|
12
|
+
when 'filesystem' then Filesystem.new(device_config)
|
|
13
|
+
when 'mtp' then Mtp.new(device_config)
|
|
14
|
+
else raise ArgumentError, "Unsupported transport: #{kind.inspect}. Supported: #{SUPPORTED_KINDS.join(', ')}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_relative 'transport/filesystem'
|
|
21
|
+
require_relative 'transport/mtp'
|
data/lib/wavesync/ui.rb
CHANGED
|
@@ -42,12 +42,12 @@ module Wavesync
|
|
|
42
42
|
sticky(parts.join(' '), 0)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#: (AudioFormat source_format, AudioFormat target_format) -> void
|
|
46
|
-
def conversion_progress(source_format, target_format)
|
|
45
|
+
#: (AudioFormat source_format, AudioFormat target_format, Integer mp3_bitrate) -> void
|
|
46
|
+
def conversion_progress(source_format, target_format, mp3_bitrate)
|
|
47
47
|
effective = source_format.merge(target_format)
|
|
48
48
|
|
|
49
|
-
source_info = audio_info(source_format
|
|
50
|
-
target_info =
|
|
49
|
+
source_info = audio_info(source_format)
|
|
50
|
+
target_info = target_audio_info(effective, mp3_bitrate)
|
|
51
51
|
|
|
52
52
|
formatted_line = in_color(
|
|
53
53
|
"Converting #{source_format.file_type} (#{source_info}) ⇢ #{effective.file_type} (#{target_info})", :highlight
|
|
@@ -57,14 +57,15 @@ module Wavesync
|
|
|
57
57
|
|
|
58
58
|
#: (AudioFormat source_format) -> void
|
|
59
59
|
def copy(source_format)
|
|
60
|
-
info = audio_info(source_format
|
|
60
|
+
info = audio_info(source_format)
|
|
61
61
|
|
|
62
62
|
sticky(in_color("Copying #{source_format.file_type} (#{info})", :highlight), 3)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
#: () -> void
|
|
66
|
-
def skip
|
|
67
|
-
|
|
65
|
+
#: (?staged: bool) -> void
|
|
66
|
+
def skip(staged: false)
|
|
67
|
+
message = staged ? '↷ Already in cache' : '↷ Already on device'
|
|
68
|
+
sticky(in_color(message, :highlight), 3)
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
#: ((String | Integer)? tbpm, ?original_bars: Integer?, ?target_bars: Integer?) -> void
|
|
@@ -79,6 +80,26 @@ module Wavesync
|
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
83
|
+
#: (Integer index, Integer total_count, Device device) -> void
|
|
84
|
+
def pull_progress(index, total_count, device)
|
|
85
|
+
parts = [
|
|
86
|
+
in_color("wavesync pull #{device.name}", :primary),
|
|
87
|
+
in_color("#{index + 1}/#{total_count}", :extra)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
sticky(parts.join(' '), 0)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (Integer index, Integer total_count, Device device) -> void
|
|
94
|
+
def pull_staging_progress(index, total_count, device)
|
|
95
|
+
parts = [
|
|
96
|
+
in_color("wavesync pull #{device.name} (staging)", :primary),
|
|
97
|
+
in_color("#{index + 1}/#{total_count}", :extra)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
sticky(parts.join(' '), 0)
|
|
101
|
+
end
|
|
102
|
+
|
|
82
103
|
#: (Integer index, Integer total_count) -> void
|
|
83
104
|
def analyze_progress(index, total_count)
|
|
84
105
|
parts = [
|
|
@@ -124,11 +145,21 @@ module Wavesync
|
|
|
124
145
|
|
|
125
146
|
private
|
|
126
147
|
|
|
127
|
-
#: (
|
|
128
|
-
def audio_info(
|
|
148
|
+
#: (AudioFormat format) -> String
|
|
149
|
+
def audio_info(format)
|
|
150
|
+
quality = format.file_type == 'mp3' ? format.bitrate&.to_s : format.bit_depth&.to_s
|
|
151
|
+
[
|
|
152
|
+
sample_rate_to_khz(format.sample_rate),
|
|
153
|
+
quality
|
|
154
|
+
].compact.join('/')
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
#: (AudioFormat format, Integer mp3_bitrate) -> String
|
|
158
|
+
def target_audio_info(format, mp3_bitrate)
|
|
159
|
+
quality = format.file_type == 'mp3' ? mp3_bitrate.to_s : format.bit_depth&.to_s
|
|
129
160
|
[
|
|
130
|
-
sample_rate_to_khz(sample_rate),
|
|
131
|
-
|
|
161
|
+
sample_rate_to_khz(format.sample_rate),
|
|
162
|
+
quality
|
|
132
163
|
].compact.join('/')
|
|
133
164
|
end
|
|
134
165
|
|
data/lib/wavesync/version.rb
CHANGED
data/lib/wavesync.rb
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
module Wavesync
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
+
require 'wavesync/logger'
|
|
6
7
|
require 'wavesync/version'
|
|
7
8
|
require 'wavesync/acid_chunk'
|
|
8
9
|
require 'wavesync/cue_chunk'
|
|
9
10
|
require 'wavesync/audio_format'
|
|
11
|
+
require 'wavesync/ffmpeg'
|
|
10
12
|
require 'wavesync/audio'
|
|
11
13
|
require 'wavesync/track_padding'
|
|
12
14
|
require 'wavesync/config'
|
|
@@ -14,10 +16,12 @@ require 'wavesync/device'
|
|
|
14
16
|
require 'wavesync/ui'
|
|
15
17
|
require 'wavesync/path_resolver'
|
|
16
18
|
require 'wavesync/file_converter'
|
|
19
|
+
require 'wavesync/libmtp'
|
|
20
|
+
require 'wavesync/transport'
|
|
17
21
|
require 'wavesync/scanner'
|
|
18
22
|
require 'wavesync/bpm_detector'
|
|
19
23
|
require 'wavesync/analyzer'
|
|
20
|
-
require 'wavesync/
|
|
21
|
-
require 'wavesync/
|
|
24
|
+
require 'wavesync/setlist'
|
|
25
|
+
require 'wavesync/setlist_editor'
|
|
22
26
|
require 'wavesync/commands'
|
|
23
27
|
require 'wavesync/cli'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wavesync
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.
|
|
4
|
+
version: 1.0.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Zecher
|
|
@@ -37,34 +37,6 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '3.1'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: streamio-ffmpeg
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '3.0'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '3.0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: taglib-ruby
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '2.0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '2.0'
|
|
68
40
|
- !ruby/object:Gem::Dependency
|
|
69
41
|
name: tty-cursor
|
|
70
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -111,22 +83,31 @@ files:
|
|
|
111
83
|
- lib/wavesync/cli.rb
|
|
112
84
|
- lib/wavesync/commands.rb
|
|
113
85
|
- lib/wavesync/commands/analyze.rb
|
|
86
|
+
- lib/wavesync/commands/clear_cache.rb
|
|
114
87
|
- lib/wavesync/commands/command.rb
|
|
115
88
|
- lib/wavesync/commands/help.rb
|
|
116
|
-
- lib/wavesync/commands/
|
|
89
|
+
- lib/wavesync/commands/pull.rb
|
|
90
|
+
- lib/wavesync/commands/setlist.rb
|
|
117
91
|
- lib/wavesync/commands/sync.rb
|
|
118
92
|
- lib/wavesync/config.rb
|
|
119
93
|
- lib/wavesync/cue_chunk.rb
|
|
120
94
|
- lib/wavesync/device.rb
|
|
121
95
|
- lib/wavesync/essentia_bpm_detector.rb
|
|
96
|
+
- lib/wavesync/ffmpeg.rb
|
|
97
|
+
- lib/wavesync/ffmpeg/probe.rb
|
|
122
98
|
- lib/wavesync/file_converter.rb
|
|
99
|
+
- lib/wavesync/libmtp.rb
|
|
100
|
+
- lib/wavesync/logger.rb
|
|
123
101
|
- lib/wavesync/path_resolver.rb
|
|
124
102
|
- lib/wavesync/percival_bpm_detector.rb
|
|
125
103
|
- lib/wavesync/python_venv.rb
|
|
126
104
|
- lib/wavesync/scanner.rb
|
|
127
|
-
- lib/wavesync/
|
|
128
|
-
- lib/wavesync/
|
|
105
|
+
- lib/wavesync/setlist.rb
|
|
106
|
+
- lib/wavesync/setlist_editor.rb
|
|
129
107
|
- lib/wavesync/track_padding.rb
|
|
108
|
+
- lib/wavesync/transport.rb
|
|
109
|
+
- lib/wavesync/transport/filesystem.rb
|
|
110
|
+
- lib/wavesync/transport/mtp.rb
|
|
130
111
|
- lib/wavesync/ui.rb
|
|
131
112
|
- lib/wavesync/version.rb
|
|
132
113
|
homepage: https://github.com/pixelate/wavesync
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
# rbs_inline: enabled
|
|
3
|
-
|
|
4
|
-
require 'optparse'
|
|
5
|
-
|
|
6
|
-
module Wavesync
|
|
7
|
-
module Commands
|
|
8
|
-
class Set < Command
|
|
9
|
-
self.name = 'set'
|
|
10
|
-
self.subcommands = [
|
|
11
|
-
Subcommand.new(usage: 'set create NAME', description: 'Create a new track set'),
|
|
12
|
-
Subcommand.new(usage: 'set edit NAME', description: 'Edit an existing track set'),
|
|
13
|
-
Subcommand.new(usage: 'set list', description: 'List all track sets')
|
|
14
|
-
].freeze
|
|
15
|
-
|
|
16
|
-
#: () -> void
|
|
17
|
-
def run
|
|
18
|
-
subcommand = ARGV.shift
|
|
19
|
-
|
|
20
|
-
_options, config = parse_options(banner: 'Usage: wavesync set <subcommand> [options]')
|
|
21
|
-
|
|
22
|
-
case subcommand
|
|
23
|
-
when 'create'
|
|
24
|
-
name = require_name('create')
|
|
25
|
-
if Wavesync::Set.exists?(config.library, name)
|
|
26
|
-
puts "Set '#{name}' already exists. Use 'wavesync set edit #{name}' to edit it."
|
|
27
|
-
exit 1
|
|
28
|
-
end
|
|
29
|
-
set = Wavesync::Set.new(config.library, name)
|
|
30
|
-
Wavesync::SetEditor.new(set, config.library).run
|
|
31
|
-
when 'edit'
|
|
32
|
-
name = require_name('edit')
|
|
33
|
-
unless Wavesync::Set.exists?(config.library, name)
|
|
34
|
-
puts "Set '#{name}' not found. Use 'wavesync set create #{name}' to create it."
|
|
35
|
-
exit 1
|
|
36
|
-
end
|
|
37
|
-
set = Wavesync::Set.load(config.library, name)
|
|
38
|
-
Wavesync::SetEditor.new(set, config.library).run
|
|
39
|
-
when 'list'
|
|
40
|
-
sets = Wavesync::Set.all(config.library)
|
|
41
|
-
if sets.empty?
|
|
42
|
-
puts 'No sets found.'
|
|
43
|
-
else
|
|
44
|
-
sets.each { |set| puts "#{set.name} (#{set.tracks.size} tracks)" }
|
|
45
|
-
end
|
|
46
|
-
else
|
|
47
|
-
puts "Unknown subcommand: #{subcommand || '(none)'}"
|
|
48
|
-
puts 'Available subcommands: create, edit, list'
|
|
49
|
-
exit 1
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
#: (String subcommand) -> String
|
|
56
|
-
def require_name(subcommand)
|
|
57
|
-
name = ARGV.shift
|
|
58
|
-
unless name
|
|
59
|
-
puts "Usage: wavesync set #{subcommand} <name>"
|
|
60
|
-
exit 1
|
|
61
|
-
end
|
|
62
|
-
name
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|