source detail page

This commit is contained in:
meeb 2020-11-28 14:41:17 +11:00
parent 22d040bc8f
commit b347959083
13 changed files with 268 additions and 28 deletions

View File

@ -10,7 +10,7 @@ $background-colour: $colour-near-white;
$text-colour: $colour-near-black; $text-colour: $colour-near-black;
$header-background-colour: $colour-red; $header-background-colour: $colour-red;
$header-text-colour: $colour-white; $header-text-colour: $colour-near-white;
$nav-background-colour: $colour-near-black; $nav-background-colour: $colour-near-black;
$nav-text-colour: $colour-near-white; $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-colour: $colour-light-blue;
$main-button-background-hover-colour: $colour-orange; $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-background-colour: $colour-red;
$footer-text-colour: $colour-white; $footer-text-colour: $colour-near-white;
$footer-link-colour: $colour-near-black; $footer-link-colour: $colour-near-black;
$footer-link-hover-colour: $colour-orange; $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-background-colour: $colour-red;
$form-error-text-colour: $colour-near-white; $form-error-text-colour: $colour-near-white;
$form-help-text-colour: $colour-light-blue; $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-background-colour: $colour-red;
$box-error-text-colour: $colour-white; $box-error-text-colour: $colour-near-white;

View File

@ -39,3 +39,10 @@ select {
border: 2px $form-select-border-colour solid; border: 2px $form-select-border-colour solid;
height: initial !important; height: initial !important;
} }
.delete-button {
background-color: $form-delete-button-background-colour !important;
&:hover {
background-color: $main-button-background-hover-colour !important;
}
}

View File

@ -10,6 +10,10 @@ strong {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.margin-bottom {
margin-bottom: 20px !important;
}
.errors { .errors {
background-color: $box-error-background-colour; background-color: $box-error-background-colour;
border-radius: 2px; border-radius: 2px;

View File

@ -64,19 +64,34 @@ main {
h1 { h1 {
margin: 0; margin: 0;
padding: 0; padding: 0 0 0.5rem 0;
font-size: 2rem; font-size: 2rem;
} }
.btn { .btn {
width: 100%; width: 100%;
background-color: $main-button-background-colour !important; background-color: $main-button-background-colour;
color: $main-button-text-colour !important; color: $main-button-text-colour !important;
i { i {
font-size: 0.9rem; font-size: 0.9rem;
} }
&:hover { &: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;
} }
} }

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -1,4 +1,5 @@
import uuid import uuid
from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ 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')), (FALLBACK_NEXT_HD, _('Get next best HD media instead')),
) )
ICONS = {
SOURCE_TYPE_YOUTUBE_CHANNEL: '<i class="fab fa-youtube"></i>',
SOURCE_TYPE_YOUTUBE_PLAYLIST: '<i class="fab fa-youtube"></i>',
}
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 = models.UUIDField(
_('uuid'), _('uuid'),
primary_key=True, primary_key=True,
@ -87,15 +98,11 @@ class Source(models.Model):
default=SOURCE_TYPE_YOUTUBE_CHANNEL, default=SOURCE_TYPE_YOUTUBE_CHANNEL,
help_text=_('Source type') help_text=_('Source type')
) )
url = models.URLField(
_('url'),
db_index=True,
help_text=_('URL of the source')
)
key = models.CharField( key = models.CharField(
_('key'), _('key'),
max_length=100, max_length=100,
db_index=True, db_index=True,
unique=True,
help_text=_('Source key, such as exact YouTube channel name or playlist ID') help_text=_('Source key, such as exact YouTube channel name or playlist ID')
) )
name = models.CharField( name = models.CharField(
@ -162,6 +169,22 @@ class Source(models.Model):
verbose_name = _('Source') verbose_name = _('Source')
verbose_name_plural = _('Sources') 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): def get_media_thumb_path(instance, filename):
fileid = str(instance.uuid) fileid = str(instance.uuid)

View File

@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block headtitle %}Source - {{ source.name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<h1 class="truncate">Source: {{ source.name }}</h1>
<p class="truncate"><strong><a href="{{ source.url }}" target="_blank">{{ source.url }}</a></strong></p>
<p class="truncate">Saving to: <strong>{{ source.directory_path }}</strong></p>
</div>
</div>
<div class="row">
<div class="col s12">
<table class="striped">
<tr title="The source type">
<td class="hide-on-small-only">Type</td>
<td><span class="hide-on-med-and-up">Type<br></span><strong>{{ source.get_source_type_display }}</strong></td>
</tr>
<tr title="Name of the souce in TubeSync for your reference">
<td class="hide-on-small-only">Name</td>
<td><span class="hide-on-med-and-up">Name<br></span><strong>{{ source.name }}</strong></td>
</tr>
<tr title="Unique key of the source, such as the channel name or playlist ID">
<td class="hide-on-small-only">Key</td>
<td><span class="hide-on-med-and-up">Key<br></span><strong>{{ source.key }}</strong></td>
</tr>
<tr title="Directory the source will save media to">
<td class="hide-on-small-only">Directory</td>
<td><span class="hide-on-med-and-up">Directory<br></span><strong>{{ source.directory }}</strong></td>
</tr>
<tr title="When then source was created locally in TubeSync">
<td class="hide-on-small-only">Created</td>
<td><span class="hide-on-med-and-up">Created<br></span><strong>{{ source.created|date:'Y-m-d H-I-S' }}</strong></td>
</tr>
<tr title="When the source last checked for available media">
<td class="hide-on-small-only">Last crawl</td>
<td><span class="hide-on-med-and-up">Last crawl<br></span><strong>{% if source.last_crawl %}{{ source.last_crawl|date:'Y-m-d H-I-S' }}{% else %}Never{% endif %}</strong></td>
</tr>
<tr title="Quality and type of media the source will attempt to sync">
<td class="hide-on-small-only">Source profile</td>
<td><span class="hide-on-med-and-up">Source profile<br></span><strong>{{ source.get_source_profile_display }}</strong></td>
</tr>
<tr title="If available from the source media in 60FPS will be preferred">
<td class="hide-on-small-only">Prefer 60FPS?</td>
<td><span class="hide-on-med-and-up">Prefer 60FPS?<br></span><strong>{% if source.prefer_60fps %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr>
<tr title="If available from the source media in HDR will be preferred">
<td class="hide-on-small-only">Prefer HDR?</td>
<td><span class="hide-on-med-and-up">Prefer HDR?<br></span><strong>{% if source.prefer_hdr %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr>
<tr title="Output file container format to sync media in">
<td class="hide-on-small-only">Output format</td>
<td><span class="hide-on-med-and-up">Output format<br></span><strong>{{ source.get_output_format_display }}</strong></td>
</tr>
<tr title="What to do if your source profile is unavailable">
<td class="hide-on-small-only">Fallback</td>
<td><span class="hide-on-med-and-up">Fallback<br></span><strong>{{ source.get_fallback_display }}</strong></td>
</tr>
{% if source.delete_old_media and source.days_to_keep > 0 %}
<tr title="Days after which your media from this source will be locally deleted">
<td class="hide-on-small-only">Delete old media</td>
<td><span class="hide-on-med-and-up">Delete old media<br></span><strong>After {{ source.days_to_keep }} days</strong></td>
</tr>
{% else %}
<tr title="Media from this source will never be deleted">
<td class="hide-on-small-only">Delete old media</td>
<td><span class="hide-on-med-and-up">Delete old media<br></span><strong>No, keep forever</strong></td>
</tr>
{% endif %}
<tr title="Unique ID used for this source in TubeSync">
<td class="hide-on-small-only">UUID</td>
<td><span class="hide-on-med-and-up">UUID<br></span><strong>{{ source.uuid }}</strong></td>
</tr>
</table>
</div>
</div>
<div class="row no-margin-bottom">
<div class="col s12 l6 margin-bottom">
<a href="" class="btn"><i class="fas fa-pen-square"></i> Edit source</a>
</div>
<div class="col s12 l6 margin-bottom">
<a href="" class="btn delete-button"><i class="fas fa-trash-alt"></i> Delete source</a>
</div>
</div>
{% endblock %}

View File

@ -3,12 +3,28 @@
{% block headtitle %}Sources{% endblock %} {% block headtitle %}Sources{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row no-margin-bottom">
<div class="col s12 l6"> <div class="col s12 l6 margin-bottom">
<a href="{% url 'sync:validate-source' source_type='youtube-channel' %}" class="btn"><i class="fas fa-plus"></i> Add a YouTube channel</a> <a href="{% url 'sync:validate-source' source_type='youtube-channel' %}" class="btn"><i class="fab fa-youtube"></i> Add a YouTube channel</a>
</div> </div>
<div class="col s12 l6"> <div class="col s12 l6 margin-bottom">
<a href="{% url 'sync:validate-source' source_type='youtube-playlist' %}" class="btn"><i class="fas fa-plus"></i> Add a YouTube playlist</a> <a href="{% url 'sync:validate-source' source_type='youtube-playlist' %}" class="btn"><i class="fab fa-youtube"></i> Add a YouTube playlist</a>
</div>
</div>
<div class="row no-margin-bottom">
<div class="col s12">
<div class="collection">
{% for source in sources %}
<a href="{% url 'sync:source' pk=source.pk %}" class="collection-item">
{{ source.icon|safe }} <strong>{{ source.name }}</strong><br>
{{ source.get_source_type_display }}<br>
Sync {{ source.get_source_profile_display }} media in a {{ source.get_output_format_display }}
{% if source.delete_old_media and source.days_to_keep > 0 %}Delete media after {{ source.days_to_keep }} days{% endif %}
</a>
{% empty %}
<span class="collection-item no-items"><i class="fas fa-info-circle"></i> You haven't added any sources.</span>
{% endfor %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView, from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView,
MediaView, TasksView, LogsView) SourceView, MediaView, TasksView, LogsView)
app_name = 'sync' app_name = 'sync'
@ -24,6 +24,10 @@ urlpatterns = [
AddSourceView.as_view(), AddSourceView.as_view(),
name='add-source'), name='add-source'),
path('source/<uuid:pk>',
SourceView.as_view(),
name='source'),
path('media', path('media',
MediaView.as_view(), MediaView.as_view(),
name='media'), name='media'),

View File

@ -1,5 +1,6 @@
from django.conf import settings
from django.http import Http404 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.views.generic.edit import FormView, CreateView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.forms import ValidationError from django.forms import ValidationError
@ -22,16 +23,37 @@ class DashboardView(TemplateView):
return super().dispatch(request, *args, **kwargs) 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. A bare list of the sources which have been created with their states.
''' '''
template_name = 'sync/sources.html' 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): 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) 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): class ValidateSourceView(FormView):
''' '''
@ -64,16 +86,15 @@ class ValidateSourceView(FormView):
), ),
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _( Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _(
'Enter a YouTube playlist URL into the box below. A playlist URL will be ' 'Enter a YouTube playlist URL into the box below. A playlist URL will be '
'in the format of <strong>https://www.youtube.com/watch?v=AAAAAA&list=' 'in the format of <strong>https://www.youtube.com/playlist?list='
'BiGLoNgUnIqUeId</strong> where <strong>BiGLoNgUnIqUeId</strong> is the ' 'BiGLoNgUnIqUeId</strong> where <strong>BiGLoNgUnIqUeId</strong> is the '
'unique ID of the playlist you want to add.' 'unique ID of the playlist you want to add.'
), ),
} }
help_examples = { help_examples = {
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/google', Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/google',
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('https://www.youtube.com/watch?v=DcKEPl' Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('https://www.youtube.com/playlist?list='
'-MpLA&list=PL590L5WQmH8dpP0RyH5pCfIaDE' 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r')
'dt9nk7r')
} }
validation_urls = { validation_urls = {
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: { Source.SOURCE_TYPE_YOUTUBE_CHANNEL: {
@ -87,10 +108,10 @@ class ValidateSourceView(FormView):
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: { Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: {
'scheme': 'https', 'scheme': 'https',
'domain': 'www.youtube.com', 'domain': 'www.youtube.com',
'path_regex': '^\/watch$', 'path_regex': '^\/playlist$',
'qs_args': ['v', 'list'], 'qs_args': ['list'],
'extract_key': ('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 = { prepopulate_fields = {
@ -186,7 +207,7 @@ class AddSourceView(CreateView):
self.prepopulated_data['source_type'] = source_type self.prepopulated_data['source_type'] = source_type
key = request.GET.get('key', '') key = request.GET.get('key', '')
if key: if key:
self.prepopulated_data['key'] = slugify(key) self.prepopulated_data['key'] = key.strip()
name = request.GET.get('name', '') name = request.GET.get('name', '')
if name: if name:
self.prepopulated_data['name'] = slugify(name) self.prepopulated_data['name'] = slugify(name)
@ -201,6 +222,16 @@ class AddSourceView(CreateView):
initial[k] = v initial[k] = v
return initial 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): class MediaView(TemplateView):
''' '''

View File

@ -1,4 +1,9 @@
import os import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
ROOT_DIR = Path('/')
SECRET_KEY = str(os.getenv('DJANGO_SECRET_KEY', '')) SECRET_KEY = str(os.getenv('DJANGO_SECRET_KEY', ''))
@ -12,3 +17,7 @@ DATABASES = {
'NAME': '/config/db.sqlite3', 'NAME': '/config/db.sqlite3',
} }
} }
SYNC_VIDEO_ROOT = ROOT_DIR / 'downloads' / 'video'
SYNC_AUDIO_ROOT = ROOT_DIR / 'downloads' / 'audio'

View File

@ -98,6 +98,8 @@ STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static' STATIC_ROOT = BASE_DIR / 'static'
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / '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 SASS_PROCESSOR_ROOT = STATIC_ROOT
@ -114,6 +116,9 @@ HEALTHCHECK_ALLOWED_IPS = ('127.0.0.1',)
DJANGO_SIMPLE_TASK_WORKERS = 2 DJANGO_SIMPLE_TASK_WORKERS = 2
SOURCES_PER_PAGE = 50
try: try:
from .local_settings import * from .local_settings import *
except ImportError as e: except ImportError as e: