initial django base
This commit is contained in:
0
app/sync/__init__.py
Normal file
0
app/sync/__init__.py
Normal file
20
app/sync/admin.py
Normal file
20
app/sync/admin.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.contrib import admin
|
||||
from .models import Source, Media
|
||||
|
||||
|
||||
@admin.register(Source)
|
||||
class SourceAdmin(admin.ModelAdmin):
|
||||
|
||||
ordering = ('-created',)
|
||||
list_display = ('name',)
|
||||
readonly_fields = ('uuid', 'created')
|
||||
search_fields = ('uuid', 'key', 'name')
|
||||
|
||||
|
||||
@admin.register(Media)
|
||||
class MediaAdmin(admin.ModelAdmin):
|
||||
|
||||
ordering = ('-created',)
|
||||
list_display = ('url',)
|
||||
readonly_fields = ('uuid', 'created')
|
||||
search_fields = ('uuid', 'key', 'url')
|
||||
5
app/sync/apps.py
Normal file
5
app/sync/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SyncConfig(AppConfig):
|
||||
name = 'sync'
|
||||
67
app/sync/migrations/0001_initial.py
Normal file
67
app/sync/migrations/0001_initial.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-23 06:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import sync.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='UUID of the source', primary_key=True, serialize=False, verbose_name='uuid')),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Date and time the source was created', verbose_name='created')),
|
||||
('last_crawl', models.DateTimeField(blank=True, db_index=True, help_text='Date and time the source was last crawled', null=True, verbose_name='last crawl')),
|
||||
('source_type', models.CharField(choices=[('c', 'YouTube channel'), ('p', 'YouTube playlist')], db_index=True, default='c', help_text='Source type', max_length=1, verbose_name='type')),
|
||||
('url', models.URLField(db_index=True, help_text='URL of the source', verbose_name='url')),
|
||||
('key', models.CharField(db_index=True, help_text='Source key, such as exact YouTube channel name or playlist ID', max_length=100, verbose_name='key')),
|
||||
('name', models.CharField(db_index=True, help_text='Friendly name for the source, used locally', max_length=100, verbose_name='name')),
|
||||
('directory', models.CharField(help_text='Directory name to save the media into', max_length=100, verbose_name='directory')),
|
||||
('delete_old_media', models.BooleanField(default=False, help_text='Delete old media after "days to keep" days?', verbose_name='delete old media')),
|
||||
('days_to_keep', models.PositiveSmallIntegerField(default=14, help_text='If "delete old media" is ticked, the number of days after which to automatically delete media', verbose_name='days to keep')),
|
||||
('source_profile', models.CharField(choices=[('360p', '360p (SD)'), ('480p', '480p (SD)'), ('720p', '720p (HD)'), ('1080p', '1080p (Full HD)'), ('2160p', '2160p (4K)'), ('audio', 'Audio only')], db_index=True, default='1080p', help_text='Source profile, the quality to attempt to download media', max_length=8, verbose_name='source profile')),
|
||||
('prefer_60fps', models.BooleanField(default=False, help_text='Where possible, prefer 60fps media for this source', verbose_name='prefer 60fps')),
|
||||
('prefer_hdr', models.BooleanField(default=False, help_text='Where possible, prefer HDR media for this source', verbose_name='prefer hdr')),
|
||||
('output_format', models.CharField(choices=[('mp4', '.mp4 container'), ('mkv', '.mkv container'), ('mkv', '.webm container'), ('m4a', '.m4a container (audio only)'), ('ogg', '.ogg container (audio only)')], db_index=True, default='mkv', help_text='Output format, the codec and container to save media', max_length=8, verbose_name='output format')),
|
||||
('fallback', models.CharField(choices=[('f', 'Fail'), ('s', 'Next best SD'), ('h', 'Next best HD')], db_index=True, default='f', help_text='What do do when your first choice is not available', max_length=1, verbose_name='fallback')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Source',
|
||||
'verbose_name_plural': 'Sources',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Media',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='UUID of the media', primary_key=True, serialize=False, verbose_name='uuid')),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Date and time the media was created', verbose_name='created')),
|
||||
('published', models.DateTimeField(blank=True, db_index=True, help_text='Date and time the media was published on the source', null=True, verbose_name='published')),
|
||||
('key', models.CharField(db_index=True, help_text='Media key, such as exact YouTube video ID', max_length=100, verbose_name='key')),
|
||||
('url', models.URLField(db_index=True, help_text='URL of the media', verbose_name='url')),
|
||||
('thumb', models.ImageField(blank=True, height_field='thumb_height', help_text='Thumbnail', null=True, upload_to=sync.models.get_media_thumb_path, verbose_name='thumb', width_field='thumb_width')),
|
||||
('thumb_width', models.PositiveSmallIntegerField(blank=True, help_text='Width (X) of the thumbnail', verbose_name='thumb width')),
|
||||
('thumb_height', models.PositiveSmallIntegerField(blank=True, help_text='Height (Y) of the thumbnail', verbose_name='thumb height')),
|
||||
('metadata', models.TextField(blank=True, help_text='JSON encoded metadata for the media', null=True, verbose_name='metadata')),
|
||||
('downloaded', models.BooleanField(db_index=True, default=False, help_text='Media has been downloaded', verbose_name='downloaded')),
|
||||
('downloaded_audio_codec', models.CharField(blank=True, db_index=True, help_text='Audio codec of the downloaded media', max_length=30, null=True, verbose_name='downloaded audio codec')),
|
||||
('downloaded_video_codec', models.CharField(blank=True, db_index=True, help_text='Video codec of the downloaded media', max_length=30, null=True, verbose_name='downloaded video codec')),
|
||||
('downloaded_container', models.CharField(blank=True, db_index=True, help_text='Container format of the downloaded media', max_length=30, null=True, verbose_name='downloaded container format')),
|
||||
('downloaded_fps', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='FPS of the downloaded media', null=True, verbose_name='downloaded fps')),
|
||||
('downloaded_hdr', models.BooleanField(default=False, help_text='Downloaded media has HDR', verbose_name='downloaded hdr')),
|
||||
('downloaded_filesize', models.PositiveBigIntegerField(blank=True, db_index=True, help_text='Size of the downloaded media in bytes', null=True, verbose_name='downloaded filesize')),
|
||||
('source', models.ForeignKey(help_text='Source the media belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='media_source', to='sync.source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Media',
|
||||
'verbose_name_plural': 'Media',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
app/sync/migrations/__init__.py
Normal file
0
app/sync/migrations/__init__.py
Normal file
297
app/sync/models.py
Normal file
297
app/sync/models.py
Normal file
@@ -0,0 +1,297 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Source(models.Model):
|
||||
'''
|
||||
A Source is a source of media. Currently, this is either a YouTube channel
|
||||
or a YouTube playlist.
|
||||
'''
|
||||
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL = 'c'
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST = 'p'
|
||||
SOURCE_TYPES = (SOURCE_TYPE_YOUTUBE_CHANNEL, SOURCE_TYPE_YOUTUBE_PLAYLIST)
|
||||
SOURCE_TYPE_CHOICES = (
|
||||
(SOURCE_TYPE_YOUTUBE_CHANNEL, _('YouTube channel')),
|
||||
(SOURCE_TYPE_YOUTUBE_PLAYLIST, _('YouTube playlist')),
|
||||
)
|
||||
|
||||
SOURCE_PROFILE_360p = '360p'
|
||||
SOURCE_PROFILE_480p = '480p'
|
||||
SOURCE_PROFILE_720P = '720p'
|
||||
SOURCE_PROFILE_1080P = '1080p'
|
||||
SOURCE_PROFILE_2160P = '2160p'
|
||||
SOURCE_PROFILE_AUDIO = 'audio'
|
||||
SOURCE_PROFILES = (SOURCE_PROFILE_360p, SOURCE_PROFILE_480p, SOURCE_PROFILE_720P,
|
||||
SOURCE_PROFILE_1080P, SOURCE_PROFILE_2160P,
|
||||
SOURCE_PROFILE_AUDIO)
|
||||
SOURCE_PROFILE_CHOICES = (
|
||||
(SOURCE_PROFILE_360p, _('360p (SD)')),
|
||||
(SOURCE_PROFILE_480p, _('480p (SD)')),
|
||||
(SOURCE_PROFILE_720P, _('720p (HD)')),
|
||||
(SOURCE_PROFILE_1080P, _('1080p (Full HD)')),
|
||||
(SOURCE_PROFILE_2160P, _('2160p (4K)')),
|
||||
(SOURCE_PROFILE_AUDIO, _('Audio only')),
|
||||
)
|
||||
|
||||
OUTPUT_FORMAT_MP4 = 'mp4'
|
||||
OUTPUT_FORMAT_MKV = 'mkv'
|
||||
OUTPUT_FORMAT_M4A = 'm4a'
|
||||
OUTPUT_FORMAT_OGG = 'ogg'
|
||||
OUTPUT_FORMATS = (OUTPUT_FORMAT_MP4, OUTPUT_FORMAT_MKV, OUTPUT_FORMAT_M4A,
|
||||
OUTPUT_FORMAT_OGG)
|
||||
OUTPUT_FORMAT_CHOICES = (
|
||||
(OUTPUT_FORMAT_MP4, _('.mp4 container')),
|
||||
(OUTPUT_FORMAT_MKV, _('.mkv container')),
|
||||
(OUTPUT_FORMAT_MKV, _('.webm container')),
|
||||
(OUTPUT_FORMAT_M4A, _('.m4a container (audio only)')),
|
||||
(OUTPUT_FORMAT_OGG, _('.ogg container (audio only)')),
|
||||
)
|
||||
|
||||
FALLBACK_FAIL = 'f'
|
||||
FALLBACK_NEXT_SD = 's'
|
||||
FALLBACK_NEXT_HD = 'h'
|
||||
FALLBACKS = (FALLBACK_FAIL, FALLBACK_NEXT_SD, FALLBACK_NEXT_HD)
|
||||
FALLBACK_CHOICES = (
|
||||
(FALLBACK_FAIL, _('Fail')),
|
||||
(FALLBACK_NEXT_SD, _('Next best SD')),
|
||||
(FALLBACK_NEXT_HD, _('Next best HD')),
|
||||
)
|
||||
|
||||
uuid = models.UUIDField(
|
||||
_('uuid'),
|
||||
primary_key=True,
|
||||
editable=False,
|
||||
default=uuid.uuid4,
|
||||
help_text=_('UUID of the source')
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
_('created'),
|
||||
auto_now_add=True,
|
||||
db_index=True,
|
||||
help_text=_('Date and time the source was created')
|
||||
)
|
||||
last_crawl = models.DateTimeField(
|
||||
_('last crawl'),
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_('Date and time the source was last crawled')
|
||||
)
|
||||
source_type = models.CharField(
|
||||
_('type'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=SOURCE_TYPE_CHOICES,
|
||||
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,
|
||||
help_text=_('Source key, such as exact YouTube channel name or playlist ID')
|
||||
)
|
||||
name = models.CharField(
|
||||
_('name'),
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text=_('Friendly name for the source, used locally')
|
||||
)
|
||||
directory = models.CharField(
|
||||
_('directory'),
|
||||
max_length=100,
|
||||
help_text=_('Directory name to save the media into')
|
||||
)
|
||||
delete_old_media = models.BooleanField(
|
||||
_('delete old media'),
|
||||
default=False,
|
||||
help_text=_('Delete old media after "days to keep" days?')
|
||||
)
|
||||
days_to_keep = models.PositiveSmallIntegerField(
|
||||
_('days to keep'),
|
||||
default=14,
|
||||
help_text=_('If "delete old media" is ticked, the number of days after which '
|
||||
'to automatically delete media')
|
||||
)
|
||||
source_profile = models.CharField(
|
||||
_('source profile'),
|
||||
max_length=8,
|
||||
db_index=True,
|
||||
choices=SOURCE_PROFILE_CHOICES,
|
||||
default=SOURCE_PROFILE_1080P,
|
||||
help_text=_('Source profile, the quality to attempt to download media')
|
||||
)
|
||||
prefer_60fps = models.BooleanField(
|
||||
_('prefer 60fps'),
|
||||
default=False,
|
||||
help_text=_('Where possible, prefer 60fps media for this source')
|
||||
)
|
||||
prefer_hdr = models.BooleanField(
|
||||
_('prefer hdr'),
|
||||
default=False,
|
||||
help_text=_('Where possible, prefer HDR media for this source')
|
||||
)
|
||||
output_format = models.CharField(
|
||||
_('output format'),
|
||||
max_length=8,
|
||||
db_index=True,
|
||||
choices=OUTPUT_FORMAT_CHOICES,
|
||||
default=OUTPUT_FORMAT_MKV,
|
||||
help_text=_('Output format, the codec and container to save media')
|
||||
)
|
||||
fallback = models.CharField(
|
||||
_('fallback'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=FALLBACK_CHOICES,
|
||||
default=FALLBACK_FAIL,
|
||||
help_text=_('What do do when your first choice is not available')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Source')
|
||||
verbose_name_plural = _('Sources')
|
||||
|
||||
|
||||
def get_media_thumb_path(instance, filename):
|
||||
fileid = str(instance.uuid)
|
||||
filename = f'{fileid.lower()}.{instance.image_type.lower()}'
|
||||
prefix = fileid[:2]
|
||||
return os.path.join('thumbs', prefix, filename)
|
||||
|
||||
|
||||
class Media(models.Model):
|
||||
'''
|
||||
Media is a single piece of media, such as a single YouTube video linked to a
|
||||
Source.
|
||||
'''
|
||||
|
||||
uuid = models.UUIDField(
|
||||
_('uuid'),
|
||||
primary_key=True,
|
||||
editable=False,
|
||||
default=uuid.uuid4,
|
||||
help_text=_('UUID of the media')
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
_('created'),
|
||||
auto_now_add=True,
|
||||
db_index=True,
|
||||
help_text=_('Date and time the media was created')
|
||||
)
|
||||
source = models.ForeignKey(
|
||||
Source,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='media_source',
|
||||
help_text=_('Source the media belongs to')
|
||||
)
|
||||
published = models.DateTimeField(
|
||||
_('published'),
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_('Date and time the media was published on the source')
|
||||
)
|
||||
key = models.CharField(
|
||||
_('key'),
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text=_('Media key, such as exact YouTube video ID')
|
||||
)
|
||||
url = models.URLField(
|
||||
_('url'),
|
||||
db_index=True,
|
||||
help_text=_('URL of the media')
|
||||
)
|
||||
thumb = models.ImageField(
|
||||
_('thumb'),
|
||||
upload_to=get_media_thumb_path,
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
width_field='thumb_width',
|
||||
height_field='thumb_height',
|
||||
help_text=_('Thumbnail')
|
||||
)
|
||||
thumb_width = models.PositiveSmallIntegerField(
|
||||
_('thumb width'),
|
||||
blank=True,
|
||||
help_text=_('Width (X) of the thumbnail')
|
||||
)
|
||||
thumb_height = models.PositiveSmallIntegerField(
|
||||
_('thumb height'),
|
||||
blank=True,
|
||||
help_text=_('Height (Y) of the thumbnail')
|
||||
)
|
||||
metadata = models.TextField(
|
||||
_('metadata'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('JSON encoded metadata for the media')
|
||||
)
|
||||
downloaded = models.BooleanField(
|
||||
_('downloaded'),
|
||||
db_index=True,
|
||||
default=False,
|
||||
help_text=_('Media has been downloaded')
|
||||
)
|
||||
downloaded_audio_codec = models.CharField(
|
||||
_('downloaded audio codec'),
|
||||
max_length=30,
|
||||
db_index=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Audio codec of the downloaded media')
|
||||
)
|
||||
downloaded_video_codec = models.CharField(
|
||||
_('downloaded video codec'),
|
||||
max_length=30,
|
||||
db_index=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Video codec of the downloaded media')
|
||||
)
|
||||
downloaded_container = models.CharField(
|
||||
_('downloaded container format'),
|
||||
max_length=30,
|
||||
db_index=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Container format of the downloaded media')
|
||||
)
|
||||
downloaded_fps = models.PositiveSmallIntegerField(
|
||||
_('downloaded fps'),
|
||||
db_index=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('FPS of the downloaded media')
|
||||
)
|
||||
downloaded_hdr = models.BooleanField(
|
||||
_('downloaded hdr'),
|
||||
default=False,
|
||||
help_text=_('Downloaded media has HDR')
|
||||
)
|
||||
downloaded_filesize = models.PositiveBigIntegerField(
|
||||
_('downloaded filesize'),
|
||||
db_index=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Size of the downloaded media in bytes')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Media')
|
||||
verbose_name_plural = _('Media')
|
||||
15
app/sync/templates/sync/index.html
Normal file
15
app/sync/templates/sync/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block headtitle %}Synchronize YouTube to your local media server{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
intro
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
app/sync/tests.py
Normal file
3
app/sync/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
app/sync/urls.py
Normal file
14
app/sync/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
from .views import IndexView
|
||||
|
||||
|
||||
app_name = 'sync'
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path('',
|
||||
IndexView.as_view(),
|
||||
name='index'),
|
||||
|
||||
]
|
||||
9
app/sync/views.py
Normal file
9
app/sync/views.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
|
||||
template_name = 'sync/index.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
Reference in New Issue
Block a user