refactoring
This commit is contained in:
parent
fa25e162b9
commit
195a1bef4d
|
@ -0,0 +1,245 @@
|
||||||
|
'''
|
||||||
|
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']:
|
||||||
|
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']:
|
||||||
|
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']:
|
||||||
|
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'])))
|
||||||
|
print('height', media.source.source_resolution_height)
|
||||||
|
print('video_formats', video_formats)
|
||||||
|
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
|
|
@ -8,6 +8,8 @@ from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .youtube import get_media_info as get_youtube_media_info
|
from .youtube import get_media_info as get_youtube_media_info
|
||||||
from .utils import seconds_to_timestr, parse_media_format
|
from .utils import seconds_to_timestr, parse_media_format
|
||||||
|
from .matching import (get_best_combined_format, get_best_audio_format,
|
||||||
|
get_best_video_format)
|
||||||
|
|
||||||
|
|
||||||
class Source(models.Model):
|
class Source(models.Model):
|
||||||
|
@ -259,7 +261,7 @@ class Source(models.Model):
|
||||||
depending on audio codec.
|
depending on audio codec.
|
||||||
'''
|
'''
|
||||||
if self.is_audio:
|
if self.is_audio:
|
||||||
if self.source_acodec == self.SOURCE_ACODEC_M4A:
|
if self.source_acodec == self.SOURCE_ACODEC_MP4A:
|
||||||
return 'm4a'
|
return 'm4a'
|
||||||
elif self.source_acodec == self.SOURCE_ACODEC_OPUS:
|
elif self.source_acodec == self.SOURCE_ACODEC_OPUS:
|
||||||
return 'ogg'
|
return 'ogg'
|
||||||
|
@ -284,9 +286,9 @@ class Source(models.Model):
|
||||||
else:
|
else:
|
||||||
vc = self.source_vcodec
|
vc = self.source_vcodec
|
||||||
ac = self.source_acodec
|
ac = self.source_acodec
|
||||||
f = '60FPS' if self.prefer_60fps else ''
|
f = ' 60FPS' if self.is_video and self.prefer_60fps else ''
|
||||||
h = 'HDR' if self.prefer_hdr else ''
|
h = ' HDR' if self.is_video and self.prefer_hdr else ''
|
||||||
return f'{self.source_resolution} (video:{vc}, audio:{ac}) {f} {h}'.strip()
|
return f'{self.source_resolution} (video:{vc}, audio:{ac}){f}{h}'.strip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def directory_path(self):
|
def directory_path(self):
|
||||||
|
@ -506,251 +508,47 @@ class Media(models.Model):
|
||||||
yield parse_media_format(fmt)
|
yield parse_media_format(fmt)
|
||||||
|
|
||||||
def get_best_combined_format(self):
|
def get_best_combined_format(self):
|
||||||
'''
|
return get_best_combined_format(self)
|
||||||
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 self.iter_formats():
|
|
||||||
# Check height matches
|
|
||||||
if self.source.source_resolution.strip().upper() != fmt['format']:
|
|
||||||
continue
|
|
||||||
# Check the video codec matches
|
|
||||||
if self.source.source_vcodec != fmt['vcodec']:
|
|
||||||
continue
|
|
||||||
# Check the audio codec matches
|
|
||||||
if self.source.source_acodec != fmt['acodec']:
|
|
||||||
continue
|
|
||||||
# if the source prefers 60fps, check for it
|
|
||||||
if self.source.prefer_60fps:
|
|
||||||
if not fmt['is_60fps']:
|
|
||||||
continue
|
|
||||||
# If the source prefers HDR, check for it
|
|
||||||
if self.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(self):
|
def get_best_audio_format(self):
|
||||||
'''
|
return get_best_audio_format(self)
|
||||||
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 self.iter_formats():
|
|
||||||
# If the format has a video stream, skip it
|
|
||||||
if fmt['vcodec']:
|
|
||||||
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 self.source.source_acodec == fmt['acodec']:
|
|
||||||
# Matched!
|
|
||||||
return True, fmt['id']
|
|
||||||
# No codecs matched
|
|
||||||
if self.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(self):
|
def get_best_video_format(self):
|
||||||
'''
|
return get_best_video_format(self)
|
||||||
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.
|
|
||||||
'''
|
|
||||||
min_height = getattr(settings, 'VIDEO_HEIGHT_CUTOFF', 360)
|
|
||||||
fallback_hd_cutoff = getattr(settings, 'VIDEO_HEIGHT_IS_HD', 500)
|
|
||||||
# Filter video-only formats by resolution that matches the source
|
|
||||||
video_formats = []
|
|
||||||
for fmt in self.iter_formats():
|
|
||||||
# If the format has an audio stream, skip it
|
|
||||||
if fmt['acodec']:
|
|
||||||
continue
|
|
||||||
if self.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 self.source.can_fallback:
|
|
||||||
# Find the next-best format matches by height
|
|
||||||
for fmt in self.iter_formats():
|
|
||||||
# If the format has an audio stream, skip it
|
|
||||||
if fmt['acodec']:
|
|
||||||
continue
|
|
||||||
if (fmt['height'] <= self.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 self.source.prefer_60fps and self.source.prefer_hdr:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for an exact match
|
|
||||||
if (self.source.source_resolution.strip().upper() == fmt['format'] and
|
|
||||||
self.source.source_vcodec == fmt['vcodec'] and
|
|
||||||
fmt['is_hdr'] and
|
|
||||||
fmt['is_60fps']):
|
|
||||||
# Exact match
|
|
||||||
exact_match, best_match = True, fmt
|
|
||||||
break
|
|
||||||
if self.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 (self.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 self.source.prefer_60fps and not self.source.prefer_hdr:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for an exact match
|
|
||||||
if (self.source.source_resolution.strip().upper() == fmt['format'] and
|
|
||||||
self.source.source_vcodec == fmt['vcodec'] and
|
|
||||||
fmt['is_60fps']):
|
|
||||||
# Exact match
|
|
||||||
exact_match, best_match = True, fmt
|
|
||||||
break
|
|
||||||
if self.source.can_fallback:
|
|
||||||
if not best_match:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for a codec and fps match but drop the resolution
|
|
||||||
if (self.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 self.source.prefer_hdr and not self.source.prefer_60fps:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for an exact match
|
|
||||||
if (self.source.source_resolution.strip().upper() == fmt['format'] and
|
|
||||||
self.source.source_vcodec == fmt['vcodec'] and
|
|
||||||
fmt['is_hdr']):
|
|
||||||
# Exact match
|
|
||||||
exact_match, best_match = True, fmt
|
|
||||||
break
|
|
||||||
if self.source.can_fallback:
|
|
||||||
if not best_match:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for a codec and hdr match but drop the resolution
|
|
||||||
if (self.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 self.source.prefer_hdr and not self.source.prefer_60fps:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for an exact match
|
|
||||||
if (self.source.source_resolution.strip().upper() == fmt['format'] and
|
|
||||||
self.source.source_vcodec == fmt['vcodec'] and
|
|
||||||
not fmt['is_60fps']):
|
|
||||||
# Exact match
|
|
||||||
exact_match, best_match = True, fmt
|
|
||||||
break
|
|
||||||
if self.source.can_fallback:
|
|
||||||
if not best_match:
|
|
||||||
for fmt in video_formats:
|
|
||||||
# Check for a codec match without 60fps and drop the resolution
|
|
||||||
if (self.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 self.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 self.source.can_fallback:
|
|
||||||
# Allow the fallback if it meets requirements
|
|
||||||
if (self.source.fallback == self.source.FALLBACK_NEXT_BEST_HD and
|
|
||||||
best_match['height'] >= fallback_hd_cutoff):
|
|
||||||
return False, best_match['id']
|
|
||||||
elif self.source.fallback == self.source.FALLBACK_NEXT_BEST:
|
|
||||||
return False, best_match['id']
|
|
||||||
# Nope, failed to find match
|
|
||||||
return False, False
|
|
||||||
|
|
||||||
|
|
||||||
def get_format_str(self):
|
def get_format_str(self):
|
||||||
'''
|
'''
|
||||||
Returns a youtube-dl compatible format string for the best matches
|
Returns a youtube-dl compatible format string for the best matches
|
||||||
combination of source requirements and available audio and video formats.
|
combination of source requirements and available audio and video formats.
|
||||||
|
Returns boolean False if there is no valid downloadable combo.
|
||||||
'''
|
'''
|
||||||
if self.source.is_audio:
|
if self.source.is_audio:
|
||||||
audio_format = self.get_best_audio_format()
|
audio_match, audio_format = self.get_best_audio_format()
|
||||||
return 'a'
|
if audio_format:
|
||||||
else:
|
return str(audio_format)
|
||||||
combined_format = self.get_best_combined_format()
|
|
||||||
if combined_format:
|
|
||||||
return 'c'
|
|
||||||
else:
|
else:
|
||||||
audio_format = self.get_best_audio_format()
|
return False
|
||||||
video_format = self.get_best_video_format()
|
else:
|
||||||
return 'a+v'
|
combined_match, combined_format = self.get_best_combined_format()
|
||||||
|
if combined_format:
|
||||||
|
return str(combined_format)
|
||||||
|
else:
|
||||||
|
audio_match, audio_format = self.get_best_audio_format()
|
||||||
|
video_match, video_format = self.get_best_video_format()
|
||||||
|
if audio_format and video_format:
|
||||||
|
return f'{audio_format}+{video_format}'
|
||||||
|
else:
|
||||||
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_download(self):
|
||||||
|
'''
|
||||||
|
Returns boolean True if the media can be downloaded, that is, the media
|
||||||
|
has stored formats which are compatible with the source requirements.
|
||||||
|
'''
|
||||||
|
return self.get_format_str() is not False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loaded_metadata(self):
|
def loaded_metadata(self):
|
||||||
if self.pk in _metadata_cache:
|
if self.pk in _metadata_cache:
|
||||||
|
|
|
@ -49,7 +49,7 @@ def source_post_save(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Source)
|
@receiver(pre_delete, sender=Source)
|
||||||
def source_post_delete(sender, instance, **kwargs):
|
def source_pre_delete(sender, instance, **kwargs):
|
||||||
# Triggered before a source is deleted, delete all media objects to trigger
|
# Triggered before a source is deleted, delete all media objects to trigger
|
||||||
# the Media models post_delete signal
|
# the Media models post_delete signal
|
||||||
for media in Media.objects.filter(source=instance):
|
for media in Media.objects.filter(source=instance):
|
||||||
|
@ -66,7 +66,7 @@ def source_post_delete(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(task_failed, sender=Task)
|
@receiver(task_failed, sender=Task)
|
||||||
def task_task_failed(sender, task_id, completed_task, **kwargs):
|
def task_task_failed(sender, task_id, completed_task, **kwargs):
|
||||||
# Triggered after a source fails by reaching its max retry attempts
|
# Triggered after a task fails by reaching its max retry attempts
|
||||||
obj, url = map_task_to_instance(completed_task)
|
obj, url = map_task_to_instance(completed_task)
|
||||||
if isinstance(obj, Source):
|
if isinstance(obj, Source):
|
||||||
log.error(f'Permanent failure for source: {obj} task: {completed_task}')
|
log.error(f'Permanent failure for source: {obj} task: {completed_task}')
|
||||||
|
@ -78,7 +78,7 @@ def task_task_failed(sender, task_id, completed_task, **kwargs):
|
||||||
def media_post_save(sender, instance, created, **kwargs):
|
def media_post_save(sender, instance, created, **kwargs):
|
||||||
# Triggered after media is saved
|
# Triggered after media is saved
|
||||||
if created:
|
if created:
|
||||||
# If the media is newly created fire a task off to download its thumbnail
|
# If the media is newly created start a task to download its thumbnail
|
||||||
metadata = instance.loaded_metadata
|
metadata = instance.loaded_metadata
|
||||||
thumbnail_url = metadata.get('thumbnail', '')
|
thumbnail_url = metadata.get('thumbnail', '')
|
||||||
if thumbnail_url:
|
if thumbnail_url:
|
||||||
|
|
|
@ -33,7 +33,7 @@ def get_hash(task_name, pk):
|
||||||
|
|
||||||
def map_task_to_instance(task):
|
def map_task_to_instance(task):
|
||||||
'''
|
'''
|
||||||
Reverse-maps an scheduled backgrond task to an instance. Requires the task name
|
Reverse-maps a scheduled backgrond task to an instance. Requires the task name
|
||||||
to be a known task function and the first argument to be a UUID. This is used
|
to be a known task function and the first argument to be a UUID. This is used
|
||||||
because UUID's are incompatible with background_task's "creator" feature.
|
because UUID's are incompatible with background_task's "creator" feature.
|
||||||
'''
|
'''
|
||||||
|
@ -45,6 +45,17 @@ def map_task_to_instance(task):
|
||||||
Source: 'sync:source',
|
Source: 'sync:source',
|
||||||
Media: 'sync:media-item',
|
Media: 'sync:media-item',
|
||||||
}
|
}
|
||||||
|
# If the task has a UUID set in its .queue it's probably a link to a Source
|
||||||
|
if task.queue:
|
||||||
|
try:
|
||||||
|
queue_uuid = uuid.UUID(task.queue)
|
||||||
|
try:
|
||||||
|
return Source.objects.get(pk=task.queue)
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
pass
|
||||||
|
except (TypeError, ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
# Unpack
|
||||||
task_func, task_args_str = task.task_name, task.task_params
|
task_func, task_args_str = task.task_name, task.task_params
|
||||||
model = TASK_MAP.get(task_func, None)
|
model = TASK_MAP.get(task_func, None)
|
||||||
if not model:
|
if not model:
|
||||||
|
@ -75,7 +86,8 @@ def map_task_to_instance(task):
|
||||||
|
|
||||||
def get_error_message(task):
|
def get_error_message(task):
|
||||||
'''
|
'''
|
||||||
Extract an error message from a failed task.
|
Extract an error message from a failed task. This is the last line of the
|
||||||
|
last_error field with the method name removed.
|
||||||
'''
|
'''
|
||||||
if not task.has_error():
|
if not task.has_error():
|
||||||
return ''
|
return ''
|
||||||
|
@ -92,8 +104,7 @@ def get_source_completed_tasks(source_id, only_errors=False):
|
||||||
'''
|
'''
|
||||||
Returns a queryset of CompletedTask objects for a source by source ID.
|
Returns a queryset of CompletedTask objects for a source by source ID.
|
||||||
'''
|
'''
|
||||||
source_hash = get_hash('sync.tasks.index_source_task', source_id)
|
q = {'queue': source_id}
|
||||||
q = {'task_hash': source_hash}
|
|
||||||
if only_errors:
|
if only_errors:
|
||||||
q['failed_at__isnull'] = False
|
q['failed_at__isnull'] = False
|
||||||
return CompletedTask.objects.filter(**q).order_by('-failed_at')
|
return CompletedTask.objects.filter(**q).order_by('-failed_at')
|
||||||
|
@ -163,7 +174,7 @@ def index_source_task(source_id):
|
||||||
@background(schedule=0)
|
@background(schedule=0)
|
||||||
def download_media_thumbnail(media_id, url):
|
def download_media_thumbnail(media_id, url):
|
||||||
'''
|
'''
|
||||||
Downloads an image from a URL and saves it as a local thumbnail attached to a
|
Downloads an image from a URL and save it as a local thumbnail attached to a
|
||||||
Media object.
|
Media object.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
<td class="hide-on-small-only">Downloaded</td>
|
<td class="hide-on-small-only">Downloaded</td>
|
||||||
<td><span class="hide-on-med-and-up">Downloaded<br></span><strong>{% if media.downloaded %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
<td><span class="hide-on-med-and-up">Downloaded<br></span><strong>{% if media.downloaded %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr title="Can the media be downloaded">
|
||||||
|
<td class="hide-on-small-only">Can download</td>
|
||||||
|
<td><span class="hide-on-med-and-up">Can download<br></span><strong>{% if youtube_dl_format %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
||||||
|
</tr>
|
||||||
<tr title="The available media formats">
|
<tr title="The available media formats">
|
||||||
<td class="hide-on-small-only">Available formats</td>
|
<td class="hide-on-small-only">Available formats</td>
|
||||||
<td><span class="hide-on-med-and-up">Available formats<br></span>
|
<td><span class="hide-on-med-and-up">Available formats<br></span>
|
||||||
|
@ -59,6 +63,10 @@
|
||||||
Video: <strong>{% if video_format %}{{ video_format }} {% if video_exact %}(exact match){% else %}(fallback){% endif %}{% else %}No match{% endif %}
|
Video: <strong>{% if video_format %}{{ video_format }} {% if video_exact %}(exact match){% else %}(fallback){% endif %}{% else %}No match{% endif %}
|
||||||
</strong></td>
|
</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr title="Format string passed to youtube-dl">
|
||||||
|
<td class="hide-on-small-only">youtube-dl format</td>
|
||||||
|
<td><span class="hide-on-med-and-up">youtube-dl format<br></span><strong>{% if youtube_dl_format %}{{ youtube_dl_format }}{% else %}No matching formats{% endif %}</strong></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -126,6 +126,11 @@ def seconds_to_timestr(seconds):
|
||||||
|
|
||||||
|
|
||||||
def parse_media_format(format_dict):
|
def parse_media_format(format_dict):
|
||||||
|
'''
|
||||||
|
This parser primarily adapts the format dict returned by youtube-dl into a
|
||||||
|
standard form used by the matchers in matching.py. If youtube-dl changes
|
||||||
|
any internals, update it here.
|
||||||
|
'''
|
||||||
vcodec_full = format_dict.get('vcodec', '')
|
vcodec_full = format_dict.get('vcodec', '')
|
||||||
vcodec_parts = vcodec_full.split('.')
|
vcodec_parts = vcodec_full.split('.')
|
||||||
if len(vcodec_parts) > 0:
|
if len(vcodec_parts) > 0:
|
||||||
|
|
|
@ -23,7 +23,7 @@ from . import youtube
|
||||||
|
|
||||||
class DashboardView(TemplateView):
|
class DashboardView(TemplateView):
|
||||||
'''
|
'''
|
||||||
The dashboard shows non-interactive totals and summaries, nothing more.
|
The dashboard shows non-interactive totals and summaries.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
template_name = 'sync/dashboard.html'
|
template_name = 'sync/dashboard.html'
|
||||||
|
@ -349,8 +349,8 @@ class MediaView(ListView):
|
||||||
|
|
||||||
class MediaThumbView(DetailView):
|
class MediaThumbView(DetailView):
|
||||||
'''
|
'''
|
||||||
Shows a media thumbnail. Whitenose doesn't support post-start media image
|
Shows a media thumbnail. Whitenoise doesn't support post-start media image
|
||||||
serving and the images here are pretty small, just serve them manually. This
|
serving and the images here are pretty small so just serve them manually. This
|
||||||
isn't fast, but it's not likely to be a serious bottleneck.
|
isn't fast, but it's not likely to be a serious bottleneck.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -389,12 +389,13 @@ class MediaItemView(DetailView):
|
||||||
data['audio_format'] = audio_format
|
data['audio_format'] = audio_format
|
||||||
data['video_exact'] = video_exact
|
data['video_exact'] = video_exact
|
||||||
data['video_format'] = video_format
|
data['video_format'] = video_format
|
||||||
|
data['youtube_dl_format'] = self.object.get_format_str()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class TasksView(ListView):
|
class TasksView(ListView):
|
||||||
'''
|
'''
|
||||||
A list of tasks queued to be completed. Typically, this is scraping for new
|
A list of tasks queued to be completed. This is, for example, scraping for new
|
||||||
media or downloading media.
|
media or downloading media.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue