about summary refs log tree commit homepage
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/dtas-archive14
-rwxr-xr-xbin/dtas-console76
-rwxr-xr-xbin/dtas-ctl2
-rwxr-xr-xbin/dtas-cueedit2
-rwxr-xr-xbin/dtas-enq2
-rwxr-xr-xbin/dtas-mlib2
-rwxr-xr-xbin/dtas-msinkctl5
-rwxr-xr-xbin/dtas-partstats12
-rwxr-xr-xbin/dtas-player3
-rwxr-xr-xbin/dtas-readahead42
-rwxr-xr-xbin/dtas-sinkedit7
-rwxr-xr-xbin/dtas-sourceedit7
-rwxr-xr-xbin/dtas-splitfx23
-rwxr-xr-xbin/dtas-tl93
-rwxr-xr-xbin/dtas-xdelay2
15 files changed, 166 insertions, 126 deletions
diff --git a/bin/dtas-archive b/bin/dtas-archive
index f47f05a..7c0e4f7 100755
--- a/bin/dtas-archive
+++ b/bin/dtas-archive
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2015-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 usage = "#$0 SOURCE DESTINATION"
@@ -29,6 +29,8 @@ repeat = 1
 stats = false
 keep_going = false
 compression = []
+comment = []
+match = nil
 
 OptionParser.new('', 24, '  ') do |op|
   op.banner = usage
@@ -36,6 +38,7 @@ OptionParser.new('', 24, '  ') do |op|
   op.on('-C', '--compression [FACTOR]', 'compression factor for sox') { |c|
     compression = [ '-C', c ]
   }
+  op.on('--comment=TEXT', String) { |c| comment.push('--comment', c) }
   op.on('-j', '--jobs [JOBS]', Integer) { |j| jobs = j }
   op.on('-S', '--stats', 'save stats on the file') { stats = true }
   op.on('-k', '--keep-going', 'continue after error') { keep_going = true }
@@ -45,6 +48,7 @@ OptionParser.new('', 24, '  ') do |op|
   op.on('-r', '--repeat [COUNT]', 'number of times to check', Integer) do |r|
     repeat = r
   end
+  op.on('-m', '--match=REGEX', String) { |s| match = Regexp.new(s) }
   op.on('-s', '--quiet', '--silent') { silent = true }
   op.on('-h', '--help') do
     puts(op.to_s)
@@ -53,6 +57,9 @@ OptionParser.new('', 24, '  ') do |op|
   op.parse!(ARGV)
 end
 
+match ||= %r/./
+comment.push('--comment', '') if comment.empty?
+
 dst = ARGV.pop
 src = ARGV.dup
 
@@ -63,6 +70,7 @@ src.each do |s|
   src_st = File.stat(s)
   if src_st.directory?
     Find.find(s) do |path|
+      path =~ match or next
       File.file?(path) or next
       dir = File.dirname(path)
       dir_st = File.stat(dir)
@@ -137,7 +145,7 @@ thrs = jobs.times.map do |i|
 
       if dry_run || !silent
         names = job.map { |x| Shellwords.escape(x) }
-        cmd = [ 'sox', *names ]
+        cmd = [ 'sox', names[0], *compression, *comment, names[1] ]
         if stats
           cmd << 'stats'
           cmd << "2>#{Shellwords.escape(stats_out)}"
@@ -151,7 +159,7 @@ thrs = jobs.times.map do |i|
         end
       end
 
-      cmd = [ 'sox', input, *compression, output ]
+      cmd = [ 'sox', input, *compression, *comment, output ]
       if stats
         cmd << 'stats'
         cmd = [ *cmd, { err: stats_out } ]
diff --git a/bin/dtas-console b/bin/dtas-console
index 00b5cd8..eedd0f0 100755
--- a/bin/dtas-console
+++ b/bin/dtas-console
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2019 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 #
@@ -11,21 +11,24 @@ require 'dtas/sigevent'
 require 'dtas/process'
 require 'dtas/format'
 include DTAS::Process
-require 'yaml'
 begin
   require 'curses'
 rescue LoadError
   abort "please install the 'curses' RubyGem to use #$0"
 end
 
+# workaround https://bugs.debian.org/958973
+$VERBOSE = nil if RUBY_VERSION.to_f < 3.0
+
 tsec = false
 se = DTAS::Sigevent.new
 trap(:WINCH) { se.signal }
 w = DTAS::UNIXClient.new
 w.req_ok('watch')
 c = DTAS::UNIXClient.new
-cur = YAML.load(c.req('current'))
+cur = DTAS.yaml_load(c.req('current'))
 readable = [ se, w, $stdin ]
+set_title = (ENV['DISPLAY'] || ENV['WAYLAND_DISPLAY']) ? $stdout : nil
 
 # current rg mode
 rg_mode = DTAS::RGState::RG_MODE.keys.unshift("off")
@@ -34,6 +37,7 @@ if (rg = cur["rg"]) && (rg = rg["mode"])
 else
   rg_mode_i = 0
 end
+show_info = false
 
 def update_tfmt(prec, tsec)
   if tsec
@@ -53,8 +57,6 @@ tfmt = update_tfmt(prec_step[prec_nr], tsec)
 events = []
 interval = 1.0 / 10 ** prec_nr
 
-pause = nil
-
 def show_events(lineno, screen, events)
   Curses.setpos(lineno += 1, 0)
   Curses.clrtoeol
@@ -114,14 +116,14 @@ def rg_string(rg, current)
   rv
 end
 
-def may_fail(res, events)
+def may_fail(c, req, events)
+  res = c.req(req)
   events << res if res != "OK"
 end
 
 pre_mute_vol = 1.0
 enc_locale = Encoding.find("locale")
 $stdout.set_encoding(enc_locale)
-enc_opts = { undef: :replace, invalid: :replace, replace: '?' }
 begin
   Curses.init_screen
   Curses.nonl
@@ -135,7 +137,6 @@ begin
     pfmt = cur['format']
     elapsed = samples = 0
     fmt = total = ''
-    paused = false
     if current = cur['current']
       infile = current['infile'] || current['command']
       elapsed = DTAS.now - current['spawn_at']
@@ -150,7 +151,6 @@ begin
       end
     elsif cur['paused'] && infile = cur['current_paused']
       fmt = "[paused] (#{fmt_to_s(pfmt)})"
-      paused = true
       infile = infile['command'] if Hash === infile
       if Array === infile
         infile, elapsed = infile
@@ -169,7 +169,12 @@ begin
       # FS encoding != locale encoding, but we need to display an FS path
       # name to whatever locale the terminal is encoded to, so force it
       # and risk mojibake...
-      infile.encode(enc_locale, enc_opts)
+      infile.encode(enc_locale,
+                    undef: :replace, invalid: :replace, replace: '?')
+      if set_title
+        dir, base = File.split(infile)
+        set_title.syswrite("\033]0;#{base} dtas-console\07")
+      end
       Curses.setpos(lineno += 1, 0)
       Curses.clrtoeol
       Curses.addstr(infile)
@@ -206,6 +211,27 @@ begin
     Curses.addstr(extra.join(' '))
     pre_mute_vol = cur_vol if cur_vol != 0
 
+    if show_info && current && comments = current['comments']
+      Curses.setpos(lineno += 1, 0)
+      Curses.clrtoeol
+      Curses.addstr('comments:')
+      comments.each do |k,v|
+        v = v.split(/\n+/)
+        k = k.dump if /[[:cntrl:]]/ =~ k
+        if first = v.shift
+          Curses.setpos(lineno += 1, 0)
+          Curses.clrtoeol
+          first = first.dump if /[[:cntrl:]]/ =~ first
+          Curses.addstr("  #{k}: #{first}")
+          v.each do |val|
+            val = val.dump if /[[:cntrl:]]/ =~ val
+            Curses.setpos(lineno += 1, 0)
+            Curses.clrtoeol
+            Curses.addstr("   #{val}")
+          end
+        end
+      end
+    end
     show_events(lineno, screen, events)
 
     Curses.refresh # draw and wait
@@ -220,29 +246,28 @@ begin
         case event
         when "pause"
           if current
-            pause = current['infile'] || current['command']
+            current['infile'] || current['command']
           end
         when %r{\Afile }
-          pause = nil
         end
         events << "#{Time.now.strftime(tfmt)} #{event}"
         # something happened, refresh current
         # we could be more intelligent here, maybe, but too much work.
-        cur = YAML.load(c.req('current'))
+        cur = DTAS.yaml_load(c.req('current'))
       when $stdin
         # keybindings taken from mplayer / vi
         case key = Curses.getch
-        when "j" then c.req_ok("seek -5")
-        when "k" then c.req_ok("seek +5")
+        when "j" then may_fail(c, "seek -5", events)
+        when "k" then may_fail(c, "seek +5", events)
         when "q" then exit(0)
-        when Curses::KEY_DOWN then c.req_ok("seek -60")
-        when Curses::KEY_UP then c.req_ok("seek +60")
-        when Curses::KEY_LEFT then c.req_ok("seek -10")
-        when Curses::KEY_RIGHT then c.req_ok("seek +10")
-        when Curses::KEY_BACKSPACE then c.req_ok("seek 0")
+        when Curses::KEY_DOWN then may_fail(c, "seek -60", events)
+        when Curses::KEY_UP then may_fail(c, "seek +60", events)
+        when Curses::KEY_LEFT then may_fail(c, "seek -10", events)
+        when Curses::KEY_RIGHT then may_fail(c, "seek +10", events)
+        when Curses::KEY_BACKSPACE then may_fail(c, "seek 0", events)
         # yes, some of us have long audio files
-        when Curses::KEY_PPAGE then c.req_ok("seek +600")
-        when Curses::KEY_NPAGE then c.req_ok("seek -600")
+        when Curses::KEY_PPAGE then may_fail(c, "seek +600", events)
+        when Curses::KEY_NPAGE then may_fail(c, "seek -600", events)
         when '9' then c.req_ok('rg volume-=0.01')
         when '0' then c.req_ok('rg volume+=0.01')
         when '=' then c.req_ok('rg volume=1')
@@ -253,8 +278,8 @@ begin
         when "f" then c.req_ok("rg fallback_gain-=1")
         when ">" then c.req_ok("tl next")
         when "<" then c.req_ok("tl prev")
-        when "!" then may_fail(c.req("cue prev"), events)
-        when "@" then may_fail(c.req("cue next"), events)
+        when "!" then may_fail(c, "cue prev", events)
+        when "@" then may_fail(c, "cue next", events)
         when "o" then tfmt = update_tfmt(prec_step[prec_nr], tsec = !tsec)
         when " "
           c.req("play_pause")
@@ -276,6 +301,9 @@ begin
             interval = 1.0 / 10 ** prec_nr
           end
         when 27 # TODO readline/edit mode?
+        when 'i'
+          show_info = !show_info
+          Curses.clear if !show_info
         else
           Curses.setpos(screen.maxy - 1, 0)
           Curses.clrtoeol
diff --git a/bin/dtas-ctl b/bin/dtas-ctl
index 171576f..2d55e16 100755
--- a/bin/dtas-ctl
+++ b/bin/dtas-ctl
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 require 'dtas/unix_client'
diff --git a/bin/dtas-cueedit b/bin/dtas-cueedit
index 127b1a0..e176271 100755
--- a/bin/dtas-cueedit
+++ b/bin/dtas-cueedit
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 require 'tempfile'
diff --git a/bin/dtas-enq b/bin/dtas-enq
index f054687..f49d3f7 100755
--- a/bin/dtas-enq
+++ b/bin/dtas-enq
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 require 'dtas/unix_client'
diff --git a/bin/dtas-mlib b/bin/dtas-mlib
index 45c3d01..7a07794 100755
--- a/bin/dtas-mlib
+++ b/bin/dtas-mlib
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2015-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) 2015-2020 all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 usage = "#$0 [-d DATABASE-URI] ACTION [ARGS]"
diff --git a/bin/dtas-msinkctl b/bin/dtas-msinkctl
index 9abf2dc..79c7f26 100755
--- a/bin/dtas-msinkctl
+++ b/bin/dtas-msinkctl
@@ -1,8 +1,7 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
-require 'yaml'
 require 'dtas/unix_client'
 usage = "#$0 <active-set|active-add|active-sub|nonblock|active> SINK"
 c = DTAS::UNIXClient.new
@@ -29,7 +28,7 @@ def filter(c, player_sinks, key)
   rv = []
   player_sinks.each do |name|
     buf = c.req("sink cat #{name}")
-    sink = YAML.load(buf)
+    sink = DTAS.yaml_load(buf)
     rv << sink["name"] if sink[key]
   end
   rv
diff --git a/bin/dtas-partstats b/bin/dtas-partstats
index 388f7ba..6a0c9d4 100755
--- a/bin/dtas-partstats
+++ b/bin/dtas-partstats
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 # TODO
@@ -8,17 +8,11 @@
 # - configurable output formatting
 # - Sequel/SQLite support
 require 'dtas/partstats'
+require 'etc'
 infile = ARGV[0] or abort "usage: #$0 INFILE"
 ps = DTAS::PartStats.new(infile)
 
-def nproc
-  require 'etc'
-  Etc.nprocessors
-rescue NoMethodError
-  `nproc 2>/dev/null || echo 2`.to_i
-end
-
-opts = { jobs: nproc }
+opts = { jobs: Etc.nprocessors }
 stats = ps.run(opts)
 
 headers = ps.key_idx.to_a
diff --git a/bin/dtas-player b/bin/dtas-player
index ccb3969..c926e5f 100755
--- a/bin/dtas-player
+++ b/bin/dtas-player
@@ -1,9 +1,8 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 Thread.abort_on_exception = $stderr.sync = $stdout.sync = true
-require 'yaml'
 require 'dtas/player'
 sock = (ENV["DTAS_PLAYER_SOCK"] || File.expand_path("~/.dtas/player.sock"))
 state = (ENV["DTAS_PLAYER_STATE"] ||
diff --git a/bin/dtas-readahead b/bin/dtas-readahead
index 93ab8c9..c61d317 100755
--- a/bin/dtas-readahead
+++ b/bin/dtas-readahead
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2015-2019 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 #
@@ -12,13 +12,11 @@ end
 @ffprobe = 'ffprobe'
 @avprobe = 'avprobe'
 
-require 'yaml'
 require 'io/wait'
 require 'dtas/unix_client'
 require 'dtas/process'
 
 include DTAS::Process
-include DTAS::SpawnFix
 trap(:CHLD) { DTAS::Process.reaper {} }
 trap(:INT) { exit(0) }
 trap(:TERM) { exit(0) }
@@ -27,21 +25,9 @@ w.req_ok('watch')
 c = DTAS::UNIXClient.new
 @max_ra = 30 * 1024 * 1024
 null = DTAS.null
-@redir = { err: null, out: null, in: null }.freeze
+@redir = { err: null, out: null, in: null, rlimit_cpu: [ 1, 2 ] }.freeze
 require 'pp'
 
-if RUBY_VERSION.to_r >= '2.3'.to_r
-  # Old Rubies did FIONREAD, which breaks on SOCK_SEQPACKET
-  def wait_read(w, timeout)
-    w.to_io.wait_readable(timeout)
-  end
-else
-  def wait_read(w, timeout)
-    r = IO.select([w], nil, nil, timeout)
-    r ? r[0] : nil
-  end
-end
-
 def seek_to_cur_pos(cur_pid, fp)
   cur_fd = []
   fpst = fp.stat
@@ -57,7 +43,7 @@ def seek_to_cur_pos(cur_pid, fp)
       end
     end
   rescue Errno::ENOENT => e # race, process is dead
-    return false
+    return nil
   rescue => e
     warn "error reading FDs from for PID:#{cur_pid}: #{e.message}"
   end
@@ -71,7 +57,7 @@ def seek_to_cur_pos(cur_pid, fp)
   end
   pos
 rescue Errno::ENOENT => e # race, process is dead
-  return false
+  return nil
 end
 
 def children_of(ppid)
@@ -122,7 +108,7 @@ def do_ra(fp, pos, w)
     len -= n
 
     # stop reading immediately if there's an event
-    if wait_read(w, 0)
+    if w.to_io.wait_readable(0)
       adj = @todo_ra
       pos += size
       break
@@ -141,8 +127,8 @@ def do_open(path)
       when "---\n"
         buf << fp.read(fp.size - 4)
         Dir.chdir(File.dirname(path)) do
-          yml = YAML.load(buf)
-          x = yml['infile'] and return File.open(File.expand_path(x).freeze)
+          yml = DTAS.yaml_load(buf)
+          x = yml['infile'] and return File.open(-File.expand_path(x))
         end
       end
     end
@@ -156,12 +142,12 @@ begin
   @todo_ra = @max_ra
   t0 = DTAS.now
   fp = nil
-  cur = YAML.load(c.req('current'))
+  cur = DTAS.yaml_load(c.req('current'))
   while @todo_ra > 0 && fp.nil?
     if current = cur['current']
       track = current['infile']
       break unless track.kind_of?(String)
-      track.freeze
+      track = -track
       fp = work[track] ||= do_open(track)
       cur_pid = current['pid']
       if fp
@@ -178,7 +164,7 @@ begin
     end
 
     # queue has priority, work on it, first
-    queue = YAML.load(c.req('queue cat'))
+    queue = DTAS.yaml_load(c.req('queue cat'))
     while @todo_ra > 0 && track = queue.shift
       next unless track.kind_of?(String)
       fp = nil
@@ -198,7 +184,7 @@ begin
     repeat = c.req('tl repeat').split[-1]
     while @todo_ra > 0 && idx && (cid = ids[idx])
       fp = nil
-      track = c.req("tl get #{cid}").sub!(/\A1 \d+=/, '').freeze
+      track = -(c.req("tl get #{cid}").sub!(/\A1 \d+=/, ''))
       begin
         fp = work[track] ||= do_open(track)
       rescue SystemCallError
@@ -210,7 +196,7 @@ begin
       end
     end
     idx or break
-    cur = YAML.load(c.req('current'))
+    cur = DTAS.yaml_load(c.req('current'))
     current = cur['current'] or break
   end
   if current
@@ -220,10 +206,10 @@ begin
     timeout = 0 if timeout < 0
   else
     work.each_value(&:close).clear
-    fp.close if fp && !fp.closed?
+    fp.close if fp
     fp = timeout = nil
   end
-  r = wait_read(w, timeout)
+  r = w.to_io.wait_readable(timeout)
   p w.res_wait if r
 rescue EOFError
   abort "dtas-player exited"
diff --git a/bin/dtas-sinkedit b/bin/dtas-sinkedit
index 61bb959..252270f 100755
--- a/bin/dtas-sinkedit
+++ b/bin/dtas-sinkedit
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 require 'optparse'
@@ -36,10 +36,10 @@ st_in = $stdin.stat
 
 buf = c.req(%W(sink cat #{name}))
 abort(buf) if buf =~ /\AERR/
-orig = YAML.load(buf)
+orig = DTAS.yaml_load(buf)
 
 commit_update = lambda do |buf|
-  sink = YAML.load(buf)
+  sink = DTAS.yaml_load(buf)
   cmd = %W(sink ed #{name})
   update_cmd_env(cmd, orig, sink)
 
@@ -68,7 +68,6 @@ if st_in.file? || st_in.pipe?
   buf = $stdin.read
   commit_update.call(buf)
 else
-  include DTAS::SpawnFix
   tmp = tmpyaml
   tmp_path = tmp.path
   do_update = lambda { commit_update.call(File.read(tmp_path)) }
diff --git a/bin/dtas-sourceedit b/bin/dtas-sourceedit
index 713c466..1b3f4ee 100755
--- a/bin/dtas-sourceedit
+++ b/bin/dtas-sourceedit
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 require 'optparse'
@@ -36,10 +36,10 @@ st_in = $stdin.stat
 
 buf = c.req(%W(source cat #{name}))
 abort(buf) if buf =~ /\AERR/
-orig = YAML.load(buf)
+orig = DTAS.yaml_load(buf)
 
 commit_update = lambda do |buf|
-  source = YAML.load(buf)
+  source = DTAS.yaml_load(buf)
   cmd = %W(source ed #{name})
   update_cmd_env(cmd, orig, source)
 
@@ -55,7 +55,6 @@ if st_in.file? || st_in.pipe?
   buf = $stdin.read
   commit_update.call(buf)
 else
-  include DTAS::SpawnFix
   tmp = tmpyaml
   tmp_path = tmp.path
   do_update = lambda { commit_update.call(File.read(tmp_path)) }
diff --git a/bin/dtas-splitfx b/bin/dtas-splitfx
index 839d273..17d915d 100755
--- a/bin/dtas-splitfx
+++ b/bin/dtas-splitfx
@@ -1,19 +1,20 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
-require 'yaml'
 require 'optparse'
 require 'dtas/splitfx'
 usage = "#$0 [-n|--dry-run][-j [JOBS]][-s|--silent] SPLITFX_FILE.yml [TARGET]"
 overrides = {} # FIXME: not tested
 default_target = "flac"
-opts = { jobs: 1 }
+opts = { jobs: nil }
 OptionParser.new('', 24, '  ') do |op|
   op.banner = usage
   op.on('-n', '--dry-run') { opts[:dryrun] = true }
   op.on('-j', '--jobs [JOBS]', Integer) { |val| opts[:jobs] = val } # nil==inf
   op.on('-s', '--quiet', '--silent') { opts[:silent] = true }
+  op.on('-S', '--stats', 'run stats every track') { opts[:stats] = true }
+  op.on('-f', '--filter FILTER') { |val| (opts[:filter] ||= []) << val }
   op.on('-D', '--no-dither') { opts[:no_dither] = true }
   op.on('-O', '--outdir OUTDIR') { |val| opts[:outdir] = val }
   op.on('-C', '--compression FACTOR') { |val| opts[:compression] = val }
@@ -23,6 +24,9 @@ OptionParser.new('', 24, '  ') do |op|
   end
   op.on('-b', '--bits RATE', Integer) { |val| opts[:bits] = val }
   op.on('-t', '--trim POSITION') { |val| opts[:trim] = val.tr(',', ' ') }
+  op.on('-E', '--err-suffix SUFFIX') do |val|
+    opts[:err_suffix] = val.start_with?('.') ? val.freeze : ".#{val}"
+  end
   op.on('-p', '--sox-pipe') do
     opts[:sox_pipe] = true
     default_target = 'sox'
@@ -30,13 +34,22 @@ OptionParser.new('', 24, '  ') do |op|
   op.parse!(ARGV)
 end
 
+if opts[:sox_pipe] && opts[:err_suffix]
+  abort '--err-suffix and --sox-pipe are mutually exclusive'
+end
+
+if opts[:jobs].nil?
+  require 'etc'
+  opts[:jobs] = Etc.nprocessors
+end
+
 args = []
 ARGV.each do |arg|
   case arg
   when %r{\A(\w+)=(.*)\z}
     key, val = $1, $2
     # only one that makes sense is infile=another_file
-    overrides[key] = YAML.load(val)
+    overrides[key] = DTAS.yaml_load(val)
   when %r{\A(\w+)\.(\w+)=(.*)\z}
     # comments.ARTIST='blah'
     top, key, val = $1, $2, $3
@@ -51,5 +64,5 @@ trap(:INT) { exit 130 }
 file = args.shift or abort usage
 target = args.shift || default_target
 splitfx = DTAS::SplitFX.new
-splitfx.import(YAML.load(File.read(file)), overrides)
+splitfx.import(DTAS.yaml_load(File.read(file)), overrides)
 splitfx.run(target, opts)
diff --git a/bin/dtas-tl b/bin/dtas-tl
index 1ce18de..c7f4c83 100755
--- a/bin/dtas-tl
+++ b/bin/dtas-tl
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 # encoding: binary
@@ -7,6 +7,8 @@
 # itself is also unstable, but better than this one probably).
 require 'dtas/unix_client'
 require 'shellwords'
+$stdout.binmode
+$stderr.binmode
 
 def get_track_ids(c)
   track_ids = c.req("tl tracks")
@@ -16,14 +18,18 @@ def get_track_ids(c)
   track_ids
 end
 
-def fix_enc!(str, enc)
-  str.force_encoding(enc)
-  str.force_encoding(Encoding::ASCII_8BIT) unless str.valid_encoding?
+def each_track(c)
+  get_track_ids(c).each_slice(128) do |track_ids|
+    res = c.req("tl get #{track_ids.join(' ')}")
+    res = Shellwords.split(res.sub!(/\A\d+ /, ''))
+    while line = res.shift
+      yield line
+    end
+  end
 end
 
 def do_edit(c)
   require 'dtas/edit_client'
-  require 'yaml'
   require 'tempfile'
   extend DTAS::EditClient
   tmp = Tempfile.new(%w(dtas-tl-edit .txt))
@@ -31,19 +37,14 @@ def do_edit(c)
   tmp_path = tmp.path
   orig = []
   orig_idx = {}
-  enc = Encoding.default_external
 
-  get_track_ids(c).each_slice(128) do |track_ids|
-    res = c.req("tl get #{track_ids.join(' ')}")
-    res = Shellwords.split(res.sub!(/\A\d+ /, ''))
-    while line = res.shift
-      line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n"
-      fix_enc!(line, enc)
-      track_id = $1.to_i
-      orig_idx[track_id] = orig.size
-      orig << track_id
-      tmp.write("#{Shellwords.escape(line)} =#{track_id}\n")
-    end
+  each_track(c) do |line|
+    line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n"
+    track_id = $1.to_i
+    orig_idx[track_id] = orig.size
+    orig << track_id
+    line = Shellwords.escape(line) if line.include?("\n")
+    tmp.write("#{line} =#{track_id}\n")
   end
   tmp.flush
 
@@ -51,7 +52,7 @@ def do_edit(c)
   # jump to the line of the currently playing track if using vi or vim
   # Patches for other editors welcome: dtas-all@nongnu.org
   if ed =~ /vim?\z/
-    cur = YAML.load(c.req('current'))
+    cur = DTAS.yaml_load(c.req('current'))
     if tl = cur['tracklist']
       if pos = tl['pos']
         ed += " +#{pos + 1}"
@@ -100,14 +101,8 @@ def do_edit(c)
   non_existent = []
   add.each do |path, after_id|
     orig = path
-    path = Shellwords.split(path)[0]
-    path = File.expand_path(path)
-    unless File.exist?(path)
-      path = orig.dup
-      fix_enc!(path, enc)
-      path = Shellwords.split(path)[0]
-      path = File.expand_path(path)
-    end
+    path = File.expand_path(orig)
+    path = File.expand_path(Shellwords.split(path)[0]) unless File.exist?(path)
 
     if File.exist?(path)
       cmd = %W(tl add #{path})
@@ -140,15 +135,35 @@ end
 
 c = DTAS::UNIXClient.new
 case cmd = ARGV[0]
-when "cat"
-  enc = Encoding.default_external
-  get_track_ids(c).each_slice(128) do |track_ids|
-    res = c.req("tl get #{track_ids.join(' ')}")
-    res = Shellwords.split(res.sub!(/\A\d+ /, ''))
-    while line = res.shift
-      fix_enc!(line, enc)
-      print "#{line}\n"
+when 'cat'
+  each_track(c) { |line| print "#{line}\n" }
+when 'prune'
+  c2 = nil
+  pending = 0
+  each_track(c) do |line|
+    line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n"
+    track_id = $1.to_i
+    ok = false
+    begin
+      st = File.stat(line)
+      ok = st.readable? && st.size?
+    rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES => e
+      warn "# #{line}: #{e.class}"
+      # raise other exceptions
     end
+    unless ok
+      c2 ||= DTAS::UNIXClient.new
+      if pending > 5
+        c2.res_wait
+        pending -= 1
+      end
+      pending += 1
+      c2.req_start("tl remove #{track_id}")
+    end
+  end
+  while pending > 0
+    c2.res_wait
+    pending -= 1
   end
 when 'aac' # add-after-current
   ARGV.shift
@@ -173,11 +188,11 @@ when "reto"
   re = ARGV[1]
   time = ARGV[2]
   re = Regexp.quote(re) if fixed
-  re = ignorecase ? %r{#{re}}i : %r{#{re}}
-  get_track_ids(c).each do |track_id|
-    res = c.req("tl get #{track_id}")
-    res.sub!(/\A1 \d+=/, '')
-    if re =~ res
+  re = ignorecase ? %r{#{re}}in : %r{#{re}}n
+  each_track(c) do |line|
+    line.sub!(/\A(\d+)=/n, '') or abort "unexpected line=#{line.inspect}\n"
+    track_id = $1
+    if re =~ line
       req = %W(tl goto #{track_id})
       req << time if time
       res = c.req(req)
diff --git a/bin/dtas-xdelay b/bin/dtas-xdelay
index 138f521..060752c 100755
--- a/bin/dtas-xdelay
+++ b/bin/dtas-xdelay
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
+# Copyright (C) 2013-2020 all contributors <dtas-all@nongnu.org>
 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
 # frozen_string_literal: true
 USAGE = "Usage: #$0 [-x FREQ] [-l] /dev/fd/LO /dev/fd/HI DELAY [DELAY ...]"