summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMikhail Osipov <mike.osipov@gmail.com>2013-11-01 02:15:42 +0400
committerMikhail Osipov <mike.osipov@gmail.com>2013-11-04 19:43:42 +0400
commit96d929f09aa28f1d5684743375721d0ac0e832e6 (patch)
tree447ac68cb51da2b05f931d27326f8175a1ffe04c
parent7b249334370477fbc2076ef6982344e26865945b (diff)
remove shntool usage
external decoders support new options: --track-total, --track-start, --verbose
-rwxr-xr-xcutter.py13
-rw-r--r--cutter/cue.py5
-rw-r--r--cutter/formats/__base__.py28
-rw-r--r--cutter/formats/__init__.py49
-rw-r--r--cutter/formats/ape.py11
-rw-r--r--cutter/formats/decoder.py160
-rw-r--r--cutter/formats/encoder.py102
-rw-r--r--cutter/formats/flac.py17
-rw-r--r--cutter/formats/handler.py12
-rw-r--r--cutter/formats/mp3.py15
-rw-r--r--cutter/formats/ogg.py15
-rw-r--r--cutter/formats/sox.py23
-rw-r--r--cutter/formats/wav.py12
-rw-r--r--cutter/formats/wavpack.py10
-rw-r--r--cutter/splitter.py202
-rw-r--r--cutter/tools.py7
16 files changed, 499 insertions, 182 deletions
diff --git a/cutter.py b/cutter.py
index 9fa0cff..5ed49ab 100755
--- a/cutter.py
+++ b/cutter.py
@@ -19,13 +19,6 @@ except Exception as err:
printerr("import config failed: %s", err)
sys.exit(0)
-def msf(ts):
- m = ts / (60 * 75)
- s = ts / 75 % 60
- f = ts % 75
-
- return "%d:%02d:%02d" % (m, s, f)
-
def print_cue(cue):
for k, v in cue.attrs():
printf("%s: %s\n", k.upper(), quote(v))
@@ -75,6 +68,9 @@ def parse_args():
parser.add_option("-n", "--dry-run",
action="store_true", default=False, dest="dry_run")
+ parser.add_option("-v", "--verbose",
+ dest="verbose", action="store_true", default=False)
+
enc = OptionGroup(parser, "Encoding options")
enc.add_option("-t", "--type", dest="type",
@@ -146,6 +142,9 @@ def parse_args():
else:
tag.add_option("--" + opt, dest=opt, default="")
+ tag.add_option("--track-total", type="int", dest="tracktotal")
+ tag.add_option("--track-start", type="int", dest="trackstart")
+
parser.add_option_group(tag)
return parser.parse_args()
diff --git a/cutter/cue.py b/cutter/cue.py
index a876eff..bfa6128 100644
--- a/cutter/cue.py
+++ b/cutter/cue.py
@@ -48,7 +48,10 @@ class File:
return self.type in ("WAVE", "FLAC")
def has_audio_tracks(self):
- return len(list(self.tracks())) > 0
+ return self.ntracks() > 0
+
+ def ntracks(self):
+ return len(list(self.tracks()))
def split_points(self, info):
rate = info.sample_rate * info.bits_per_sample * info.channels // 8
diff --git a/cutter/formats/__base__.py b/cutter/formats/__base__.py
deleted file mode 100644
index f17748a..0000000
--- a/cutter/formats/__base__.py
+++ /dev/null
@@ -1,28 +0,0 @@
-class BaseHandler:
- def __init__(self, logger = None):
- self.logger = logger
- self.buf = []
-
- def log(self, fmt, *args):
- if self.logger is not None:
- self.logger(fmt, *args)
-
- def add(self, *args):
- self.buf.extend(args)
-
- def build(self, join=True):
- data = " ".join(self.buf) if join else self.buf
- self.buf = []
-
- return data
-
- def add_sox_args(self, opt, info):
- if opt.sample_rate and opt.sample_rate != info.sample_rate:
- self.add("-r %d" % opt.sample_rate)
- if opt.bits_per_sample and opt.bits_per_sample != info.bits_per_sample:
- self.add("-b %d" % opt.bits_per_sample)
- if opt.channels and opt.channels != info.channels:
- self.add("-c %d" % opt.channels)
-
- def is_tag_supported(self):
- return hasattr(self, "tag")
diff --git a/cutter/formats/__init__.py b/cutter/formats/__init__.py
index 60a35fc..8d003f6 100644
--- a/cutter/formats/__init__.py
+++ b/cutter/formats/__init__.py
@@ -1,23 +1,50 @@
+from . handler import DecoderHandler, EncoderHandler
+
import os
+handlers = ["flac", "ape", "ogg", "mp3", "wav", "wavpack"]
+
path = os.path.dirname(__file__) or "."
-__formats = {}
+__encoders = {}
+__decoders = {}
+
+def __can_decode(obj):
+ return hasattr(obj, "decode")
-for entry in sorted(os.listdir(path)):
- if not entry.endswith(".py") or entry.startswith("_"):
- continue
+def __can_encode(obj):
+ return hasattr(obj, "encode")
- modname = entry.replace(".py", "")
- mod = __import__(modname, globals(), locals(), ["init"], 1)
+for entry in handlers:
+ mod = __import__(entry, globals(), locals(), ["init"], 1)
fmt = mod.init()
- __formats[fmt.name] = fmt
+
+ if __can_encode(fmt):
+ __encoders[fmt.name] = fmt
+ if __can_decode(fmt):
+ __decoders[fmt.ext] = fmt
def supported():
- return sorted(__formats.keys())
+ return sorted(__encoders.keys())
def issupported(name):
- return name in __formats
+ return name in __encoders
+
+def encoder(name):
+ return EncoderHandler(__encoders.get(name)())
+
+def decoder(name):
+ handler_type = __decoders.get(name)
+
+ if not handler_type:
+ return None
+
+ return DecoderHandler(handler_type())
+
+def decoder_open(filename, *args, **kwargs):
+ ext = filename.rpartition(".")[-1]
+ handler = decoder(ext)
+ if handler is None:
+ return None
-def handler(name, logger = None):
- return __formats.get(name)(logger)
+ return handler.open(filename, *args, **kwargs)
diff --git a/cutter/formats/ape.py b/cutter/formats/ape.py
new file mode 100644
index 0000000..8d5b1b7
--- /dev/null
+++ b/cutter/formats/ape.py
@@ -0,0 +1,11 @@
+class ApeHandler:
+ name = "ape"
+ ext = "ape"
+ cmd = "mac"
+
+ def decode(self, filename):
+ args = [self.cmd, filename, "-", "-d"]
+ return args
+
+def init():
+ return ApeHandler
diff --git a/cutter/formats/decoder.py b/cutter/formats/decoder.py
new file mode 100644
index 0000000..99f2cbe
--- /dev/null
+++ b/cutter/formats/decoder.py
@@ -0,0 +1,160 @@
+from . handler import *
+from .. tools import quote
+from .. coding import to_unicode
+
+import subprocess
+import wave
+
+class DecoderError(Exception):
+ pass
+
+class StreamInfo:
+ pass
+
+class Decoder:
+ class Reader:
+ def __init__(self, stream, nframes):
+ self.stream = stream
+ self.nframes = nframes
+ self.bytes_per_frame = stream._channels * stream._bytes_per_sample
+ self.nread = 0
+
+ def info(self):
+ return self.stream.info()
+
+ def wave_params(self):
+ params = list(self.stream.reader.getparams())
+ params[3] = self.nframes
+ return params
+
+ def read(self, maxframes=None):
+ if self.nread >= self.nframes:
+ return []
+
+ avail = self.nframes - self.nread
+ if maxframes and maxframes < avail:
+ avail = maxframes
+
+ data = self.stream.reader.readframes(avail)
+ count = len(data) // self.bytes_per_frame
+ self.nread += count
+
+ return data
+
+ def __init__(self, handler, filename, options=None):
+ self.reader = None
+ self.closed = False
+ self.handler = handler
+
+ args = self.handler.decode(filename)
+ self.command = " ".join(map(quote, args))
+
+ self.proc = subprocess.Popen(
+ args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+
+ try:
+ self.reader = wave.open(self.proc.stdout, "r")
+ except Exception as exc:
+ self.close()
+ if self.status:
+ self.status_msg = to_unicode(self.proc.stderr.read())
+ else:
+ self.status_msg = "wave.open: %s" % exc
+ return
+
+ self._channels = self.reader.getnchannels()
+ self._bytes_per_sample = self.reader.getsampwidth()
+ self._sample_rate = self.reader.getframerate()
+
+ def ready(self):
+ return self.reader is not None
+
+ def channels(self):
+ return self._channels
+
+ def bits_per_sample(self):
+ return self._bytes_per_sample * 8
+
+ def sample_rate(self):
+ return self._sample_rate
+
+ def info(self):
+ info = StreamInfo()
+
+ info.channels = self._channels
+ info.bits_per_sample = self.bits_per_sample()
+ info.sample_rate = self._sample_rate
+ info.type = self.handler.name
+
+ return info
+
+ def seek(self, pos):
+ nframes = pos * self._sample_rate // 75 - self.reader.tell()
+
+ if nframes:
+ r = self.Reader(self, nframes)
+ while len(r.read()):
+ pass
+
+ return nframes
+
+ def get_reader(self, npsecs):
+ if npsecs is None:
+ nframes = self.reader.getnframes() - self.reader.tell()
+ else:
+ nframes = npsecs * self._sample_rate // 75
+
+ return self.Reader(self, nframes)
+
+ def get_command(self):
+ return self.command
+
+ def get_status(self):
+ return self.status, self.status_msg
+
+ def close(self):
+ if self.closed:
+ return
+
+ if self.reader:
+ self.reader.close()
+
+ if self.proc:
+ self.proc.stdout.close()
+ self.status = self.proc.wait()
+ else:
+ self.status = 0
+
+ self.status_msg = ""
+ self.closed = True
+
+ def __del__(self):
+ self.close()
+
+class DummyDecoder(Decoder):
+ class DummyReader(Decoder.Reader):
+ def __init__(self, *args):
+ Decoder.Reader.__init__(self, *args)
+
+ def info(self):
+ return self.stream.info()
+
+ def read(self, *args):
+ return []
+
+ def __init__(self, *args, **kwargs):
+ Decoder.__init__(self, *args, **kwargs)
+
+ def seek(self, *args):
+ pass
+
+ def get_reader(self, *args):
+ return self.DummyReader(self, *args)
+
+class DecoderHandler(Handler):
+ def open(self, filename, options=None):
+ if options and options.dry_run:
+ return DummyDecoder(self.handler, filename, options)
+ else:
+ return Decoder(self.handler, filename, options)
diff --git a/cutter/formats/encoder.py b/cutter/formats/encoder.py
new file mode 100644
index 0000000..23d0c51
--- /dev/null
+++ b/cutter/formats/encoder.py
@@ -0,0 +1,102 @@
+from . handler import *
+from .. tools import quote
+
+import subprocess
+import wave
+
+class EncoderError(Exception):
+ pass
+
+class Encoder:
+ FRAME_BUFFER_SIZE=0x4000
+
+ class SafeStream:
+ MIN_CHUNK_SIZE=64
+
+ def __init__(self, fileobj):
+ self.fileobj = fileobj
+ self.seek_called = False
+ self.written = 0
+
+ self.buffered = b""
+
+ def seek(self, *args):
+ self.seek_called = True
+
+ def write(self, data):
+ if self.seek_called:
+ return
+
+ nbytes = len(data)
+ if nbytes < self.MIN_CHUNK_SIZE:
+ self.buffered += data
+ else:
+ if len(self.buffered):
+ data = self.buffered + data
+ self.buffered = b""
+
+ self.fileobj.write(data)
+
+ self.written += nbytes
+
+ def tell(self):
+ return self.written
+
+ def close(self):
+ if len(self.buffered):
+ self.fileobj.write(self.buffered)
+
+ self.fileobj.close()
+
+ def __getattr__(self, attr):
+ return getattr(self.fileobj, attr)
+
+ def __init__(self, handler, reader, filename, options):
+ self.closed = False
+ self.handler = handler
+
+ args = self.handler.encode(filename, options, reader.info())
+ self.command = " ".join(map(quote, args))
+
+ if options.dry_run:
+ self.proc = None
+ return
+
+ self.proc = subprocess.Popen(args, stdin=subprocess.PIPE)
+
+ self.reader = reader
+ self.stream = self.SafeStream(self.proc.stdin)
+ self.writer = wave.open(self.stream, "w")
+ self.writer.setparams(reader.wave_params())
+
+ def process(self):
+ if self.proc is None:
+ return
+
+ while True:
+ data = self.reader.read(self.FRAME_BUFFER_SIZE)
+ if not len(data):
+ break
+
+ self.writer.writeframesraw(data)
+
+ def get_command(self):
+ return self.command
+
+ def close(self):
+ if self.closed or self.proc is None:
+ return
+
+ self.writer.close()
+ self.stream.close()
+ self.proc.wait()
+
+ def __del__(self):
+ self.close()
+
+class EncoderHandler(Handler):
+ def open(self, reader, filename, options):
+ return Encoder(self.handler, reader, filename, options)
+
+ def is_tag_supported(self):
+ return hasattr(self.handler, "tag")
diff --git a/cutter/formats/flac.py b/cutter/formats/flac.py
index 2723344..6f47d94 100644
--- a/cutter/formats/flac.py
+++ b/cutter/formats/flac.py
@@ -1,22 +1,21 @@
-from . __base__ import *
+from . sox import *
from .. coding import to_bytes
import subprocess
-class FlacHandler(BaseHandler):
+class FlacHandler(SoxHandler):
name = "flac"
ext = "flac"
+ cmd = "flac"
- def encode(self, opt, info):
- self.add("flac sox -")
-
+ def encode(self, path, opt, info):
if opt.compression is not None:
- self.add("-C %d" % opt.compression)
+ self.set_compression(opt.compression)
- self.add_sox_args(opt, info)
- self.add("%f")
+ return self.sox_args(path, opt, info)
- return self.build()
+ def decode(self, filename):
+ return [self.cmd, "-d", "-c", "-s", filename]
def tag(self, path, tags):
args = ["metaflac", "--remove-all-tags", "--import-tags-from=-", path]
diff --git a/cutter/formats/handler.py b/cutter/formats/handler.py
new file mode 100644
index 0000000..9a322c1
--- /dev/null
+++ b/cutter/formats/handler.py
@@ -0,0 +1,12 @@
+def dev_null():
+ return open("/dev/null")
+
+class Handler:
+ def __init__(self, handler):
+ self.handler = handler
+
+ def __getattr__(self, attr):
+ return getattr(self.handler, attr)
+
+from . encoder import EncoderHandler
+from . decoder import DecoderHandler
diff --git a/cutter/formats/mp3.py b/cutter/formats/mp3.py
index ecccbb4..ba78b85 100644
--- a/cutter/formats/mp3.py
+++ b/cutter/formats/mp3.py
@@ -1,4 +1,4 @@
-from . __base__ import *
+from . sox import *
from .. coding import to_bytes
import subprocess
@@ -84,20 +84,15 @@ class ID3Tagger:
fp.close()
-class Mp3Handler(BaseHandler):
+class Mp3Handler(SoxHandler):
name = "mp3"
ext = "mp3"
- def encode(self, opt, info):
- self.add("cust ext=%s sox -" % self.ext)
-
+ def encode(self, path, opt, info):
if opt.bitrate is not None:
- self.add("-C %d" % opt.bitrate)
-
- self.add_sox_args(opt, info)
- self.add("%f")
+ self.set_compression(opt.bitrate)
- return self.build()
+ return self.sox_args(path, opt, info)
def tag(self, path, tags):
tagger = ID3Tagger()
diff --git a/cutter/formats/ogg.py b/cutter/formats/ogg.py
index c881058..dc6f9f3 100644
--- a/cutter/formats/ogg.py
+++ b/cutter/formats/ogg.py
@@ -1,22 +1,17 @@
-from . __base__ import *
+from . sox import *
from .. coding import to_bytes
import subprocess
-class OggHandler(BaseHandler):
+class OggHandler(SoxHandler):
name = "ogg"
ext = "ogg"
- def encode(self, opt, info):
- self.add("cust ext=%s sox -" % self.ext)
-
+ def encode(self, path, opt, info):
if opt.compression is not None:
- self.add("-C %d" % opt.compression)
-
- self.add_sox_args(opt, info)
- self.add("%f")
+ self.set_compression(opt.compression)
- return self.build()
+ return self.sox_args(path, opt, info)
def tag(self, path, tags):
args = ["vorbiscomment", "--raw", "--write", path]
diff --git a/cutter/formats/sox.py b/cutter/formats/sox.py
new file mode 100644
index 0000000..2a28a24
--- /dev/null
+++ b/cutter/formats/sox.py
@@ -0,0 +1,23 @@
+class SoxHandler:
+ def __init__(self):
+ self.compression = None
+
+ def set_compression(self, value):
+ self.compression = value
+
+ def sox_args(self, path, opt, info):
+ args = ["sox", "-V2", "-"]
+
+ if self.compression is not None:
+ args.extend(["-C", str(self.compression)])
+
+ if opt.sample_rate and opt.sample_rate != info.sample_rate:
+ args.extend(["-r", str(opt.sample_rate)])
+ if opt.bits_per_sample and opt.bits_per_sample != info.bits_per_sample:
+ args.extend(["-b", str(opt.bits_per_sample)])
+ if opt.channels and opt.channels != info.channels:
+ args.extend(["-c", str(opt.channels)])
+
+ args.append(path)
+
+ return args
diff --git a/cutter/formats/wav.py b/cutter/formats/wav.py
index 131ea73..41e18d9 100644
--- a/cutter/formats/wav.py
+++ b/cutter/formats/wav.py
@@ -1,15 +1,11 @@
-from . __base__ import *
+from . sox import *
-class WavHandler(BaseHandler):
+class WavHandler(SoxHandler):
name = "wav"
ext = "wav"
- def encode(self, opt, info):
- self.add("wav sox -")
- self.add_sox_args(opt, info)
- self.add("%f")
-
- return self.build()
+ def encode(self, path, opt, info):
+ return self.sox_args(path, opt, info)
def init():
return WavHandler
diff --git a/cutter/formats/wavpack.py b/cutter/formats/wavpack.py
new file mode 100644
index 0000000..d575481
--- /dev/null
+++ b/cutter/formats/wavpack.py
@@ -0,0 +1,10 @@
+class WavpackHandler:
+ name = "wavpack"
+ ext = "wv"
+ cmd = "wvunpack"
+
+ def decode(self, filename):
+ return [self.cmd, "-q", filename, "-o", "-"]
+
+def init():
+ return WavpackHandler
diff --git a/cutter/splitter.py b/cutter/splitter.py
index 45cd0be..24e8eac 100644
--- a/cutter/splitter.py
+++ b/cutter/splitter.py
@@ -65,37 +65,22 @@ class TempLink:
self.remove()
class StreamInfo:
- __mapping = {
- b"Channels:": "channels",
- b"Bits/sample:": "bits_per_sample",
- b"Samples/sec:": "sample_rate"
- }
-
@staticmethod
def get(name):
- info = StreamInfo()
- proc = subprocess.Popen(["shninfo", name], stdout = subprocess.PIPE)
- for line in proc.stdout.readlines():
- data = line.split()
- attr = StreamInfo.__mapping.get(data[0])
- if attr:
- setattr(info, attr, int(data[1]))
- elif line.startswith(b"Handled by:"):
- info.type = to_unicode(data[2])
+ stream = formats.decoder_open(name)
- if proc.wait():
+ if not stream or not stream.ready():
return None
- return info
+ return stream.info()
class Splitter:
EXT = ["ape", "flac", "wv"]
class File:
- def __init__(self, fileobj, name, info):
+ def __init__(self, fileobj, path):
self.fileobj = fileobj
- self.name = name
- self.info = info
+ self.path = path
def __getattr__(self, attr):
return getattr(self.fileobj, attr)
@@ -125,6 +110,8 @@ class Splitter:
sys.exit(1)
def init_tags(self):
+ self.tracktotal = self.opt.tracktotal or len(list(self.all_tracks()))
+
self.tags = {
"album": self.opt.album or self.cue.get("title"),
"date": self.opt.date or self.cue.get("date"),
@@ -145,21 +132,20 @@ class Splitter:
self.dest = os.path.join(self.opt.dir, tmp)
track_fmt = os.path.basename(self.opt.fmt)
- tracknumber = 0
+ tracknumber = self.opt.trackstart or 1
self.track_info = {}
for track in self.all_tracks():
- tracknumber += 1
self.track_info[track] = self.get_track_info(
track, tracknumber, track_fmt
)
+ tracknumber += 1
def __init__(self, cue, opt):
self.cue = cue
self.opt = opt
- self.tracktotal = len(list(self.all_tracks()))
- self.enctype = formats.handler(opt.type, logger=printf)
- self.tag_supported = self.enctype.is_tag_supported()
+ self.encoder = formats.encoder(opt.type)
+ self.tag_supported = self.encoder.is_tag_supported()
self.init_tags()
@@ -181,7 +167,7 @@ class Splitter:
if self.opt.convert_chars:
name = convert_characters(name)
- return self.TrackInfo(name + "." + self.enctype.ext, tags)
+ return self.TrackInfo(name + "." + self.encoder.ext, tags)
def find_realfile(self, name):
if not name.endswith(".wav"):
@@ -203,25 +189,20 @@ class Splitter:
debug("skip file %s: no tracks", quote(file.name))
continue
- name = self.cue.dir + file.name
- if not os.path.exists(name):
+ path = self.cue.dir + file.name
+ if not os.path.exists(path):
real = self.find_realfile(file.name)
if not real:
printerr("no such file %s", quote(file.name))
sys.exit(1)
- name = self.cue.dir + real
-
- info = StreamInfo.get(name)
- if info is None:
- printerr("%s: unknown type", quote(file.name))
- sys.exit(1)
+ path = self.cue.dir + real
- lst.append(self.File(file, name, info))
+ lst.append(self.File(file, path))
return lst
def shntool_args(self, tool, info):
- encode = self.enctype.encode(self.opt, info)
+ encode = self.encoder.encode(self.opt, info)
return [tool, "-w", "-d", self.dest, "-o", encode]
def track_name(self, track):
@@ -234,37 +215,41 @@ class Splitter:
if not self.tag_supported:
return
- printf("Tag [%s] : ", path)
- if not self.enctype.tag(path, self.track_tags(track)):
+ printf("tag %s: ", quote(self.track_name(track)))
+ if not self.encoder.tag(path, self.track_tags(track)):
printf("FAILED\n")
sys.exit(1)
printf("OK\n")
- def copy_file(self, file):
+ def is_need_convert(self, info):
noteq = lambda a, b: a and a != b
- if file.info.type != self.enctype.name:
- return False
- if noteq(self.opt.sample_rate, file.info.sample_rate):
- return False
- if noteq(self.opt.bits_per_sample, file.info.bits_per_sample):
- return False
- if noteq(self.opt.channels, file.info.channels):
- return False
+ if info.type != self.encoder.name:
+ return True
+ if noteq(self.opt.sample_rate, info.sample_rate):
+ return True
+ if noteq(self.opt.bits_per_sample, info.bits_per_sample):
+ return True
+ if noteq(self.opt.channels, info.channels):
+ return True
+
+ return False
+ def copy_file(self, file):
track = list(file.tracks())[0]
trackname = self.track_name(track)
path = os.path.join(self.dest, trackname)
+ printf("copy %s -> %s", quote(file.path), quote(path))
if self.opt.dry_run:
- printf("Copy [%s] --> [%s]\n", file.name, path)
- return True
+ printf("\n")
+ return
- printf("Copy [%s] --> [%s] : ", file.name, path)
+ printf(": ")
try:
- shutil.copyfile(file.name, path)
+ shutil.copyfile(file.path, path)
except Exception as err:
printf("FAILED: %s\n", err)
sys.exit(1)
@@ -273,65 +258,91 @@ class Splitter:
self.tag(track, path)
- return True
+ def open_decode(self, path):
+ stream = formats.decoder_open(path, self.opt)
- def convert_file(self, file):
- track = list(file.tracks())[0]
- trackname = self.track_name(track)
+ if stream is None:
+ printerr("%s: unsupported type", quote(path))
+ elif not stream.ready():
+ printerr("decode failed, command: %s", stream.get_command())
- args = self.shntool_args("shnconv", file.info)
+ status, msg = stream.get_status()
+ if not len(msg):
+ printerr("exit code %d", status)
+ else:
+ printerr("exit code %d, stderr:\n%s", status, msg)
- if self.opt.dry_run:
- name = "link to " + quote(file.name, "'")
- debug("run %s", " ".join(map(quote, args + [name])))
- return
+ stream = None
- try:
- link = TempLink(os.path.abspath(file.name), trackname)
- except OSError as err:
- printerr("create symlink %s failed: %s", quote(trackname), err)
- sys.exit(1)
+ return stream
- ret = subprocess.call(args + [str(link)])
- link.remove()
+ def print_decode_info(self, path, stream):
+ info = stream.info()
+ debug("decode: %s", stream.get_command())
+ debug("input: %s [%s] (%d/%d, %d ch)", quote(path),
+ info.type, info.bits_per_sample, info.sample_rate,
+ info.channels)
- if ret:
- printerr("shnconv failed: exit code %d", ret);
- sys.exit(1)
+ def print_encode_info(self, stream):
+ debug("encode: %s", stream.get_command())
- self.tag(track, os.path.join(self.dest, trackname))
+ @staticmethod
+ def track_timerange(track):
+ ts = "%8s -" % msf(track.begin)
- def split_file(self, file, points):
- args = self.shntool_args("shnsplit", file.info) + [file.name]
+ if track.end is not None:
+ ts += " %8s" % msf(track.end)
- if self.opt.dry_run:
- debug("run %s", " ".join(map(quote, args)))
+ return ts
+
+ @staticmethod
+ def track_length(track):
+ if track.end is None:
+ return None
+
+ return track.end - track.begin
+
+ def split_file(self, file):
+ stream = self.open_decode(file.path)
+ if not stream:
return
- proc = subprocess.Popen(args, stdin = subprocess.PIPE)
- proc.stdin.write(to_bytes("\n".join(map(str, points))))
- proc.stdin.close()
+ if self.opt.verbose and self.opt.dry_run:
+ self.print_decode_info(file.path, stream)
- if proc.wait():
- printerr("shnsplit failed: exit code %d", proc.returncode)
- sys.exit(1)
+ if file.ntracks() == 1:
+ if not self.is_need_convert(stream.info()):
+ stream.close()
+ self.copy_file(file)
+ return
- splitted = filterdir(self.dest, "split-track")
- for track, filename in zip(file.tracks(), splitted):
+ for track in file.tracks():
trackname = self.track_name(track)
path = os.path.join(self.dest, trackname)
- printf("Rename [%s] --> [%s] : ", filename, trackname)
- try:
- os.rename(os.path.join(self.dest, filename), path)
- except OSError as err:
- printf("FAILED: %s\n", err)
- sys.exit(1)
- else:
- printf("OK\n")
+ ts = self.track_timerange(track)
+ printf("split %s (%s) -> %s", quote(file.path), ts, quote(trackname))
+ printf("\n" if self.opt.dry_run else ": ")
+
+ stream.seek(track.begin)
+ reader = stream.get_reader(self.track_length(track))
+
+ out = self.encoder.open(reader, path, self.opt)
+
+ if self.opt.dry_run:
+ if self.opt.verbose:
+ self.print_encode_info(out)
+ continue
+
+ out.process()
+ out.close()
+
+ printf("OK\n")
self.tag(track, path)
+ stream.close()
+
def check_duplicates(self):
names = [x.name for x in self.track_info.values()]
dup = [k for k, v in collections.Counter(names).items() if v > 1]
@@ -346,7 +357,7 @@ class Splitter:
debug("skip non file %s", quote(file))
continue
- printf("Copy [%s] into [%s] : ", file, dest)
+ printf("copy %s -> %s: ", quote(file), quote(dest))
try:
shutil.copy(path, dest)
@@ -370,12 +381,7 @@ class Splitter:
self.dest = to_unicode(tempdir)
for file in files:
- points = list(file.split_points(file.info))
- if not points:
- if not self.copy_file(file):
- self.convert_file(file)
- else:
- self.split_file(file, points)
+ self.split_file(file)
if self.realpath:
self.transfer_files(self.dest, self.realpath)
diff --git a/cutter/tools.py b/cutter/tools.py
index 69d8bdb..8afbaa4 100644
--- a/cutter/tools.py
+++ b/cutter/tools.py
@@ -24,3 +24,10 @@ def debug(fmt, *args):
if msg[-1] != "\n":
msg += "\n"
sys.stderr.write("-- " + msg)
+
+def msf(ts):
+ m = ts / (60 * 75)
+ s = ts / 75 % 60
+ f = ts % 75
+
+ return "%d:%02d.%02d" % (m, s, f)