Initial commit
This commit is contained in:
commit
467e8001d8
48 changed files with 2963 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
venv/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
settings.ini
|
||||
db.sqlite3
|
23
README.md
Normal file
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Steel Donut Collection
|
||||
|
||||
Steel Donut Collection is a simple web app that will take videos downloaded using youtube-dl/yt-dlp from an S3 bucket and make them watchable. This was created as a way to have our own Steel Donut Collective repository at home, just in case their YouTube channel ever goes down after they disbanded. (insert sad emoji here)
|
||||
|
||||
It will also work with other YouTube videos, I'm told, but I didn't test that.
|
||||
|
||||
## Downloading videos
|
||||
|
||||
I used the following command to download the videos from the SDC channel:
|
||||
|
||||
```
|
||||
yt-dlp --write-info-json --write-description --write-annotations --all-subs --write-thumbnail -f best --format mp4 https://www.youtube.com/c/SteelDonutCollective/videos
|
||||
```
|
||||
|
||||
I just uploaded the resulting files to a Minio (S3 compatible) bucket.
|
||||
|
||||
## Setting up application
|
||||
|
||||
The easiest way to get started is copying the provided "settings.dist.ini" to "settings.ini" and setting the required values therein. Then run `./manage.py migrate` to set up the sqlite3 database and you are good to go.
|
||||
|
||||
## Importing videos
|
||||
|
||||
Once you have the database set up, you can run `./manage.py importvideos` to automatically import videos from the S3 storage.
|
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
7
backend/admin.py
Normal file
7
backend/admin.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Video, Playlist
|
||||
|
||||
|
||||
admin.site.register(Video)
|
||||
admin.site.register(Playlist)
|
6
backend/apps.py
Normal file
6
backend/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BackendConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'backend'
|
48
backend/management/commands/importvideos.py
Normal file
48
backend/management/commands/importvideos.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from json import loads
|
||||
|
||||
from backend.s3 import build_s3_from_settings
|
||||
from backend.models import Video
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Imports all videos from storage'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
s3 = build_s3_from_settings()
|
||||
|
||||
if not s3.bucket_exists(settings.S3_BUCKET):
|
||||
s3.make_bucket(settings.S3_BUCKET)
|
||||
|
||||
with open(settings.BASE_DIR / "sdc.json") as policyfile:
|
||||
policy = policyfile.read()
|
||||
|
||||
s3.set_bucket_policy(settings.S3_BUCKET, policy)
|
||||
|
||||
objects = list(s3.list_objects(settings.S3_BUCKET))
|
||||
names = [obj.object_name for obj in objects]
|
||||
|
||||
for obj in objects:
|
||||
if obj.object_name.endswith(".info.json"):
|
||||
base = obj.object_name.rsplit(".info.json")[0]
|
||||
if (videoname := f"{base}.mp4") in names:
|
||||
vjson = s3.get_object(
|
||||
settings.S3_BUCKET, obj.object_name).read().decode()
|
||||
vdata = loads(vjson)
|
||||
|
||||
try:
|
||||
vobj = Video.objects.get(id=vdata["id"])
|
||||
except Video.DoesNotExist:
|
||||
vobj = Video()
|
||||
|
||||
vobj.title = vdata["title"]
|
||||
vobj.published = datetime.strptime(vdata["upload_date"], "%Y%m%d")
|
||||
vobj.description = vdata["description"]
|
||||
vobj.path = videoname
|
||||
vobj.thumbnail_path = f"{base}.webp"
|
||||
vobj.original_json = vjson
|
||||
|
||||
vobj.save()
|
32
backend/migrations/0001_initial.py
Normal file
32
backend/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 09:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Video',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=11, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('path', models.CharField(max_length=1024)),
|
||||
('original_json', models.JSONField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Playlist',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=34, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('videos', models.ManyToManyField(to='backend.video')),
|
||||
],
|
||||
),
|
||||
]
|
19
backend/migrations/0002_video_thumbnail_path.py
Normal file
19
backend/migrations/0002_video_thumbnail_path.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 10:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='video',
|
||||
name='thumbnail_path',
|
||||
field=models.CharField(default='', max_length=1024),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
19
backend/migrations/0003_video_published.py
Normal file
19
backend/migrations/0003_video_published.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 11:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0002_video_thumbnail_path'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='video',
|
||||
name='published',
|
||||
field=models.DateTimeField(default="1970-01-01 00:00:00"),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 14:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0003_video_published'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='playlist',
|
||||
name='videos',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PlaylistVideo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order', models.PositiveIntegerField(default=0)),
|
||||
('playlist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.playlist')),
|
||||
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.video')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 15:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('backend', '0004_remove_playlist_videos_playlistvideo'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='playlist',
|
||||
name='videos',
|
||||
field=models.ManyToManyField(to='backend.video'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PlaylistVideo',
|
||||
),
|
||||
]
|
0
backend/migrations/__init__.py
Normal file
0
backend/migrations/__init__.py
Normal file
44
backend/models.py
Normal file
44
backend/models.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from datetime import timedelta
|
||||
from json import loads
|
||||
|
||||
from .s3 import build_presigned_url
|
||||
|
||||
|
||||
class Video(models.Model):
|
||||
id = models.CharField(max_length=11, primary_key=True)
|
||||
title = models.CharField(max_length=100)
|
||||
published = models.DateTimeField()
|
||||
description = models.TextField(blank=True, null=True)
|
||||
path = models.CharField(max_length=1024)
|
||||
thumbnail_path = models.CharField(max_length=1024)
|
||||
original_json = models.JSONField(blank=True, null=True)
|
||||
|
||||
def get_video_url(self):
|
||||
return build_presigned_url(self.path)
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
return build_presigned_url(self.thumbnail_path, timedelta(minutes=5))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("video", kwargs={"pk": self.id})
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return loads(self.original_json)["tags"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Playlist(models.Model):
|
||||
id = models.CharField(max_length=34, primary_key=True)
|
||||
title = models.CharField(max_length=100)
|
||||
videos = models.ManyToManyField(Video)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("playlist", kwargs={"pk": self.id})
|
18
backend/s3.py
Normal file
18
backend/s3.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
def build_s3_from_settings():
|
||||
return Minio(
|
||||
endpoint = settings.S3_ENDPOINT,
|
||||
access_key= settings.S3_ACCESS_KEY,
|
||||
secret_key= settings.S3_SECRET_KEY
|
||||
)
|
||||
|
||||
def build_presigned_url(path, validity=timedelta(hours=12)):
|
||||
s3 = build_s3_from_settings()
|
||||
return s3.get_presigned_url("GET", settings.S3_BUCKET, path)
|
3
backend/tests.py
Normal file
3
backend/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
backend/views.py
Normal file
3
backend/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
frontend/__init__.py
Normal file
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
3
frontend/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
frontend/apps.py
Normal file
6
frontend/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'frontend'
|
0
frontend/migrations/__init__.py
Normal file
0
frontend/migrations/__init__.py
Normal file
3
frontend/models.py
Normal file
3
frontend/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
0
frontend/templatetags/__init__.py
Normal file
0
frontend/templatetags/__init__.py
Normal file
14
frontend/templatetags/random.py
Normal file
14
frontend/templatetags/random.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from random import choices
|
||||
|
||||
from django import template
|
||||
|
||||
from backend.models import Video
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def random_videos(count=4):
|
||||
ids = Video.objects.values_list("id", flat=True)
|
||||
for vid in choices(ids, k=count):
|
||||
yield Video.objects.get(id=vid)
|
3
frontend/tests.py
Normal file
3
frontend/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
frontend/urls.py
Normal file
10
frontend/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import PlaylistView, VideoView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', PlaylistView.as_view(), name="home"),
|
||||
path('video/<slug:pk>/', VideoView.as_view(), name="video"),
|
||||
path('playlist/<slug:pk>/', PlaylistView.as_view(), name="playlist"),
|
||||
]
|
44
frontend/views.py
Normal file
44
frontend/views.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from django.views.generic import ListView, DetailView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from backend.models import Video, Playlist
|
||||
|
||||
|
||||
class PlaylistView(ListView):
|
||||
template_name = "frontend/playlist.html"
|
||||
model = Video
|
||||
|
||||
paginate_by = 30
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = "All Videos"
|
||||
context["playlists"] = Playlist.objects.all()
|
||||
return context
|
||||
|
||||
def get_playlist(self):
|
||||
if "pk" in self.kwargs.keys():
|
||||
return get_object_or_404(Playlist, id=self.kwargs["pk"])
|
||||
|
||||
def get_queryset(self):
|
||||
if playlist := self.get_playlist():
|
||||
queryset = playlist.videos.all()
|
||||
else:
|
||||
queryset = Video.objects.all()
|
||||
|
||||
queryset = queryset.order_by(self.get_ordering())
|
||||
|
||||
return queryset
|
||||
|
||||
def get_ordering(self):
|
||||
return self.request.GET.get('sort', '-published')
|
||||
|
||||
|
||||
class VideoView(DetailView):
|
||||
template_name = "frontend/video.html"
|
||||
model = Video
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = self.object.title
|
||||
return context
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'steeldonutcollection.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
django
|
||||
minio
|
||||
django-autosecretkey
|
14
sdc.json
Normal file
14
sdc.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::sdc/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
settings.dist.ini
Normal file
5
settings.dist.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[S3]
|
||||
Endpoint = your.minio
|
||||
AccessKey = your_access_key
|
||||
SecretKey = supposed_to_be_secret_and_secure!
|
||||
Bucket = steeldonutbucket
|
10
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
10
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
static/bootstrap/js/bootstrap.min.js
vendored
Normal file
6
static/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
static/css/pikaday.min.css
vendored
Normal file
5
static/css/pikaday.min.css
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
@charset "UTF-8";/*!
|
||||
* Pikaday
|
||||
* Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
|
||||
*/.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid #ccc;border-bottom-color:#bbb;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;background-color:#fff}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;opacity:0}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:0;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:center center;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.is-rtl .pika-next,.pika-prev{float:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==)}.is-rtl .pika-prev,.pika-next{float:right;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=)}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-button{cursor:pointer;display:block;box-sizing:border-box;-moz-box-sizing:border-box;outline:0;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5}.pika-week{font-size:11px;color:#999}.is-today .pika-button{color:#3af;font-weight:700}.has-event .pika-button,.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.has-event .pika-button{background:#005da9;box-shadow:inset 0 1px 3px #0076c9}.is-disabled .pika-button,.is-inrange .pika-button{background:#d5e9f7}.is-startrange .pika-button{color:#fff;background:#6cb31d;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.is-disabled .pika-button{pointer-events:none;cursor:default;color:#999;opacity:.3}.is-outside-current-month .pika-button{color:#999;opacity:.3}.is-selection-disabled{pointer-events:none;cursor:default}.pika-button:hover,.pika-row.pick-whole-week:hover .pika-button{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-table abbr{border-bottom:none;cursor:help}
|
||||
/*# sourceMappingURL=pikaday.min.css.map */
|
BIN
static/fonts/ionicons.eot
Normal file
BIN
static/fonts/ionicons.eot
Normal file
Binary file not shown.
11
static/fonts/ionicons.min.css
vendored
Normal file
11
static/fonts/ionicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2230
static/fonts/ionicons.svg
Normal file
2230
static/fonts/ionicons.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 326 KiB |
BIN
static/fonts/ionicons.ttf
Normal file
BIN
static/fonts/ionicons.ttf
Normal file
Binary file not shown.
BIN
static/fonts/ionicons.woff
Normal file
BIN
static/fonts/ionicons.woff
Normal file
Binary file not shown.
1
static/js/pikaday.min.js
vendored
Normal file
1
static/js/pikaday.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
static/js/theme.js
Normal file
5
static/js/theme.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
document.querySelectorAll('.datepicker').forEach(function(field) {
|
||||
var picker = new Pikaday({
|
||||
field: field
|
||||
});
|
||||
});
|
0
steeldonutcollection/__init__.py
Normal file
0
steeldonutcollection/__init__.py
Normal file
16
steeldonutcollection/asgi.py
Normal file
16
steeldonutcollection/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for steeldonutcollection project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'steeldonutcollection.settings')
|
||||
|
||||
application = get_asgi_application()
|
124
steeldonutcollection/settings.py
Normal file
124
steeldonutcollection/settings.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from pathlib import Path
|
||||
|
||||
from autosecretkey import AutoSecretKey
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SETTINGS_FILE = AutoSecretKey(
|
||||
BASE_DIR / "settings.ini",
|
||||
template=BASE_DIR/"settings.dist.ini")
|
||||
|
||||
SECRET_KEY = SETTINGS_FILE.secret_key
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'frontend',
|
||||
'backend',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'steeldonutcollection.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / "templates"],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'steeldonutcollection.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# S3 settings imported from AutoSecretKey config file
|
||||
|
||||
S3_ENDPOINT = SETTINGS_FILE.config["S3"]["Endpoint"]
|
||||
S3_ACCESS_KEY = SETTINGS_FILE.config["S3"]["AccessKey"]
|
||||
S3_SECRET_KEY = SETTINGS_FILE.config["S3"]["SecretKey"]
|
||||
S3_BUCKET = SETTINGS_FILE.config["S3"]["Bucket"]
|
7
steeldonutcollection/urls.py
Normal file
7
steeldonutcollection/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include("frontend.urls")),
|
||||
]
|
16
steeldonutcollection/wsgi.py
Normal file
16
steeldonutcollection/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for steeldonutcollection project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'steeldonutcollection.settings')
|
||||
|
||||
application = get_wsgi_application()
|
37
templates/frontend/base.html
Normal file
37
templates/frontend/base.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{title}} - Steel Donut Collection</title>
|
||||
<meta name="description" content="The collection of Steel Donut Collective videos">
|
||||
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://fontproxy.kumi.systems/css?family=Lato:300,400,700">
|
||||
<link rel="stylesheet" href="/static/fonts/ionicons.min.css">
|
||||
<link rel="stylesheet" href="/static/css/pikaday.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-dark navbar-expand-lg fixed-top bg-white portfolio-navbar gradient">
|
||||
<div class="container"><a class="navbar-brand logo" href="/">Steel Donut Collection</a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navbarNav"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="page projects-page">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
<div class="container">
|
||||
<div class="links"><a href="https://kumi.website">About me</a><a href="https://kumi.website">Contact me</a><a href="https://kumig.it/kumitterer/steeldonutcollection">Git</a></div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/pikaday.min.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
53
templates/frontend/playlist.html
Normal file
53
templates/frontend/playlist.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "frontend/base.html" %}
|
||||
{% block content %}
|
||||
<section class="portfolio-block projects-with-sidebar">
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<h2>{{title}}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<ul class="list-unstyled sidebar">
|
||||
<li><a class="active" href="/">All</a></li>
|
||||
{% for playlist in playlists %}
|
||||
<li><a href="{{playlist.get_absolute_url}}">{{playlist.title}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="row">
|
||||
{% for video in object_list %}
|
||||
<div class="col-md-6 col-lg-4 project-sidebar-card"><a href="{{video.get_absolute_url}}"><img
|
||||
class="img-fluid image scale-on-hover"
|
||||
src="{{video.get_thumbnail_url}}">{{video.title}}</a></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{% if not page_obj.number <= 2 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" tabindex="-1">First<a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page_obj.number != 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{page_obj.previous_page_number}}">{{page_obj.previous_page_number}}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="?page={{page_obj.number}}">{{page_obj.number}}</a>
|
||||
</li>
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{page_obj.next_page_number}}">{{page_obj.next_page_number}}</a></li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{page_obj.paginator.num_pages}}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
40
templates/frontend/video.html
Normal file
40
templates/frontend/video.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "frontend/base.html" %}
|
||||
{% load random %}
|
||||
{% block content %}
|
||||
<section class="portfolio-block project">
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<h2>{{object.title}}</h2>
|
||||
</div>
|
||||
<div class="image"><video controls poster="{{object.get_thumbnail_url}}" src="{{object.get_video_url}}" width="100%" height="100%"></div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 offset-md-1 info">
|
||||
<h3>Description</h3>
|
||||
<p>{{object.description|linebreaksbr}}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-3 offset-md-1 meta">
|
||||
<div class="tags"><span class="meta-heading">Tags</span>
|
||||
{% for tag in object.tags %}
|
||||
<a href="#">{{tag}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="tags"><span class="meta-heading">Date</span>
|
||||
{{ object.published.date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="more-projects">
|
||||
<h3 class="text-center">Random Videos</h3>
|
||||
<div class="row gallery">
|
||||
{% random_videos as suggestions %}
|
||||
{% for video in suggestions %}
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="item"><a href="{{video.get_absolute_url}}"><img class="img-fluid scale-on-hover" src="{{video.get_thumbnail_url}}">{{video.title}}</a></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue