techrec/server/forge.py

154 lines
5.1 KiB
Python
Raw Normal View History

2013-11-26 20:46:23 +01:00
from datetime import datetime, timedelta
from time import sleep
2013-12-23 20:37:54 +01:00
import os
2013-12-03 02:24:08 +01:00
from subprocess import Popen
import logging
2013-11-26 20:46:23 +01:00
2019-11-15 22:00:31 +01:00
from .config_manager import get_config
2013-12-04 16:48:17 +01:00
2013-11-26 20:46:23 +01:00
def get_timefile_exact(time):
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
time is of type `datetime`; it is not "rounded" to match the real file;
that work is done in get_timefile(time)
2019-11-15 22:35:45 +01:00
"""
2013-12-04 16:48:17 +01:00
return os.path.join(
2019-11-15 22:35:45 +01:00
get_config()["AUDIO_INPUT"], time.strftime(get_config()["AUDIO_INPUT_FORMAT"])
)
2013-11-26 20:46:23 +01:00
def round_timefile(exact):
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
This will round the datetime, so to match the file organization structure
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
return datetime(exact.year, exact.month, exact.day, exact.hour)
def get_timefile(exact):
return get_timefile_exact(round_timefile(exact))
def get_files_and_intervals(start, end, rounder=round_timefile):
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
both arguments are datetime objects
returns an iterator whose elements are (filename, start_cut, end_cut)
Cuts are expressed in seconds
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
if end <= start:
raise ValueError("end < start!")
while start <= end:
begin = rounder(start)
start_cut = (start - begin).total_seconds()
if end < begin + timedelta(seconds=3599):
end_cut = (begin + timedelta(seconds=3599) - end).total_seconds()
else:
end_cut = 0
yield (begin, start_cut, end_cut)
start = begin + timedelta(hours=1)
2014-05-03 03:27:06 +02:00
def mp3_join(named_intervals):
2019-11-15 22:35:45 +01:00
"""
2013-11-26 20:46:23 +01:00
Note that these are NOT the intervals returned by get_files_and_intervals,
as they do not supply a filename, but only a datetime.
What we want in input is basically the same thing, but with get_timefile()
applied on the first element
2013-11-29 23:42:27 +01:00
This function make the (quite usual) assumption that the only start_cut (if
any) is at the first file, and the last one is at the last file
2019-11-15 22:35:45 +01:00
"""
ffmpeg = get_config()["FFMPEG_PATH"]
2013-11-29 23:42:27 +01:00
startskip = None
endskip = None
files = []
2013-11-26 20:46:23 +01:00
for (filename, start_cut, end_cut) in named_intervals:
2013-11-29 23:42:27 +01:00
# this happens only one time, and only at the first iteration
if start_cut:
assert startskip is None
startskip = start_cut
# this happens only one time, and only at the first iteration
if end_cut:
assert endskip is None
endskip = end_cut
2019-11-15 22:35:45 +01:00
assert "|" not in filename
2013-11-29 23:42:27 +01:00
files.append(filename)
2019-11-15 22:35:45 +01:00
cmdline = [ffmpeg, "-i", "concat:%s" % "|".join(files)]
cmdline += get_config()["FFMPEG_OUT_CODEC"]
2013-11-29 23:42:27 +01:00
if startskip is not None:
2019-11-15 22:35:45 +01:00
cmdline += ["-ss", str(startskip)]
else:
startskip = 0
2013-11-29 23:42:27 +01:00
if endskip is not None:
2019-11-15 22:35:45 +01:00
cmdline += ["-t", str(len(files) * 3600 - (startskip + endskip))]
2013-11-29 23:42:27 +01:00
return cmdline
2013-12-03 02:24:08 +01:00
def create_mp3(start, end, outfile, options={}, **kwargs):
2019-11-15 22:35:45 +01:00
intervals = [
(get_timefile(begin), start_cut, end_cut)
for begin, start_cut, end_cut in get_files_and_intervals(start, end)
]
if os.path.exists(outfile):
raise OSError("file '%s' already exists" % outfile)
2014-03-02 01:17:12 +01:00
for path, _s, _e in intervals:
if not os.path.exists(path):
2019-11-15 22:35:45 +01:00
raise OSError("file '%s' does not exist; recording system broken?" % path)
2014-05-03 03:27:06 +02:00
# metadata date/time formatted according to
# https://wiki.xiph.org/VorbisComment#Date_and_time
metadata = {}
2019-11-15 22:35:45 +01:00
if outfile.endswith(".mp3"):
metadata["TRDC"] = start.replace(microsecond=0).isoformat()
metadata["RECORDINGTIME"] = metadata["TRDC"]
metadata["ENCODINGTIME"] = datetime.now().replace(microsecond=0).isoformat()
2014-05-03 03:27:06 +02:00
else:
2019-11-15 22:35:45 +01:00
metadata["DATE"] = start.replace(microsecond=0).isoformat()
metadata["ENCODER"] = "https://github.com/boyska/techrec"
if "title" in options:
metadata["TITLE"] = options["title"]
if options.get("license_uri", None) is not None:
metadata["RIGHTS-DATE"] = start.strftime("%Y-%m")
metadata["RIGHTS-URI"] = options["license_uri"]
if "extra_tags" in options:
metadata.update(options["extra_tags"])
2014-05-03 03:27:06 +02:00
metadata_list = []
for tag, value in metadata.items():
2019-11-15 22:35:45 +01:00
if "=" in tag:
2014-05-03 03:27:06 +02:00
logging.error('Received a tag with "=" inside, skipping')
continue
2019-11-15 22:35:45 +01:00
metadata_list.append("-metadata")
metadata_list.append("%s=%s" % (tag, value))
2014-05-03 03:27:06 +02:00
2019-11-15 22:35:45 +01:00
p = Popen(
mp3_join(intervals) + metadata_list + get_config()["FFMPEG_OPTIONS"] + [outfile]
)
if get_config()["FORGE_TIMEOUT"] == 0:
p.wait()
else:
start = datetime.now()
2019-11-15 22:35:45 +01:00
while (datetime.now() - start).total_seconds() < get_config()["FORGE_TIMEOUT"]:
p.poll()
if p.returncode is None:
sleep(1)
else:
break
if p.returncode is None:
2013-12-23 20:37:54 +01:00
os.kill(p.pid, 15)
try:
os.remove(outfile)
except:
pass
2019-11-15 22:35:45 +01:00
raise Exception("timeout") # TODO: make a specific TimeoutError
2013-12-03 02:24:08 +01:00
if p.returncode != 0:
raise OSError("return code was %d" % p.returncode)
return True
def main_cmd(options):
2019-11-15 22:35:45 +01:00
log = logging.getLogger("forge_main")
outfile = os.path.abspath(os.path.join(options.cwd, options.outfile))
2019-11-15 22:35:45 +01:00
log.debug("will forge an mp3 into %s" % (outfile))
create_mp3(options.starttime, options.endtime, outfile)