tubesync/app/sync/matching.py

244 lines
10 KiB
Python

'''
Match functions take a single Media object instance as its only argument and return
two boolean values. The first value is if the match was exact or "best fit", the
second argument is the ID of the format that was matched.
'''
from django.conf import settings
min_height = getattr(settings, 'VIDEO_HEIGHT_CUTOFF', 360)
fallback_hd_cutoff = getattr(settings, 'VIDEO_HEIGHT_IS_HD', 500)
def get_best_combined_format(media):
'''
Attempts to see if there is a single, combined audio and video format that
exactly matches the source requirements. This is used over separate audio
and video formats if possible. Combined formats are the easiest to check
for as they must exactly match the source profile be be valid.
'''
for fmt in media.iter_formats():
# Check height matches
if media.source.source_resolution.strip().upper() != fmt['format']:
continue
# Check the video codec matches
if media.source.source_vcodec != fmt['vcodec']:
continue
# Check the audio codec matches
if media.source.source_acodec != fmt['acodec']:
continue
# if the source prefers 60fps, check for it
if media.source.prefer_60fps:
if not fmt['is_60fps']:
continue
# If the source prefers HDR, check for it
if media.source.prefer_hdr:
if not fmt['is_hdr']:
continue
# If we reach here, we have a combined match!
return True, fmt['id']
return False, False
def get_best_audio_format(media):
'''
Finds the best match for the source required audio format. If the source
has a 'fallback' of fail this can return no match.
'''
# Order all audio-only formats by bitrate
audio_formats = []
for fmt in media.iter_formats():
# If the format has a video stream, skip it
if fmt['vcodec'] is not None:
continue
audio_formats.append(fmt)
audio_formats = list(reversed(sorted(audio_formats, key=lambda k: k['abr'])))
if not audio_formats:
# Media has no audio formats at all
return False, False
# Find the highest bitrate audio format with a matching codec
for fmt in audio_formats:
if media.source.source_acodec == fmt['acodec']:
# Matched!
return True, fmt['id']
# No codecs matched
if media.source.can_fallback:
# Can fallback, find the next highest bitrate non-matching codec
return False, audio_formats[0]
else:
# Can't fallback
return False, False
def get_best_video_format(media):
'''
Finds the best match for the source required video format. If the source
has a 'fallback' of fail this can return no match. Resolution is treated
as the most important factor to match. This is pretty verbose due to the
'soft' matching requirements for prefer_hdr and prefer_60fps.
'''
# Check if the source wants audio only, fast path to return
if media.source.is_audio:
return False, False
# Filter video-only formats by resolution that matches the source
video_formats = []
for fmt in media.iter_formats():
# If the format has an audio stream, skip it
if fmt['acodec'] is not None:
continue
if media.source.source_resolution.strip().upper() == fmt['format']:
video_formats.append(fmt)
# Check we matched some streams
if not video_formats:
# No streams match the requested resolution, see if we can fallback
if media.source.can_fallback:
# Find the next-best format matches by height
for fmt in media.iter_formats():
# If the format has an audio stream, skip it
if fmt['acodec'] is not None:
continue
if (fmt['height'] <= media.source.source_resolution_height and
fmt['height'] >= min_height):
video_formats.append(fmt)
else:
# Can't fallback
return False, False
video_formats = list(reversed(sorted(video_formats, key=lambda k: k['height'])))
if not video_formats:
# Still no matches
return False, False
exact_match, best_match = None, None
# Of our filtered video formats, check for resolution + codec + hdr + fps match
if media.source.prefer_60fps and media.source.prefer_hdr:
for fmt in video_formats:
# Check for an exact match
if (media.source.source_resolution.strip().upper() == fmt['format'] and
media.source.source_vcodec == fmt['vcodec'] and
fmt['is_hdr'] and
fmt['is_60fps']):
# Exact match
exact_match, best_match = True, fmt
break
if media.source.can_fallback:
if not best_match:
for fmt in video_formats:
# Check for a codec, hdr and fps match but drop the resolution
if (media.source.source_vcodec == fmt['vcodec'] and
fmt['is_hdr'] and fmt['is_60fps']):
# Close match
exact_match, best_match = False, fmt
break
if not best_match:
for fmt in video_formats:
# Check for hdr and fps match but drop the resolution and codec
if fmt['is_hdr'] and fmt['is_60fps']:
exact_match, best_match = False, fmt
break
if not best_match:
for fmt in video_formats:
# Check for fps match but drop the resolution and codec and hdr
if fmt['is_hdr'] and fmt['is_60fps']:
exact_match, best_match = False, fmt
break
if not best_match:
# Match the highest resolution
exact_match, best_match = False, video_formats[0]
# Check for resolution + codec + fps match
if media.source.prefer_60fps and not media.source.prefer_hdr:
for fmt in video_formats:
# Check for an exact match
if (media.source.source_resolution.strip().upper() == fmt['format'] and
media.source.source_vcodec == fmt['vcodec'] and
fmt['is_60fps']):
# Exact match
exact_match, best_match = True, fmt
break
if media.source.can_fallback:
if not best_match:
for fmt in video_formats:
# Check for a codec and fps match but drop the resolution
if (media.source.source_vcodec == fmt['vcodec'] and
fmt['is_60fps']):
exact_match, best_match = False, fmt
break
if not best_match:
for fmt in video_formats:
# Check for an fps match but drop the resolution and codec
if fmt['is_60fps']:
exact_match, best_match = False, fmt
break
if not best_match:
# Match the highest resolution
exact_match, best_match = False, video_formats[0]
# Check for resolution + codec + hdr
if media.source.prefer_hdr and not media.source.prefer_60fps:
for fmt in video_formats:
# Check for an exact match
if (media.source.source_resolution.strip().upper() == fmt['format'] and
media.source.source_vcodec == fmt['vcodec'] and
fmt['is_hdr']):
# Exact match
exact_match, best_match = True, fmt
break
if media.source.can_fallback:
if not best_match:
for fmt in video_formats:
# Check for a codec and hdr match but drop the resolution
if (media.source.source_vcodec == fmt['vcodec'] and
fmt['is_hdr']):
exact_match, best_match = True, fmt
break
if not best_match:
for fmt in video_formats:
# Check for an hdr match but drop the resolution and codec
if fmt['is_hdr']:
exact_match, best_match = False, fmt
break
if not best_match:
# Match the highest resolution
exact_match, best_match = False, video_formats[0]
# check for resolution + codec
if not media.source.prefer_hdr and not media.source.prefer_60fps:
for fmt in video_formats:
# Check for an exact match
if (media.source.source_resolution.strip().upper() == fmt['format'] and
media.source.source_vcodec == fmt['vcodec'] and
not fmt['is_60fps']):
# Exact match
exact_match, best_match = True, fmt
break
if media.source.can_fallback:
if not best_match:
for fmt in video_formats:
# Check for a codec match without 60fps and drop the resolution
if (media.source.source_vcodec == fmt['vcodec'] and
not fmt['is_60fps']):
exact_match, best_match = False, fmt
break
if not best_match:
for fmt in video_formats:
# Check for a codec match but drop the resolution
if media.source.source_vcodec == fmt['vcodec']:
# Close match
exact_match, best_match = False, fmt
break
if not best_match:
# Match the highest resolution
exact_match, best_match = False, video_formats[0]
# See if we found a match
if best_match:
# Final check to see if the match we found was good enough
if exact_match:
return True, best_match['id']
elif media.source.can_fallback:
# Allow the fallback if it meets requirements
if (media.source.fallback == media.source.FALLBACK_NEXT_BEST_HD and
best_match['height'] >= fallback_hd_cutoff):
return False, best_match['id']
elif media.source.fallback == media.source.FALLBACK_NEXT_BEST:
return False, best_match['id']
# Nope, failed to find match
return False, False