refactor media format codes, media skipping and re-enabling feature

This commit is contained in:
meeb 2020-12-11 16:11:31 +11:00
parent 6b138fb02e
commit 096c48ce1b
20 changed files with 409 additions and 39 deletions

View File

@ -54,6 +54,7 @@ $infobox-text-colour: $colour-near-white;
$errorbox-background-colour: $colour-red; $errorbox-background-colour: $colour-red;
$errorbox-text-colour: $colour-near-white; $errorbox-text-colour: $colour-near-white;
$error-text-colour: $colour-red;
$pagination-background-colour: $colour-near-white; $pagination-background-colour: $colour-near-white;
$pagination-text-colour: $colour-near-black; $pagination-text-colour: $colour-near-black;

View File

@ -18,6 +18,10 @@ strong {
padding-top: 20px; padding-top: 20px;
} }
.error-text {
color: $error-text-colour;
}
.errors { .errors {
background-color: $box-error-background-colour; background-color: $box-error-background-colour;
border-radius: 2px; border-radius: 2px;

View File

@ -6,7 +6,8 @@ from .models import Source, Media
class SourceAdmin(admin.ModelAdmin): class SourceAdmin(admin.ModelAdmin):
ordering = ('-created',) ordering = ('-created',)
list_display = ('name', 'get_source_type_display', 'last_crawl', 'has_failed') list_display = ('uuid', 'name', 'source_type', 'last_crawl',
'has_failed')
readonly_fields = ('uuid', 'created') readonly_fields = ('uuid', 'created')
search_fields = ('uuid', 'key', 'name') search_fields = ('uuid', 'key', 'name')
@ -15,6 +16,6 @@ class SourceAdmin(admin.ModelAdmin):
class MediaAdmin(admin.ModelAdmin): class MediaAdmin(admin.ModelAdmin):
ordering = ('-created',) ordering = ('-created',)
list_display = ('key', 'source', 'can_download', 'downloaded') list_display = ('uuid', 'key', 'source', 'can_download', 'skip', 'downloaded')
readonly_fields = ('uuid', 'created') readonly_fields = ('uuid', 'created')
search_fields = ('uuid', 'source__key', 'key') search_fields = ('uuid', 'source__key', 'key')

View File

@ -2,4 +2,5 @@ from django.apps import AppConfig
class SyncConfig(AppConfig): class SyncConfig(AppConfig):
name = 'sync' name = 'sync'

View File

@ -27,3 +27,13 @@ class ConfirmDeleteSourceForm(forms.Form):
class RedownloadMediaForm(forms.Form): class RedownloadMediaForm(forms.Form):
pass pass
class SkipMediaForm(forms.Form):
pass
class EnableMediaForm(forms.Form):
pass

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.4 on 2020-12-11 03:06
import django.core.files.storage
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0019_auto_20201209_0857'),
]
operations = [
migrations.AddField(
model_name='media',
name='download_date',
field=models.DateTimeField(blank=True, db_index=True, help_text='Date and time the download completed', null=True, verbose_name='download date'),
),
migrations.AlterField(
model_name='media',
name='media_file',
field=models.FileField(blank=True, help_text='Media file', max_length=200, null=True, storage=django.core.files.storage.FileSystemStorage(location='/home/meeb/Repos/github.com/meeb/tubesync/tubesync/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
),
migrations.AlterUniqueTogether(
name='media',
unique_together={('source', 'key')},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2020-12-11 03:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0020_auto_20201211_0306'),
]
operations = [
migrations.AddField(
model_name='media',
name='downloaded_height',
field=models.PositiveIntegerField(blank=True, help_text='Height in pixels of the downloaded media', null=True, verbose_name='downloaded height'),
),
migrations.AddField(
model_name='media',
name='downloaded_width',
field=models.PositiveIntegerField(blank=True, help_text='Width in pixels of the downloaded media', null=True, verbose_name='downloaded width'),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 3.1.4 on 2020-12-11 03:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0021_auto_20201211_0351'),
]
operations = [
migrations.AddField(
model_name='media',
name='downloaded_format',
field=models.CharField(blank=True, help_text='Audio codec of the downloaded media', max_length=30, null=True, verbose_name='downloaded format'),
),
migrations.AlterField(
model_name='media',
name='downloaded_audio_codec',
field=models.CharField(blank=True, help_text='Audio codec of the downloaded media', max_length=30, null=True, verbose_name='downloaded audio codec'),
),
migrations.AlterField(
model_name='media',
name='downloaded_container',
field=models.CharField(blank=True, help_text='Container format of the downloaded media', max_length=30, null=True, verbose_name='downloaded container format'),
),
migrations.AlterField(
model_name='media',
name='downloaded_fps',
field=models.PositiveSmallIntegerField(blank=True, help_text='FPS of the downloaded media', null=True, verbose_name='downloaded fps'),
),
migrations.AlterField(
model_name='media',
name='downloaded_video_codec',
field=models.CharField(blank=True, help_text='Video codec of the downloaded media', max_length=30, null=True, verbose_name='downloaded video codec'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-11 04:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0022_auto_20201211_0354'),
]
operations = [
migrations.AddField(
model_name='media',
name='skip',
field=models.BooleanField(db_index=True, default=False, help_text='Media will be skipped and not downloaded', verbose_name='skip'),
),
]

View File

@ -16,7 +16,7 @@ from .matching import (get_best_combined_format, get_best_audio_format,
get_best_video_format) get_best_video_format)
media_file_storage = FileSystemStorage(location=settings.DOWNLOAD_ROOT) media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT))
class Source(models.Model): class Source(models.Model):
@ -491,16 +491,47 @@ class Media(models.Model):
storage=media_file_storage, storage=media_file_storage,
help_text=_('Media file') help_text=_('Media file')
) )
skip = models.BooleanField(
_('skip'),
db_index=True,
default=False,
help_text=_('Media will be skipped and not downloaded')
)
downloaded = models.BooleanField( downloaded = models.BooleanField(
_('downloaded'), _('downloaded'),
db_index=True, db_index=True,
default=False, default=False,
help_text=_('Media has been downloaded') help_text=_('Media has been downloaded')
) )
download_date = models.DateTimeField(
_('download date'),
db_index=True,
blank=True,
null=True,
help_text=_('Date and time the download completed')
)
downloaded_format = models.CharField(
_('downloaded format'),
max_length=30,
blank=True,
null=True,
help_text=_('Audio codec of the downloaded media')
)
downloaded_height = models.PositiveIntegerField(
_('downloaded height'),
blank=True,
null=True,
help_text=_('Height in pixels of the downloaded media')
)
downloaded_width = models.PositiveIntegerField(
_('downloaded width'),
blank=True,
null=True,
help_text=_('Width in pixels of the downloaded media')
)
downloaded_audio_codec = models.CharField( downloaded_audio_codec = models.CharField(
_('downloaded audio codec'), _('downloaded audio codec'),
max_length=30, max_length=30,
db_index=True,
blank=True, blank=True,
null=True, null=True,
help_text=_('Audio codec of the downloaded media') help_text=_('Audio codec of the downloaded media')
@ -508,7 +539,6 @@ class Media(models.Model):
downloaded_video_codec = models.CharField( downloaded_video_codec = models.CharField(
_('downloaded video codec'), _('downloaded video codec'),
max_length=30, max_length=30,
db_index=True,
blank=True, blank=True,
null=True, null=True,
help_text=_('Video codec of the downloaded media') help_text=_('Video codec of the downloaded media')
@ -516,14 +546,12 @@ class Media(models.Model):
downloaded_container = models.CharField( downloaded_container = models.CharField(
_('downloaded container format'), _('downloaded container format'),
max_length=30, max_length=30,
db_index=True,
blank=True, blank=True,
null=True, null=True,
help_text=_('Container format of the downloaded media') help_text=_('Container format of the downloaded media')
) )
downloaded_fps = models.PositiveSmallIntegerField( downloaded_fps = models.PositiveSmallIntegerField(
_('downloaded fps'), _('downloaded fps'),
db_index=True,
blank=True, blank=True,
null=True, null=True,
help_text=_('FPS of the downloaded media') help_text=_('FPS of the downloaded media')
@ -567,7 +595,7 @@ class Media(models.Model):
def get_best_video_format(self): def get_best_video_format(self):
return get_best_video_format(self) return get_best_video_format(self)
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
@ -593,6 +621,55 @@ class Media(models.Model):
return False return False
return False return False
def get_display_format(self, format_str):
'''
Returns a tuple used in the format component of the output filename. This
is the format(s) found by matching. Examples:
# Audio and video streams
('1080p', 'vp9', 'opus')
# Audio only stream
('opus',)
# Audio and video streams with additional flags
('720p', 'avc1', 'mp4a', '60fps', 'hdr')
'''
fmt = []
# If the download has completed use existing values
if self.downloaded:
if self.downloaded_format != 'audio':
fmt.append(self.downloaded_format.lower())
fmt.append(self.downloaded_video_codec.lower())
fmt.append(self.downloaded_audio_codec.lower())
if self.downloaded_format != 'audio':
fmt.append(str(self.downloaded_fps))
if self.downloaded_hdr:
fmt.append('hdr')
return fmt
# Otherwise, calculate from matched format codes
vformat = None
aformat = None
if '+' in format_str:
# Seperate audio and video streams
vformat_code, aformat_code = format_str.split('+')
vformat = self.get_format_by_code(vformat_code)
aformat = self.get_format_by_code(aformat_code)
else:
# Combined stream or audio only
cformat = self.get_format_by_code(format_str)
aformat = cformat
if cformat['vcodec']:
# Combined
vformat = cformat
if vformat:
fmt.append(vformat['format'].lower())
fmt.append(vformat['vcodec'].lower())
fmt.append(aformat['acodec'].lower())
if vformat:
if vformat['is_60fps']:
fmt.append('60fps')
if vformat['is_hdr']:
fmt.append('hdr')
return tuple(fmt)
def get_format_by_code(self, format_code): def get_format_by_code(self, format_code):
''' '''
Matches a format code, such as '22', to a processed format dict. Matches a format code, such as '22', to a processed format dict.
@ -671,18 +748,9 @@ class Media(models.Model):
name = slugify(self.name.replace('&', 'and').replace('+', 'and')) name = slugify(self.name.replace('&', 'and').replace('+', 'and'))
name = name.replace('_', '-')[:80] name = name.replace('_', '-')[:80]
key = self.key.strip().replace('_', '-')[:20] key = self.key.strip().replace('_', '-')[:20]
fmt = [] format_str = self.get_format_str()
if self.source.is_audio: format_tuple = self.get_display_format(format_str)
fmt.append(self.source.source_acodec.lower()) fmt = '-'.join(format_tuple)
else:
fmt.append(self.source.source_resolution.lower())
fmt.append(self.source.source_vcodec.lower())
fmt.append(self.source.source_acodec.lower())
if self.source.prefer_60fps:
fmt.append('60fps')
if self.source.prefer_hdr:
fmt.append('hdr')
fmt = '-'.join(fmt)
ext = self.source.extension ext = self.source.extension
return f'{datestr}_{source_name}_{name}_{key}_{fmt}.{ext}' return f'{datestr}_{source_name}_{name}_{key}_{fmt}.{ext}'
@ -721,7 +789,7 @@ class Media(models.Model):
def download_media(self): def download_media(self):
format_str = self.get_format_str() format_str = self.get_format_str()
if not format_str: if not format_str:
raise NoFormatException(f'Cannot download, media "{self.pk}" ({media}) has ' raise NoFormatException(f'Cannot download, media "{self.pk}" ({self}) has '
f'no valid format available') f'no valid format available')
# Download the media with youtube-dl # Download the media with youtube-dl
download_youtube_media(self.url, format_str, self.source.extension, download_youtube_media(self.url, format_str, self.source.extension,

View File

@ -37,7 +37,12 @@ def source_pre_save(sender, instance, **kwargs):
@receiver(post_save, sender=Source) @receiver(post_save, sender=Source)
def source_post_save(sender, instance, created, **kwargs): def source_post_save(sender, instance, created, **kwargs):
# Triggered after a source is saved, Create a new task to check the directory exists # Triggered after a source is saved, Create a new task to check the directory exists
check_source_directory_exists(str(instance.pk)) verbose_name = _('Check download directory exists for source "{}"')
check_source_directory_exists(
str(instance.pk),
priority=0,
verbose_name=verbose_name.format(instance.name)
)
if created: if created:
# Create a new indexing task for newly created sources # Create a new indexing task for newly created sources
delete_task_by_source('sync.tasks.index_source_task', instance.pk) delete_task_by_source('sync.tasks.index_source_task', instance.pk)
@ -86,14 +91,16 @@ 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, Recalculate the "can_download" flag, this may # Triggered after media is saved, Recalculate the "can_download" flag, this may
# need to change if the source specifications have been changed # need to change if the source specifications have been changed
post_save.disconnect(media_post_save, sender=Media)
if instance.get_format_str(): if instance.get_format_str():
if not instance.can_download: if not instance.can_download:
instance.can_download = True instance.can_download = True
instance.save() instance.save()
else: else:
if instance.can_download: if instance.can_download:
instance.can_download = True instance.can_download = False
instance.save() instance.save()
post_save.connect(media_post_save, sender=Media)
# If the media is missing a thumbnail schedule it to be downloaded # If the media is missing a thumbnail schedule it to be downloaded
if not instance.thumb: if not instance.thumb:
thumbnail_url = instance.thumbnail thumbnail_url = instance.thumbnail
@ -109,7 +116,7 @@ def media_post_save(sender, instance, created, **kwargs):
verbose_name=verbose_name.format(instance.name) verbose_name=verbose_name.format(instance.name)
) )
# If the media has not yet been downloaded schedule it to be downloaded # If the media has not yet been downloaded schedule it to be downloaded
if not instance.downloaded: if not instance.downloaded and instance.can_download and not instance.skip:
delete_task_by_media('sync.tasks.download_media', (str(instance.pk),)) delete_task_by_media('sync.tasks.download_media', (str(instance.pk),))
verbose_name = _('Downloading media for "{}"') verbose_name = _('Downloading media for "{}"')
download_media( download_media(

View File

@ -249,25 +249,37 @@ def download_media(media_id):
# Link the media file to the object and update info about the download # Link the media file to the object and update info about the download
media.media_file.name = str(media.filepath) media.media_file.name = str(media.filepath)
media.downloaded = True media.downloaded = True
media.download_date = timezone.now()
media.downloaded_filesize = os.path.getsize(media.filepath)
media.downloaded_container = container
if '+' in format_str: if '+' in format_str:
# Seperate audio and video streams
vformat_code, aformat_code = format_str.split('+') vformat_code, aformat_code = format_str.split('+')
aformat = media.get_format_by_code(aformat_code) aformat = media.get_format_by_code(aformat_code)
vformat = media.get_format_by_code(vformat_code) vformat = media.get_format_by_code(vformat_code)
media.downloaded_format = vformat['format']
media.downloaded_height = vformat['height']
media.downloaded_width = vformat['width']
media.downloaded_audio_codec = aformat['acodec'] media.downloaded_audio_codec = aformat['acodec']
media.downloaded_video_codec = vformat['vcodec'] media.downloaded_video_codec = vformat['vcodec']
media.downloaded_container = container media.downloaded_container = container
media.downloaded_fps = vformat['fps'] media.downloaded_fps = vformat['fps']
media.downloaded_hdr = vformat['is_hdr'] media.downloaded_hdr = vformat['is_hdr']
media.downloaded_filesize = os.path.getsize(media.filepath)
else: else:
# Combined stream or audio-only stream
cformat_code = format_str cformat_code = format_str
cformat = media.get_format_by_code(cformat_code) cformat = media.get_format_by_code(cformat_code)
media.downloaded_audio_codec = cformat['acodec'] media.downloaded_audio_codec = cformat['acodec']
media.downloaded_video_codec = cformat['vcodec'] if cformat['vcodec']:
media.downloaded_container = container # Combined
media.downloaded_fps = cformat['fps'] media.downloaded_format = vformat['format']
media.downloaded_hdr = cformat['is_hdr'] media.downloaded_height = cformat['height']
media.downloaded_filesize = os.path.getsize(media.filepath) media.downloaded_width = cformat['width']
media.downloaded_video_codec = cformat['vcodec']
media.downloaded_fps = cformat['fps']
media.downloaded_hdr = cformat['is_hdr']
else:
media.downloaded_format = 'audio'
media.save() media.save()
else: else:
# Expected file doesn't exist on disk # Expected file doesn't exist on disk

View File

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block headtitle %}Enable (unskip) media - {{ media }}{% endblock %}
{% block content %}
<div class="row no-margin-bottom">
<div class="col s12">
<h1>Enable (unskip) <strong>{{ media }}</strong></h1>
<p>
You can enable your previously skipped media <strong>{{ media }}</strong>. This
will re-enable the media to be downloaded.
</p>
</div>
</div>
<div class="row">
<form method="post" action="{% url 'sync:enable-media' pk=media.pk %}" class="col s12 simpleform">
{% csrf_token %}
{% include 'simpleform.html' with form=form %}
<div class="row no-margin-bottom padding-top">
<div class="col s12">
<button class="btn" type="submit" name="action">Enable and download media <i class="fas fa-cloud-download-alt"></i></button>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -12,6 +12,7 @@
</div> </div>
</div> </div>
{% if not media.can_download %}{% include 'errorbox.html' with message='Media cannot be downloaded because it has no formats which match the source requirements.' %}{% endif %} {% if not media.can_download %}{% include 'errorbox.html' with message='Media cannot be downloaded because it has no formats which match the source requirements.' %}{% endif %}
{% if media.skip %}{% include 'errorbox.html' with message='Media is marked to be skipped and will not be downloaded.' %}{% endif %}
{% include 'infobox.html' with message=message %} {% include 'infobox.html' with message=message %}
<div class="row"> <div class="row">
<div class="col s12 m7"> <div class="col s12 m7">
@ -63,10 +64,17 @@
<td class="hide-on-small-only">Fallback</td> <td class="hide-on-small-only">Fallback</td>
<td><span class="hide-on-med-and-up">Fallback<br></span><strong>{{ media.source.get_fallback_display }}</strong></td> <td><span class="hide-on-med-and-up">Fallback<br></span><strong>{{ media.source.get_fallback_display }}</strong></td>
</tr> </tr>
{% if media.skip %}
<tr title="Has the media been downloaded?">
<td class="hide-on-small-only">Skipping?</td>
<td><span class="hide-on-med-and-up">Skipping?<br></span><strong>{% if media.skip %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr>
{% else %}
<tr title="Has the media been downloaded?"> <tr title="Has the media been downloaded?">
<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>
{% endif %}
{% if media.downloaded %} {% if media.downloaded %}
<tr title="The filename the media will be downloaded as"> <tr title="The filename the media will be downloaded as">
<td class="hide-on-small-only">Filename</td> <td class="hide-on-small-only">Filename</td>
@ -106,8 +114,8 @@
{% for format in media.formats %} {% for format in media.formats %}
<div> <div>
ID: <strong>{{ format.format_id }}</strong> ID: <strong>{{ format.format_id }}</strong>
{% if format.vcodec|lower != 'none' %}, <strong>{{ format.format_note }} ({{ format.width }}x{{ format.height }})</strong>, fps:<strong>{{ format.fps|lower }}</strong>, video:<strong>{{ format.vcodec }} @{{ format.tbr }}k</strong>{% endif %} {% if format.vcodec|lower != 'none' %}, {{ format.format_note }} ({{ format.width }}x{{ format.height }}), fps:{{ format.fps|lower }}, video:{{ format.vcodec }} @{{ format.tbr }}k{% endif %}
{% if format.acodec|lower != 'none' %}, audio:<strong>{{ format.acodec }} @{{ format.abr }}k / {{ format.asr }}Hz</strong>{% endif %} {% if format.acodec|lower != 'none' %}, audio:{{ format.acodec }} @{{ format.abr }}k / {{ format.asr }}Hz{% endif %}
{% if format.format_id == combined_format or format.format_id == audio_format or format.format_id == video_format %}<strong>(matched)</strong>{% endif %} {% if format.format_id == combined_format or format.format_id == audio_format or format.format_id == video_format %}<strong>(matched)</strong>{% endif %}
</div> </div>
{% empty %} {% empty %}
@ -127,9 +135,18 @@
</div> </div>
</div> </div>
{% if media.downloaded %} {% if media.downloaded %}
<div class="row">
<div class="col s12 l6">
<a href="{% url 'sync:redownload-media' pk=media.pk %}" class="btn">Redownload media <i class="fas fa-cloud-download-alt"></i></a>
</div>
<div class="col s12 l6">
<a href="{% url 'sync:skip-media' pk=media.pk %}" class="btn delete-button">Delete and skip media <i class="fas fa-times-circle"></i></a>
</div>
</div>
{% elif media.skip %}
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<a href="{% url 'sync:redownload-media' pk=media.pk %}" class="btn">Delete and redownload media <i class="fas fa-cloud-download-alt"></i></a> <a href="{% url 'sync:enable-media' pk=media.pk %}" class="btn">Enable (unskip) media <i class="fas fa-cloud-download-alt"></i></a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block headtitle %}Skip media - {{ media }}{% endblock %}
{% block content %}
<div class="row no-margin-bottom">
<div class="col s12">
<h1>Delete and skip media <strong>{{ media }}</strong></h1>
<p>
You can delete the downloaded file for your media <strong>{{ media }}</strong> and
mark it to never be downloaded. You might want to do this if you don't want a local
copy of some media or want to skip a single video from a source.
</p>
</div>
</div>
<div class="row">
<form method="post" action="{% url 'sync:skip-media' pk=media.pk %}" class="col s12 simpleform">
{% csrf_token %}
{% include 'simpleform.html' with form=form %}
<div class="row no-margin-bottom padding-top">
<div class="col s12">
<button class="btn" type="submit" name="action">Really delete and skip media <i class="fas fa-trash-alt"></i></button>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -18,7 +18,19 @@
<img src="{% if m.thumb %}{% url 'sync:media-thumb' pk=m.pk %}{% else %}{% static 'images/nothumb.png' %}{% endif %}"> <img src="{% if m.thumb %}{% url 'sync:media-thumb' pk=m.pk %}{% else %}{% static 'images/nothumb.png' %}{% endif %}">
<span class="card-title truncate">{{ m.source }}<br> <span class="card-title truncate">{{ m.source }}<br>
<span>{{ m.name }}</span><br> <span>{{ m.name }}</span><br>
<span>{% if m.can_download %}{% if m.downloaded %}<i class="fas fa-check-circle" title="Downloaded"></i>{% else %}<i class="far fa-clock" title="Waiting to download or downloading"></i>{% endif %} {{ m.published|date:'Y-m-d' }}{% else %}<i class="fas fa-exclamation-triangle"></i> No matching formats{% endif %}</span> <span>
{% if m.downloaded %}
<i class="fas fa-check-circle" title="Downloaded"></i> {{ m.download_date|date:'Y-m-d' }}
{% else %}
{% if m.skip %}
<span class="error-text"><i class="fas fa-times" title="Skipping media"></i> Skipped</span>
{% elif m.can_download %}
<i class="far fa-clock" title="Waiting to download or downloading"></i> {{ m.published|date:'Y-m-d' }}
{% else %}
<span class="error-text"><i class="fas fa-exclamation-triangle" title="No matching formats to download"></i> No matching formats</span>
{% endif %}
{% endif %}
</span>
</span> </span>
</div> </div>
</a> </a>

View File

@ -1,8 +1,8 @@
from django.urls import path from django.urls import path
from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView, from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView,
SourceView, UpdateSourceView, DeleteSourceView, MediaView, SourceView, UpdateSourceView, DeleteSourceView, MediaView,
MediaThumbView, MediaItemView, MediaRedownloadView, TasksView, MediaThumbView, MediaItemView, MediaRedownloadView, MediaSkipView,
CompletedTasksView) MediaEnableView, TasksView, CompletedTasksView)
app_name = 'sync' app_name = 'sync'
@ -60,6 +60,14 @@ urlpatterns = [
MediaRedownloadView.as_view(), MediaRedownloadView.as_view(),
name='redownload-media'), name='redownload-media'),
path('media-skip/<uuid:pk>',
MediaSkipView.as_view(),
name='skip-media'),
path('media-enable/<uuid:pk>',
MediaEnableView.as_view(),
name='enable-media'),
# Task URLs # Task URLs
path('tasks', path('tasks',

View File

@ -161,6 +161,7 @@ def parse_media_format(format_dict):
'format': format_str, 'format': format_str,
'format_verbose': format_dict.get('format', ''), 'format_verbose': format_dict.get('format', ''),
'height': format_dict.get('height', 0), 'height': format_dict.get('height', 0),
'width': format_dict.get('width', 0),
'vcodec': vcodec, 'vcodec': vcodec,
'fps': format_dict.get('fps', 0), 'fps': format_dict.get('fps', 0),
'vbr': format_dict.get('tbr', 0), 'vbr': format_dict.get('tbr', 0),

View File

@ -15,7 +15,8 @@ from django.utils.translation import gettext_lazy as _
from common.utils import append_uri_params from common.utils import append_uri_params
from background_task.models import Task, CompletedTask from background_task.models import Task, CompletedTask
from .models import Source, Media from .models import Source, Media
from .forms import ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
SkipMediaForm, EnableMediaForm)
from .utils import validate_url, delete_file from .utils import validate_url, delete_file
from .tasks import (map_task_to_instance, get_error_message, from .tasks import (map_task_to_instance, get_error_message,
get_source_completed_tasks, get_media_download_task, get_source_completed_tasks, get_media_download_task,
@ -384,6 +385,8 @@ class MediaItemView(DetailView):
model = Media model = Media
messages = { messages = {
'redownloading': _('Media file has been deleted and scheduled to redownload'), 'redownloading': _('Media file has been deleted and scheduled to redownload'),
'skipped': _('Media file has been deleted and marked to never download'),
'enabled': _('Media has been re-enabled and will be downloaded'),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -457,6 +460,70 @@ class MediaRedownloadView(FormView, SingleObjectMixin):
return append_uri_params(url, {'message': 'redownloading'}) return append_uri_params(url, {'message': 'redownloading'})
class MediaSkipView(FormView, SingleObjectMixin):
template_name = 'sync/media-skip.html'
form_class = SkipMediaForm
model = Media
def __init__(self, *args, **kwargs):
self.object = None
super().__init__(*args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# Delete any active download tasks for the media
delete_task_by_media('sync.tasks.download_media', (str(self.object.pk),))
# If the media file exists on disk, delete it
if self.object.media_file_exists:
delete_file(self.object.media_file.path)
self.object.media_file = None
# Reset all download data
self.object.downloaded = False
self.object.downloaded_audio_codec = None
self.object.downloaded_video_codec = None
self.object.downloaded_container = None
self.object.downloaded_fps = None
self.object.downloaded_hdr = False
self.object.downloaded_filesize = None
# Mark it to be skipped
self.object.skip = True
self.object.save()
return super().form_valid(form)
def get_success_url(self):
url = reverse_lazy('sync:media-item', kwargs={'pk': self.object.pk})
return append_uri_params(url, {'message': 'skipped'})
class MediaEnableView(FormView, SingleObjectMixin):
template_name = 'sync/media-enable.html'
form_class = EnableMediaForm
model = Media
def __init__(self, *args, **kwargs):
self.object = None
super().__init__(*args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# Mark it as not skipped
self.object.skip = False
self.object.save()
return super().form_valid(form)
def get_success_url(self):
url = reverse_lazy('sync:media-item', kwargs={'pk': self.object.pk})
return append_uri_params(url, {'message': 'enabled'})
class TasksView(ListView): class TasksView(ListView):
''' '''
A list of tasks queued to be completed. This is, for example, scraping for new A list of tasks queued to be completed. This is, for example, scraping for new

View File

@ -53,8 +53,8 @@ def download_media(url, media_format, extension, output_file):
if event['status'] == 'error': if event['status'] == 'error':
log.error(f'[youtube-dl] error occured downloading: {filename}') log.error(f'[youtube-dl] error occured downloading: {filename}')
elif event['status'] == 'downloading': elif event['status'] == 'downloading':
p = round((event['downloaded_bytes'] / event['total_bytes']) * 100, -1) p = round((event['downloaded_bytes'] / event['total_bytes']) * 100)
if p > hook.download_progress: if (p % 5 == 0) and p > hook.download_progress:
hook.download_progress = p hook.download_progress = p
eta = event.get('_eta_str', '?').strip() eta = event.get('_eta_str', '?').strip()
percent_done = event.get('_percent_str', '?').strip() percent_done = event.get('_percent_str', '?').strip()