diff --git a/app/common/static/styles/_colours.scss b/app/common/static/styles/_colours.scss index a71d43f..153cf92 100644 --- a/app/common/static/styles/_colours.scss +++ b/app/common/static/styles/_colours.scss @@ -10,7 +10,7 @@ $background-colour: $colour-near-white; $text-colour: $colour-near-black; $header-background-colour: $colour-red; -$header-text-colour: $colour-white; +$header-text-colour: $colour-near-white; $nav-background-colour: $colour-near-black; $nav-text-colour: $colour-near-white; @@ -18,10 +18,10 @@ $nav-link-background-hover-colour: $colour-orange; $main-button-background-colour: $colour-light-blue; $main-button-background-hover-colour: $colour-orange; -$main-button-text-colour: $colour-white; +$main-button-text-colour: $colour-near-white; $footer-background-colour: $colour-red; -$footer-text-colour: $colour-white; +$footer-text-colour: $colour-near-white; $footer-link-colour: $colour-near-black; $footer-link-hover-colour: $colour-orange; @@ -32,6 +32,11 @@ $form-select-border-colour: $colour-light-blue; $form-error-background-colour: $colour-red; $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-background-hover-colour: $colour-orange; +$collection-text-hover-colour: $colour-near-white; $box-error-background-colour: $colour-red; -$box-error-text-colour: $colour-white; +$box-error-text-colour: $colour-near-white; diff --git a/app/common/static/styles/_forms.scss b/app/common/static/styles/_forms.scss index f0bfc6e..03a73cd 100644 --- a/app/common/static/styles/_forms.scss +++ b/app/common/static/styles/_forms.scss @@ -39,3 +39,10 @@ select { border: 2px $form-select-border-colour solid; height: initial !important; } + +.delete-button { + background-color: $form-delete-button-background-colour !important; + &:hover { + background-color: $main-button-background-hover-colour !important; + } +} diff --git a/app/common/static/styles/_helpers.scss b/app/common/static/styles/_helpers.scss index 082ef8a..ee6a139 100644 --- a/app/common/static/styles/_helpers.scss +++ b/app/common/static/styles/_helpers.scss @@ -10,6 +10,10 @@ strong { margin-bottom: 0 !important; } +.margin-bottom { + margin-bottom: 20px !important; +} + .errors { background-color: $box-error-background-colour; border-radius: 2px; diff --git a/app/common/static/styles/_template.scss b/app/common/static/styles/_template.scss index 3bddd0f..d81429a 100644 --- a/app/common/static/styles/_template.scss +++ b/app/common/static/styles/_template.scss @@ -64,19 +64,34 @@ main { h1 { margin: 0; - padding: 0; + padding: 0 0 0.5rem 0; font-size: 2rem; } .btn { width: 100%; - background-color: $main-button-background-colour !important; + background-color: $main-button-background-colour; color: $main-button-text-colour !important; i { font-size: 0.9rem; } &:hover { - background-color: $main-button-background-hover-colour !important; + background-color: $main-button-background-hover-colour; + } + } + + .collection { + .collection-item { + display: block; + } + a.collection-item { + &:hover { + background-color: $collection-background-hover-colour !important; + color: $collection-text-hover-colour !important; + } + } + .no-items { + color: $collection-no-items-text-colour; } } diff --git a/app/sync/migrations/0003_auto_20201127_0838.py b/app/sync/migrations/0003_auto_20201127_0838.py new file mode 100644 index 0000000..b8f364e --- /dev/null +++ b/app/sync/migrations/0003_auto_20201127_0838.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2020-11-27 08:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0002_auto_20201126_0504'), + ] + + operations = [ + migrations.AlterField( + model_name='source', + name='key', + field=models.CharField(db_index=True, help_text='Source key, such as exact YouTube channel name or playlist ID', max_length=100, unique=True, verbose_name='key'), + ), + ] diff --git a/app/sync/migrations/0004_remove_source_url.py b/app/sync/migrations/0004_remove_source_url.py new file mode 100644 index 0000000..6c18299 --- /dev/null +++ b/app/sync/migrations/0004_remove_source_url.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.3 on 2020-11-28 03:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0003_auto_20201127_0838'), + ] + + operations = [ + migrations.RemoveField( + model_name='source', + name='url', + ), + ] diff --git a/app/sync/models.py b/app/sync/models.py index 09c155b..5e1e29c 100644 --- a/app/sync/models.py +++ b/app/sync/models.py @@ -1,4 +1,5 @@ import uuid +from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ @@ -59,6 +60,16 @@ class Source(models.Model): (FALLBACK_NEXT_HD, _('Get next best HD media instead')), ) + ICONS = { + SOURCE_TYPE_YOUTUBE_CHANNEL: '', + SOURCE_TYPE_YOUTUBE_PLAYLIST: '', + } + + URLS = { + SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/{key}', + SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/playlist?list={key}', + } + uuid = models.UUIDField( _('uuid'), primary_key=True, @@ -87,15 +98,11 @@ class Source(models.Model): default=SOURCE_TYPE_YOUTUBE_CHANNEL, help_text=_('Source type') ) - url = models.URLField( - _('url'), - db_index=True, - help_text=_('URL of the source') - ) key = models.CharField( _('key'), max_length=100, db_index=True, + unique=True, help_text=_('Source key, such as exact YouTube channel name or playlist ID') ) name = models.CharField( @@ -162,6 +169,22 @@ class Source(models.Model): verbose_name = _('Source') verbose_name_plural = _('Sources') + @property + def icon(self): + return self.ICONS.get(self.source_type) + + @property + def url(self): + url = self.URLS.get(self.source_type) + return url.format(key=self.key) + + @property + def directory_path(self): + if self.source_profile == self.SOURCE_PROFILE_AUDIO: + return settings.SYNC_AUDIO_ROOT / self.directory + else: + return settings.SYNC_VIDEO_ROOT / self.directory + def get_media_thumb_path(instance, filename): fileid = str(instance.uuid) diff --git a/app/sync/templates/sync/source.html b/app/sync/templates/sync/source.html new file mode 100644 index 0000000..3b3b3c3 --- /dev/null +++ b/app/sync/templates/sync/source.html @@ -0,0 +1,86 @@ +{% extends 'base.html' %} + +{% block headtitle %}Source - {{ source.name }}{% endblock %} + +{% block content %} +
+
+

Source: {{ source.name }}

+

{{ source.url }}

+

Saving to: {{ source.directory_path }}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if source.delete_old_media and source.days_to_keep > 0 %} + + + + + {% else %} + + + + + {% endif %} + + + + +
TypeType
{{ source.get_source_type_display }}
NameName
{{ source.name }}
KeyKey
{{ source.key }}
DirectoryDirectory
{{ source.directory }}
CreatedCreated
{{ source.created|date:'Y-m-d H-I-S' }}
Last crawlLast crawl
{% if source.last_crawl %}{{ source.last_crawl|date:'Y-m-d H-I-S' }}{% else %}Never{% endif %}
Source profileSource profile
{{ source.get_source_profile_display }}
Prefer 60FPS?Prefer 60FPS?
{% if source.prefer_60fps %}{% else %}{% endif %}
Prefer HDR?Prefer HDR?
{% if source.prefer_hdr %}{% else %}{% endif %}
Output formatOutput format
{{ source.get_output_format_display }}
FallbackFallback
{{ source.get_fallback_display }}
Delete old mediaDelete old media
After {{ source.days_to_keep }} days
Delete old mediaDelete old media
No, keep forever
UUIDUUID
{{ source.uuid }}
+
+
+
+
+ Edit source +
+
+ Delete source +
+
+{% endblock %} diff --git a/app/sync/templates/sync/sources.html b/app/sync/templates/sync/sources.html index 30082cd..8abeaaa 100644 --- a/app/sync/templates/sync/sources.html +++ b/app/sync/templates/sync/sources.html @@ -3,12 +3,28 @@ {% block headtitle %}Sources{% endblock %} {% block content %} -
-
- Add a YouTube channel +
+ - + {% endblock %} diff --git a/app/sync/urls.py b/app/sync/urls.py index 818fd9b..9bc9864 100644 --- a/app/sync/urls.py +++ b/app/sync/urls.py @@ -1,6 +1,6 @@ from django.urls import path from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView, - MediaView, TasksView, LogsView) + SourceView, MediaView, TasksView, LogsView) app_name = 'sync' @@ -24,6 +24,10 @@ urlpatterns = [ AddSourceView.as_view(), name='add-source'), + path('source/', + SourceView.as_view(), + name='source'), + path('media', MediaView.as_view(), name='media'), diff --git a/app/sync/views.py b/app/sync/views.py index 52cadae..1338863 100644 --- a/app/sync/views.py +++ b/app/sync/views.py @@ -1,5 +1,6 @@ +from django.conf import settings from django.http import Http404 -from django.views.generic import TemplateView +from django.views.generic import TemplateView, ListView, DetailView from django.views.generic.edit import FormView, CreateView from django.urls import reverse_lazy from django.forms import ValidationError @@ -22,16 +23,37 @@ class DashboardView(TemplateView): return super().dispatch(request, *args, **kwargs) -class SourcesView(TemplateView): +class SourcesView(ListView): ''' A bare list of the sources which have been created with their states. ''' template_name = 'sync/sources.html' + context_object_name = 'sources' + paginate_by = settings.SOURCES_PER_PAGE + messages = { + 'source-added': _('Your new source has been added'), + 'source-deleted': _('Your selected source has been deleted.'), + 'source-updated': _('Your selected source has been updated.'), + } + + def __init__(self, *args, **kwargs): + self.message = None + super().__init__(*args, **kwargs) def dispatch(self, request, *args, **kwargs): + message_key = request.GET.get('message', '') + self.message = self.messages.get(message_key, '') return super().dispatch(request, *args, **kwargs) + def get_queryset(self): + return Source.objects.all().order_by('name') + + def get_context_data(self, *args, **kwargs): + data = super().get_context_data(*args, **kwargs) + data['message'] = self.message + return data + class ValidateSourceView(FormView): ''' @@ -64,16 +86,15 @@ class ValidateSourceView(FormView): ), Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _( 'Enter a YouTube playlist URL into the box below. A playlist URL will be ' - 'in the format of https://www.youtube.com/watch?v=AAAAAA&list=' + 'in the format of https://www.youtube.com/playlist?list=' 'BiGLoNgUnIqUeId where BiGLoNgUnIqUeId is the ' 'unique ID of the playlist you want to add.' ), } help_examples = { Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/google', - Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('https://www.youtube.com/watch?v=DcKEPl' - '-MpLA&list=PL590L5WQmH8dpP0RyH5pCfIaDE' - 'dt9nk7r') + Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('https://www.youtube.com/playlist?list=' + 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r') } validation_urls = { Source.SOURCE_TYPE_YOUTUBE_CHANNEL: { @@ -87,10 +108,10 @@ class ValidateSourceView(FormView): Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: { 'scheme': 'https', 'domain': 'www.youtube.com', - 'path_regex': '^\/watch$', - 'qs_args': ['v', 'list'], + 'path_regex': '^\/playlist$', + 'qs_args': ['list'], 'extract_key': ('qs_args', 'list'), - 'example': 'https://www.youtube.com/watch?v=VIDEOID&list=PLAYLISTID' + 'example': 'https://www.youtube.com/playlist?list=PLAYLISTID' }, } prepopulate_fields = { @@ -186,7 +207,7 @@ class AddSourceView(CreateView): self.prepopulated_data['source_type'] = source_type key = request.GET.get('key', '') if key: - self.prepopulated_data['key'] = slugify(key) + self.prepopulated_data['key'] = key.strip() name = request.GET.get('name', '') if name: self.prepopulated_data['name'] = slugify(name) @@ -201,6 +222,16 @@ class AddSourceView(CreateView): initial[k] = v return initial + def get_success_url(self): + url = reverse_lazy('sync:sources') + return append_uri_params(url, {'message': 'source-created'}) + + +class SourceView(DetailView): + + template_name = 'sync/source.html' + model = Source + class MediaView(TemplateView): ''' diff --git a/app/tubesync/local_settings.py.container b/app/tubesync/local_settings.py.container index c0fc918..af049fc 100644 --- a/app/tubesync/local_settings.py.container +++ b/app/tubesync/local_settings.py.container @@ -1,4 +1,9 @@ import os +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parent.parent +ROOT_DIR = Path('/') SECRET_KEY = str(os.getenv('DJANGO_SECRET_KEY', '')) @@ -12,3 +17,7 @@ DATABASES = { 'NAME': '/config/db.sqlite3', } } + + +SYNC_VIDEO_ROOT = ROOT_DIR / 'downloads' / 'video' +SYNC_AUDIO_ROOT = ROOT_DIR / 'downloads' / 'audio' diff --git a/app/tubesync/settings.py b/app/tubesync/settings.py index 79fa22c..2189fd3 100644 --- a/app/tubesync/settings.py +++ b/app/tubesync/settings.py @@ -98,6 +98,8 @@ STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'static' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' +SYNC_VIDEO_ROOT = BASE_DIR / 'downloads' / 'video' +SYNC_AUDIO_ROOT = BASE_DIR / 'downloads' / 'audio' SASS_PROCESSOR_ROOT = STATIC_ROOT @@ -114,6 +116,9 @@ HEALTHCHECK_ALLOWED_IPS = ('127.0.0.1',) DJANGO_SIMPLE_TASK_WORKERS = 2 +SOURCES_PER_PAGE = 50 + + try: from .local_settings import * except ImportError as e: