def ParseFFMPEGNumFramesManually( lines ): frame_lines = [ l for l in lines if l.startswith( 'frame=' ) ] if len( frame_lines ) == 0: raise HydrusExceptions.MimeException( 'Video appears to be broken and non-renderable--perhaps a damaged single-frame video?' ) final_line = frame_lines[-1] # there will be many progress rows, counting up as the file renders. we hence want the final one l = final_line l = l.replace( 'frame=', '' ) while l.startswith( ' ' ): l = l[1:] try: frames_string = l.split( ' ' )[0] num_frames = int( frames_string ) except: raise HydrusExceptions.MimeException( 'Video was unable to render correctly--could not parse ffmpeg output line: "{}"'.format( final_line ) ) return num_frames
def CheckFFMPEGError( lines ): if len( lines ) == 0: raise HydrusExceptions.MimeException( 'Could not parse that file--no FFMPEG output given.' ) if "No such file or directory" in lines[-1]: raise IOError( "File not found!" ) if 'Invalid data' in lines[-1]: raise HydrusExceptions.MimeException( 'FFMPEG could not parse.' )
def ParseFFMPEGDuration( lines ): # get duration (in seconds) # Duration: 00:00:02.46, start: 0.033000, bitrate: 1069 kb/s try: # had a vid with 'Duration:' in title, ha ha, so now a regex line = [ l for l in lines if re.search( r'^\s*Duration:', l ) is not None ][0] if 'Duration: N/A' in line: return ( None, None ) if 'start:' in line: m = re.search( '(start\\: )' + '-?[0-9]+\\.[0-9]*', line ) start_offset = float( line[ m.start() + 7 : m.end() ] ) if abs( start_offset ) > 1.0: # once had a file with start offset of 957499 seconds jej start_offset = 0 else: start_offset = 0 match = re.search("[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]", line) hms = list(map(float, line[match.start()+1:match.end()].split(':'))) if len( hms ) == 1: duration = hms[0] elif len( hms ) == 2: duration = 60 * hms[0] + hms[1] elif len( hms ) ==3: duration = 3600 * hms[0] + 60 * hms[1] + hms[2] if duration == 0: return ( None, None ) file_duration = duration + start_offset stream_duration = duration return ( file_duration, stream_duration ) except: raise HydrusExceptions.MimeException( 'Error reading duration!' )
def ParseFFMPEGVideoLine( lines ): # get the output line that speaks about video # the ^\sStream is to exclude the 'title' line, when it exists, includes the string 'Video: ', ha ha lines_video = [ l for l in lines if re.search( r'^\s*Stream', l ) is not None and 'Video: ' in l and not ( 'Video: png' in l or 'Video: jpg' in l ) ] # mp3 says it has a 'png' video stream if len( lines_video ) == 0: raise HydrusExceptions.MimeException( 'Could not find video information!' ) line = lines_video[0] return line
def ParseFFMPEGMimeText( lines ): try: ( input_line, ) = [ l for l in lines if l.startswith( 'Input #0' ) ] # Input #0, matroska, webm, from 'm.mkv': text = input_line[10:] mime_text = text.split( ', from' )[0] return mime_text except: raise HydrusExceptions.MimeException( 'Error reading mime!' )
def ParseFFMPEGVideoResolution( lines ): try: line = ParseFFMPEGVideoLine( lines ) # get the size, of the form 460x320 (w x h) match = re.search(" [0-9]*x[0-9]*(,| )", line) resolution = list(map(int, line[match.start():match.end()-1].split('x'))) sar_match = re.search( " SAR [0-9]*:[0-9]* ", line ) if sar_match is not None: # ' SAR 2:3 ' sar_string = line[ sar_match.start() : sar_match.end() ] # '2:3' sar_string = sar_string[5:-1] ( sar_w, sar_h ) = sar_string.split( ':' ) ( sar_w, sar_h ) = ( int( sar_w ), int( sar_h ) ) ( x, y ) = resolution x *= sar_w x //= sar_h resolution = ( x, y ) return resolution except: raise HydrusExceptions.MimeException( 'Error parsing resolution!' )
def GeneratePILImage( path ): try: pil_image = PILImage.open( path ) except Exception as e: raise HydrusExceptions.MimeException( 'Could not load the image--it was likely malformed!' ) if pil_image.format == 'JPEG' and hasattr( pil_image, '_getexif' ): try: exif_dict = pil_image._getexif() except: exif_dict = None if exif_dict is not None: EXIF_ORIENTATION = 274 if EXIF_ORIENTATION in exif_dict: orientation = exif_dict[ EXIF_ORIENTATION ] if orientation == 1: pass # normal elif orientation == 2: # mirrored horizontal pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT ) elif orientation == 3: # 180 pil_image = pil_image.transpose( PILImage.ROTATE_180 ) elif orientation == 4: # mirrored vertical pil_image = pil_image.transpose( PILImage.FLIP_TOP_BOTTOM ) elif orientation == 5: # seems like these 90 degree rotations are wrong, but fliping them works for my posh example images, so I guess the PIL constants are odd # mirrored horizontal, then 90 CCW pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT ).transpose( PILImage.ROTATE_90 ) elif orientation == 6: # 90 CW pil_image = pil_image.transpose( PILImage.ROTATE_270 ) elif orientation == 7: # mirrored horizontal, then 90 CCW pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT ).transpose( PILImage.ROTATE_270 ) elif orientation == 8: # 90 CCW pil_image = pil_image.transpose( PILImage.ROTATE_90 ) if pil_image is None: raise Exception( 'The file at ' + path + ' could not be rendered!' ) return pil_image
def GetFileInfo(path, mime=None, ok_to_look_for_hydrus_updates=False): size = os.path.getsize(path) if size == 0: raise HydrusExceptions.SizeException('File is of zero length!') if mime is None: mime = GetMime( path, ok_to_look_for_hydrus_updates=ok_to_look_for_hydrus_updates) if mime not in HC.ALLOWED_MIMES: if mime == HC.TEXT_HTML: raise HydrusExceptions.MimeException( 'Looks like HTML -- maybe the client needs to be taught how to parse this?' ) elif mime == HC.APPLICATION_UNKNOWN: raise HydrusExceptions.MimeException('Unknown filetype!') else: raise HydrusExceptions.MimeException('Filetype is not permitted!') width = None height = None duration = None num_frames = None num_words = None if mime in (HC.IMAGE_JPEG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_WEBP, HC.IMAGE_TIFF, HC.IMAGE_ICON): ((width, height), duration, num_frames) = HydrusImageHandling.GetImageProperties(path, mime) elif mime == HC.APPLICATION_FLASH: ((width, height), duration, num_frames) = HydrusFlashHandling.GetFlashProperties(path) elif mime in (HC.IMAGE_APNG, HC.VIDEO_AVI, HC.VIDEO_FLV, HC.VIDEO_WMV, HC.VIDEO_MOV, HC.VIDEO_MP4, HC.VIDEO_MKV, HC.VIDEO_REALMEDIA, HC.VIDEO_WEBM, HC.VIDEO_MPEG): ((width, height), duration, num_frames) = HydrusVideoHandling.GetFFMPEGVideoProperties(path) elif mime == HC.APPLICATION_PDF: num_words = HydrusDocumentHandling.GetPDFNumWords( path) # this now give None until a better solution can be found elif mime == HC.APPLICATION_PSD: (width, height) = HydrusImageHandling.GetPSDResolution(path) elif mime in HC.AUDIO: ffmpeg_lines = HydrusVideoHandling.GetFFMPEGInfoLines(path) (file_duration_in_s, stream_duration_in_s ) = HydrusVideoHandling.ParseFFMPEGDuration(ffmpeg_lines) duration = int(file_duration_in_s * 1000) if mime in HC.MIMES_THAT_DEFINITELY_HAVE_AUDIO: has_audio = True elif mime in HC.MIMES_THAT_MAY_HAVE_AUDIO: has_audio = HydrusAudioHandling.VideoHasAudio(path) else: has_audio = False if width is not None and width < 0: width *= -1 if height is not None and height < 0: width *= -1 if duration is not None and duration < 0: duration *= -1 if num_frames is not None and num_frames < 0: num_frames *= -1 if num_words is not None and num_words < 0: num_words *= -1 return (size, mime, width, height, duration, num_frames, has_audio, num_words)
def ParseFFMPEGFPS( first_second_lines ): try: line = ParseFFMPEGVideoLine( first_second_lines ) # get the frame rate possible_results = set() match = re.search("( [0-9]*.| )[0-9]* tbr", line) if match is not None: tbr = line[match.start():match.end()].split(' ')[1] tbr_fps_is_likely_garbage = match is None or tbr.endswith( 'k' ) or float( tbr ) > 144 if not tbr_fps_is_likely_garbage: possible_results.add( float( tbr ) ) # match = re.search("( [0-9]*.| )[0-9]* fps", line) if match is not None: fps = line[match.start():match.end()].split(' ')[1] fps_is_likely_garbage = match is None or fps.endswith( 'k' ) or float( fps ) > 144 if not fps_is_likely_garbage: possible_results.add( float( fps ) ) num_frames_in_first_second = ParseFFMPEGNumFramesManually( first_second_lines ) confident = len( possible_results ) <= 1 if len( possible_results ) == 0: fps = num_frames_in_first_second confident = False else: # in some cases, fps is 0.77 and tbr is incorrectly 20. extreme values cause bad results. let's default to slowest, but test our actual first second for most legit-looking sensible_first_second = num_frames_in_first_second > 1 fps = min( possible_results ) fps_matches_with_first_second = False for possible_fps in possible_results: if num_frames_in_first_second - 1 <= possible_fps and possible_fps <= num_frames_in_first_second + 1: fps = possible_fps fps_matches_with_first_second = True break confident = sensible_first_second and fps_matches_with_first_second if fps is None or fps == 0: fps = 1 confident = False return ( fps, confident ) except: raise HydrusExceptions.MimeException( 'Error estimating framerate!' )
def GetFFMPEGVideoProperties( path, force_count_frames_manually = False ): first_second_lines = GetFFMPEGInfoLines( path, count_frames_manually = True, only_first_second = True ) ( has_video, video_format ) = ParseFFMPEGVideoFormat( first_second_lines ) if not has_video: raise HydrusExceptions.MimeException( 'File did not appear to have a video stream!' ) resolution = ParseFFMPEGVideoResolution( first_second_lines ) ( file_duration_in_s, stream_duration_in_s ) = ParseFFMPEGDuration( first_second_lines ) # this will have to be fixed when I add audio, and dynamically accounted for on dual vid/audio rendering duration = stream_duration_in_s ( fps, confident_fps ) = ParseFFMPEGFPS( first_second_lines ) if duration is None and not confident_fps: # ok default to fall back on ( fps, confident_fps ) = ( 24, True ) if fps is None or fps == 0: fps = 1 if duration is None: force_count_frames_manually = True num_frames_inferrence_likely_odd = True # i.e. inferrence not possible! else: num_frames_estimate = int( duration * fps ) # if file is big or long, don't try to force a manual count when one not explicitly asked for # we don't care about a dropped frame on a 10min vid tbh num_frames_seems_ok_to_count = num_frames_estimate < 2400 file_is_ok_size = os.path.getsize( path ) < 128 * 1024 * 1024 if num_frames_seems_ok_to_count and file_is_ok_size: last_frame_has_unusual_duration = num_frames_estimate != duration * fps unusual_video_start = file_duration_in_s != stream_duration_in_s if not confident_fps or last_frame_has_unusual_duration or unusual_video_start: force_count_frames_manually = True if force_count_frames_manually: lines = GetFFMPEGInfoLines( path, count_frames_manually = True ) num_frames = ParseFFMPEGNumFramesManually( lines ) if duration is None: duration = num_frames / fps else: num_frames = int( duration * fps ) duration_in_ms = int( duration * 1000 ) return ( resolution, duration_in_ms, num_frames )