From 060691202bf4d6788fb7b61eee66fa9503642dab Mon Sep 17 00:00:00 2001 From: meeb Date: Sat, 28 Nov 2020 22:29:42 +1100 Subject: [PATCH] delete source page, start of youtube-dl wrapper --- app/common/static/styles/_colours.scss | 2 +- app/sync/forms.py | 11 +++++- app/sync/models.py | 12 ++++--- app/sync/templates/sync/source-delete.html | 28 +++++++++++++++ app/sync/templates/sync/source.html | 2 +- app/sync/urls.py | 11 ++++-- app/sync/views.py | 32 +++++++++++++++-- app/sync/youtube.py | 41 ++++++++++++++++++++++ app/tubesync/settings.py | 5 +++ 9 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 app/sync/templates/sync/source-delete.html create mode 100644 app/sync/youtube.py diff --git a/app/common/static/styles/_colours.scss b/app/common/static/styles/_colours.scss index e3cd44e..b5f4b70 100644 --- a/app/common/static/styles/_colours.scss +++ b/app/common/static/styles/_colours.scss @@ -36,7 +36,7 @@ $form-error-text-colour: $colour-near-white; $form-help-text-colour: $colour-light-blue; $form-delete-button-background-colour: $colour-red; -$collection-no-items-text-colour: $colour-light-blue; +$collection-no-items-text-colour: $colour-near-black; $collection-background-hover-colour: $colour-orange; $collection-text-hover-colour: $colour-near-white; diff --git a/app/sync/forms.py b/app/sync/forms.py index d0c3298..e09cc8d 100644 --- a/app/sync/forms.py +++ b/app/sync/forms.py @@ -1,5 +1,6 @@ from django import forms +from django.utils.translation import gettext_lazy as _ class ValidateSourceForm(forms.Form): @@ -10,6 +11,14 @@ class ValidateSourceForm(forms.Form): widget=forms.HiddenInput() ) source_url = forms.URLField( - label='Source URL', + label=_('Source URL'), required=True ) + + +class ConfirmDeleteSourceForm(forms.Form): + + delete_media = forms.BooleanField( + label=_('Also delete downloaded media'), + required=False + ) diff --git a/app/sync/models.py b/app/sync/models.py index 5e1e29c..befeb92 100644 --- a/app/sync/models.py +++ b/app/sync/models.py @@ -66,7 +66,7 @@ class Source(models.Model): } URLS = { - SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/{key}', + SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/c/{key}', SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/playlist?list={key}', } @@ -172,11 +172,15 @@ class Source(models.Model): @property def icon(self): return self.ICONS.get(self.source_type) - + + @classmethod + def create_url(obj, source_type, key): + url = obj.URLS.get(source_type) + return url.format(key=key) + @property def url(self): - url = self.URLS.get(self.source_type) - return url.format(key=self.key) + return Source.create_url(self.source_type, self.key) @property def directory_path(self): diff --git a/app/sync/templates/sync/source-delete.html b/app/sync/templates/sync/source-delete.html new file mode 100644 index 0000000..bdc9520 --- /dev/null +++ b/app/sync/templates/sync/source-delete.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block headtitle %}Delete source - {{ source.name }}{% endblock %} + +{% block content %} +
+
+

Delete source {{ source.name }}

+

+ Are you sure you want to delete this source? Deleting a source is permanent. + By default, deleting a source does not delete any saved media files. You can + tick the "also delete downloaded media" checkbox to also remove save + media when you delete the source. Deleting a source cannot be undone. +

+
+
+
+
+ {% csrf_token %} + {% include 'simpleform.html' with form=form %} +
+
+ +
+
+
+
+{% endblock %} diff --git a/app/sync/templates/sync/source.html b/app/sync/templates/sync/source.html index 7c9c3b0..e48e8f2 100644 --- a/app/sync/templates/sync/source.html +++ b/app/sync/templates/sync/source.html @@ -80,7 +80,7 @@ Edit source
- Delete source + Delete source
{% endblock %} diff --git a/app/sync/urls.py b/app/sync/urls.py index b6e48ec..d6ee938 100644 --- a/app/sync/urls.py +++ b/app/sync/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView, - SourceView, UpdateSourceView, MediaView, TasksView, LogsView) + SourceView, UpdateSourceView, DeleteSourceView, MediaView, + TasksView, LogsView) app_name = 'sync' @@ -36,9 +37,13 @@ urlpatterns = [ UpdateSourceView.as_view(), name='update-source'), - # Media URLs + path('source-delete/', + DeleteSourceView.as_view(), + name='delete-source'), - path('media', + # Media URLs (note /media/ is the static media URL, don't use that) + + path('mediafiles', MediaView.as_view(), name='media'), diff --git a/app/sync/views.py b/app/sync/views.py index 13267c4..8fabbbe 100644 --- a/app/sync/views.py +++ b/app/sync/views.py @@ -1,15 +1,17 @@ from django.conf import settings from django.http import Http404 from django.views.generic import TemplateView, ListView, DetailView -from django.views.generic.edit import FormView, CreateView, UpdateView +from django.views.generic.edit import (FormView, FormMixin, CreateView, UpdateView, + DeleteView) from django.urls import reverse_lazy from django.forms import ValidationError from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from common.utils import append_uri_params from .models import Source -from .forms import ValidateSourceForm +from .forms import ValidateSourceForm, ConfirmDeleteSourceForm from .utils import validate_url +from . import youtube class DashboardView(TemplateView): @@ -155,7 +157,7 @@ class ValidateSourceView(FormView): ValidationError(self.errors['invalid_source']) ) source_url = form.cleaned_data['source_url'] - validation_url = self.validation_urls.get(self.source_type) + validation_url = self.validation_urls.get(source_type) try: self.key = validate_url(source_url, validation_url) except ValidationError as e: @@ -246,6 +248,30 @@ class UpdateSourceView(UpdateView): return append_uri_params(url, {'message': 'source-updated'}) +class DeleteSourceView(DeleteView, FormMixin): + ''' + Confirm the deletion of a source with an option to delete all the media + associated with the source from disk when the source is deleted. + ''' + + template_name = 'sync/source-delete.html' + model = Source + form_class = ConfirmDeleteSourceForm + context_object_name = 'source' + + def post(self, request, *args, **kwargs): + delete_media_val = request.POST.get('delete_media', False) + delete_media = True if delete_media_val is not False else False + if delete_media: + # TODO: delete media files from disk linked to this source + pass + return super().post(request, *args, **kwargs) + + def get_success_url(self): + url = reverse_lazy('sync:sources') + return append_uri_params(url, {'message': 'source-deleted'}) + + class MediaView(TemplateView): ''' A bare list of media added with their states. diff --git a/app/sync/youtube.py b/app/sync/youtube.py new file mode 100644 index 0000000..27de315 --- /dev/null +++ b/app/sync/youtube.py @@ -0,0 +1,41 @@ +''' + Wrapper for the youtube-dl library. Used so if there are any library interface + updates we only need to udpate them in one place. +''' + + +from django.conf import settings +import youtube_dl + + +_defaults = getattr(settings, 'YOUTUBE_DEFAULTS', {}) + + +class YouTubeError(youtube_dl.utils.DownloadError): + ''' + Generic wrapped error for all errors that could be raised by youtube-dl. + ''' + + pass + + +def extract_info(url): + ''' + Extracts information from a YouTube URL and returns it as a dict. For a channel + or playlist this returns a dict of all the videos on the channel or playlist + as well as associated metadata. + ''' + opts = _defaults.update({ + 'skip_download': True, + 'forcejson': True, + 'simulate': True, + 'extract_flat': 'in_playlist', + 'playlist_items': 1, + }) + response = {} + with youtube_dl.YoutubeDL(opts) as y: + try: + response = y.extract_info(url, download=False) + except youtube_dl.utils.DownloadError as e: + raise YouTubeError(f'Failed to extract_info for "{url}": {e}') from e + return response diff --git a/app/tubesync/settings.py b/app/tubesync/settings.py index 0e8256c..70d65d7 100644 --- a/app/tubesync/settings.py +++ b/app/tubesync/settings.py @@ -119,6 +119,11 @@ DJANGO_SIMPLE_TASK_WORKERS = 2 SOURCES_PER_PAGE = 25 +YOUTUBE_DEFAULTS = { + 'age_limit': 99, +} + + try: from .local_settings import * except ImportError as e: