User conf endpoints, fix channel parser, #build

Changed:
- [API] Added endpoints to CRUD user conf vars
- [API] Added backup endpoints
- Fix channel about page parsing
- Add custom CSS files
- Remember player volume
This commit is contained in:
Simon 2023-11-09 09:40:54 +07:00
commit 9c26357f76
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
25 changed files with 420 additions and 245 deletions

View File

@ -96,6 +96,16 @@ urlpatterns = [
views.SnapshotApiView.as_view(),
name="api-snapshot",
),
path(
"backup/",
views.BackupApiListView.as_view(),
name="api-backup-list",
),
path(
"backup/<str:filename>/",
views.BackupApiView.as_view(),
name="api-backup",
),
path(
"task-name/",
views.TaskListView.as_view(),
@ -111,6 +121,11 @@ urlpatterns = [
views.TaskIDView.as_view(),
name="api-task-id",
),
path(
"config/user/",
views.UserConfigView.as_view(),
name="api-config-user",
),
path(
"cookie/",
views.CookieView.as_view(),

View File

@ -8,6 +8,7 @@ from home.src.download.subscriptions import (
PlaylistSubscription,
)
from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.backup import ElasticBackup
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.searching import SearchForm
@ -22,11 +23,13 @@ from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.task_manager import TaskCommand, TaskManager
from home.src.ta.urlparser import Parser
from home.src.ta.users import UserConfig
from home.tasks import (
BaseTask,
check_reindex,
download_pending,
extrac_dl,
run_restore_backup,
subscribe_to,
)
from rest_framework import permissions
@ -764,6 +767,80 @@ class SnapshotApiView(ApiBaseView):
return Response(response)
class BackupApiListView(ApiBaseView):
"""resolves to /api/backup/
GET: returns list of available zip backups
POST: take zip backup now
"""
permission_classes = [AdminOnly]
task_name = "run_backup"
@staticmethod
def get(request):
"""handle get request"""
# pylint: disable=unused-argument
backup_files = ElasticBackup().get_all_backup_files()
return Response(backup_files)
def post(self, request):
"""handle post request"""
# pylint: disable=unused-argument
response = TaskCommand().start(self.task_name)
message = {
"message": "backup task started",
"task_id": response["task_id"],
}
return Response(message)
class BackupApiView(ApiBaseView):
"""resolves to /api/backup/<filename>/
GET: return a single backup
POST: restore backup
DELETE: delete backup
"""
permission_classes = [AdminOnly]
task_name = "restore_backup"
@staticmethod
def get(request, filename):
"""get single backup"""
# pylint: disable=unused-argument
backup_file = ElasticBackup().build_backup_file_data(filename)
if not backup_file:
message = {"message": "file not found"}
return Response(message, status=404)
return Response(backup_file)
def post(self, request, filename):
"""restore backup file"""
# pylint: disable=unused-argument
task = run_restore_backup.delay(filename)
message = {
"message": "backup restore task started",
"filename": filename,
"task_id": task.id,
}
return Response(message)
@staticmethod
def delete(request, filename):
"""delete backup file"""
# pylint: disable=unused-argument
backup_file = ElasticBackup().delete_file(filename)
if not backup_file:
message = {"message": "file not found"}
return Response(message, status=404)
message = {"message": f"file {filename} deleted"}
return Response(message)
class TaskListView(ApiBaseView):
"""resolves to /api/task-name/
GET: return a list of all stored task results
@ -905,6 +982,42 @@ class RefreshView(ApiBaseView):
return Response(data)
class UserConfigView(ApiBaseView):
"""resolves to /api/config/user/
GET: return current user config
POST: update user config
"""
def get(self, request):
"""get config"""
user_id = request.user.id
response = UserConfig(user_id).get_config()
response.update({"user_id": user_id})
return Response(response)
def post(self, request):
"""update config"""
user_id = request.user.id
data = request.data
user_conf = UserConfig(user_id)
for key, value in data.items():
try:
user_conf.set_value(key, value)
except ValueError as err:
message = {
"status": "Bad Request",
"message": f"failed updating {key} to '{value}', {err}",
}
return Response(message, status=400)
response = user_conf.get_config()
response.update({"user_id": user_id})
return Response(response)
class CookieView(ApiBaseView):
"""resolves to /api/cookie/
GET: check if cookie is enabled

View File

@ -162,11 +162,11 @@ class Command(BaseCommand):
for user in users:
new_conf = UserConfig(user)
colors_key = f"{user}:colors"
colors = redis.get_message(colors_key).get("status")
if colors:
new_conf.set_value("colors", colors)
redis.del_message(colors_key)
stylesheet_key = f"{user}:color"
stylesheet = redis.get_message(stylesheet_key).get("status")
if stylesheet:
new_conf.set_value("stylesheet", stylesheet)
redis.del_message(stylesheet_key)
sort_by_key = f"{user}:sort_by"
sort_by = redis.get_message(sort_by_key).get("status")

View File

@ -20,10 +20,11 @@ class ElasticBackup:
"""dump index to nd-json files for later bulk import"""
INDEX_SPLIT = ["comment"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
BACKUP_DIR = os.path.join(CACHE_DIR, "backup")
def __init__(self, reason=False, task=False):
self.config = AppConfig().config
self.cache_dir = EnvironmentSettings.CACHE_DIR
self.timestamp = datetime.now().strftime("%Y%m%d")
self.index_config = get_mapping()
self.reason = reason
@ -79,14 +80,13 @@ class ElasticBackup:
def zip_it(self):
"""pack it up into single zip file"""
file_name = f"ta_backup-{self.timestamp}-{self.reason}.zip"
folder = os.path.join(self.cache_dir, "backup")
to_backup = []
for file in os.listdir(folder):
for file in os.listdir(self.BACKUP_DIR):
if file.endswith(".json"):
to_backup.append(os.path.join(folder, file))
to_backup.append(os.path.join(self.BACKUP_DIR, file))
backup_file = os.path.join(folder, file_name)
backup_file = os.path.join(self.BACKUP_DIR, file_name)
comp = zipfile.ZIP_DEFLATED
with zipfile.ZipFile(backup_file, "w", compression=comp) as zip_f:
@ -99,7 +99,7 @@ class ElasticBackup:
def post_bulk_restore(self, file_name):
"""send bulk to es"""
file_path = os.path.join(self.cache_dir, file_name)
file_path = os.path.join(self.CACHE_DIR, file_name)
with open(file_path, "r", encoding="utf-8") as f:
data = f.read()
@ -110,9 +110,7 @@ class ElasticBackup:
def get_all_backup_files(self):
"""build all available backup files for view"""
backup_dir = os.path.join(self.cache_dir, "backup")
backup_files = os.listdir(backup_dir)
all_backup_files = ignore_filelist(backup_files)
all_backup_files = ignore_filelist(os.listdir(self.BACKUP_DIR))
all_available_backups = [
i
for i in all_backup_files
@ -121,24 +119,36 @@ class ElasticBackup:
all_available_backups.sort(reverse=True)
backup_dicts = []
for backup_file in all_available_backups:
file_split = backup_file.split("-")
if len(file_split) == 2:
timestamp = file_split[1].strip(".zip")
reason = False
elif len(file_split) == 3:
timestamp = file_split[1]
reason = file_split[2].strip(".zip")
to_add = {
"filename": backup_file,
"timestamp": timestamp,
"reason": reason,
}
backup_dicts.append(to_add)
for filename in all_available_backups:
data = self.build_backup_file_data(filename)
backup_dicts.append(data)
return backup_dicts
def build_backup_file_data(self, filename):
"""build metadata of single backup file"""
file_path = os.path.join(self.BACKUP_DIR, filename)
if not os.path.exists(file_path):
return False
file_split = filename.split("-")
if len(file_split) == 2:
timestamp = file_split[1].strip(".zip")
reason = False
elif len(file_split) == 3:
timestamp = file_split[1]
reason = file_split[2].strip(".zip")
data = {
"filename": filename,
"file_path": file_path,
"file_size": os.path.getsize(file_path),
"timestamp": timestamp,
"reason": reason,
}
return data
def restore(self, filename):
"""
restore from backup zip file
@ -149,22 +159,19 @@ class ElasticBackup:
def _unpack_zip_backup(self, filename):
"""extract backup zip and return filelist"""
backup_dir = os.path.join(self.cache_dir, "backup")
file_path = os.path.join(backup_dir, filename)
file_path = os.path.join(self.BACKUP_DIR, filename)
with zipfile.ZipFile(file_path, "r") as z:
zip_content = z.namelist()
z.extractall(backup_dir)
z.extractall(self.BACKUP_DIR)
return zip_content
def _restore_json_files(self, zip_content):
"""go through the unpacked files and restore"""
backup_dir = os.path.join(self.cache_dir, "backup")
for idx, json_f in enumerate(zip_content):
self._notify_restore(idx, json_f, len(zip_content))
file_name = os.path.join(backup_dir, json_f)
file_name = os.path.join(self.BACKUP_DIR, json_f)
if not json_f.startswith("es_") or not json_f.endswith(".json"):
os.remove(file_name)
@ -201,13 +208,21 @@ class ElasticBackup:
print("no backup files to rotate")
return
backup_dir = os.path.join(self.cache_dir, "backup")
all_to_delete = auto[rotate:]
for to_delete in all_to_delete:
file_path = os.path.join(backup_dir, to_delete["filename"])
print(f"remove old backup file: {file_path}")
os.remove(file_path)
self.delete_file(to_delete["filename"])
def delete_file(self, filename):
"""delete backup file"""
file_path = os.path.join(self.BACKUP_DIR, filename)
if not os.path.exists(file_path):
print(f"backup file not found: {filename}")
return False
print(f"remove old backup file: {file_path}")
os.remove(file_path)
return file_path
class BackupCallback:

View File

@ -1,92 +0,0 @@
"""
Functionality:
- collection of functions and tasks from frontend
- called via user input
"""
from home.src.ta.users import UserConfig
from home.tasks import run_restore_backup
class PostData:
"""
map frontend http post values to backend funcs
handover long running tasks to celery
"""
def __init__(self, post_dict, current_user):
self.post_dict = post_dict
self.to_exec, self.exec_val = list(post_dict.items())[0]
self.current_user = current_user
def run_task(self):
"""execute and return task result"""
to_exec = self.exec_map()
task_result = to_exec()
return task_result
def exec_map(self):
"""map dict key and return function to execute"""
exec_map = {
"change_view": self._change_view,
"change_grid": self._change_grid,
"sort_order": self._sort_order,
"hide_watched": self._hide_watched,
"show_subed_only": self._show_subed_only,
"show_ignored_only": self._show_ignored_only,
"db-restore": self._db_restore,
}
return exec_map[self.to_exec]
def _change_view(self):
"""process view changes in home, channel, and downloads"""
view, setting = self.exec_val.split(":")
UserConfig(self.current_user).set_value(f"view_style_{view}", setting)
return {"success": True}
def _change_grid(self):
"""process change items in grid"""
grid_items = int(self.exec_val)
grid_items = max(grid_items, 3)
grid_items = min(grid_items, 7)
UserConfig(self.current_user).set_value("grid_items", grid_items)
return {"success": True}
def _sort_order(self):
"""change the sort between published to downloaded"""
if self.exec_val in ["asc", "desc"]:
UserConfig(self.current_user).set_value(
"sort_order", self.exec_val
)
else:
UserConfig(self.current_user).set_value("sort_by", self.exec_val)
return {"success": True}
def _hide_watched(self):
"""toggle if to show watched vids or not"""
UserConfig(self.current_user).set_value(
"hide_watched", bool(int(self.exec_val))
)
return {"success": True}
def _show_subed_only(self):
"""show or hide subscribed channels only on channels page"""
UserConfig(self.current_user).set_value(
"show_subed_only", bool(int(self.exec_val))
)
return {"success": True}
def _show_ignored_only(self):
"""switch view on /downloads/ to show ignored only"""
UserConfig(self.current_user).set_value(
"show_ignored_only", bool(int(self.exec_val))
)
return {"success": True}
def _db_restore(self):
"""restore es zip from settings page"""
print("restoring index from backup zip")
filename = self.exec_val
run_restore_backup.delay(filename)
return {"success": True}

View File

@ -2,9 +2,12 @@
- hold all form classes used in the views
"""
import os
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.forms.widgets import PasswordInput, TextInput
from home.src.ta.helper import get_stylesheets
class CustomAuthForm(AuthenticationForm):
@ -29,14 +32,16 @@ class CustomAuthForm(AuthenticationForm):
class UserSettingsForm(forms.Form):
"""user configurations values"""
CHOICES = [
("", "-- change color scheme --"),
("dark", "Dark"),
("light", "Light"),
]
STYLESHEET_CHOICES = [("", "-- change stylesheet --")]
STYLESHEET_CHOICES.extend(
[
(stylesheet, os.path.splitext(stylesheet)[0].title())
for stylesheet in get_stylesheets()
]
)
colors = forms.ChoiceField(
widget=forms.Select, choices=CHOICES, required=False
stylesheet = forms.ChoiceField(
widget=forms.Select, choices=STYLESHEET_CHOICES, required=False
)
page_size = forms.IntegerField(required=False)

View File

@ -23,20 +23,13 @@ class YoutubeChannel(YouTubeItem):
es_path = False
index_name = "ta_channel"
yt_base = "https://www.youtube.com/channel/"
yt_obs = {
"extract_flat": True,
"allow_playlist_files": True,
}
yt_obs = {"playlist_items": "0,0"}
def __init__(self, youtube_id, task=False):
super().__init__(youtube_id)
self.all_playlists = False
self.task = task
def build_yt_url(self):
"""overwrite base to use channel about page"""
return f"{self.yt_base}{self.youtube_id}/about"
def build_json(self, upload=False, fallback=False):
"""get from es or from youtube"""
self.get_from_es()
@ -69,7 +62,7 @@ class YoutubeChannel(YouTubeItem):
"channel_banner_url": self._get_banner_art(),
"channel_thumb_url": self._get_thumb_art(),
"channel_tvart_url": self._get_tv_art(),
"channel_views": self.youtube_meta.get("view_count", 0),
"channel_views": self.youtube_meta.get("view_count") or 0,
}
def _parse_tags(self, tags):

View File

@ -83,7 +83,7 @@ class Scanner:
if self.task:
self.task.send_progress(
message_lines=[
f"Index missing video {youtube_id}, {idx}/{total}"
f"Index missing video {youtube_id}, {idx + 1}/{total}"
],
progress=(idx + 1) / total,
)

View File

@ -12,6 +12,7 @@ from datetime import datetime
from urllib.parse import urlparse
import requests
from home.src.ta.settings import EnvironmentSettings
def ignore_filelist(filelist: list[str]) -> list[str]:
@ -203,3 +204,21 @@ def ta_host_parser(ta_host: str) -> tuple[list[str], list[str]]:
csrf_trusted_origins.append(f"{parsed.scheme}://{parsed.hostname}")
return allowed_hosts, csrf_trusted_origins
def get_stylesheets():
"""Get all valid stylesheets from /static/css"""
app_root = EnvironmentSettings.APP_DIR
stylesheets = os.listdir(os.path.join(app_root, "static/css"))
stylesheets.remove("style.css")
stylesheets.sort()
stylesheets = list(filter(lambda x: x.endswith(".css"), stylesheets))
return stylesheets
def check_stylesheet(stylesheet: str):
"""Check if a stylesheet exists. Return dark.css as a fallback"""
if stylesheet in get_stylesheets():
return stylesheet
return "dark.css"

View File

@ -7,12 +7,13 @@ Functionality:
from typing import TypedDict
from home.src.es.connect import ElasticWrap
from home.src.ta.helper import get_stylesheets
class UserConfigType(TypedDict, total=False):
"""describes the user configuration"""
colors: str
stylesheet: str
page_size: int
sort_by: str
sort_order: str
@ -31,7 +32,7 @@ class UserConfig:
"""Handle settings for an individual user"""
_DEFAULT_USER_SETTINGS = UserConfigType(
colors="dark",
stylesheet="dark.css",
page_size=12,
sort_by="published",
sort_order="desc",
@ -46,9 +47,15 @@ class UserConfig:
sponsorblock_id=None,
)
VALID_STYLESHEETS = get_stylesheets()
VALID_VIEW_STYLE = ["grid", "list"]
VALID_SORT_ORDER = ["asc", "desc"]
VALID_SORT_BY = ["published", "downloaded", "views", "likes"]
VALID_GRID_ITEMS = range(3, 8)
def __init__(self, user_id: str):
self._user_id: str = user_id
self._config: UserConfigType = self._get_config()
self._config: UserConfigType = self.get_config()
def get_value(self, key: str):
"""Get the given key from the users configuration
@ -60,15 +67,8 @@ class UserConfig:
return self._config.get(key) or self._DEFAULT_USER_SETTINGS.get(key)
def set_value(self, key: str, value: str | bool | int):
"""Set or replace a configuration value for the user
Throws a KeyError if the requested Key is not a permitted value"""
if not self._user_id:
raise ValueError("Unable to persist config for null user_id")
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(f"Unable to persist config for unknown key '{key}'")
"""Set or replace a configuration value for the user"""
self._validate(key, value)
old = self.get_value(key)
self._config[key] = value
@ -79,9 +79,45 @@ class UserConfig:
if status < 200 or status > 299:
raise ValueError(f"Failed storing user value {status}: {response}")
print(f"User {self._user_id} value '{key}' change: {old} > {value}")
print(f"User {self._user_id} value '{key}' change: {old} -> {value}")
def _get_config(self) -> UserConfigType:
def _validate(self, key, value):
"""validate key and value"""
if not self._user_id:
raise ValueError("Unable to persist config for null user_id")
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(
f"Unable to persist config for an unknown key '{key}'"
)
valid_values = {
"stylesheet": self.VALID_STYLESHEETS,
"sort_by": self.VALID_SORT_BY,
"sort_order": self.VALID_SORT_ORDER,
"view_style_home": self.VALID_VIEW_STYLE,
"view_style_channel": self.VALID_VIEW_STYLE,
"view_style_download": self.VALID_VIEW_STYLE,
"view_style_playlist": self.VALID_VIEW_STYLE,
"grid_items": self.VALID_GRID_ITEMS,
"page_size": int,
"hide_watched": bool,
"show_ignored_only": bool,
"show_subed_only": bool,
}
validation_value = valid_values.get(key)
if isinstance(validation_value, (list, range)):
if value not in validation_value:
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == int:
if not isinstance(value, int):
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == bool:
if not isinstance(value, bool):
raise ValueError(f"Invalid value for {key}: {value}")
def get_config(self) -> UserConfigType:
"""get config from ES or load from the application defaults"""
if not self._user_id:
# this is for a non logged-in user so use all the defaults

View File

@ -294,7 +294,7 @@ def run_restore_backup(self, filename):
if manager.is_pending(self):
print(f"[task][{self.name}] restore is already running")
self.send_progress("Restore is already running.")
return
return None
manager.init(self)
self.send_progress(["Reset your Index"])
@ -302,6 +302,8 @@ def run_restore_backup(self, filename):
ElasticBackup(task=self).restore(filename)
print("index restore finished")
return f"backup restore completed: {filename}"
@shared_task(bind=True, name="rescan_filesystem", base=BaseTask)
def rescan_filesystem(self):

View File

@ -23,11 +23,7 @@
{% else %}
<title>TubeArchivist</title>
{% endif %}
{% if colors == "dark" %}
<link rel="stylesheet" href="{% static 'css/dark.css' %}">
{% else %}
<link rel="stylesheet" href="{% static 'css/light.css' %}">
{% endif %}
<link rel="stylesheet" href="{% static 'css/' %}{{ stylesheet }}">
<script type="text/javascript" src="{% static 'script.js' %}"></script>
{% if cast %}
<script type="text/javascript" src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
@ -39,12 +35,7 @@
<div class="boxed-content">
<div class="top-banner">
<a href="{% url 'home' %}">
{% if colors == 'dark' %}
<img src="{% static 'img/banner-tube-archivist-dark.png' %}" alt="tube-archivist-banner">
{% endif %}
{% if colors == 'light' %}
<img src="{% static 'img/banner-tube-archivist-light.png' %}" alt="tube-archivist-banner">
{% endif %}
<img alt="tube-archivist-banner">
</a>
</div>
<div class="top-nav">

View File

@ -77,13 +77,13 @@
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<select name="sort_by" id="sort" onchange="sortChange(this)">
<option value="published" {% if sort_by == "published" %}selected{% endif %}>date published</option>
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</option>
<option value="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>

View File

@ -60,13 +60,13 @@
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<select name="sort_by" id="sort" onchange="sortChange(this)">
<option value="published" {% if sort_by == "published" %}selected{% endif %}>date published</option>
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</option>
<option value="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>

View File

@ -18,20 +18,11 @@
<meta name="msapplication-TileColor" content="#01202e">
<meta name="msapplication-config" content="{% static 'favicon/browserconfig.xml' %}">
<meta name="theme-color" content="#01202e">
{% if colors == "dark" %}
<link rel="stylesheet" href="{% static 'css/dark.css' %}">
{% else %}
<link rel="stylesheet" href="{% static 'css/light.css' %}">
{% endif %}
<link rel="stylesheet" href="{% static 'css/' %}{{ stylesheet }}">
</head>
<body>
<div class="boxed-content login-page">
{% if colors == 'dark' %}
<img src="{% static 'img/logo-tube-archivist-dark.png' %}" alt="tube-archivist-logo">
{% endif %}
{% if colors == 'light' %}
<img src="{% static 'img/logo-tube-archivist-light.png' %}" alt="tube-archivist-banner">
{% endif %}
<img alt="tube-archivist-logo">
<h1>Tube Archivist</h1>
<h2>Your Self Hosted YouTube Media Server</h2>
{% if form_error %}

View File

@ -7,11 +7,11 @@
<form action="{% url 'settings_user' %}" method="POST" name="user-update">
{% csrf_token %}
<div class="settings-group">
<h2>Color scheme</h2>
<h2>Stylesheet</h2>
<div class="settings-item">
<p>Current color scheme: <span class="settings-current">{{ colors }}</span></p>
<i>Select your preferred color scheme between dark and light mode.</i><br>
{{ user_form.colors }}
<p>Current stylesheet: <span class="settings-current">{{ stylesheet }}</span></p>
<i>Select your preferred stylesheet.</i><br>
{{ user_form.stylesheet }}
</div>
</div>
<div class="settings-group">

View File

@ -58,7 +58,6 @@ urlpatterns = [
login_required(views.SettingsActionsView.as_view()),
name="settings_actions",
),
path("process/", login_required(views.process), name="process"),
path(
"channel/",
login_required(views.ChannelView.as_view()),

View File

@ -4,7 +4,6 @@ Functionality:
- holds base classes to inherit from
"""
import enum
import json
import urllib.parse
from time import sleep
@ -14,7 +13,7 @@ from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.forms import AuthenticationForm
from django.http import Http404, JsonResponse
from django.http import Http404
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
@ -23,7 +22,6 @@ from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.backup import ElasticBackup
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.api_calls import PostData
from home.src.frontend.forms import (
AddToQueueForm,
ApplicationSettingsForm,
@ -41,7 +39,7 @@ from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.helper import time_parser
from home.src.ta.helper import check_stylesheet, time_parser
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.users import UserConfig
@ -75,7 +73,9 @@ class ArchivistViewConfig(View):
self.user_conf = UserConfig(self.user_id)
self.context = {
"colors": self.user_conf.get_value("colors"),
"stylesheet": check_stylesheet(
self.user_conf.get_value("stylesheet")
),
"cast": EnvironmentSettings.ENABLE_CAST,
"sort_by": self.user_conf.get_value("sort_by"),
"sort_order": self.user_conf.get_value("sort_order"),
@ -222,7 +222,9 @@ class MinView(View):
def get_min_context(request):
"""build minimal vars for context"""
return {
"colors": UserConfig(request.user.id).get_value("colors"),
"stylesheet": check_stylesheet(
UserConfig(request.user.id).get_value("stylesheet")
),
"version": settings.TA_VERSION,
"ta_update": ReleaseVersion().get_update(),
}
@ -979,9 +981,9 @@ class SettingsUserView(MinView):
config_handler = UserConfig(request.user.id)
if user_form.is_valid():
user_form_post = user_form.cleaned_data
if user_form_post.get("colors"):
if user_form_post.get("stylesheet"):
config_handler.set_value(
"colors", user_form_post.get("colors")
"stylesheet", user_form_post.get("stylesheet")
)
if user_form_post.get("page_size"):
config_handler.set_value(
@ -1133,16 +1135,3 @@ class SettingsActionsView(MinView):
)
return render(request, "home/settings_actions.html", context)
def process(request):
"""handle all the buttons calls via POST ajax"""
if request.method == "POST":
current_user = request.user.id
post_dict = json.loads(request.body.decode())
post_handler = PostData(post_dict, current_user)
if post_handler.to_exec:
task_result = post_handler.run_task()
return JsonResponse(task_result)
return JsonResponse({"success": False})

View File

@ -1,6 +1,6 @@
apprise==1.6.0
celery==5.3.4
Django==4.2.6
Django==4.2.7
django-auth-ldap==4.6.0
django-cors-headers==4.3.0
djangorestframework==3.14.0
@ -8,6 +8,6 @@ Pillow==10.1.0
redis==5.0.1
requests==2.31.0
ryd-client==0.0.6
uWSGI==2.0.22
uWSGI==2.0.23
whitenoise==6.6.0
yt-dlp==2023.10.13

View File

@ -9,4 +9,6 @@
--accent-font-light: #97d4c8;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
}

View File

@ -9,4 +9,6 @@
--accent-font-light: #35b399;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-light.png");
--logo: url("../img/logo-tube-archivist-light.png");
}

View File

@ -0,0 +1,75 @@
:root {
--main-bg: #000000;
--highlight-bg: #080808;
--highlight-error: #880000;
--highlight-error-light: #aa0000;
--highlight-bg-transparent: #0c0c0caf;
--main-font: #00aa00;
--accent-font-dark: #007700;
--accent-font-light: #00aa00;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
--outline: 1px solid green;
--filter: hue-rotate(310deg);
}
.settings-group {
outline: var(--outline);
}
.info-box-item {
outline: var(--outline);
}
.footer {
outline: var(--outline);
}
.nav-icons {
filter: var(--filter);
}
.view-icons {
filter: var(--filter);
}
.top-banner {
filter: var(--filter);
}
.icon-text {
outline: var(--outline);
}
.video-item {
outline: var(--outline);
}
.channel-banner {
outline: var(--outline);
}
.description-box {
outline: var(--outline);
}
.video-player {
outline: var(--outline);
}
#notification {
outline: var(--outline);
}
textarea {
background-color: var(--highlight-bg);
outline: var(--outline);
color: var(--main-font);
}
input {
background-color: var(--highlight-bg);
color: var(--main-font);
}

View File

@ -0,0 +1,14 @@
:root {
--main-bg: #000000;
--highlight-bg: #0c0c0c;
--highlight-error: #220000;
--highlight-error-light: #330000;
--highlight-bg-transparent: #0c0c0caf;
--main-font: #888888;
--accent-font-dark: #555555;
--accent-font-light: #999999;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
}

View File

@ -165,12 +165,13 @@ button:hover {
.top-banner img {
width: 100%;
max-width: 700px;
content: var(--banner);
}
.footer {
margin: 0;
padding: 20px 0;
background-color: var(--accent-font-dark);
background-color: var(--highlight-bg);
grid-row-start: 2;
grid-row-end: 3;
}
@ -725,6 +726,7 @@ video:-webkit-full-screen {
max-width: 200px;
max-height: 200px;
margin-bottom: 40px;
content: var(--logo);
}
.login-page form {

View File

@ -2,9 +2,11 @@
/* globals checkMessages */
function sortChange(sortValue) {
let payload = JSON.stringify({ sort_order: sortValue });
sendPost(payload);
function sortChange(button) {
let apiEndpoint = '/api/config/user/';
let data = {};
data[button.name] = button.value;
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
@ -105,17 +107,21 @@ function subscribeStatus(subscribeButton) {
function changeView(image) {
let sourcePage = image.getAttribute('data-origin');
let newView = image.getAttribute('data-value');
let payload = JSON.stringify({ change_view: sourcePage + ':' + newView });
sendPost(payload);
let apiEndpoint = '/api/config/user/';
let data = {};
data[`view_style_${sourcePage}`] = newView;
console.log(data);
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
}
function changeGridItems(image) {
let operator = image.getAttribute('data-value');
let payload = JSON.stringify({ change_grid: operator });
sendPost(payload);
let newGridItems = Number(image.getAttribute('data-value'));
let apiEndpoint = '/api/config/user/';
let data = { grid_items: newGridItems };
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
@ -123,12 +129,10 @@ function changeGridItems(image) {
function toggleCheckbox(checkbox) {
// pass checkbox id as key and checkbox.checked as value
let toggleId = checkbox.id;
let toggleVal = checkbox.checked;
let payloadDict = {};
payloadDict[toggleId] = toggleVal;
let payload = JSON.stringify(payloadDict);
sendPost(payload);
let apiEndpoint = '/api/config/user/';
let data = {};
data[checkbox.id] = checkbox.checked;
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
let currPage = window.location.pathname;
window.location.replace(currPage);
@ -283,7 +287,7 @@ function reEmbed() {
}
function dbBackup() {
let apiEndpoint = '/api/task-name/run_backup/';
let apiEndpoint = '/api/backup/';
apiRequest(apiEndpoint, 'POST');
// clear button
let message = document.createElement('p');
@ -299,8 +303,8 @@ function dbBackup() {
function dbRestore(button) {
let fileName = button.getAttribute('data-id');
let payload = JSON.stringify({ 'db-restore': fileName });
sendPost(payload);
let apiEndpoint = `/api/backup/${fileName}/`;
apiRequest(apiEndpoint, 'POST');
// clear backup row
let message = document.createElement('p');
message.innerText = 'restoring from backup';
@ -542,7 +546,7 @@ function createVideoTag(videoData, videoProgress) {
}
let videoTag = `
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
<video poster="${videoThumbUrl}" onvolumechange="onVolumeChange(this)" onloadstart="this.volume=getPlayerVolume()" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source" videoid="${videoId}">
${subtitles}
</video>
@ -550,6 +554,14 @@ function createVideoTag(videoData, videoProgress) {
return videoTag;
}
function onVolumeChange(videoTag) {
localStorage.setItem('playerVolume', videoTag.volume);
}
function getPlayerVolume() {
return localStorage.getItem('playerVolume') ?? 1;
}
// Gets video tag
function getVideoPlayer() {
let videoElement = document.getElementById('video-item');
@ -1304,14 +1316,6 @@ function populateEmpty() {
// generic
function sendPost(payload) {
let http = new XMLHttpRequest();
http.open('POST', '/process/', true);
http.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
http.setRequestHeader('Content-type', 'application/json');
http.send(payload);
}
function getCookie(c_name) {
if (document.cookie.length > 0) {
let c_start = document.cookie.indexOf(c_name + '=');