def deleteRecording(uuid): try: data = {"uuid": uuid} sendToTVH("dvr/entry/remove", data) except Exception as e: fname = sys._getframe().f_code.co_name errorNotify(fname, e)
def getStreamType(finfo, stype="video"): try: for stream in finfo["streams"]: if "codec_type" in stream and stream["codec_type"] == stype: return stream return None except Exception as e: errorNotify("getStreamType", e)
def makeCmd(tracks, fqfn, ofn): try: cmdstub, mapcmd, ascmd = makeStub(tracks, fqfn) convcmd = ["-c:v", "libx265", "-crf", "28"] cmd = cmdstub + mapcmd + convcmd + ascmd + [ofn] msg = "SD Command:" for thing in cmd: msg += " " + thing return (cmd, msg) except Exception as e: errorNotify("makeCmd", e)
def makeStub(tracks, fqfn): try: cmdstub = ["nice", "-n", "19", "ffmpeg", "-i", fqfn] mapcmd = ["-map", f"0:{tracks[0]}", "-map", f"0:{tracks[1]}"] ascmd = ["-acodec", "copy"] withsubs = True if tracks[2] > 0 else False if withsubs: mapcmd += ["-map", f"0:{tracks[2]}"] ascmd += ["-scodec", "copy"] return (cmdstub, mapcmd, ascmd) except Exception as e: errorNotify("makeStub", e)
def fileDuration(finfo): try: dur = 0 sdur = "" stream = getStreamType(finfo, "video") if stream is not None and "duration" in stream: xtmp = stream["duration"].split(".") dur = int(xtmp[0]) sdur = UT.hms(dur) return (dur, sdur) except Exception as e: errorNotify("fileDuration", e)
def hasSubtitles(finfo): try: ret = False try: stream = getStreamType(finfo, stype="subtitle") if stream is not None: ret = True except Exception as e: errorNotify("hasSubtitles", e) return ret except Exception as e: errorNotify("hasSubtitles", e)
def channelPrograms(channel="BBC Four HD"): """ return a time sorted dict of programs for the named channel each event looks like: { 'eventId': 5050806, 'episodeId': 5050807, 'serieslinkId': 91157, 'serieslinkUri': 'ddprogid:///usr/bin/tv_grab_zz_sdjson/SH000191120000', 'channelName': 'BBC Four HD', 'channelUuid': '2f6501b00ef0982c8fc3aa67b0229ecc', 'channelNumber': '106', 'channelIcon': 'https://s3.amazonaws.com/schedulesdirect/assets/stationLogos/s83282_h3_aa.png', 'start': 1567936800, 'stop': 1567965480, 'title': 'SIGN OFF', 'description': 'Sign off.', 'summary': 'Sign off.', # optional 'nextEventId': 5050808, } """ try: # chans = channels() # now = int(time.time()) # xfilter = [ { "field": "name", "type": "string", "value": channel, "comparison": "eq", } ] # xfilter = [ # {"field": "stop", "type": "numeric", "value": str(now), "comparison": "gt"}, # { # "field": "start", # "type": "numeric", # "value": str(now + (3600 * 24)), # "comparison": "lt", # }, # ] # if chans is not None: # for chan in chans: # if chan["name"] == channel: # data = {"filter": xfilter} data = {"limit": "999"} j = sendToTVH("epg/events/grid", data) print(str(j["totalCount"]) + " programs") mindur, minprog = UT.displayProgramList(j["entries"], 24, channel) print("min duration: {}".format(mindur)) print("{}".format(minprog)) # break # else: # print("chans is none") except Exception as e: fname = sys._getframe().f_code.co_name errorNotify(fname, e)
def checkRemoveOutputFile(ofn): try: if FUT.fileExists(ofn): size = FUT.fileSize(ofn) if size > 0: msg = f"Destination file '{ofn}' exists: {FUT.sizeof_fmt(size)}, not converting" log.info(msg) raise ConvertFailure(msg) else: msg = "Deleting existing zero length destination file '{ofn}'" log.info(msg) os.remove(ofn) except Exception as e: errorNotify("checkRemoveOutputFile", e)
def canConvert(finfo): try: ret = False try: stream = getStreamType(finfo, "video") if stream is not None: if stream["codec_name"] == "mpeg2video": ret = 1 elif stream["codec_name"] == "h264": ret = 2 except Exception as e: errorNotify("canConvert", e) return ret except Exception as e: errorNotify("canConvert", e)
def makeHDCmd(tracks, fqfn, ofn): try: cmdstub, mapcmd, ascmd = makeStub(tracks, fqfn) # convcmd = ["-c:v", "libx265", "-preset", "ultrafast", "-x265-params"] # convcmd.append( # "crf=22:qcomp=0.8:aq-mode=1:aq_strength=1.0:qg-size=16:psy-rd=0.7:psy-rdoq=5.0:rdoq-level=1:merange=44" # ) convcmd = ["-vcodec", "copy"] cmd = cmdstub + mapcmd + convcmd + ascmd + [ofn] msg = "HD Command:" for thing in cmd: msg += " " + thing return (cmd, msg) except Exception as e: errorNotify("makeHDCmd", e)
def channels(): """ return a sorted list of enabled channels """ try: sents = None data = {"limit": 200} j = sendToTVH("channel/grid", data) if "entries" in j: sents = sorted(j["entries"], key=itemgetter("number"), reverse=False) except Exception as e: fname = sys._getframe().f_code.co_name errorNotify(fname, e) finally: return sents
def trackIndexes(finfo): try: vtrack = atrack = strack = -1 try: for stream in finfo["streams"]: if "codec_type" in stream: if stream["codec_type"] == "video": vtrack = stream["index"] elif stream["codec_type"] == "audio": if int(stream["channels"]) > 1: atrack = stream["index"] elif stream["codec_type"] == "subtitle": strack = stream["index"] except Exception as e: errorNotify("trackIndexes", e) return (vtrack, atrack, strack) except Exception as e: errorNotify("trackIndexes", e)
def processProc(proc, regex, duration, outq): """ looking for the output lines from ffmpeg that look like frame= 71 fps=0.0 q=-0.0 size= 3kB time=00:00:03.43 bitrate= 7.7kbits/s speed=6.86x frame=34458 fps= 35 q=-0.0 size= 1031676kB time=00:22:59.64 bitrate=6125.9kbits/s speed= 1.4x using the python regex extensions to name the groups (see https://docs.python.org/3/howto/regex.html#non-capturing-and-named-groups) The regular expression is now in the convert function so that it is only compiled once. """ global olines try: pc = 0 sthen = stleft = ssize = "" for line in iter(proc.stdout.readline, ""): # print(line) # canoutput = False m = regex.match(line) if m is not None: xdict = m.groupdict() if "time" in xdict: now = int(time.time()) # print("getting tsecs") tsecs = UT.secondsFromHMS(xdict["time"]) # print("getting pc") pc = int((tsecs * 100) / duration) if "speed" in xdict: tleft = int((duration - tsecs) / float(xdict["speed"])) stleft = UT.hms(tleft) then = now + tleft dtts = datetime.datetime.fromtimestamp(then) sthen = dtts.strftime("%H:%M:%S") if "size" in xdict: tsz = xdict["size"].split("k") ssize = FUT.sizeof_fmt(int(int(tsz[0]) * 1000)) else: olines.append(line) outq.put(f"Complete: {pc}% {ssize} ETA: {sthen} ({stleft})") except Exception as e: errorNotify("processProc", e)
def convert(fqfn): rc = 1 try: finfo = fileInfo(fqfn) rstr = r"frame=\s*(?P<frame>[0-9]+)\s+" rstr += r"fps=\s*(?P<fps>[0-9.]+)\s+.*" rstr += r"size=\s*(?P<size>[0-9kmgB]+)\s+" rstr += r"time=(?P<time>[0-9:.]+)\s+" rstr += r"bitrate=\s*(?P<bitrate>[0-9.]+[km]bits/s)\s+" rstr += r"speed=\s*(?P<speed>[0-9.]+)x" regex = re.compile(rstr) if finfo is not None and canConvert(finfo): cconv = canConvert(finfo) if cconv == 1: tracks = trackIndexes(finfo) withsubs = True if tracks[2] > 0 else False fn, fext = os.path.splitext(fqfn) ofn = fn + ".mkv" checkRemoveOutputFile(ofn) if cconv == 1: cmd, msg = makeCmd(tracks, fqfn, ofn) sizecheck = True else: cmd, msg = makeHDCmd(tracks, fqfn, ofn) sizecheck = False log.info(msg) xmsg = ", with subtitles," if withsubs else "" msg = f"Converting{xmsg} '{fqfn}' to '{ofn}'" log.info(msg) dur, sdur = fileDuration(finfo) log.info(f"file duration: {sdur}") rc = runThreadConvert(cmd, fqfn, ofn, dur, regex) tidy(rc, fqfn, ofn, sizecheck=sizecheck) elif cconv == 2: msg = f"Not converting HD stream {fqfn}" log.warning(msg) else: msg = f"Cannot convert {fqfn}" log.error(msg) raise ConvertFailure(msg) except Exception as e: errorNotify("convert", e) return rc
def runThreadConvert(cmd, fqfn, ofn, duration, regex): global gpid try: print("") proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) gpid = proc.pid outq = queue.Queue() t = threading.Thread(target=processProc, args=(proc, regex, duration, outq)) # wait a bit before processing output time.sleep(10) # start the thread to read and process output from ffmpeg t.start() try: print("") while True: try: line = outq.get(block=False) # log.info(line) print(f"\r{line:>56}", end="") except queue.Empty: time.sleep(10) if proc.poll() is not None: print("") break finally: rc = proc.returncode # wait for ffmpeg to finish producing output t.join() return rc except Exception as e: errorNotify("runThreadConvert", e)
def fileInfo(fqfn): try: finfo = None try: if FUT.fileExists(fqfn): cmd = [ "ffprobe", "-loglevel", "quiet", "-of", "json", "-show_streams", fqfn, ] proc = subprocess.run(cmd, capture_output=True) if proc.returncode == 0: xstr = proc.stdout.decode("utf-8") # print(xstr) finfo = json.loads(xstr) except Exception as e: errorNotify("fileInfo", e) return finfo except Exception as e: errorNotify("fileInfo", e)
def finishedRecordings(): """ grid_finished returns a dict single program looks like: { 'uuid': 'a64b50c335f01c0df1a49f11a0f8d3f4', 'enabled': True, 'start': 1576749600, 'start_extra': 0, 'start_real': 1576749570, 'stop': 1576751400, 'stop_extra': 0, 'stop_real': 1576752300, 'duration': 2700, 'channel': 'c5827940c7ae3d76ea80df053112dc9c', 'channel_icon': '', 'channelname': 'BBC ONE HD', 'title': {'eng': 'Homes Under the Hammer'}, 'disp_title': 'Homes Under the Hammer', 'subtitle': {'eng': 'A house with a stream but close to a railway line in Handsacre in Staffordshire and a large three-storey property in Plymouth are sold under the hammer. [S] [HD]'}, 'disp_subtitle': 'A house with a stream but close to a railway line in Handsacre in Staffordshire and a large three-storey property in Plymouth are sold under the hammer. [S] [HD]', 'description': {'eng': 'A house with a stream but close to a railway line in Handsacre in Staffordshire and a large three-storey property in Plymouth are sold under the hammer. [S] [HD]'}, 'disp_description': 'A house with a stream but close to a railway line in Handsacre in Staffordshire and a large three-storey property in Plymouth are sold under the hammer. [S] [HD]', 'pri': 2, 'retention': 0, 'removal': 0, 'playposition': 0, 'playcount': 0, 'config_name': 'ee86fd19903e87679e5fdfe243966ad0', 'owner': 'chris', 'creator': 'chris', 'filename': '/home/hts/Railway/Homes-Under-the-Hammer.ts', 'directory': 'Railway', 'errorcode': 0, 'errors': 0, 'data_errors': 0, 'dvb_eid': 0, 'noresched': True, 'norerecord': False, 'fileremoved': 0, 'autorec': '0dee37cd9707da051618045a73c9aa43', 'autorec_caption': 'railway', 'timerec': '', 'timerec_caption': '', 'parent': '', 'child': '', 'content_type': 2, 'broadcast': 0, 'url': 'dvrfile/a64b50c335f01c0df1a49f11a0f8d3f4', 'filesize': 1045284324, 'status': 'Completed OK', 'sched_status': 'completed', 'duplicate': 0, 'comment': 'Auto recording: railway' } """ entries = None total = 0 try: data = {"limit": 100} j = sendToTVH("dvr/entry/grid_finished", data) if "entries" in j: entries = j["entries"] if "total" in j: total = int(j["total"]) except Exception as e: fname = sys._getframe().f_code.co_name errorNotify(fname, e) finally: return (total, entries)