def select_audioch(inFile: str, tsn: str) -> List[str]: # AC-3 max channels is 5.1 vInfo = video_info(inFile) if vInfo.aCh is not None and vInfo.aCh > 6: LOGGER.debug("Too many audio channels for AC-3, using 5.1 instead") return ["-ac", "6"] return []
def supported_format(inFile: str) -> bool: vInfo = video_info(inFile) if vInfo.Supported: return True else: LOGGER.debug("FALSE, file not supported %s" % inFile) return False
def tivo_compatible(inFile: str, tsn: str = "", mime: str = "") -> Tuple[bool, str]: vInfo = video_info(inFile) message = (True, "all compatible") if not get_bin("ffmpeg"): if mime not in ["video/x-tivo-mpeg", "video/mpeg", ""]: message = (False, "no ffmpeg") return message while True: vmessage = tivo_compatible_video(vInfo, tsn, mime) if not vmessage[0]: message = vmessage break amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime) if not amessage[0]: message = amessage break cmessage = tivo_compatible_container(vInfo, inFile, mime) if not cmessage[0]: message = cmessage break LOGGER.debug("TRANSCODE=%s, %s, %s" % (["YES", "NO"][message[0]], message[1], inFile)) return message
def select_audiofr(inFile: str, tsn: str) -> List[str]: freq = "48000" # default vInfo = video_info(inFile) if vInfo.aFreq == "44100": # compatible frequency freq = vInfo.aFreq return ["-ar", freq]
def select_audiocodec(isQuery: bool, inFile: str, tsn: str = "", mime: str = "") -> List[str]: if inFile[-5:].lower() == ".tivo": return ["-c:a", "copy"] vInfo = video_info(inFile) codectype = vInfo.vCodec # Default, compatible with all TiVo's codec = "ac3" compatiblecodecs = ("ac3", "liba52", "mp2") if vInfo.aCodec in compatiblecodecs: aKbps = vInfo.aKbps aCh = vInfo.aCh if aKbps is None: if not isQuery: vInfoQuery = audio_check(inFile, tsn) if vInfoQuery is None: aKbps = None aCh = None else: aKbps = vInfoQuery.aKbps aCh = vInfoQuery.aCh else: codec = "TBA" if aKbps and aKbps <= getMaxAudioBR(tsn): # compatible codec and bitrate, do not reencode audio codec = "copy" if vInfo.aCodec != "ac3" and (aCh is None or aCh > 2): codec = "ac3" val = ["-c:a", codec] if not (codec == "copy" and codectype == "mpeg2video"): val.append("-copyts") return val
def select_videostr(inFile: str, tsn: str, mime: str = "") -> int: vInfo = video_info(inFile) if tivo_compatible_video(vInfo, tsn, mime)[0] and vInfo.kbps is not None: video_str = vInfo.kbps if vInfo.aKbps is not None: video_str -= vInfo.aKbps video_str *= 1000 else: video_str = strtod(getVideoBR(tsn)) if isHDtivo(tsn) and vInfo.kbps: video_str = max(video_str, vInfo.kbps * 1000) video_str = int(min(strtod(getMaxVideoBR(tsn)) * 0.95, video_str)) return video_str
def select_videocodec(inFile: str, tsn: str, mime: str = "") -> List[str]: codec = ["-c:v"] vInfo = video_info(inFile) if tivo_compatible_video(vInfo, tsn, mime)[0]: codec.append("copy") if mime == "video/x-tivo-mpeg-ts": org_codec = vInfo.vCodec if org_codec == "h264": codec += ["-bsf:v", "h264_mp4toannexb", "-muxdelay", "0"] elif org_codec == "hevc": codec += ["-bsf:v", "hevc_mp4toannexb"] else: codec += ["mpeg2video", "-pix_fmt", "yuv420p"] # default return codec
def select_audiolang(inFile: str, tsn: str) -> str: vInfo = video_info(inFile) audio_lang = get_tsn("audio_lang", tsn) LOGGER.debug("audio_lang: %s" % audio_lang) if vInfo.mapAudio: # default to first detected audio stream to begin with stream = vInfo.mapAudio[0][0] LOGGER.debug("set first detected audio stream by default: %s" % stream) # TODO: why do we check mapVideo in the following? if (audio_lang is not None and vInfo.mapAudio is not None and vInfo.mapVideo is not None): langmatch_curr = [] langmatch_prev = vInfo.mapAudio[:] for lang in audio_lang.replace(" ", "").lower().split(","): LOGGER.debug("matching lang: %s" % lang) for s, l in langmatch_prev: if lang in s + l.replace(" ", "").lower(): LOGGER.debug("matched: %s" % s + l.replace(" ", "").lower()) langmatch_curr.append((s, l)) # if only 1 item matched we're done if len(langmatch_curr) == 1: stream = langmatch_curr[0][0] LOGGER.debug("found exactly one match: %s" % stream) break # if more than 1 item matched copy the curr area to the prev # array we only need to look at the new shorter list from # now on elif len(langmatch_curr) > 1: langmatch_prev = langmatch_curr[:] # default to the first item matched thus far stream = langmatch_curr[0][0] LOGGER.debug("remember first match: %s" % stream) langmatch_curr = [] # don't let FFmpeg auto select audio stream, pyTivo defaults to # first detected if stream and vInfo.mapVideo is not None: LOGGER.debug("selected audio stream: %s" % stream) return "-map " + vInfo.mapVideo + " -map " + stream # if no audio is found LOGGER.debug("selected audio stream: None detected") return ""
def audio_check(inFile: str, tsn: str) -> Optional[VideoInfo]: cmd_string = ("-y -c:v mpeg2video -r 29.97 -b:v 1000k -c:a copy " + select_audiolang(inFile, tsn) + " -t 00:00:01 -f vob -") ffmpeg_bin = get_bin("ffmpeg") if ffmpeg_bin is None: LOGGER.error("Can't locate ffmpeg binary.") return None cmd = [ffmpeg_bin, "-i", inFile] + cmd_string.split() ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE) fd, testname = tempfile.mkstemp() testfile = os.fdopen(fd, "wb") try: shutil.copyfileobj(ffmpeg.stdout, testfile) except: kill(ffmpeg) testfile.close() vInfo = None else: testfile.close() vInfo = video_info(testname, False) os.remove(testname) return vInfo
def metadata_full( self, full_path: str, tsn: str = "", mime: str = "", mtime: Optional[float] = None, ) -> Dict[str, Any]: data: Dict[str, Any] = {} vInfo = video_info(full_path) if vInfo.vHeight is None or vInfo.vWidth is None: LOGGER.error("vInfo.vHeight or vInfo.vWidth is None") return data if (vInfo.vHeight >= 720 and getTivoHeight(tsn) >= 720) or ( vInfo.vWidth >= 1280 and getTivoWidth(tsn) >= 1280): data["showingBits"] = "4096" data.update(basic(full_path, mtime)) if full_path[-5:].lower() == ".tivo": data.update(from_tivo(full_path)) if full_path[-4:].lower() == ".wtv": data.update(from_mscore(vInfo.rawmeta)) if "episodeNumber" in data: try: ep = int(data["episodeNumber"]) except: ep = 0 data["episodeNumber"] = str(ep) if getDebug() and "vHost" not in data: compatible, reason = tivo_compatible(full_path, tsn, mime) if compatible: transcode_options: List[str] = [] else: transcode_options = transcode_settings(True, full_path, tsn, mime) data["vHost"] = ([ "TRANSCODE=%s, %s" % (["YES", "NO"][compatible], reason) ] + ["SOURCE INFO: "] + [ "%s=%s" % (k, v) for k, v in sorted(list(vInfo._asdict().items()), reverse=True) ] + ["TRANSCODE OPTIONS: "] + transcode_options + ["SOURCE FILE: ", os.path.basename(full_path)]) now = datetime.utcnow() if "time" in data: if data["time"].lower() == "file": if not mtime: mtime = os.path.getmtime(full_path) try: now = datetime.utcfromtimestamp(mtime) except: LOGGER.warning("Bad file time on " + full_path) elif data["time"].lower() == "oad": now = isodt(data["originalAirDate"]) else: try: now = isodt(data["time"]) except: LOGGER.warning("Bad time format: " + data["time"] + " , using current time") duration = self.__duration(full_path) if duration is None: LOGGER.error("duration is None") return data duration_delta = timedelta(milliseconds=duration) min = duration_delta.seconds / 60 sec = duration_delta.seconds % 60 hours = min / 60 min = min % 60 data.update({ "time": now.isoformat(), "startTime": now.isoformat(), "stopTime": (now + duration_delta).isoformat(), "size": self.__est_size(full_path, tsn, mime), "duration": duration, "iso_duration": ("P%sDT%sH%sM%sS" % (duration_delta.days, hours, min, sec)), }) return data
def __duration(self, full_path: str) -> Optional[float]: return video_info(full_path).millisecs
def select_aspect(inFile: str, tsn: str = "") -> List[str]: tivo_width = getTivoWidth(tsn) tivo_height = getTivoHeight(tsn) vInfo = video_info(inFile) LOGGER.debug("tsn: %s" % tsn) aspect169 = get169Setting(tsn) LOGGER.debug("aspect169: %s" % aspect169) optres = getOptres(tsn) LOGGER.debug("optres: %s" % optres) if vInfo.vHeight is None or vInfo.vWidth is None: LOGGER.error( "Internal Error: vInfo.vHeight is None or vInfo.vHeight is None.") return [] if optres: optHeight = nearestTivoHeight(vInfo.vHeight) optWidth = nearestTivoWidth(vInfo.vWidth) if optHeight < tivo_height: tivo_height = optHeight if optWidth < tivo_width: tivo_width = optWidth if vInfo.par2: par2 = vInfo.par2 elif vInfo.par: par2 = float(vInfo.par) else: # Assume PAR = 1.0 par2 = 1.0 LOGGER.debug( ("File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s " + "tivo_height=%s tivo_width=%s") % ( inFile, vInfo.vCodec, vInfo.vWidth, vInfo.vHeight, vInfo.vFps, vInfo.millisecs, tivo_height, tivo_width, )) if isHDtivo(tsn) and not optres: if vInfo.par: npar = par2 # adjust for pixel aspect ratio, if set if npar < 1.0: return [ "-s", "%dx%d" % (vInfo.vWidth, math.ceil(vInfo.vHeight / npar)) ] elif npar > 1.0: # FFMPEG expects width to be a multiple of two return [ "-s", "%dx%d" % (math.ceil(vInfo.vWidth * npar / 2.0) * 2, vInfo.vHeight), ] if vInfo.vHeight <= tivo_height: # pass all resolutions to S3, except heights greater than # conf height return [] # else, resize video. d = gcd(vInfo.vHeight, vInfo.vWidth) rheight, rwidth = vInfo.vHeight / d, vInfo.vWidth / d LOGGER.debug("rheight=%s rwidth=%s" % (rheight, rwidth)) if (rwidth, rheight) in [(1, 1)] and vInfo.par1 == "8:9": LOGGER.debug("File + PAR is within 4:3.") return ["-aspect", "4:3", "-s", "%sx%s" % (tivo_width, tivo_height)] elif (rwidth, rheight) in [ (4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54), ] or vInfo.dar1 == "4:3": LOGGER.debug("File is within 4:3 list.") return ["-aspect", "4:3", "-s", "%sx%s" % (tivo_width, tivo_height)] elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)] or vInfo.dar1 == "16:9") and (aspect169 or get169Letterbox(tsn)): LOGGER.debug("File is within 16:9 list and 16:9 allowed.") if get169Blacklist(tsn) or (aspect169 and get169Letterbox(tsn)): aspect = "4:3" else: aspect = "16:9" return ["-aspect", aspect, "-s", "%sx%s" % (tivo_width, tivo_height)] else: settings = ["-aspect"] multiplier16by9 = (16.0 * tivo_height) / (9.0 * tivo_width) / par2 multiplier4by3 = (4.0 * tivo_height) / (3.0 * tivo_width) / par2 ratio = vInfo.vWidth * 100 * par2 / vInfo.vHeight LOGGER.debug("par2=%.3f ratio=%.3f mult4by3=%.3f" % (par2, ratio, multiplier4by3)) # If video is wider than 4:3 add top and bottom padding if ratio > 133: # Might be 16:9 file, or just need padding on # top and bottom if aspect169 and ratio > 135: # If file would fall in 4:3 # assume it is supposed to be 4:3 if get169Blacklist(tsn) or get169Letterbox(tsn): settings.append("4:3") else: settings.append("16:9") if ratio > 177: # too short needs padding top and bottom settings += pad_TB(tivo_width, tivo_height, multiplier16by9, vInfo) LOGGER.debug(("16:9 aspect allowed, file is wider " + "than 16:9 padding top and bottom\n%s") % " ".join(settings)) else: # too skinny needs padding on left and right. settings += pad_LR(tivo_width, tivo_height, multiplier16by9, vInfo) LOGGER.debug(("16:9 aspect allowed, file is narrower " + "than 16:9 padding left and right\n%s") % " ".join(settings)) else: # this is a 4:3 file or 16:9 output not allowed if ratio > 135 and get169Letterbox(tsn): settings.append("16:9") multiplier = multiplier16by9 else: settings.append("4:3") multiplier = multiplier4by3 settings += pad_TB(tivo_width, tivo_height, multiplier, vInfo) LOGGER.debug( ("File is wider than 4:3 padding " + "top and bottom\n%s") % " ".join(settings)) # If video is taller than 4:3 add left and right padding, this # is rare. All of these files will always be sent in an aspect # ratio of 4:3 since they are so narrow. else: settings.append("4:3") settings += pad_LR(tivo_width, tivo_height, multiplier4by3, vInfo) LOGGER.debug("File is taller than 4:3 padding left and right\n%s" % " ".join(settings)) return settings
def select_videofps(inFile: str, tsn: str) -> List[str]: vInfo = video_info(inFile) fps = ["-r", "29.97"] # default if isHDtivo(tsn) and vInfo.vFps in GOOD_MPEG_FPS: fps = [] return fps