start of adding sources interface
This commit is contained in:
		
							parent
							
								
									bf27c43bbb
								
							
						
					
					
						commit
						37d390c8d8
					
				
							
								
								
									
										16
									
								
								Dockerfile
								
								
								
								
							
							
						
						
									
										16
									
								
								Dockerfile
								
								
								
								
							| 
						 | 
				
			
			@ -3,9 +3,6 @@ FROM debian:buster-slim
 | 
			
		|||
ARG DEBIAN_FRONTEND="noninteractive"
 | 
			
		||||
 | 
			
		||||
# Third party software versions
 | 
			
		||||
ARG YOUTUBE_DL_VERSION="2020.11.24"
 | 
			
		||||
ENV YOUTUBE_DL_EXPECTED_SHA256="7d70f2e2d6b42d7c948a418744cd5c89832d67f4fb36f01f1cf4ea7dc8fe537a"
 | 
			
		||||
ENV YOUTUBE_DL_TARBALL="https://github.com/ytdl-org/youtube-dl/releases/download/${YOUTUBE_DL_VERSION}/youtube-dl-${YOUTUBE_DL_VERSION}.tar.gz"
 | 
			
		||||
ARG FFMPEG_VERSION="4.3.1"
 | 
			
		||||
ENV FFMPEG_EXPECTED_MD5="ee235393ec7778279144ee6cbdd9eb64"
 | 
			
		||||
ENV FFMPEG_TARBALL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-${FFMPEG_VERSION}-amd64-static.tar.xz"
 | 
			
		||||
| 
						 | 
				
			
			@ -14,12 +11,7 @@ ENV FFMPEG_TARBALL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-${FFMPEG_VE
 | 
			
		|||
RUN set -x && \
 | 
			
		||||
    # Install required distro packages
 | 
			
		||||
    apt-get update && \
 | 
			
		||||
    apt-get -y --no-install-recommends install curl xz-utils ca-certificates binutils python3 python3-setuptools && \
 | 
			
		||||
    # Install youtube-dl
 | 
			
		||||
    curl -L ${YOUTUBE_DL_TARBALL} --output /tmp/youtube-dl-${YOUTUBE_DL_VERSION}.tar.gz && \
 | 
			
		||||
    echo "${YOUTUBE_DL_EXPECTED_SHA256}  /tmp/youtube-dl-${YOUTUBE_DL_VERSION}.tar.gz" | sha256sum -c - && \
 | 
			
		||||
    tar -zxvf /tmp/youtube-dl-${YOUTUBE_DL_VERSION}.tar.gz -C /tmp && \
 | 
			
		||||
    (cd /tmp/youtube-dl; python3 /tmp/youtube-dl/setup.py install) && \
 | 
			
		||||
    apt-get -y --no-install-recommends install curl xz-utils ca-certificates binutils && \
 | 
			
		||||
    # Install ffmpeg
 | 
			
		||||
    curl -L ${FFMPEG_TARBALL} --output /tmp/ffmpeg-${FFMPEG_VERSION}-amd64-static.tar.xz && \
 | 
			
		||||
    echo "${FFMPEG_EXPECTED_MD5}  tmp/ffmpeg-${FFMPEG_VERSION}-amd64-static.tar.xz" | md5sum -c - && \
 | 
			
		||||
| 
						 | 
				
			
			@ -28,8 +20,6 @@ RUN set -x && \
 | 
			
		|||
    ls -lat /tmp/ffmpeg-4.3.1-amd64-static && \
 | 
			
		||||
    install -v -s -g root -o root -m 0755 -s /tmp/ffmpeg-${FFMPEG_VERSION}-amd64-static/ffmpeg -t /usr/local/bin && \
 | 
			
		||||
    # Clean up
 | 
			
		||||
    rm /tmp/youtube-dl-${YOUTUBE_DL_VERSION}.tar.gz && \
 | 
			
		||||
    rm -rf /tmp/youtube-dl && \
 | 
			
		||||
    rm -rf /tmp/ffmpeg-${FFMPEG_VERSION}-amd64-static.tar && \
 | 
			
		||||
    rm -rf /tmp/ffmpeg-${FFMPEG_VERSION}-amd64-static && \
 | 
			
		||||
    apt-get -y autoremove --purge curl xz-utils binutils
 | 
			
		||||
| 
						 | 
				
			
			@ -54,14 +44,14 @@ ENV UID="${default_uid}"
 | 
			
		|||
ENV GID="${default_gid}"
 | 
			
		||||
RUN set -x && \
 | 
			
		||||
  # Install required distro packages
 | 
			
		||||
  apt-get -y --no-install-recommends install python3-pip python3-dev gcc make && \
 | 
			
		||||
  apt-get -y --no-install-recommends install python3 python3-setuptools python3-pip python3-dev gcc make && \
 | 
			
		||||
  # Install wheel which is required for pipenv
 | 
			
		||||
  pip3 --disable-pip-version-check install wheel && \
 | 
			
		||||
  # Then install pipenv
 | 
			
		||||
  pip3 --disable-pip-version-check install pipenv && \
 | 
			
		||||
  # Create a 'www' user which the workers drop to
 | 
			
		||||
  groupadd -g ${GID} www && \
 | 
			
		||||
  useradd -M -d /dev/null -s /bin/false -u ${UID} -g www www && \
 | 
			
		||||
  useradd -M -d /app -s /bin/false -u ${UID} -g www www && \
 | 
			
		||||
  # Install non-distro packages
 | 
			
		||||
  pipenv install --system  && \
 | 
			
		||||
  # Make absolutely sure we didn't accidentally bundle a SQLite dev database
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								Pipfile
								
								
								
								
							
							
						
						
									
										1
									
								
								Pipfile
								
								
								
								
							| 
						 | 
				
			
			@ -17,6 +17,7 @@ uvicorn = "*"
 | 
			
		|||
uvloop = "*"
 | 
			
		||||
httptools = "*"
 | 
			
		||||
django-simple-task = "*"
 | 
			
		||||
youtube-dl = "*"
 | 
			
		||||
 | 
			
		||||
[requires]
 | 
			
		||||
python_version = "3"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
    "_meta": {
 | 
			
		||||
        "hash": {
 | 
			
		||||
            "sha256": "10bcf36ee023c01949edbe0dbe22eefbe603580488e3498cc5f211f3d1b221ed"
 | 
			
		||||
            "sha256": "b2f3530bcd9d615f37ba75913336690a3b61e5cb9bf2659212431cbe11dbef90"
 | 
			
		||||
        },
 | 
			
		||||
        "pipfile-spec": 6,
 | 
			
		||||
        "requires": {
 | 
			
		||||
| 
						 | 
				
			
			@ -238,6 +238,14 @@
 | 
			
		|||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.2.0"
 | 
			
		||||
        },
 | 
			
		||||
        "youtube-dl": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:f61c8e4855559c33df66234b7e7ba303f4bcbef59639fb15825504d6484fd25f",
 | 
			
		||||
                "sha256:f701befffe00ae4b0d56f88ed45e1295c151c340d0011efdb1005012abc81996"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2020.11.24"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "develop": {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
from django.conf import settings
 | 
			
		||||
from youtube_dl import version as yt_version
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def app_details(request):
 | 
			
		||||
    return {
 | 
			
		||||
        'app_version': str(settings.VERSION)
 | 
			
		||||
        'app_version': str(settings.VERSION),
 | 
			
		||||
        'youtube_dl_version': str(yt_version.__version__)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,21 @@ $nav-background-colour: $colour-near-black;
 | 
			
		|||
$nav-text-colour: $colour-near-white;
 | 
			
		||||
$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;
 | 
			
		||||
 | 
			
		||||
$footer-background-colour: $colour-red;
 | 
			
		||||
$footer-text-colour: $colour-white;
 | 
			
		||||
$footer-link-colour: $colour-near-black;
 | 
			
		||||
$footer-link-colour: $colour-near-black;
 | 
			
		||||
$footer-link-hover-colour: $colour-orange;
 | 
			
		||||
 | 
			
		||||
$form-label-text-colour: $colour-near-black;
 | 
			
		||||
$form-input-border-colour: $colour-light-blue;
 | 
			
		||||
$form-input-border-active-colour: $colour-orange;
 | 
			
		||||
$form-select-border-colour: $colour-light-blue;
 | 
			
		||||
$form-error-background-colour: $colour-red;
 | 
			
		||||
$form-error-text-colour: $colour-near-white;
 | 
			
		||||
 | 
			
		||||
$box-error-background-colour: $colour-red;
 | 
			
		||||
$box-error-text-colour: $colour-white;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
.simpleform {
 | 
			
		||||
    .row {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    label {
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
        display: block;
 | 
			
		||||
        font-size: 0.9rem;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        transition: none;
 | 
			
		||||
        top: initial;
 | 
			
		||||
        left: initial !important;
 | 
			
		||||
        transform: none;
 | 
			
		||||
        color: $form-label-text-colour;
 | 
			
		||||
    }
 | 
			
		||||
    input {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        padding: 5px 8px 5px 8px;
 | 
			
		||||
        font-size: 1.1rem;
 | 
			
		||||
        border: 2px $form-input-border-colour solid;
 | 
			
		||||
        border-radius: 2px;
 | 
			
		||||
        outline: none;
 | 
			
		||||
        &:focus {
 | 
			
		||||
            outline: none;
 | 
			
		||||
            border: 2px $form-input-border-active-colour solid;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    textarea {
 | 
			
		||||
        min-height: 150px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
select {
 | 
			
		||||
    display: initial !important;
 | 
			
		||||
    border: 2px $form-select-border-colour solid;
 | 
			
		||||
    height: initial !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
strong {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nowrap {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-margin-bottom {
 | 
			
		||||
    margin-bottom: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.errors {
 | 
			
		||||
    background-color: $box-error-background-colour;
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    padding: 10px 0 5px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.errorlist {
 | 
			
		||||
    li {
 | 
			
		||||
        color: $box-error-text-colour;
 | 
			
		||||
        padding: 0 10px 5px 10px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +62,24 @@ main {
 | 
			
		|||
    
 | 
			
		||||
    padding: 2rem 0 2rem 0;
 | 
			
		||||
 | 
			
		||||
    h1 {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        font-size: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .btn {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        background-color: $main-button-background-colour !important;
 | 
			
		||||
        color: $main-button-text-colour !important;
 | 
			
		||||
        i {
 | 
			
		||||
            font-size: 0.9rem;
 | 
			
		||||
        }
 | 
			
		||||
        &:hover {
 | 
			
		||||
            background-color: $main-button-background-hover-colour !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +107,8 @@ footer {
 | 
			
		|||
        color: $footer-link-colour;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
        &:hover {
 | 
			
		||||
            text-decoration: underline;
 | 
			
		||||
            color: $footer-link-hover-colour;
 | 
			
		||||
            text-decoration: none;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,8 @@
 | 
			
		|||
 | 
			
		||||
@import "fonts";
 | 
			
		||||
@import "variables";
 | 
			
		||||
@import "helpers";
 | 
			
		||||
@import "colours";
 | 
			
		||||
@import "helpers";
 | 
			
		||||
@import "forms";
 | 
			
		||||
@import "template";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@
 | 
			
		|||
 | 
			
		||||
    <header>
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <a href="{% url 'sync:index' %}">
 | 
			
		||||
        <a href="{% url 'sync:dashboard' %}">
 | 
			
		||||
          {% include 'tubesync.svg' with width='3rem' height='3rem' %}
 | 
			
		||||
          <h1>TubeSync</h1>
 | 
			
		||||
        </a>
 | 
			
		||||
| 
						 | 
				
			
			@ -28,10 +28,11 @@
 | 
			
		|||
    <nav>
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <ul>
 | 
			
		||||
          <li><a href=""><i class="fas fa-fw fa-th-large"></i><span class="hide-on-med-and-down"> Dashboard</span></a></li>
 | 
			
		||||
          <li><a href=""><i class="fas fa-fw fa-play"></i><span class="hide-on-med-and-down"> Sources</span></a></li>
 | 
			
		||||
          <li><a href=""><i class="fas fa-fw fa-film"></i><span class="hide-on-med-and-down"> Media</span></a></li>
 | 
			
		||||
          <li><a href=""><i class="fas fa-fw fa-clock"></i><span class="hide-on-med-and-down"> Tasks</span></a></li>
 | 
			
		||||
          <li><a href="{% url 'sync:dashboard' %}"><i class="fas fa-fw fa-th-large"></i><span class="hide-on-med-and-down"> Dashboard</span></a></li>
 | 
			
		||||
          <li><a href="{% url 'sync:sources' %}"><i class="fas fa-fw fa-play"></i><span class="hide-on-med-and-down"> Sources</span></a></li>
 | 
			
		||||
          <li><a href="{% url 'sync:media' %}"><i class="fas fa-fw fa-film"></i><span class="hide-on-med-and-down"> Media</span></a></li>
 | 
			
		||||
          <li><a href="{% url 'sync:tasks' %}"><i class="fas fa-fw fa-clock"></i><span class="hide-on-med-and-down"> Tasks</span></a></li>
 | 
			
		||||
          <li><a href="{% url 'sync:logs' %}"><i class="fas fa-fw fa-list"></i><span class="hide-on-med-and-down"> Logs</span></a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
| 
						 | 
				
			
			@ -45,13 +46,12 @@
 | 
			
		|||
    <footer>
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <p>
 | 
			
		||||
          <a href="{% url 'sync:index' %}">{% include 'tubesync.svg' with width='0.8rem' height='0.8rem' %} TubeSync</a>
 | 
			
		||||
          is an open source synchronisation tool to automatically download videos from online video platforms.
 | 
			
		||||
          <br>
 | 
			
		||||
          The original code under a GPLv3 licence is available at
 | 
			
		||||
          <a href="https://github.com/meeb/tubesync"><i class="fab fa-github"></i> https://github.com/meeb/tubesync</a>.
 | 
			
		||||
          <a href="{% url 'sync:dashboard' %}" class="nowrap">{% include 'tubesync.svg' with width='0.8rem' height='0.8rem' %} TubeSync</a>
 | 
			
		||||
          is an open source synchronisation tool to automatically download videos from online video platforms. The
 | 
			
		||||
          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>.
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>Version {{ app_version }}.</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>
 | 
			
		||||
      </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
{% if form %}
 | 
			
		||||
  {% if form.errors %}
 | 
			
		||||
  <ul class="errors">
 | 
			
		||||
    {% for _, error in form.errors.items %}
 | 
			
		||||
    <li>{{ error }}</li>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  </ul>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
  {% for field in form %}
 | 
			
		||||
  {% if field.field.widget.input_type == 'hidden' %}{{ field }}{% else %}
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="input-field col s12">
 | 
			
		||||
      {{ field.label_tag }}
 | 
			
		||||
      {{ field }}
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
  {% endfor %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ValidateSourceForm(forms.Form):
 | 
			
		||||
 | 
			
		||||
    source_type = forms.CharField(
 | 
			
		||||
        max_length=1,
 | 
			
		||||
        required=True,
 | 
			
		||||
        widget=forms.HiddenInput()
 | 
			
		||||
    )
 | 
			
		||||
    source_url = forms.URLField(
 | 
			
		||||
        label='Source URL',
 | 
			
		||||
        required=True
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Synchronize YouTube to your local media server{% endblock %}
 | 
			
		||||
{% block headtitle %}Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Logs{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col s12">
 | 
			
		||||
    logs    
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Media{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col s12">
 | 
			
		||||
    media    
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Source - Add{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row no-margin-bottom">
 | 
			
		||||
  <div class="col s12">
 | 
			
		||||
    <h1>Add a {{ help_item }}</h1>
 | 
			
		||||
    <p>{{ help_text|safe }}</p>
 | 
			
		||||
    <p>Example: <strong>{{ help_example }}</strong></p>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <form method="post" action="{% url 'sync:validate-source' source_type=source_type %}" 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 {{ help_item }} <i class="fas fa-fw fa-plus"></i></button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Sources{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col s12 l6">
 | 
			
		||||
    <a href="{% url 'sync:validate-source' source_type='youtube-channel' %}" class="btn"><i class="fas fa-plus"></i> Add a YouTube channel</a>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="col s12 l6">
 | 
			
		||||
    <a href="{% url 'sync:validate-source' source_type='youtube-playlist' %}" class="btn"><i class="fas fa-plus"></i> Add a YouTube playlist</a>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block headtitle %}Tasks{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col s12">
 | 
			
		||||
    tasks
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
from django.urls import path
 | 
			
		||||
from .views import IndexView
 | 
			
		||||
from .views import (DashboardView, SourcesView, ValidateSourceView, MediaView,
 | 
			
		||||
                    TasksView, LogsView)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app_name = 'sync'
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +9,27 @@ app_name = 'sync'
 | 
			
		|||
urlpatterns = [
 | 
			
		||||
 | 
			
		||||
    path('',
 | 
			
		||||
         IndexView.as_view(),
 | 
			
		||||
         name='index'),
 | 
			
		||||
         DashboardView.as_view(),
 | 
			
		||||
         name='dashboard'),
 | 
			
		||||
        
 | 
			
		||||
    path('sources',
 | 
			
		||||
         SourcesView.as_view(),
 | 
			
		||||
         name='sources'),
 | 
			
		||||
 | 
			
		||||
    path('source/validate/<slug:source_type>',
 | 
			
		||||
         ValidateSourceView.as_view(),
 | 
			
		||||
         name='validate-source'),
 | 
			
		||||
 | 
			
		||||
    path('media',
 | 
			
		||||
         MediaView.as_view(),
 | 
			
		||||
         name='media'),
 | 
			
		||||
 | 
			
		||||
    path('tasks',
 | 
			
		||||
         TasksView.as_view(),
 | 
			
		||||
         name='tasks'),
 | 
			
		||||
 | 
			
		||||
    path('logs',
 | 
			
		||||
         LogsView.as_view(),
 | 
			
		||||
         name='logs'),
 | 
			
		||||
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import re
 | 
			
		||||
from urllib.parse import urlsplit, parse_qs
 | 
			
		||||
from django.forms import ValidationError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_url(url, validator):
 | 
			
		||||
    '''
 | 
			
		||||
        Validate a URL against a dict of validation requirements.
 | 
			
		||||
    '''
 | 
			
		||||
    valid_scheme, valid_netloc, valid_path, valid_query = (validator['scheme'],
 | 
			
		||||
        validator['domain'], validator['path_regex'], validator['qs_args'])
 | 
			
		||||
    url_parts = urlsplit(str(url).strip())
 | 
			
		||||
    url_scheme = str(url_parts.scheme).strip().lower()
 | 
			
		||||
    if url_scheme != valid_scheme:
 | 
			
		||||
        raise ValidationError(f'scheme "{url_scheme}" must be "{valid_scheme}"')
 | 
			
		||||
    url_netloc = str(url_parts.netloc).strip().lower()
 | 
			
		||||
    if url_netloc != valid_netloc:
 | 
			
		||||
        raise ValidationError(f'domain "{url_netloc}" must be "{valid_netloc}"')
 | 
			
		||||
    url_path = str(url_parts.path).strip()
 | 
			
		||||
    matches = re.match(valid_path, url_path)
 | 
			
		||||
    if matches is None:
 | 
			
		||||
        raise ValidationError(f'path "{url_path}" must match "{valid_path}"')
 | 
			
		||||
    url_query = str(url_parts.query).strip()
 | 
			
		||||
    url_query_parts = parse_qs(url_query)
 | 
			
		||||
    for required_query in valid_query:
 | 
			
		||||
        if required_query not in url_query_parts:
 | 
			
		||||
            raise ValidationError(f'query string "{url_query}" must '
 | 
			
		||||
                                  f'contain "{required_query}"')
 | 
			
		||||
    return True
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,188 @@
 | 
			
		|||
from django.http import Http404
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
from django.views.generic.edit import FormView
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.forms import ValidationError
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from .models import Source
 | 
			
		||||
from .forms import ValidateSourceForm
 | 
			
		||||
from .utils import validate_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IndexView(TemplateView):
 | 
			
		||||
class DashboardView(TemplateView):
 | 
			
		||||
    '''
 | 
			
		||||
        The dashboard shows non-interactive totals and summaries, nothing more.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/index.html'
 | 
			
		||||
    template_name = 'sync/dashboard.html'
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SourcesView(TemplateView):
 | 
			
		||||
    '''
 | 
			
		||||
        A bare list of the sources which have been created with their states.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/sources.html'
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ValidateSourceView(FormView):
 | 
			
		||||
    '''
 | 
			
		||||
        Validate a URL and prepopulate a create source view form with confirmed
 | 
			
		||||
        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"
 | 
			
		||||
        being just a playlist ID or some other reasonably unobvious internals.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/source-validate.html'
 | 
			
		||||
    form_class = ValidateSourceForm
 | 
			
		||||
    errors = {
 | 
			
		||||
        'invalid_url': _('Invalid URL, the URL must for a "{item}" must be in '
 | 
			
		||||
                         'the format of "{example}". The error was: {error}.'),
 | 
			
		||||
    }
 | 
			
		||||
    source_types = {
 | 
			
		||||
        'youtube-channel': Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
 | 
			
		||||
        'youtube-playlist': Source.SOURCE_TYPE_YOUTUBE_PLAYLIST,
 | 
			
		||||
    }
 | 
			
		||||
    help_item = {
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_CHANNEL: _('YouTube channel'),
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _('YouTube playlist'),
 | 
			
		||||
    }
 | 
			
		||||
    help_texts = {
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_CHANNEL: _(
 | 
			
		||||
            'Enter a YouTube channel URL into the box below. A channel URL will be in '
 | 
			
		||||
            'the format of <strong>https://www.youtube.com/CHANNELNAME</strong> '
 | 
			
		||||
            'where <strong>CHANNELNAME</strong> is the name of the channel you want '
 | 
			
		||||
            'to add.'
 | 
			
		||||
        ),
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _(
 | 
			
		||||
            '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='
 | 
			
		||||
            'BiGLoNgUnIqUeId</strong> where <strong>BiGLoNgUnIqUeId</strong> 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')
 | 
			
		||||
    }
 | 
			
		||||
    validation_urls = {
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_CHANNEL: {
 | 
			
		||||
            'scheme': 'https',
 | 
			
		||||
            'domain': 'www.youtube.com',
 | 
			
		||||
            'path_regex': '^\/(c\/)?[^\/]+$',
 | 
			
		||||
            'qs_args': [],
 | 
			
		||||
            'example': 'https://www.youtube.com/SOMECHANNEL'
 | 
			
		||||
        },
 | 
			
		||||
        Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: {
 | 
			
		||||
            'scheme': 'https',
 | 
			
		||||
            'domain': 'www.youtube.com',
 | 
			
		||||
            'path_regex': '^\/watch$',
 | 
			
		||||
            'qs_args': ['v', 'list'],
 | 
			
		||||
            'example': 'https://www.youtube.com/watch?v=VIDEOID&list=PLAYLISTID'
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        self.source_type_str = ''
 | 
			
		||||
        self.source_type = None
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        self.source_type_str = kwargs.get('source_type', '').strip().lower()
 | 
			
		||||
        self.source_type = self.source_types.get(self.source_type_str, None)
 | 
			
		||||
        if not self.source_type:
 | 
			
		||||
            raise Http404
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
        initial = super().get_initial()
 | 
			
		||||
        initial['source_type'] = self.source_type
 | 
			
		||||
        return initial
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, *args, **kwargs):
 | 
			
		||||
        data = super().get_context_data(*args, **kwargs)
 | 
			
		||||
        data['source_type'] = self.source_type_str
 | 
			
		||||
        data['help_item'] = self.help_item.get(self.source_type)
 | 
			
		||||
        data['help_text'] = self.help_texts.get(self.source_type)
 | 
			
		||||
        data['help_example'] = self.help_examples.get(self.source_type)
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        # Perform extra validation on the URL, we need to extract the channel name or
 | 
			
		||||
        # playlist ID and check they are valid
 | 
			
		||||
        source_type = form.cleaned_data['source_type']
 | 
			
		||||
        if source_type not in self.source_types.values():
 | 
			
		||||
            form.add_error(
 | 
			
		||||
                'source_type',
 | 
			
		||||
                ValidationError(self.errors['invalid_source'])
 | 
			
		||||
            )
 | 
			
		||||
        source_url = form.cleaned_data['source_url']
 | 
			
		||||
        validation_url = self.validation_urls.get(self.source_type)
 | 
			
		||||
        try:
 | 
			
		||||
            validate_url(source_url, validation_url)
 | 
			
		||||
        except ValidationError as e:
 | 
			
		||||
            print(e)
 | 
			
		||||
            error = self.errors.get('invalid_url')
 | 
			
		||||
            item = self.help_item.get(self.source_type)
 | 
			
		||||
            form.add_error(
 | 
			
		||||
                'source_url',
 | 
			
		||||
                ValidationError(error.format(
 | 
			
		||||
                    item=item,
 | 
			
		||||
                    example=validation_url['example'],
 | 
			
		||||
                    error=e.message)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        if form.errors:
 | 
			
		||||
            return super().form_invalid(form)
 | 
			
		||||
        return super().form_valid(form)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        cleaned_data = super().clean()
 | 
			
		||||
        
 | 
			
		||||
        print(cleaned_data)
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy('sync:dashboard')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MediaView(TemplateView):
 | 
			
		||||
    '''
 | 
			
		||||
        A bare list of media added with their states.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/media.html'
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TasksView(TemplateView):
 | 
			
		||||
    '''
 | 
			
		||||
        A list of tasks queued to be completed. Typically, this is scraping for new
 | 
			
		||||
        media or downloading media.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/tasks.html'
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogsView(TemplateView):
 | 
			
		||||
    '''
 | 
			
		||||
        The last X days of logs.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    template_name = 'sync/logs.html'
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue