Compare commits
4 commits
778b23ecd4
...
f79220515b
Author | SHA1 | Date | |
---|---|---|---|
f79220515b | |||
69c27d5266 | |||
40bc4d9147 | |||
33212a7963 |
29 changed files with 513 additions and 91 deletions
23
.dockerignore
Normal file
23
.dockerignore
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Python
|
||||||
|
venv
|
||||||
|
dist
|
||||||
|
.tox
|
||||||
|
__pycache__
|
||||||
|
*.egg-info
|
||||||
|
.pytest_cache
|
||||||
|
|
||||||
|
# JS
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Application data
|
||||||
|
/.env
|
||||||
|
/_run*
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
assets
|
||||||
|
notes
|
2
assets/icons/icon.svg
Normal file
2
assets/icons/icon.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="12.084mm" height="12.084mm" version="1.1" viewBox="0 0 12.084 12.084" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.75 -1.9565)"><path d="m2 2.2065v7.3223l4.2617 4.2617h3.0605l4.2617-4.2617v-7.3223h-1v6.9082l-3.6758 3.6758h-2.2324l-3.6758-3.6758v-6.9082z" color="#000000" fill="#e00" stroke="#fff" stroke-linecap="square" stroke-width=".5"/></g><g transform="translate(-3.2188 -20.416)"><path d="m3.4688 20.666v7.3223l4.2617 4.2617h3.0605l4.2617-4.2617v-7.3223h-1v6.9082l-3.6758 3.6758h-2.2324l-3.6758-3.6758v-6.9082z" color="#000000" fill="#e00"/></g></svg>
|
After Width: | Height: | Size: 626 B |
|
@ -39,3 +39,7 @@
|
||||||
|
|
||||||
&:last-child
|
&:last-child
|
||||||
padding-bottom: 0.5vw
|
padding-bottom: 0.5vw
|
||||||
|
|
||||||
|
// Fix almost invisible navbar items on mobile
|
||||||
|
.navbar-item
|
||||||
|
color: #fff
|
||||||
|
|
47
deploy/Dockerfile
Normal file
47
deploy/Dockerfile
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
FROM thetadev256/ucast-dev
|
||||||
|
|
||||||
|
COPY . /build
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN poetry build -f wheel
|
||||||
|
|
||||||
|
FROM python:3.10
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
COPY --from=0 /build/dist /install
|
||||||
|
RUN pip install -- /install/*.whl gunicorn honcho
|
||||||
|
|
||||||
|
# ffmpeg static source (https://johnvansickle.com/ffmpeg/)
|
||||||
|
RUN set -e; \
|
||||||
|
mkdir /build_ffmpeg; \
|
||||||
|
cd /build_ffmpeg; \
|
||||||
|
case "$TARGETPLATFORM" in \
|
||||||
|
"linux/amd64") ffmpeg_arch="amd64";; \
|
||||||
|
"linux/arm64") ffmpeg_arch="arm64";; \
|
||||||
|
"linux/arm/v7") ffmpeg_arch="armhf";; \
|
||||||
|
*) echo "TARGETPLATFORM $TARGETPLATFORM not found"; exit 1 ;;\
|
||||||
|
esac; \
|
||||||
|
wget "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ffmpeg_arch}-static.tar.xz"; \
|
||||||
|
wget "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ffmpeg_arch}-static.tar.xz.md5"; \
|
||||||
|
md5sum -c "ffmpeg-release-${ffmpeg_arch}-static.tar.xz.md5"; \
|
||||||
|
tar Jxf "ffmpeg-release-${ffmpeg_arch}-static.tar.xz"; \
|
||||||
|
mv "ffmpeg-5.0.1-${ffmpeg_arch}-static/ffmpeg" /usr/bin; \
|
||||||
|
cd /; \
|
||||||
|
rm -rf /build_ffmpeg;
|
||||||
|
|
||||||
|
# nginx
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y nginx && \
|
||||||
|
apt-get clean && \
|
||||||
|
mkdir /ucast && \
|
||||||
|
chown 1000:1000 /ucast && \
|
||||||
|
chown -R 1000:1000 /var/lib/nginx /var/log/nginx
|
||||||
|
|
||||||
|
COPY ./deploy/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY ./deploy/nginx /etc/nginx/conf.d
|
||||||
|
COPY ./deploy/entrypoint.py /entrypoint.py
|
||||||
|
|
||||||
|
ENV UCAST_WORKDIR=/ucast
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
ENTRYPOINT /entrypoint.py
|
16
deploy/docker-compose.yml
Normal file
16
deploy/docker-compose.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
ucast:
|
||||||
|
image: thetadev256/ucast
|
||||||
|
user: 1000:1000
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
volumes:
|
||||||
|
- "../_run:/ucast"
|
||||||
|
environment:
|
||||||
|
UCAST_REDIS_URL: "redis://redis:6379"
|
||||||
|
UCAST_SECRET_KEY: "django-insecure-Es/+plApGxNBy8+ewB+74zMlmfV2H3whw6gu7i0ESwGrEWAUYRP3HM2EX0PLr3UJ"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: redis
|
||||||
|
image: redis:alpine
|
|
@ -11,5 +11,4 @@ services:
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
volumes:
|
volumes:
|
||||||
- "./nginx:/etc/nginx/conf.d:ro"
|
- "./nginx:/etc/nginx/conf.d:ro"
|
||||||
- "../_run/static:/static:ro"
|
- "../_run:/ucast:ro"
|
||||||
- "../_run/data:/files:ro"
|
|
||||||
|
|
30
deploy/entrypoint.py
Executable file
30
deploy/entrypoint.py
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from honcho import manager
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(cmd):
|
||||||
|
returncode = subprocess.call(cmd)
|
||||||
|
if returncode != 0:
|
||||||
|
sys.exit(returncode)
|
||||||
|
|
||||||
|
|
||||||
|
n_workers = int(os.environ.get("UCAST_N_WORKERS", "1"))
|
||||||
|
|
||||||
|
run_cmd(["ucast-manage", "collectstatic", "--noinput"])
|
||||||
|
run_cmd(["ucast-manage", "migrate"])
|
||||||
|
|
||||||
|
m = manager.Manager()
|
||||||
|
m.add_process("ucast", "gunicorn ucast_project.wsgi")
|
||||||
|
m.add_process("nginx", "nginx")
|
||||||
|
|
||||||
|
for i in range(n_workers):
|
||||||
|
m.add_process(f"worker_{i}", "ucast-manage rqworker")
|
||||||
|
|
||||||
|
m.add_process("scheduler", "ucast-manage rqscheduler")
|
||||||
|
|
||||||
|
m.loop()
|
||||||
|
sys.exit(m.returncode)
|
61
deploy/nginx.conf
Normal file
61
deploy/nginx.conf
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
worker_processes auto;
|
||||||
|
daemon off;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
include /etc/nginx/modules-enabled/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 768;
|
||||||
|
# multi_accept on;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
##
|
||||||
|
# Basic Settings
|
||||||
|
##
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
# server_tokens off;
|
||||||
|
|
||||||
|
# server_names_hash_bucket_size 64;
|
||||||
|
# server_name_in_redirect off;
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
##
|
||||||
|
# SSL Settings
|
||||||
|
##
|
||||||
|
|
||||||
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
##
|
||||||
|
# Logging Settings
|
||||||
|
##
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log stderr;
|
||||||
|
|
||||||
|
##
|
||||||
|
# Gzip Settings
|
||||||
|
##
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
|
||||||
|
# gzip_vary on;
|
||||||
|
# gzip_proxied any;
|
||||||
|
# gzip_comp_level 6;
|
||||||
|
# gzip_buffers 16 8k;
|
||||||
|
# gzip_http_version 1.1;
|
||||||
|
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
##
|
||||||
|
# Virtual Host Configs
|
||||||
|
##
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
include /etc/nginx/sites-enabled/*;
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8001;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
client_max_body_size 1M;
|
client_max_body_size 1M;
|
||||||
|
|
||||||
# serve media files
|
# serve media files
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /static/;
|
alias /ucast/static/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /internal_files/ {
|
location /internal_files/ {
|
||||||
internal;
|
internal;
|
||||||
alias /files/;
|
alias /ucast/data/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
20
poetry.lock
generated
20
poetry.lock
generated
|
@ -268,6 +268,20 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "20.1.0"
|
||||||
|
description = "WSGI HTTP Server for UNIX"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
eventlet = ["eventlet (>=0.24.1)"]
|
||||||
|
gevent = ["gevent (>=1.4.0)"]
|
||||||
|
setproctitle = ["setproctitle"]
|
||||||
|
tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "honcho"
|
name = "honcho"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -754,7 +768,7 @@ websockets = "*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "5c24c22351390a472cd905bf1d08890314441ef590c46a64e7940fb180f909a2"
|
content-hash = "a3da05c0c8552c9149eb04dee6e52c7f3fffd2de297a82595422511e3674f861"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -1034,6 +1048,10 @@ fonts = [
|
||||||
{file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"},
|
{file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"},
|
||||||
{file = "fonts-0.0.3.tar.gz", hash = "sha256:c626655b75a60715e118e44e270656fd22fd8f54252901ff6ebf1308ad01c405"},
|
{file = "fonts-0.0.3.tar.gz", hash = "sha256:c626655b75a60715e118e44e270656fd22fd8f54252901ff6ebf1308ad01c405"},
|
||||||
]
|
]
|
||||||
|
gunicorn = [
|
||||||
|
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
|
||||||
|
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||||
|
]
|
||||||
honcho = [
|
honcho = [
|
||||||
{file = "honcho-1.1.0-py2.py3-none-any.whl", hash = "sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f"},
|
{file = "honcho-1.1.0-py2.py3-none-any.whl", hash = "sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f"},
|
||||||
{file = "honcho-1.1.0.tar.gz", hash = "sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"},
|
{file = "honcho-1.1.0.tar.gz", hash = "sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"},
|
||||||
|
|
|
@ -39,6 +39,7 @@ pre-commit = "^2.19.0"
|
||||||
honcho = "^1.1.0"
|
honcho = "^1.1.0"
|
||||||
pytest-mock = "^3.7.0"
|
pytest-mock = "^3.7.0"
|
||||||
fakeredis = "^1.7.5"
|
fakeredis = "^1.7.5"
|
||||||
|
gunicorn = "^20.1.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
"ucast-manage" = "ucast_project.manage:main"
|
"ucast-manage" = "ucast_project.manage:main"
|
||||||
|
|
|
@ -7,3 +7,20 @@ class AddChannelForm(forms.Form):
|
||||||
|
|
||||||
class DeleteVideoForm(forms.Form):
|
class DeleteVideoForm(forms.Form):
|
||||||
id = forms.IntegerField()
|
id = forms.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
class EditChannelForm(forms.Form):
|
||||||
|
skip_shorts = forms.BooleanField(
|
||||||
|
label="Skip shorts (vertical videos < 1m)", required=False
|
||||||
|
)
|
||||||
|
skip_livestreams = forms.BooleanField(label="Skip livestreams", required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadChannelForm(forms.Form):
|
||||||
|
n_videos = forms.IntegerField(
|
||||||
|
label="Number of videos (counting from most recent)", initial=50, min_value=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequeueForm(forms.Form):
|
||||||
|
id = forms.UUIDField()
|
||||||
|
|
|
@ -59,20 +59,21 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
click.echo(
|
click.echo(
|
||||||
"""| %-15s|%10s |%10s |%10s |%10s |%10s |"""
|
"""| %-15s|%10s |%10s |%10s |%10s |%10s |%10s |"""
|
||||||
% ("Name", "Queued", "Active", "Deferred", "Finished", "Workers")
|
% ("Name", "Queued", "Active", "Deferred", "Finished", "Failed", "Workers")
|
||||||
)
|
)
|
||||||
|
|
||||||
self._print_separator()
|
self._print_separator()
|
||||||
|
|
||||||
click.echo(
|
click.echo(
|
||||||
"""| %-15s|%10s |%10s |%10s |%10s |%10s |"""
|
"""| %-15s|%10s |%10s |%10s |%10s |%10s |%10s |"""
|
||||||
% (
|
% (
|
||||||
statistics["name"],
|
statistics["name"],
|
||||||
statistics["jobs"],
|
statistics["jobs"],
|
||||||
statistics["started_jobs"],
|
statistics["started_jobs"],
|
||||||
statistics["deferred_jobs"],
|
statistics["deferred_jobs"],
|
||||||
statistics["finished_jobs"],
|
statistics["finished_jobs"],
|
||||||
|
statistics["failed_jobs"],
|
||||||
statistics["workers"],
|
statistics["workers"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -105,7 +106,7 @@ class Command(BaseCommand):
|
||||||
self.interval = options.get("interval")
|
self.interval = options.get("interval")
|
||||||
|
|
||||||
# Arbitrary
|
# Arbitrary
|
||||||
self.table_width = 78
|
self.table_width = 90
|
||||||
|
|
||||||
# Do not continuously poll
|
# Do not continuously poll
|
||||||
if not self.interval:
|
if not self.interval:
|
||||||
|
|
|
@ -85,3 +85,8 @@ def get_statistics() -> dict:
|
||||||
"failed_jobs": len(failed_job_registry),
|
"failed_jobs": len(failed_job_registry),
|
||||||
"scheduled_jobs": len(scheduled_job_registry),
|
"scheduled_jobs": len(scheduled_job_registry),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_failed_job_registry():
|
||||||
|
queue = get_queue()
|
||||||
|
return registry.FailedJobRegistry(queue.name, queue.connection)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -141,7 +142,8 @@ def download_audio(
|
||||||
:param sponsorblock: Enable Sponsorblock
|
:param sponsorblock: Enable Sponsorblock
|
||||||
:return: VideoDetails
|
:return: VideoDetails
|
||||||
"""
|
"""
|
||||||
tmp_dld_file = download_path.with_suffix(".dld" + download_path.suffix)
|
tmpdir = tempfile.TemporaryDirectory(prefix="ucast_")
|
||||||
|
tmp_dld_file = Path(tmpdir.name) / "audio.mp3"
|
||||||
|
|
||||||
ydl_params = {
|
ydl_params = {
|
||||||
"format": "bestaudio",
|
"format": "bestaudio",
|
||||||
|
|
|
@ -215,8 +215,7 @@ fieldset[disabled] .file-name, fieldset[disabled] .select select, .select fields
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bulma Base */
|
/* Bulma Base */ /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
|
||||||
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
p,
|
p,
|
||||||
|
@ -1576,16 +1575,13 @@ a.box:active {
|
||||||
.button.is-responsive.is-small {
|
.button.is-responsive.is-small {
|
||||||
font-size: 0.5625rem;
|
font-size: 0.5625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive,
|
.button.is-responsive,
|
||||||
.button.is-responsive.is-normal {
|
.button.is-responsive.is-normal {
|
||||||
font-size: 0.65625rem;
|
font-size: 0.65625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive.is-medium {
|
.button.is-responsive.is-medium {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive.is-large {
|
.button.is-responsive.is-large {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -1594,16 +1590,13 @@ a.box:active {
|
||||||
.button.is-responsive.is-small {
|
.button.is-responsive.is-small {
|
||||||
font-size: 0.65625rem;
|
font-size: 0.65625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive,
|
.button.is-responsive,
|
||||||
.button.is-responsive.is-normal {
|
.button.is-responsive.is-normal {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive.is-medium {
|
.button.is-responsive.is-medium {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-responsive.is-large {
|
.button.is-responsive.is-large {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
@ -5250,17 +5243,14 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
|
||||||
.navbar > .container {
|
.navbar > .container {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand .navbar-item,
|
.navbar-brand .navbar-item,
|
||||||
.navbar-tabs .navbar-item {
|
.navbar-tabs .navbar-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-link::after {
|
.navbar-link::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-menu {
|
.navbar-menu {
|
||||||
background-color: hsl(0deg, 0%, 100%);
|
background-color: hsl(0deg, 0%, 100%);
|
||||||
box-shadow: 0 8px 16px rgba(10, 10, 10, 0.1);
|
box-shadow: 0 8px 16px rgba(10, 10, 10, 0.1);
|
||||||
|
@ -5269,7 +5259,6 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
|
||||||
.navbar-menu.is-active {
|
.navbar-menu.is-active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.is-fixed-bottom-touch, .navbar.is-fixed-top-touch {
|
.navbar.is-fixed-bottom-touch, .navbar.is-fixed-top-touch {
|
||||||
left: 0;
|
left: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -5290,7 +5279,6 @@ a.navbar-item:focus, a.navbar-item:focus-within, a.navbar-item:hover, a.navbar-i
|
||||||
max-height: calc(100vh - 3.25rem);
|
max-height: calc(100vh - 3.25rem);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.has-navbar-fixed-top-touch,
|
html.has-navbar-fixed-top-touch,
|
||||||
body.has-navbar-fixed-top-touch {
|
body.has-navbar-fixed-top-touch {
|
||||||
padding-top: 3.25rem;
|
padding-top: 3.25rem;
|
||||||
|
@ -5308,7 +5296,6 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
min-height: 3.25rem;
|
min-height: 3.25rem;
|
||||||
}
|
}
|
||||||
|
@ -5340,17 +5327,14 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
background-color: hsl(0deg, 0%, 96%);
|
background-color: hsl(0deg, 0%, 96%);
|
||||||
color: hsl(229deg, 53%, 53%);
|
color: hsl(229deg, 53%, 53%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-burger {
|
.navbar-burger {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item,
|
.navbar-item,
|
||||||
.navbar-link {
|
.navbar-link {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item.has-dropdown {
|
.navbar-item.has-dropdown {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
@ -5373,22 +5357,18 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-menu {
|
.navbar-menu {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-start {
|
.navbar-start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-end {
|
.navbar-end {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-dropdown {
|
.navbar-dropdown {
|
||||||
background-color: hsl(0deg, 0%, 100%);
|
background-color: hsl(0deg, 0%, 100%);
|
||||||
border-bottom-left-radius: 6px;
|
border-bottom-left-radius: 6px;
|
||||||
|
@ -5434,11 +5414,9 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-divider {
|
.navbar-divider {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar > .container .navbar-brand,
|
.navbar > .container .navbar-brand,
|
||||||
.container > .navbar .navbar-brand {
|
.container > .navbar .navbar-brand {
|
||||||
margin-left: -0.75rem;
|
margin-left: -0.75rem;
|
||||||
|
@ -5447,7 +5425,6 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
.container > .navbar .navbar-menu {
|
.container > .navbar .navbar-menu {
|
||||||
margin-right: -0.75rem;
|
margin-right: -0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.is-fixed-bottom-desktop, .navbar.is-fixed-top-desktop {
|
.navbar.is-fixed-bottom-desktop, .navbar.is-fixed-top-desktop {
|
||||||
left: 0;
|
left: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -5463,7 +5440,6 @@ body.has-navbar-fixed-bottom-touch {
|
||||||
.navbar.is-fixed-top-desktop {
|
.navbar.is-fixed-top-desktop {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.has-navbar-fixed-top-desktop,
|
html.has-navbar-fixed-top-desktop,
|
||||||
body.has-navbar-fixed-top-desktop {
|
body.has-navbar-fixed-top-desktop {
|
||||||
padding-top: 3.25rem;
|
padding-top: 3.25rem;
|
||||||
|
@ -5480,7 +5456,6 @@ body.has-spaced-navbar-fixed-top {
|
||||||
body.has-spaced-navbar-fixed-bottom {
|
body.has-spaced-navbar-fixed-bottom {
|
||||||
padding-bottom: 5.25rem;
|
padding-bottom: 5.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.navbar-item.is-active,
|
a.navbar-item.is-active,
|
||||||
.navbar-link.is-active {
|
.navbar-link.is-active {
|
||||||
color: hsl(0deg, 0%, 4%);
|
color: hsl(0deg, 0%, 4%);
|
||||||
|
@ -5489,7 +5464,6 @@ body.has-spaced-navbar-fixed-bottom {
|
||||||
.navbar-link.is-active:not(:focus):not(:hover) {
|
.navbar-link.is-active:not(:focus):not(:hover) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item.has-dropdown:focus .navbar-link, .navbar-item.has-dropdown:hover .navbar-link, .navbar-item.has-dropdown.is-active .navbar-link {
|
.navbar-item.has-dropdown:focus .navbar-link, .navbar-item.has-dropdown:hover .navbar-link, .navbar-item.has-dropdown.is-active .navbar-link {
|
||||||
background-color: hsl(0deg, 0%, 98%);
|
background-color: hsl(0deg, 0%, 98%);
|
||||||
}
|
}
|
||||||
|
@ -5605,13 +5579,11 @@ body.has-spaced-navbar-fixed-bottom {
|
||||||
.pagination {
|
.pagination {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-previous,
|
.pagination-previous,
|
||||||
.pagination-next {
|
.pagination-next {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-list li {
|
.pagination-list li {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
@ -5624,7 +5596,6 @@ body.has-spaced-navbar-fixed-bottom {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-previous,
|
.pagination-previous,
|
||||||
.pagination-next,
|
.pagination-next,
|
||||||
.pagination-link,
|
.pagination-link,
|
||||||
|
@ -5632,15 +5603,12 @@ body.has-spaced-navbar-fixed-bottom {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-previous {
|
.pagination-previous {
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-next {
|
.pagination-next {
|
||||||
order: 3;
|
order: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -8847,27 +8815,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-mobile {
|
.is-size-1-mobile {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-mobile {
|
.is-size-2-mobile {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-mobile {
|
.is-size-3-mobile {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-mobile {
|
.is-size-4-mobile {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-mobile {
|
.is-size-5-mobile {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-mobile {
|
.is-size-6-mobile {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-mobile {
|
.is-size-7-mobile {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -8876,27 +8838,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-tablet {
|
.is-size-1-tablet {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-tablet {
|
.is-size-2-tablet {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-tablet {
|
.is-size-3-tablet {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-tablet {
|
.is-size-4-tablet {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-tablet {
|
.is-size-5-tablet {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-tablet {
|
.is-size-6-tablet {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-tablet {
|
.is-size-7-tablet {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -8905,27 +8861,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-touch {
|
.is-size-1-touch {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-touch {
|
.is-size-2-touch {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-touch {
|
.is-size-3-touch {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-touch {
|
.is-size-4-touch {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-touch {
|
.is-size-5-touch {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-touch {
|
.is-size-6-touch {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-touch {
|
.is-size-7-touch {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -8934,27 +8884,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-desktop {
|
.is-size-1-desktop {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-desktop {
|
.is-size-2-desktop {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-desktop {
|
.is-size-3-desktop {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-desktop {
|
.is-size-4-desktop {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-desktop {
|
.is-size-5-desktop {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-desktop {
|
.is-size-6-desktop {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-desktop {
|
.is-size-7-desktop {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -8963,27 +8907,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-widescreen {
|
.is-size-1-widescreen {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-widescreen {
|
.is-size-2-widescreen {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-widescreen {
|
.is-size-3-widescreen {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-widescreen {
|
.is-size-4-widescreen {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-widescreen {
|
.is-size-5-widescreen {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-widescreen {
|
.is-size-6-widescreen {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-widescreen {
|
.is-size-7-widescreen {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -8992,27 +8930,21 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
.is-size-1-fullhd {
|
.is-size-1-fullhd {
|
||||||
font-size: 3rem !important;
|
font-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-2-fullhd {
|
.is-size-2-fullhd {
|
||||||
font-size: 2.5rem !important;
|
font-size: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-3-fullhd {
|
.is-size-3-fullhd {
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-4-fullhd {
|
.is-size-4-fullhd {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-5-fullhd {
|
.is-size-5-fullhd {
|
||||||
font-size: 1.25rem !important;
|
font-size: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-6-fullhd {
|
.is-size-6-fullhd {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-size-7-fullhd {
|
.is-size-7-fullhd {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
@ -10427,4 +10359,8 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
|
||||||
padding-bottom: 0.5vw;
|
padding-bottom: 0.5vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-item {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=style.css.map */
|
/*# sourceMappingURL=style.css.map */
|
File diff suppressed because one or more lines are too long
2
ucast/static/bulma/css/style.min.css
vendored
2
ucast/static/bulma/css/style.min.css
vendored
File diff suppressed because one or more lines are too long
BIN
ucast/static/ucast/favicon.ico
Normal file
BIN
ucast/static/ucast/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -52,6 +52,11 @@ def download_video(video: Video):
|
||||||
|
|
||||||
:param video: Video object
|
:param video: Video object
|
||||||
"""
|
"""
|
||||||
|
# Return if the video was already downloaded by a previous task
|
||||||
|
video.refresh_from_db()
|
||||||
|
if video.downloaded:
|
||||||
|
return
|
||||||
|
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
channel_folder = store.get_or_create_channel_folder(video.channel.slug)
|
channel_folder = store.get_or_create_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
|
@ -109,3 +114,17 @@ def update_channels():
|
||||||
"""
|
"""
|
||||||
for channel in Channel.objects.filter(active=True):
|
for channel in Channel.objects.filter(active=True):
|
||||||
queue.enqueue(update_channel, channel)
|
queue.enqueue(update_channel, channel)
|
||||||
|
|
||||||
|
|
||||||
|
def download_channel(channel: Channel, limit: int):
|
||||||
|
"""
|
||||||
|
Download maximum number of videos from a channel.
|
||||||
|
|
||||||
|
:param channel: Channel object
|
||||||
|
:param limit: Max number of videos
|
||||||
|
"""
|
||||||
|
if limit < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
for vid in youtube.get_channel_videos_from_scraper(channel.channel_id, limit):
|
||||||
|
_load_scraped_video(vid, channel)
|
||||||
|
|
|
@ -26,14 +26,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
<a class="navbar-item" href="{% url 'download_errors' %}">
|
||||||
|
Errors
|
||||||
|
</a>
|
||||||
{% url 'login' as login_url %}
|
{% url 'login' as login_url %}
|
||||||
{% url 'logout' as logout_url %}
|
{% url 'logout' as logout_url %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a class="navbar-item is-hidden-desktop-only" href="{{ logout_url }}">
|
<a class="navbar-item" href="{{ logout_url }}">
|
||||||
Logout
|
Logout
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="navbar-item is-hidden-desktop-only" href="{{ login_url }}">
|
<a class="navbar-item" href="{{ login_url }}">
|
||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
24
ucast/templates/ucast/channel_download.html
Normal file
24
ucast/templates/ucast/channel_download.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load bulma_tags %}
|
||||||
|
|
||||||
|
{% block title %}ucast - {{ channel.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<a class="title" href="{% url 'videos' channel.slug %}">{{ channel.name }}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="subtitle">Batch download</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|bulma }}
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit" class="button is-primary">Download</button>
|
||||||
|
<a href="{% url 'videos' channel.slug %}" class="button">Back</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
24
ucast/templates/ucast/channel_edit.html
Normal file
24
ucast/templates/ucast/channel_edit.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load bulma_tags %}
|
||||||
|
|
||||||
|
{% block title %}ucast - {{ channel.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<a class="title" href="{% url 'videos' channel.slug %}">{{ channel.name }}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="subtitle">Edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|bulma }}
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit" class="button is-primary">OK</button>
|
||||||
|
<a href="{% url 'videos' channel.slug %}" class="button">Back</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
|
@ -45,7 +45,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 is-flex is-flex-direction-column is-flex-grow-1">
|
<div class="ml-3 is-flex is-flex-direction-column is-flex-grow-1">
|
||||||
<a class="subtitle" href="/channel/{{ channel.slug }}">{{ channel.name }}</a>
|
<a class="subtitle" href="{% url 'videos' channel.slug %}">{{ channel.name }}</a>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a href="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
<a href="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
||||||
|
|
50
ucast/templates/ucast/download_errors.html
Normal file
50
ucast/templates/ucast/download_errors.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}ucast - Errors{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="title">Download errors</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if jobs %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<form method="post" action="{% url 'download_errors_requeue_all' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-primary">Requeue all</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Function</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>Requeue</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in jobs %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ job.id }}</td>
|
||||||
|
<td>{{ job.func_name }}</td>
|
||||||
|
<td><a href="{% url 'error_details' job.id %}">Details</a></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{% url 'download_errors_requeue' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ job.id }}">
|
||||||
|
<button class="button is-small">Requeue</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>No download errors</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock content %}
|
27
ucast/templates/ucast/error_details.html
Normal file
27
ucast/templates/ucast/error_details.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}ucast - Error Details{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="title">Job {{ job.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="subtitle">{{ job.func_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre class="mb-4">
|
||||||
|
{{ job.exc_info }}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form method="post" action="{% url 'download_errors_requeue' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ job.id }}">
|
||||||
|
<button class="button is-primary">Requeue</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}ucast - Videos{% endblock %}
|
{% block title %}ucast - {{ channel.name }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="level">
|
<div class="level">
|
||||||
|
@ -39,13 +39,16 @@
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-info">
|
<a class="button is-info" href="{% url 'channel_edit' channel.slug %}">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</button>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-info" href="{% url 'channel_download' channel.slug %}">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" name="delete_channel" class="button is-danger dialog-confirm"
|
<button type="submit" name="delete_channel" class="button is-danger dialog-confirm"
|
||||||
confirm-msg="Do you want to delete the channel '{{ channel.name }}' including {{ videos|length }} videos?">
|
confirm-msg="Do you want to delete the channel '{{ channel.name }}' including {{ videos|length }} videos?">
|
||||||
|
|
|
@ -1,11 +1,33 @@
|
||||||
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from ucast import views
|
from ucast import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.home),
|
path("", views.home),
|
||||||
|
path(
|
||||||
|
"favicon.ico",
|
||||||
|
RedirectView.as_view(url=staticfiles_storage.url("ucast/favicon.ico")),
|
||||||
|
),
|
||||||
path("channel/<str:channel>", views.videos, name="videos"),
|
path("channel/<str:channel>", views.videos, name="videos"),
|
||||||
path("feed/<str:channel>", views.podcast_feed),
|
path("channel/<str:channel>/edit", views.channel_edit, name="channel_edit"),
|
||||||
|
path(
|
||||||
|
"channel/<str:channel>/download",
|
||||||
|
views.channel_download,
|
||||||
|
name="channel_download",
|
||||||
|
),
|
||||||
|
path("errors", views.download_errors, name="download_errors"),
|
||||||
|
path(
|
||||||
|
"errors/requeue", views.download_errors_requeue, name="download_errors_requeue"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"errors/requeue_all",
|
||||||
|
views.download_errors_requeue_all,
|
||||||
|
name="download_errors_requeue_all",
|
||||||
|
),
|
||||||
|
path("errors/<str:job_id>", views.error_details, name="error_details"),
|
||||||
|
path("feed/<str:channel>", views.podcast_feed, name="feed"),
|
||||||
path("files/audio/<str:channel>/<str:video>", views.audio),
|
path("files/audio/<str:channel>/<str:video>", views.audio),
|
||||||
path("files/cover/<str:channel>/<str:video>", views.cover),
|
path("files/cover/<str:channel>/<str:video>", views.cover),
|
||||||
path("files/thumbnail/<str:channel>/<str:video>", views.thumbnail),
|
path("files/thumbnail/<str:channel>/<str:video>", views.thumbnail),
|
||||||
|
|
|
@ -102,6 +102,94 @@ def videos(request: http.HttpRequest, channel: str):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def channel_edit(request: http.HttpRequest, channel: str):
|
||||||
|
chan = Channel.objects.get(slug=channel)
|
||||||
|
form = forms.EditChannelForm(
|
||||||
|
initial={
|
||||||
|
"skip_shorts": chan.skip_shorts,
|
||||||
|
"skip_livestreams": chan.skip_livestreams,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = forms.EditChannelForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
chan.skip_shorts = form.cleaned_data["skip_shorts"]
|
||||||
|
chan.skip_livestreams = form.cleaned_data["skip_livestreams"]
|
||||||
|
chan.save()
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(videos, args=[channel]))
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"ucast/channel_edit.html",
|
||||||
|
{
|
||||||
|
"channel": chan,
|
||||||
|
"form": form,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def channel_download(request: http.HttpRequest, channel: str):
|
||||||
|
chan = Channel.objects.get(slug=channel)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = forms.DownloadChannelForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
queue.enqueue(
|
||||||
|
download.download_channel, chan, form.cleaned_data["n_videos"]
|
||||||
|
)
|
||||||
|
return http.HttpResponseRedirect(reverse(videos, args=[channel]))
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"ucast/channel_download.html",
|
||||||
|
{
|
||||||
|
"channel": chan,
|
||||||
|
"form": forms.DownloadChannelForm(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def download_errors(request: http.HttpRequest):
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
ids = freg.get_job_ids(0, 50)
|
||||||
|
jobs = freg.job_class.fetch_many(ids, freg.connection, freg.serializer)
|
||||||
|
|
||||||
|
return render(request, "ucast/download_errors.html", {"jobs": jobs})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def error_details(request: http.HttpRequest, job_id: str):
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
job = freg.job_class.fetch(job_id, freg.connection, freg.serializer)
|
||||||
|
|
||||||
|
return render(request, "ucast/error_details.html", {"job": job})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def download_errors_requeue(request: http.HttpRequest):
|
||||||
|
form = forms.RequeueForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
freg.requeue(str(form.cleaned_data["id"]))
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(download_errors))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def download_errors_requeue_all(request: http.HttpRequest):
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
for job_id in freg.get_job_ids():
|
||||||
|
freg.requeue(job_id)
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(download_errors))
|
||||||
|
|
||||||
|
|
||||||
def _channel_file(channel: str, get_file: Callable[[storage.ChannelFolder], Path]):
|
def _channel_file(channel: str, get_file: Callable[[storage.ChannelFolder], Path]):
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue