add app/static and app/media

This commit is contained in:
meeb 2020-11-26 16:01:47 +11:00
parent 37d390c8d8
commit cde919422f
13 changed files with 108 additions and 32 deletions

2
.gitignore vendored
View File

@ -60,6 +60,8 @@ coverage.xml
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
/app/static/
/app/media/
# Flask stuff: # Flask stuff:
instance/ instance/

View File

@ -31,6 +31,7 @@ $form-input-border-active-colour: $colour-orange;
$form-select-border-colour: $colour-light-blue; $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;
$box-error-background-colour: $colour-red; $box-error-background-colour: $colour-red;
$box-error-text-colour: $colour-white; $box-error-text-colour: $colour-white;

View File

@ -2,6 +2,10 @@
.row { .row {
margin-bottom: 0; margin-bottom: 0;
} }
.help-text {
color: $form-help-text-colour;
padding: 1rem 0 1rem 0;
}
label { label {
text-transform: uppercase; text-transform: uppercase;
display: block; display: block;

View File

@ -51,7 +51,7 @@
original code under a GPLv3 licence is available at original code under a GPLv3 licence is available at
<a href="https://github.com/meeb/tubesync" class="nowrap"><i class="fab fa-github"></i> https://github.com/meeb/tubesync</a>. <a href="https://github.com/meeb/tubesync" class="nowrap"><i class="fab fa-github"></i> https://github.com/meeb/tubesync</a>.
</p> </p>
<p>TubeSync version {{ app_version }} with embedded <a href="https://yt-dl.org/"><i class="fas fa-link"></i> youtube-dl</a> version {{ youtube_dl_version }}.</p> <p>TubeSync version {{ app_version }} with <a href="https://yt-dl.org/"><i class="fas fa-link"></i> youtube-dl</a> version {{ youtube_dl_version }}.</p>
</div> </div>
</footer> </footer>

View File

@ -10,8 +10,16 @@
{% if field.field.widget.input_type == 'hidden' %}{{ field }}{% else %} {% if field.field.widget.input_type == 'hidden' %}{{ field }}{% else %}
<div class="row"> <div class="row">
<div class="input-field col s12"> <div class="input-field col s12">
{% if field.field.widget.input_type == 'checkbox' %}
<label>
{{ field }}
<span>{{ field.label }}</span>
</label>
{% else %}
{{ field.label_tag }} {{ field.label_tag }}
{{ field }} {{ field }}
{% endif %}
{% if field.help_text %}<span class="help-text"><i class="fas fa-info-circle"></i> {{ field.help_text }}</span>{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -11127,6 +11127,10 @@ strong {
.simpleform .row { .simpleform .row {
margin-bottom: 0; } margin-bottom: 0; }
.simpleform .help-text {
color: #2ec4b6;
padding: 1rem 0 1rem 0; }
.simpleform label { .simpleform label {
text-transform: uppercase; text-transform: uppercase;
display: block; display: block;

File diff suppressed because one or more lines are too long

View File

@ -54,9 +54,9 @@ class Source(models.Model):
FALLBACK_NEXT_HD = 'h' FALLBACK_NEXT_HD = 'h'
FALLBACKS = (FALLBACK_FAIL, FALLBACK_NEXT_SD, FALLBACK_NEXT_HD) FALLBACKS = (FALLBACK_FAIL, FALLBACK_NEXT_SD, FALLBACK_NEXT_HD)
FALLBACK_CHOICES = ( FALLBACK_CHOICES = (
(FALLBACK_FAIL, _('Fail')), (FALLBACK_FAIL, _('Fail, do not download any media')),
(FALLBACK_NEXT_SD, _('Next best SD')), (FALLBACK_NEXT_SD, _('Get next best SD media instead')),
(FALLBACK_NEXT_HD, _('Next best HD')), (FALLBACK_NEXT_HD, _('Get next best HD media instead')),
) )
uuid = models.UUIDField( uuid = models.UUIDField(
@ -102,7 +102,7 @@ class Source(models.Model):
_('name'), _('name'),
max_length=100, max_length=100,
db_index=True, db_index=True,
help_text=_('Friendly name for the source, used locally') help_text=_('Friendly name for the source, used locally in TubeSync only')
) )
directory = models.CharField( directory = models.CharField(
_('directory'), _('directory'),
@ -144,7 +144,7 @@ class Source(models.Model):
db_index=True, db_index=True,
choices=OUTPUT_FORMAT_CHOICES, choices=OUTPUT_FORMAT_CHOICES,
default=OUTPUT_FORMAT_MKV, default=OUTPUT_FORMAT_MKV,
help_text=_('Output format, the codec and container to save media') help_text=_('Output format, the file format container in which to save media')
) )
fallback = models.CharField( fallback = models.CharField(
_('fallback'), _('fallback'),
@ -152,7 +152,7 @@ class Source(models.Model):
db_index=True, db_index=True,
choices=FALLBACK_CHOICES, choices=FALLBACK_CHOICES,
default=FALLBACK_FAIL, default=FALLBACK_FAIL,
help_text=_('What do do when your first choice is not available') help_text=_('What do do when media in your source profile is not available')
) )
def __str__(self): def __str__(self):

View File

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block headtitle %}Add a new source{% endblock %}
{% block content %}
<div class="row no-margin-bottom">
<div class="col s12">
<h1>Add a source</h1>
<p>
You can use this form to add a new source. A source is what's polled on regular
basis to find new media to download, such as a channel or playlist.
</p>
</div>
</div>
<div class="row">
<form method="post" action="{% url 'sync:add-source' %}" 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">Add source <i class="fas fa-fw fa-plus"></i></button>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,11 +1,11 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block headtitle %}Source - Add{% endblock %} {% block headtitle %}Validate a {{ help_item }}{% endblock %}
{% block content %} {% block content %}
<div class="row no-margin-bottom"> <div class="row no-margin-bottom">
<div class="col s12"> <div class="col s12">
<h1>Add a {{ help_item }}</h1> <h1>Validate a {{ help_item }}</h1>
<p>{{ help_text|safe }}</p> <p>{{ help_text|safe }}</p>
<p>Example: <strong>{{ help_example }}</strong></p> <p>Example: <strong>{{ help_example }}</strong></p>
</div> </div>
@ -16,7 +16,7 @@
{% include 'simpleform.html' with form=form %} {% include 'simpleform.html' with form=form %}
<div class="row no-margin-bottom padding-top"> <div class="row no-margin-bottom padding-top">
<div class="col s12"> <div class="col s12">
<button class="btn" type="submit" name="action">Add {{ help_item }} <i class="fas fa-fw fa-plus"></i></button> <button class="btn" type="submit" name="action">Validate {{ help_item }} <i class="fas fa-check"></i></button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import (DashboardView, SourcesView, ValidateSourceView, MediaView, from .views import (DashboardView, SourcesView, ValidateSourceView, AddSourceView,
TasksView, LogsView) MediaView, TasksView, LogsView)
app_name = 'sync' app_name = 'sync'
@ -20,6 +20,10 @@ urlpatterns = [
ValidateSourceView.as_view(), ValidateSourceView.as_view(),
name='validate-source'), name='validate-source'),
path('source/add',
AddSourceView.as_view(),
name='add-source'),
path('media', path('media',
MediaView.as_view(), MediaView.as_view(),
name='media'), name='media'),

View File

@ -5,10 +5,12 @@ from django.forms import ValidationError
def validate_url(url, validator): def validate_url(url, validator):
''' '''
Validate a URL against a dict of validation requirements. Validate a URL against a dict of validation requirements. Returns an extracted
part of the URL if the URL is valid, if invalid raises a ValidationError.
''' '''
valid_scheme, valid_netloc, valid_path, valid_query = (validator['scheme'], valid_scheme, valid_netloc, valid_path, valid_query, extract_parts = (
validator['domain'], validator['path_regex'], validator['qs_args']) validator['scheme'], validator['domain'], validator['path_regex'],
validator['qs_args'], validator['extract_key'])
url_parts = urlsplit(str(url).strip()) url_parts = urlsplit(str(url).strip())
url_scheme = str(url_parts.scheme).strip().lower() url_scheme = str(url_parts.scheme).strip().lower()
if url_scheme != valid_scheme: if url_scheme != valid_scheme:
@ -17,13 +19,26 @@ def validate_url(url, validator):
if url_netloc != valid_netloc: if url_netloc != valid_netloc:
raise ValidationError(f'domain "{url_netloc}" must be "{valid_netloc}"') raise ValidationError(f'domain "{url_netloc}" must be "{valid_netloc}"')
url_path = str(url_parts.path).strip() url_path = str(url_parts.path).strip()
matches = re.match(valid_path, url_path) matches = re.findall(valid_path, url_path)
if matches is None: if not matches:
raise ValidationError(f'path "{url_path}" must match "{valid_path}"') raise ValidationError(f'path "{url_path}" must match "{valid_path}"')
url_query = str(url_parts.query).strip() url_query = str(url_parts.query).strip()
url_query_parts = parse_qs(url_query) url_query_parts = parse_qs(url_query)
for required_query in valid_query: for required_query in valid_query:
if required_query not in url_query_parts: if required_query not in url_query_parts:
raise ValidationError(f'query string "{url_query}" must ' raise ValidationError(f'query string "{url_query}" must '
f'contain "{required_query}"') f'contain the parameter "{required_query}"')
return True extract_from, extract_param = extract_parts
extract_value = ''
if extract_from == 'path_regex':
try:
submatches = matches[0]
try:
extract_value = submatches[extract_param]
except IndexError:
pass
except IndexError:
pass
elif extract_from == 'qs_args':
extract_value = url_query_parts[extract_param][0]
return extract_value

View File

@ -1,9 +1,10 @@
from django.http import Http404 from django.http import Http404
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.edit import FormView 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
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.utils import append_uri_params
from .models import Source from .models import Source
from .forms import ValidateSourceForm from .forms import ValidateSourceForm
from .utils import validate_url from .utils import validate_url
@ -36,7 +37,7 @@ class ValidateSourceView(FormView):
Validate a URL and prepopulate a create source view form with confirmed Validate a URL and prepopulate a create source view form with confirmed
accurate data. The aim here is to streamline onboarding of new sources accurate data. The aim here is to streamline onboarding of new sources
which otherwise may not be entirely obvious to add, such as the "key" which otherwise may not be entirely obvious to add, such as the "key"
being just a playlist ID or some other reasonably unobvious internals. being just a playlist ID or some other reasonably opaque internals.
''' '''
template_name = 'sync/source-validate.html' template_name = 'sync/source-validate.html'
@ -77,8 +78,9 @@ class ValidateSourceView(FormView):
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: { Source.SOURCE_TYPE_YOUTUBE_CHANNEL: {
'scheme': 'https', 'scheme': 'https',
'domain': 'www.youtube.com', 'domain': 'www.youtube.com',
'path_regex': '^\/(c\/)?[^\/]+$', 'path_regex': '^\/(c\/)?([^\/]+)$',
'qs_args': [], 'qs_args': [],
'extract_key': ('path_regex', 1),
'example': 'https://www.youtube.com/SOMECHANNEL' 'example': 'https://www.youtube.com/SOMECHANNEL'
}, },
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: { Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: {
@ -86,6 +88,7 @@ class ValidateSourceView(FormView):
'domain': 'www.youtube.com', 'domain': 'www.youtube.com',
'path_regex': '^\/watch$', 'path_regex': '^\/watch$',
'qs_args': ['v', 'list'], 'qs_args': ['v', 'list'],
'extract_key': ('qs_args', 'list'),
'example': 'https://www.youtube.com/watch?v=VIDEOID&list=PLAYLISTID' 'example': 'https://www.youtube.com/watch?v=VIDEOID&list=PLAYLISTID'
}, },
} }
@ -93,6 +96,7 @@ class ValidateSourceView(FormView):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.source_type_str = '' self.source_type_str = ''
self.source_type = None self.source_type = None
self.key = ''
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -127,9 +131,8 @@ class ValidateSourceView(FormView):
source_url = form.cleaned_data['source_url'] source_url = form.cleaned_data['source_url']
validation_url = self.validation_urls.get(self.source_type) validation_url = self.validation_urls.get(self.source_type)
try: try:
validate_url(source_url, validation_url) self.key = validate_url(source_url, validation_url)
except ValidationError as e: except ValidationError as e:
print(e)
error = self.errors.get('invalid_url') error = self.errors.get('invalid_url')
item = self.help_item.get(self.source_type) item = self.help_item.get(self.source_type)
form.add_error( form.add_error(
@ -144,14 +147,23 @@ class ValidateSourceView(FormView):
return super().form_invalid(form) return super().form_invalid(form)
return super().form_valid(form) return super().form_valid(form)
def clean(self):
cleaned_data = super().clean()
print(cleaned_data)
return cleaned_data
def get_success_url(self): def get_success_url(self):
return reverse_lazy('sync:dashboard') url = reverse_lazy('sync:add-source')
return append_uri_params(url, {'source_type': self.source_type,
'key': self.key})
class AddSourceView(CreateView):
'''
Adds a new source, optionally takes some initial data querystring values to
prepopulate some of the more unclear values.
'''
template_name = 'sync/source-add.html'
model = Source
fields = ('source_type', 'key', 'name', 'directory', 'delete_old_media',
'days_to_keep', 'source_profile', 'prefer_60fps', 'prefer_hdr',
'output_format', 'fallback')
class MediaView(TemplateView): class MediaView(TemplateView):