add app/static and app/media
This commit is contained in:
parent
37d390c8d8
commit
cde919422f
|
@ -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/
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
@ -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):
|
||||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue