diff --git a/tubearchivist/config/settings.py b/tubearchivist/config/settings.py
index 83c74bb1..83569820 100644
--- a/tubearchivist/config/settings.py
+++ b/tubearchivist/config/settings.py
@@ -70,7 +70,6 @@ INSTALLED_APPS = [
"stats",
"user",
"config",
- "home",
]
MIDDLEWARE = [
diff --git a/tubearchivist/config/urls.py b/tubearchivist/config/urls.py
index 8477a794..2fbf84b3 100644
--- a/tubearchivist/config/urls.py
+++ b/tubearchivist/config/urls.py
@@ -18,7 +18,6 @@ from django.contrib import admin
from django.urls import include, path
urlpatterns = [
- path("", include("home.urls")),
path("api/", include("common.urls")),
path("api/video/", include("video.urls")),
path("api/channel/", include("channel.urls")),
diff --git a/tubearchivist/home/__init__.py b/tubearchivist/home/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tubearchivist/home/migrations/__init__.py b/tubearchivist/home/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tubearchivist/home/src/__init__.py b/tubearchivist/home/src/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tubearchivist/home/src/frontend/__init__.py b/tubearchivist/home/src/frontend/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tubearchivist/home/src/frontend/forms.py b/tubearchivist/home/src/frontend/forms.py
deleted file mode 100644
index 903f6a7f..00000000
--- a/tubearchivist/home/src/frontend/forms.py
+++ /dev/null
@@ -1,270 +0,0 @@
-"""functionality:
-- hold all form classes used in the views
-"""
-
-import os
-
-from common.src.helper import get_stylesheets
-from django import forms
-from django.contrib.auth.forms import AuthenticationForm
-from django.forms.widgets import PasswordInput, TextInput
-
-
-class CustomAuthForm(AuthenticationForm):
- """better styled login form"""
-
- username = forms.CharField(
- widget=TextInput(
- attrs={
- "placeholder": "Username",
- "autofocus": True,
- "autocomplete": True,
- }
- ),
- label=False,
- )
- password = forms.CharField(
- widget=PasswordInput(attrs={"placeholder": "Password"}), label=False
- )
- remember_me = forms.BooleanField(required=False)
-
-
-class UserSettingsForm(forms.Form):
- """user configurations values"""
-
- STYLESHEET_CHOICES = [("", "-- change stylesheet --")]
- STYLESHEET_CHOICES.extend(
- [
- (stylesheet, os.path.splitext(stylesheet)[0].title())
- for stylesheet in get_stylesheets()
- ]
- )
-
- stylesheet = forms.ChoiceField(
- widget=forms.Select, choices=STYLESHEET_CHOICES, required=False
- )
- page_size = forms.IntegerField(required=False)
-
-
-class ApplicationSettingsForm(forms.Form):
- """handle all application settings"""
-
- AUTOSTART_CHOICES = [
- ("", "-- change subscription autostart --"),
- ("0", "disable auto start"),
- ("1", "enable auto start"),
- ]
-
- METADATA_CHOICES = [
- ("", "-- change metadata embed --"),
- ("0", "don't embed metadata"),
- ("1", "embed metadata"),
- ]
-
- THUMBNAIL_CHOICES = [
- ("", "-- change thumbnail embed --"),
- ("0", "don't embed thumbnail"),
- ("1", "embed thumbnail"),
- ]
-
- RYD_CHOICES = [
- ("", "-- change ryd integrations"),
- ("0", "disable ryd integration"),
- ("1", "enable ryd integration"),
- ]
-
- SP_CHOICES = [
- ("", "-- change sponsorblock integrations"),
- ("0", "disable sponsorblock integration"),
- ("1", "enable sponsorblock integration"),
- ]
-
- SNAPSHOT_CHOICES = [
- ("", "-- change snapshot settings --"),
- ("0", "disable system snapshots"),
- ("1", "enable system snapshots"),
- ]
-
- SUBTITLE_SOURCE_CHOICES = [
- ("", "-- change subtitle source settings"),
- ("user", "only download user created"),
- ("auto", "also download auto generated"),
- ]
-
- SUBTITLE_INDEX_CHOICES = [
- ("", "-- change subtitle index settings --"),
- ("0", "disable subtitle index"),
- ("1", "enable subtitle index"),
- ]
-
- COMMENT_SORT_CHOICES = [
- ("", "-- change comments sort settings --"),
- ("top", "sort comments by top"),
- ("new", "sort comments by new"),
- ]
-
- COOKIE_IMPORT_CHOICES = [
- ("", "-- change cookie settings"),
- ("0", "remove cookie"),
- ("1", "import cookie"),
- ]
-
- subscriptions_channel_size = forms.IntegerField(
- required=False, min_value=1
- )
- subscriptions_live_channel_size = forms.IntegerField(
- required=False, min_value=0
- )
- subscriptions_shorts_channel_size = forms.IntegerField(
- required=False, min_value=0
- )
- subscriptions_auto_start = forms.ChoiceField(
- widget=forms.Select, choices=AUTOSTART_CHOICES, required=False
- )
- downloads_limit_speed = forms.IntegerField(required=False)
- downloads_throttledratelimit = forms.IntegerField(required=False)
- downloads_sleep_interval = forms.IntegerField(required=False)
- downloads_autodelete_days = forms.IntegerField(required=False)
- downloads_format = forms.CharField(required=False)
- downloads_format_sort = forms.CharField(required=False)
- downloads_extractor_lang = forms.CharField(required=False)
- downloads_add_metadata = forms.ChoiceField(
- widget=forms.Select, choices=METADATA_CHOICES, required=False
- )
- downloads_add_thumbnail = forms.ChoiceField(
- widget=forms.Select, choices=THUMBNAIL_CHOICES, required=False
- )
- downloads_subtitle = forms.CharField(required=False)
- downloads_subtitle_source = forms.ChoiceField(
- widget=forms.Select, choices=SUBTITLE_SOURCE_CHOICES, required=False
- )
- downloads_subtitle_index = forms.ChoiceField(
- widget=forms.Select, choices=SUBTITLE_INDEX_CHOICES, required=False
- )
- downloads_comment_max = forms.CharField(required=False)
- downloads_comment_sort = forms.ChoiceField(
- widget=forms.Select, choices=COMMENT_SORT_CHOICES, required=False
- )
- downloads_cookie_import = forms.ChoiceField(
- widget=forms.Select, choices=COOKIE_IMPORT_CHOICES, required=False
- )
- downloads_integrate_ryd = forms.ChoiceField(
- widget=forms.Select, choices=RYD_CHOICES, required=False
- )
- downloads_integrate_sponsorblock = forms.ChoiceField(
- widget=forms.Select, choices=SP_CHOICES, required=False
- )
- application_enable_snapshot = forms.ChoiceField(
- widget=forms.Select, choices=SNAPSHOT_CHOICES, required=False
- )
-
-
-class MultiSearchForm(forms.Form):
- """multi search form for /search/"""
-
- searchInput = forms.CharField(
- label="",
- widget=forms.TextInput(
- attrs={
- "autocomplete": "off",
- "oninput": "searchMulti(this.value)",
- "autofocus": True,
- }
- ),
- )
- home = forms.CharField(widget=forms.HiddenInput())
- channel = forms.CharField(widget=forms.HiddenInput())
- playlist = forms.CharField(widget=forms.HiddenInput())
-
-
-class AddToQueueForm(forms.Form):
- """text area form to add to downloads"""
-
- HELP_TEXT = "Enter at least one video, channel or playlist id/URL here..."
-
- vid_url = forms.CharField(
- label=False,
- widget=forms.Textarea(
- attrs={
- "rows": 4,
- "placeholder": HELP_TEXT,
- }
- ),
- )
-
-
-class SubscribeToChannelForm(forms.Form):
- """text area form to subscribe to multiple channels"""
-
- subscribe = forms.CharField(
- label="Subscribe to channels",
- widget=forms.Textarea(
- attrs={
- "rows": 3,
- "placeholder": "Input channel ID, URL or Video of a channel",
- }
- ),
- )
-
-
-class SubscribeToPlaylistForm(forms.Form):
- """text area form to subscribe to multiple playlists"""
-
- subscribe = forms.CharField(
- label="Subscribe to playlists",
- widget=forms.Textarea(
- attrs={
- "rows": 3,
- "placeholder": "Input playlist IDs or URLs",
- }
- ),
- )
-
-
-class CreatePlaylistForm(forms.Form):
- """text area form to create a single custom playlist"""
-
- create = forms.CharField(
- label="Or create custom playlist",
- widget=forms.Textarea(
- attrs={
- "rows": 1,
- "placeholder": "Input playlist name",
- }
- ),
- )
-
-
-class ChannelOverwriteForm(forms.Form):
- """custom overwrites for channel settings"""
-
- PLAYLIST_INDEX = [
- ("", "-- change playlist index --"),
- ("0", "Disable playlist index"),
- ("1", "Enable playlist index"),
- ]
-
- SP_CHOICES = [
- ("", "-- change sponsorblock integrations"),
- ("disable", "disable sponsorblock integration"),
- ("1", "enable sponsorblock integration"),
- ("0", "unset sponsorblock integration"),
- ]
-
- download_format = forms.CharField(label=False, required=False)
- autodelete_days = forms.IntegerField(label=False, required=False)
- index_playlists = forms.ChoiceField(
- widget=forms.Select, choices=PLAYLIST_INDEX, required=False
- )
- integrate_sponsorblock = forms.ChoiceField(
- widget=forms.Select, choices=SP_CHOICES, required=False
- )
- subscriptions_channel_size = forms.IntegerField(
- label=False, required=False
- )
- subscriptions_live_channel_size = forms.IntegerField(
- label=False, required=False
- )
- subscriptions_shorts_channel_size = forms.IntegerField(
- label=False, required=False
- )
diff --git a/tubearchivist/home/src/frontend/forms_schedule.py b/tubearchivist/home/src/frontend/forms_schedule.py
deleted file mode 100644
index baeeab24..00000000
--- a/tubearchivist/home/src/frontend/forms_schedule.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""
-Functionality:
-- handle schedule forms
-- implement form validation
-"""
-
-from celery.schedules import crontab
-from django import forms
-from task.src.task_config import TASK_CONFIG
-
-
-class CrontabValidator:
- """validate crontab"""
-
- @staticmethod
- def validate_fields(cron_fields):
- """expect 3 cron fields"""
- if not len(cron_fields) == 3:
- raise forms.ValidationError("expected three cron schedule fields")
-
- @staticmethod
- def validate_minute(minute_field):
- """expect minute int"""
- try:
- minute_value = int(minute_field)
- if not 0 <= minute_value <= 59:
- raise forms.ValidationError(
- "Invalid value for minutes. Must be between 0 and 59."
- )
- except ValueError as err:
- raise forms.ValidationError(
- "Invalid value for minutes. Must be an integer."
- ) from err
-
- @staticmethod
- def validate_cron_tab(minute, hour, day_of_week):
- """check if crontab can be created"""
- try:
- crontab(minute=minute, hour=hour, day_of_week=day_of_week)
- except ValueError as err:
- raise forms.ValidationError(f"invalid crontab: {err}") from err
-
- def validate(self, cron_expression):
- """create crontab schedule"""
- if cron_expression == "auto":
- return
-
- cron_fields = cron_expression.split()
- self.validate_fields(cron_fields)
-
- minute, hour, day_of_week = cron_fields
- self.validate_minute(minute)
- self.validate_cron_tab(minute, hour, day_of_week)
-
-
-def validate_cron(cron_expression):
- """callable for field"""
- CrontabValidator().validate(cron_expression)
-
-
-class SchedulerSettingsForm(forms.Form):
- """handle scheduler settings"""
-
- update_subscribed = forms.CharField(
- required=False, validators=[validate_cron]
- )
- download_pending = forms.CharField(
- required=False, validators=[validate_cron]
- )
- check_reindex = forms.CharField(required=False, validators=[validate_cron])
- check_reindex_days = forms.IntegerField(required=False)
- thumbnail_check = forms.CharField(
- required=False, validators=[validate_cron]
- )
- run_backup = forms.CharField(required=False, validators=[validate_cron])
- run_backup_rotate = forms.IntegerField(required=False)
-
-
-class NotificationSettingsForm(forms.Form):
- """add notification URL"""
-
- SUPPORTED_TASKS = [
- "update_subscribed",
- "extract_download",
- "download_pending",
- "check_reindex",
- ]
- TASK_LIST = [(i, TASK_CONFIG[i]["title"]) for i in SUPPORTED_TASKS]
-
- TASK_CHOICES = [("", "-- select task --")]
- TASK_CHOICES.extend(TASK_LIST)
-
- PLACEHOLDER = "Apprise notification URL"
-
- task = forms.ChoiceField(
- widget=forms.Select, choices=TASK_CHOICES, required=False
- )
- notification_url = forms.CharField(
- required=False,
- widget=forms.TextInput(attrs={"placeholder": PLACEHOLDER}),
- )
diff --git a/tubearchivist/home/templates/home/about.html b/tubearchivist/home/templates/home/about.html
deleted file mode 100644
index d3f3183f..00000000
--- a/tubearchivist/home/templates/home/about.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "home/base.html" %}
-{% load static %}
-{% block content %}
-
-
-
About The Tube Archivist
-
-
-
Useful Links
-
This project is in active and constant development, take a look at the roadmap for a overview.
-
All functionality is documented in our up-to-date user guide .
-
All contributions are welcome: Open an issue for any bugs and errors, join us on Discord to discuss details. The contributing page is a good place to get started.
-
-
-
Donate
-
Here are some links , if you want to buy the developer a coffee. Thank you for your support!
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/base.html b/tubearchivist/home/templates/home/base.html
deleted file mode 100644
index 2bfdef8f..00000000
--- a/tubearchivist/home/templates/home/base.html
+++ /dev/null
@@ -1,141 +0,0 @@
-{% load static %}
-{% load auth_extras %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if title %}
- TA | {{ title }}
- {% else %}
- TubeArchivist
- {% endif %}
-
-
- {% if cast %}
-
-
- {% endif %}
-
-
-
-
- {% block content %}{% endblock %}
-
-
-
-
-
-
-
diff --git a/tubearchivist/home/templates/home/base_settings.html b/tubearchivist/home/templates/home/base_settings.html
deleted file mode 100644
index 31787ef5..00000000
--- a/tubearchivist/home/templates/home/base_settings.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{# Base file for all of the settings pages to ensure a common menu #}
-{% extends "home/base.html" %}
-{% load static %}
-{% load auth_extras %}
-{% block content %}
-
-
-
Dashboard
-
User
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
Application
-
Scheduling
-
Actions
- {% endif %}
-
-
- {% block settings_content %}{% endblock %}
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/channel.html b/tubearchivist/home/templates/home/channel.html
deleted file mode 100644
index ce5eab54..00000000
--- a/tubearchivist/home/templates/home/channel.html
+++ /dev/null
@@ -1,87 +0,0 @@
-{% extends "home/base.html" %}
-{% load static %}
-{% load humanize %}
-{% block content %}
-{% load auth_extras %}
-
-
-
-
Channels
-
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
- {% endif %}
-
-
-
-
-
Show subscribed only:
-
-
- {% if not show_subed_only %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
-
-
-
-
-
Total channels: {{ max_hits }}
-
- {% if results %}
- {% for channel in results %}
-
-
-
-
-
-
-
- {% if channel.channel_subs >= 1000000 %}
-
Subscribers: {{ channel.channel_subs|intword }}
- {% else %}
-
Subscribers: {{ channel.channel_subs|intcomma }}
- {% endif %}
-
-
-
-
-
Last refreshed: {{ channel.channel_last_refresh }}
- {% if channel.channel_subscribed %}
-
Unsubscribe
- {% else %}
-
Subscribe
- {% endif %}
-
-
-
-
- {% endfor %}
- {% else %}
-
No channels found...
- {% endif %}
-
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/channel_id.html b/tubearchivist/home/templates/home/channel_id.html
deleted file mode 100644
index 367074e9..00000000
--- a/tubearchivist/home/templates/home/channel_id.html
+++ /dev/null
@@ -1,154 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-{% load static %}
-{% load humanize %}
-{% load auth_extras %}
-
-
-
-
-
-
-
Videos
- {% if has_streams %}
-
Streams
- {% endif %}
- {% if has_shorts %}
-
Shorts
- {% endif %}
- {% if has_playlists %}
-
Playlists
- {% endif %}
-
About
- {% if has_pending %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
Downloads
- {% endif %}
- {% endif %}
-
-
-
-
-
-
-
- {% if channel_info.channel_subs >= 1000000 %}
-
Subscribers: {{ channel_info.channel_subs|intword }}
- {% else %}
-
Subscribers: {{ channel_info.channel_subs|intcomma }}
- {% endif %}
- {% if channel_info.channel_subscribed %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
Unsubscribe
- {% endif %}
- {% else %}
-
Subscribe
- {% endif %}
-
-
-
- {% if aggs %}
-
{{ aggs.total_items.value }} videos | {{ aggs.total_duration.value_str }} playback | Total size {{ aggs.total_size.value|filesizeformat }}
-
- Mark as watched
- Mark as unwatched
-
- {% endif %}
-
-
-
-
-
-
-
Hide watched videos:
-
-
- {% if not hide_watched %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
-
- Sort by:
-
- date published
- date downloaded
- views
- likes
- duration
- file size
-
-
- asc
- desc
-
-
-
-
-
- {% if view_style == "grid" %}
-
- {% if grid_items < 7 %}
-
- {% endif %}
- {% if grid_items > 3 %}
-
- {% endif %}
-
- {% endif %}
-
-
-
-
-
-
-
-
- {% if results %}
- {% for video in results %}
-
- {% endfor %}
- {% else %}
-
No videos found...
-
Try going to the downloads page to start the scan and download tasks.
- {% endif %}
-
-
-
-{% endblock content %}
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/channel_id_about.html b/tubearchivist/home/templates/home/channel_id_about.html
deleted file mode 100644
index 56fae3dc..00000000
--- a/tubearchivist/home/templates/home/channel_id_about.html
+++ /dev/null
@@ -1,186 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-{% load static %}
-{% load humanize %}
-{% load auth_extras %}
-
-
-
-
-
-
Videos
- {% if has_streams %}
-
Streams
- {% endif %}
- {% if has_shorts %}
-
Shorts
- {% endif %}
- {% if has_playlists %}
-
Playlists
- {% endif %}
-
About
- {% if has_pending %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
Downloads
- {% endif %}
- {% endif %}
-
-
-
-
-
-
-
- {% if channel_info.channel_subs >= 1000000 %}
-
Subscribers: {{ channel_info.channel_subs|intword }}
- {% else %}
-
Subscribers: {{ channel_info.channel_subs|intcomma }}
- {% endif %}
-
-
-
-
-
Last refreshed: {{ channel_info.channel_last_refresh }}
- {% if channel_info.channel_active %}
-
Youtube: Active
- {% else %}
-
Youtube: Deactivated
- {% endif %}
-
-
-
-
- {% if channel_info.channel_views >= 1000000 %}
-
Channel views: {{ channel_info.channel_views|intword }}
- {% elif channel_info.channel_views > 0 %}
-
Channel views: {{ channel_info.channel_views|intcomma }}
- {% endif %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
- {% if reindex %}
-
Reindex scheduled
- {% else %}
-
- Reindex
- Reindex Videos
-
- {% endif %}
- {% endif %}
-
-
-
- {% if channel_info.channel_description %}
-
-
- {{ channel_info.channel_description|linebreaksbr|urlizetrunc:50 }}
-
-
Show more
-
- {% endif %}
- {% if channel_info.channel_tags %}
-
-
- {% for tag in channel_info.channel_tags %}
- {{ tag }}
- {% endfor %}
-
-
- {% endif %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
- {% endif %}
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/channel_id_playlist.html b/tubearchivist/home/templates/home/channel_id_playlist.html
deleted file mode 100644
index ba9341f2..00000000
--- a/tubearchivist/home/templates/home/channel_id_playlist.html
+++ /dev/null
@@ -1,74 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-{% load static %}
-{% load humanize %}
-{% load auth_extras %}
-
-
-
-
-
-
Videos
- {% if has_streams %}
-
Streams
- {% endif %}
- {% if has_shorts %}
-
Shorts
- {% endif %}
- {% if has_playlists %}
-
Playlists
- {% endif %}
-
About
- {% if has_pending %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
Downloads
- {% endif %}
- {% endif %}
-
-
-
-
-
Show subscribed only:
-
-
- {% if not show_subed_only %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
-
-
-
-
-
- {% if results %}
- {% for playlist in results %}
-
-
-
-
{{ playlist.playlist_name }}
-
Last refreshed: {{ playlist.playlist_last_refresh }}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
- {% if playlist.playlist_subscribed %}
-
Unsubscribe
- {% else %}
-
Subscribe
- {% endif %}
- {% endif %}
-
-
- {% endfor %}
- {% else %}
-
No playlists found...
- {% endif %}
-
-
-
-{% endblock content %}
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/downloads.html b/tubearchivist/home/templates/home/downloads.html
deleted file mode 100644
index 16e1f154..00000000
--- a/tubearchivist/home/templates/home/downloads.html
+++ /dev/null
@@ -1,122 +0,0 @@
-{% extends "home/base.html" %}
-{% load static %}
-{% block content %}
-
-
-
Downloads {% if channel_filter_id %} for {{ channel_filter_name }}{% endif %}
-
-
-
-
-
-
-
Rescan subscriptions
-
-
-
-
Start download
-
-
-
-
Add to download queue
-
-
-
-
-
-
Show only ignored videos:
-
-
- {% if not show_ignored_only %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
- {% if channel_agg_list|length > 1 %}
-
- all
- {% for channel in channel_agg_list %}
- {{ channel.name }} ({{channel.count}})
- {% endfor %}
-
- {% endif %}
- {% if view_style == "grid" %}
-
- {% if grid_items < 7 %}
-
- {% endif %}
- {% if grid_items > 3 %}
-
- {% endif %}
-
- {% endif %}
-
-
-
-
-
Total videos in queue: {{ max_hits }}{% if max_hits == 10000 %}+{% endif %} {% if channel_filter_id %} - from channel {{ channel_filter_name }} {% endif %}
-
-
-
- {% if results %}
- {% for video in results %}
-
-
-
-
-
- {% if show_ignored_only %}
- ignored
- {% else %}
- queued
- {% endif %}
- {{ video.vid_type }}
- {% if video.auto_start %}
- auto
- {% endif %}
-
-
-
-
-
-
Published: {{ video.published }} | Duration: {{ video.duration }} | {{ video.youtube_id }}
- {% if video.message %}
-
{{ video.message }}
- {% endif %}
-
- {% if show_ignored_only %}
- Forget
- Add to queue
- {% else %}
- Ignore
- Download now
- {% endif %}
- {% if video.message %}
- Delete
- {% endif %}
-
-
-
- {% endfor %}
- {% endif %}
-
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/home.html b/tubearchivist/home/templates/home/home.html
deleted file mode 100644
index f55d2412..00000000
--- a/tubearchivist/home/templates/home/home.html
+++ /dev/null
@@ -1,137 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-{% load static %}
-
- {% if continue_vids %}
-
-
Continue Watching
-
-
- {% for video in continue_vids %}
-
- {% endfor %}
-
- {% endif %}
-
-
Recent Videos
-
-
-
-
Hide watched:
-
-
- {% if not hide_watched %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
-
- Sort by:
-
- date published
- date downloaded
- views
- likes
- duration
- file size
-
-
- asc
- desc
-
-
-
-
-
- {% if view_style == "grid" %}
-
- {% if grid_items < 7 %}
-
- {% endif %}
- {% if grid_items > 3 %}
-
- {% endif %}
-
- {% endif %}
-
-
-
-
-
-
-
-
- {% if results %}
- {% for video in results %}
-
- {% endfor %}
- {% else %}
-
No videos found...
-
If you've already added a channel or playlist, try going to the downloads page to start the scan and download tasks.
- {% endif %}
-
-
-{% endblock content %}
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/login.html b/tubearchivist/home/templates/home/login.html
deleted file mode 100644
index fe1d24f2..00000000
--- a/tubearchivist/home/templates/home/login.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% load static %}
-
-
-
-
-
-
- TA | Welcome
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tube Archivist
-
Your Self Hosted YouTube Media Server
- {% if form_error %}
-
Failed to login.
- {% endif %}
-
-
Github Donate
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/playlist.html b/tubearchivist/home/templates/home/playlist.html
deleted file mode 100644
index 6983372f..00000000
--- a/tubearchivist/home/templates/home/playlist.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% extends "home/base.html" %}
-{% load static %}
-{% block content %}
-{% load auth_extras %}
-
-
-
-
-
Playlists
-
- {% if request.user|has_group:"admin" or request.user.is_staff %}
-
-
- {% endif %}
-
-
-
-
-
-
Show subscribed only:
-
-
- {% if not show_subed_only %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
-
-
-
-
-
- {% if results %}
- {% for playlist in results %}
-
-
-
- {% if playlist.playlist_type != "custom" %}
-
{{ playlist.playlist_channel }}
- {% endif %}
-
{{ playlist.playlist_name }}
-
Last refreshed: {{ playlist.playlist_last_refresh }}
- {% if playlist.playlist_type != "custom" %}
- {% if playlist.playlist_subscribed %}
-
Unsubscribe
- {% else %}
-
Subscribe
- {% endif %}
- {% endif %}
-
-
- {% endfor %}
- {% else %}
-
No playlists found...
- {% endif %}
-
-
-
-{% endblock content %}
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/playlist_id.html b/tubearchivist/home/templates/home/playlist_id.html
deleted file mode 100644
index 942ab8ea..00000000
--- a/tubearchivist/home/templates/home/playlist_id.html
+++ /dev/null
@@ -1,180 +0,0 @@
-{% extends "home/base.html" %}
-{% load static %}
-{% load humanize %}
-{% block content %}
-{% load auth_extras %}
-
-
-
-
{{ playlist_info.playlist_name }}
-
-
- {% if playlist_info.playlist_type != "custom" %}
-
-
-
-
- {% if channel_info.channel_subs >= 1000000 %}
-
Subscribers: {{ channel_info.channel_subs|intword }}
- {% else %}
-
Subscribers: {{ channel_info.channel_subs|intcomma }}
- {% endif %}
-
-
- {% endif %}
-
-
-
-
Last refreshed: {{ playlist_info.playlist_last_refresh }}
- {% if playlist_info.playlist_type != "custom" %}
-
Playlist:
- {% if playlist_info.playlist_subscribed %}
- {% if request.user|has_group:"admin" or request.user.is_staff %}
- Unsubscribe
- {% endif %}
- {% else %}
- Subscribe
- {% endif %}
-
- {% if playlist_info.playlist_active %}
-
Youtube: Active
- {% else %}
-
Youtube: Deactivated
- {% endif %}
- {% endif %}
-
Delete Playlist
-
- Delete {{ playlist_info.playlist_name }}?
- Delete metadata
- Delete all
- Cancel
-
-
-
-
-
- {% if max_hits %}
-
Total Videos archived: {{ max_hits }}/{{ playlist_info.playlist_entries|length }}
-
- Mark as watched
- Mark as unwatched
-
- {% endif %}
- {% if reindex %}
-
Reindex scheduled
- {% else %}
-
- {% if playlist_info.playlist_type != "custom" %}
- Reindex
- {% endif %}
- Reindex Videos
-
- {% endif %}
-
-
-
- {% if playlist_info.playlist_description %}
-
-
- {{ playlist_info.playlist_description|linebreaksbr|urlizetrunc:50 }}
-
-
Show more
-
- {% endif %}
-
-
-
-
-
Hide watched videos:
-
-
- {% if not hide_watched %}
- Off
- {% else %}
- On
- {% endif %}
-
-
-
- {% if view_style == "grid" %}
-
- {% if grid_items < 7 %}
-
- {% endif %}
- {% if grid_items > 3 %}
-
- {% endif %}
-
- {% endif %}
-
-
-
-
-
-
-
-
- {% if results %}
- {% for video in results %}
-
- {% endfor %}
- {% else %}
-
No videos found...
- {% if playlist_info.playlist_type == "custom" %}
-
Try going to the home page to add videos to this playlist.
- {% else %}
-
Try going to the downloads page to start the scan and download tasks.
- {% endif %}
- {% endif %}
-
-
-{% endblock content %}
\ No newline at end of file
diff --git a/tubearchivist/home/templates/home/search.html b/tubearchivist/home/templates/home/search.html
deleted file mode 100644
index 0936af4a..00000000
--- a/tubearchivist/home/templates/home/search.html
+++ /dev/null
@@ -1,87 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-
-
-
-
Search your Archive
-
-
- {{ search_form }}
-
-
-
-
-
-
-
Fulltext Results
-
-
No fulltext results found.
-
-
-
-
-
-
Example queries
-
- music video — basic search
- video: active: no — all videos deleted from YouTube
- video: learn javascript channel: corey schafer active: yes
- channel: linux subscribed: yes
- playlist: backend engineering active: yes subscribed: yes
-
-
-
-
Keywords cheatsheet
-
For detailed usage check wiki .
-
-
- simple: (implied) — search in video titles, channel names and playlist titles
-
- video: — search in video titles, tags and category field
-
- channel: — channel name
- active: yes/no — whether the video is still active on YouTube
-
-
-
- channel: — search in channel name and channel description
-
- subscribed: yes/no — whether you are subscribed to the channel
- active: yes/no — whether the video is still active on YouTube
-
-
-
- playlist: — search in channel name and channel description
-
- subscribed: yes/no — whether you are subscribed to the channel
- active: yes/no — whether the video is still active on YouTube
-
-
-
- full: — search in video subtitles
-
- lang: — subtitles language (use two-letter ISO country code, same as the one from settings page)
- source: auto/user — auto to search though auto-generated subtitles only, or user to search through user-uploaded subtitles only
-
-
-
-
-
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templates/home/settings.html b/tubearchivist/home/templates/home/settings.html
deleted file mode 100644
index e72f7359..00000000
--- a/tubearchivist/home/templates/home/settings.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% extends "home/base_settings.html" %}
-{% load static %}
-{% block settings_content %}
-
-
Your Archive
-
-
-
-
-
-
-
-
-
Biggest Channels
-
-
-
-
-
- Name
- Videos
-
-
-
-
-
-
-
-
-
-
- Name
- Duration
-
-
-
-
-
-
-
-
-
-
- Name
- Media Size
-
-
-
-
-
-
-
-
-
-{% endblock settings_content %}
diff --git a/tubearchivist/home/templates/home/settings_actions.html b/tubearchivist/home/templates/home/settings_actions.html
deleted file mode 100644
index be6ffba6..00000000
--- a/tubearchivist/home/templates/home/settings_actions.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{% extends "home/base_settings.html" %}
-{% load static %}
-{% block settings_content %}
-
-
Actions
-
-
-
Delete download queue
-
Delete your pending or previously ignored videos from your download queue.
- Delete all ignored
- Delete all queued
-
-
-
Manual media files import.
-
Add files to the cache/import folder. Make sure to follow the instructions in the Github Wiki .
-
- Start import
-
-
-
-
Embed thumbnails into media file.
-
Set extracted youtube thumbnail as cover art of the media file.
-
- Start process
-
-
-
-
ZIP file index backup
-
Export your database to a zip file stored at cache/backup .
-
Zip file backups are very slow for large archives and consistency is not guaranteed, use snapshots instead. Make sure no other tasks are running when creating a Zip file backup.
-
- Start backup
-
-
-
-
Restore from backup
-
Danger Zone : This will replace your existing index with the backup.
-
Restore from available backup files from cache/backup .
- {% if available_backups %}
-
-
- Timestamp
- Source
- Filename
-
- {% for backup in available_backups %}
-
- Restore
- {{ backup.timestamp }}
- {{ backup.reason }}
- {{ backup.filename }}
-
- {% endfor %}
- {% else %}
-
No backups found.
- {% endif %}
-
-
-
Rescan filesystem
-
Danger Zone : This will delete the metadata of deleted videos from the filesystem.
-
Rescan your media folder looking for missing videos and clean up index. More infos on the Github Wiki .
-
- Rescan filesystem
-
-
-{% endblock settings_content %}
diff --git a/tubearchivist/home/templates/home/settings_application.html b/tubearchivist/home/templates/home/settings_application.html
deleted file mode 100644
index 813ad022..00000000
--- a/tubearchivist/home/templates/home/settings_application.html
+++ /dev/null
@@ -1,192 +0,0 @@
-{% extends "home/base_settings.html" %}
-{% load static %}
-{% block settings_content %}
-
-
Application Configurations
-
-
-{% endblock settings_content %}
diff --git a/tubearchivist/home/templates/home/settings_scheduling.html b/tubearchivist/home/templates/home/settings_scheduling.html
deleted file mode 100644
index b01f4644..00000000
--- a/tubearchivist/home/templates/home/settings_scheduling.html
+++ /dev/null
@@ -1,152 +0,0 @@
-{% extends "home/base_settings.html" %}
-{% load static %}
-{% block settings_content %}
-
-
Scheduler Setup
-
-
Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week.
-
Examples:
-
- 0 15 * : Run task every day at 15:00 in the afternoon.
- 30 8 */2 : Run task every second day of the week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
- auto : Sensible default.
-
-
Note:
-
- Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that reason, the scheduler doesn't support schedules that trigger more than once per hour.
-
-
-
-
-{% endblock settings_content %}
diff --git a/tubearchivist/home/templates/home/settings_user.html b/tubearchivist/home/templates/home/settings_user.html
deleted file mode 100644
index d6d97217..00000000
--- a/tubearchivist/home/templates/home/settings_user.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% extends "home/base_settings.html" %}
-{% load static %}
-{% block settings_content %}
-
-
User Configurations
-
-
-{% if request.user.is_superuser %}
-
-
Users
-
-
-
User Management
-
Access the admin interface for basic user management functionality like adding and deleting users, changing passwords and more.
-
Admin Interface
-
-{% endif %}
-{% endblock settings_content %}
diff --git a/tubearchivist/home/templates/home/video.html b/tubearchivist/home/templates/home/video.html
deleted file mode 100644
index 70b95b10..00000000
--- a/tubearchivist/home/templates/home/video.html
+++ /dev/null
@@ -1,198 +0,0 @@
-{% extends "home/base.html" %}
-{% block content %}
-{% load static %}
-{% load humanize %}
-{% load auth_extras %}
-
-
-
-
-
- {% if cast %}
-
- {% endif %}
-
{{ video.title }}
-
-
-
-
-
-
- {% if video.channel.channel_subs >= 1000000 %}
-
Subscribers: {{ video.channel.channel_subs|intword }}
- {% else %}
-
Subscribers: {{ video.channel.channel_subs|intcomma }}
- {% endif %}
-
-
-
-
-
Published: {{ video.published }}
-
Last refreshed: {{ video.vid_last_refresh }}
-
Watched:
- {% if video.player.watched %}
-
- {% else %}
-
- {% endif %}
-
- {% if video.active %}
-
Youtube: Active
- {% else %}
-
Youtube: Deactivated
- {% endif %}
-
-
-
-
-
: {{ video.stats.view_count|intcomma }}
-
: {{ video.stats.like_count|intcomma }}
- {% if video.stats.dislike_count %}
-
: {{ video.stats.dislike_count|intcomma }}
- {% endif %}
- {% if video.stats.average_rating %}
-
- {% for star in video.stats.average_rating %}
-
- {% endfor %}
-
- {% endif %}
-
-
-
-
-
-
- {% if video.media_size %}
-
File size: {{ video.media_size|filesizeformat }}
- {% endif %}
- {% if video.streams %}
- {% for stream in video.streams %}
-
{{ stream.type|title }}: {{ stream.codec }} {{ stream.bitrate|filesizeformat }}/s{% if stream.width %} | {{ stream.width }}x{{ stream.height}}{% endif %}
- {% endfor %}
- {% endif %}
-
-
- {% if video.tags %}
-
-
- {% for tag in video.tags %}
- {{ tag }}
- {% endfor %}
-
-
- {% endif %}
- {% if video.description %}
-
-
- {{ video.description|linebreaksbr|urlizetrunc:50 }}
-
-
Show more
-
- {% endif %}
- {% if playlist_nav %}
- {% for playlist_item in playlist_nav %}
-
- {% endfor %}
- {% endif %}
-
- {% if video.comment_count == 0 %}
-
- {% elif video.comment_count %}
-
- {% endif %}
-
-
-{% endblock content %}
diff --git a/tubearchivist/home/templatetags/__init__.py b/tubearchivist/home/templatetags/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tubearchivist/home/templatetags/auth_extras.py b/tubearchivist/home/templatetags/auth_extras.py
deleted file mode 100644
index a41b4b1e..00000000
--- a/tubearchivist/home/templatetags/auth_extras.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django import template
-
-register = template.Library()
-
-
-@register.filter(name="has_group")
-def has_group(user, group_name):
- return user.groups.filter(name=group_name).exists()
diff --git a/tubearchivist/home/urls.py b/tubearchivist/home/urls.py
deleted file mode 100644
index 363f5054..00000000
--- a/tubearchivist/home/urls.py
+++ /dev/null
@@ -1,107 +0,0 @@
-""" all home app urls """
-
-from django.conf import settings
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.views import LogoutView
-from django.shortcuts import redirect
-from django.urls import path
-from home import views
-
-if hasattr(settings, "TA_AUTH_PROXY_LOGOUT_URL"):
- logout_path = path(
- "logout/",
- lambda request: redirect(
- settings.TA_AUTH_PROXY_LOGOUT_URL, permanent=False
- ),
- name="logout",
- )
-else:
- logout_path = path(
- "logout/",
- LogoutView.as_view(),
- {"next_page": settings.LOGOUT_REDIRECT_URL},
- name="logout",
- )
-
-urlpatterns = [
- path("", login_required(views.HomeView.as_view()), name="home"),
- path("login/", views.LoginView.as_view(), name="login"),
- logout_path,
- path("about/", views.AboutView.as_view(), name="about"),
- path(
- "downloads/",
- login_required(views.DownloadView.as_view()),
- name="downloads",
- ),
- path(
- "settings/",
- login_required(views.SettingsView.as_view()),
- name="settings",
- ),
- path(
- "settings/user/",
- login_required(views.SettingsUserView.as_view()),
- name="settings_user",
- ),
- path(
- "settings/application/",
- login_required(views.SettingsApplicationView.as_view()),
- name="settings_application",
- ),
- path(
- "settings/scheduling/",
- login_required(views.SettingsSchedulingView.as_view()),
- name="settings_scheduling",
- ),
- path(
- "settings/actions/",
- login_required(views.SettingsActionsView.as_view()),
- name="settings_actions",
- ),
- path(
- "channel/",
- login_required(views.ChannelView.as_view()),
- name="channel",
- ),
- path(
- "channel//",
- login_required(views.ChannelIdView.as_view()),
- name="channel_id",
- ),
- path(
- "channel//streams/",
- login_required(views.ChannelIdLiveView.as_view()),
- name="channel_id_live",
- ),
- path(
- "channel//shorts/",
- login_required(views.ChannelIdShortsView.as_view()),
- name="channel_id_shorts",
- ),
- path(
- "channel//about/",
- login_required(views.ChannelIdAboutView.as_view()),
- name="channel_id_about",
- ),
- path(
- "channel//playlist/",
- login_required(views.ChannelIdPlaylistView.as_view()),
- name="channel_id_playlist",
- ),
- path(
- "video//",
- login_required(views.VideoView.as_view()),
- name="video",
- ),
- path(
- "playlist/",
- login_required(views.PlaylistView.as_view()),
- name="playlist",
- ),
- path(
- "playlist//",
- login_required(views.PlaylistIdView.as_view()),
- name="playlist_id",
- ),
- path("search/", login_required(views.SearchView.as_view()), name="search"),
-]
diff --git a/tubearchivist/home/views.py b/tubearchivist/home/views.py
deleted file mode 100644
index d032714c..00000000
--- a/tubearchivist/home/views.py
+++ /dev/null
@@ -1,1199 +0,0 @@
-"""
-Functionality:
-- all views for home app
-- holds base classes to inherit from
-"""
-
-import enum
-import urllib.parse
-import uuid
-from time import sleep
-
-from appsettings.src.backup import ElasticBackup
-from appsettings.src.config import AppConfig, ReleaseVersion
-from appsettings.src.reindex import ReindexProgress
-from appsettings.src.snapshot import ElasticSnapshot
-from channel.src.index import channel_overwrites
-from common.src.env_settings import EnvironmentSettings
-from common.src.es_connect import ElasticWrap
-from common.src.helper import check_stylesheet, time_parser
-from common.src.index_generic import Pagination
-from common.src.search_processor import SearchProcess, process_aggs
-from common.src.ta_redis import RedisArchivist
-from common.views_base import check_admin
-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
-from django.shortcuts import redirect, render
-from django.utils.decorators import method_decorator
-from django.views import View
-from download.src.queue import PendingInteract
-from download.src.yt_dlp_base import CookieHandler
-from home.src.frontend.forms import (
- AddToQueueForm,
- ApplicationSettingsForm,
- ChannelOverwriteForm,
- CreatePlaylistForm,
- CustomAuthForm,
- MultiSearchForm,
- SubscribeToChannelForm,
- SubscribeToPlaylistForm,
- UserSettingsForm,
-)
-from home.src.frontend.forms_schedule import (
- NotificationSettingsForm,
- SchedulerSettingsForm,
-)
-from playlist.src.index import YoutubePlaylist
-from rest_framework.authtoken.models import Token
-from task.models import CustomPeriodicTask
-from task.src.config_schedule import ScheduleBuilder
-from task.src.notify import Notifications, get_all_notifications
-from task.tasks import index_channel_playlists, subscribe_to
-from user.src.user_config import UserConfig
-from video.src.constants import VideoTypeEnum
-
-
-class ArchivistViewConfig(View):
- """base view class to generate initial config context"""
-
- def __init__(self, view_origin):
- super().__init__()
- self.view_origin = view_origin
- self.user_id = False
- self.user_conf: UserConfig = False
- self.context = False
-
- def get_all_view_styles(self):
- """get dict of all view styles for search form"""
- all_styles = {}
- for view_origin in ["channel", "playlist", "home", "downloads"]:
- all_styles[view_origin] = self.user_conf.get_value(
- f"view_style_{view_origin}"
- )
-
- return all_styles
-
- def config_builder(self, user_id):
- """build default context for every view"""
- self.user_id = user_id
- self.user_conf = UserConfig(self.user_id)
-
- self.context = {
- "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"),
- "view_style": self.user_conf.get_value(
- f"view_style_{self.view_origin}"
- ),
- "grid_items": self.user_conf.get_value("grid_items"),
- "hide_watched": self.user_conf.get_value("hide_watched"),
- "show_ignored_only": self.user_conf.get_value("show_ignored_only"),
- "show_subed_only": self.user_conf.get_value("show_subed_only"),
- "version": settings.TA_VERSION,
- "ta_update": ReleaseVersion().get_update(),
- }
-
-
-class ArchivistResultsView(ArchivistViewConfig):
- """View class to inherit from when searching data in es"""
-
- view_origin = ""
- es_search = ""
-
- def __init__(self):
- super().__init__(self.view_origin)
- self.pagination_handler = False
- self.search_get = False
- self.data = False
- self.sort_by = False
-
- def _sort_by_overwrite(self):
- """overwrite sort by key to match with es keys"""
- sort_by_map = {
- "views": "stats.view_count",
- "likes": "stats.like_count",
- "downloaded": "date_downloaded",
- "published": "published",
- "duration": "player.duration",
- "filesize": "media_size",
- }
- sort_by = sort_by_map[self.context["sort_by"]]
-
- return sort_by
-
- @staticmethod
- def _url_encode(search_get):
- """url encode search form request"""
- if search_get:
- search_encoded = urllib.parse.quote(search_get)
- else:
- search_encoded = False
-
- return search_encoded
-
- def _initial_data(self):
- """add initial data dict"""
- sort_order = self.context["sort_order"]
- data = {
- "size": self.pagination_handler.pagination["page_size"],
- "from": self.pagination_handler.pagination["page_from"],
- "query": {"match_all": {}},
- "sort": [{self.sort_by: {"order": sort_order}}],
- }
- self.data = data
-
- def match_progress(self):
- """add video progress to result context"""
- results = RedisArchivist().list_items(f"{self.user_id}:progress:")
- if not results or not self.context["results"]:
- return
-
- self.context["continue_vids"] = self.get_in_progress(results)
-
- in_progress = {i["youtube_id"]: i["position"] for i in results}
- for video in self.context["results"]:
- if video["youtube_id"] in in_progress:
- played_sec = in_progress.get(video["youtube_id"])
- total = video["player"]["duration"]
- if not total:
- total = played_sec * 2
- video["player"]["progress"] = 100 * (played_sec / total)
-
- def get_in_progress(self, results):
- """get all videos in progress"""
- ids = [{"match": {"youtube_id": i.get("youtube_id")}} for i in results]
- data = {
- "size": UserConfig(self.user_id).get_value("page_size"),
- "query": {"bool": {"should": ids}},
- "sort": [{"published": {"order": "desc"}}],
- }
- response, _ = ElasticWrap("ta_video/_search").get(data)
- videos = SearchProcess(response).process()
-
- if not videos:
- return False
-
- for video in videos:
- youtube_id = video["youtube_id"]
- matched = [i for i in results if i["youtube_id"] == youtube_id]
- played_sec = matched[0]["position"]
- total = video["player"]["duration"]
- if not total:
- total = matched[0].get("position") * 2
- video["player"]["progress"] = 100 * (played_sec / total)
-
- return videos
-
- def single_lookup(self, es_path):
- """retrieve a single item from url"""
- response, status_code = ElasticWrap(es_path).get()
- if not status_code == 200:
- raise Http404
-
- result = SearchProcess(response).process()
-
- return result
-
- def initiate_vars(self, request):
- """search in es for vidoe hits"""
- self.user_id = request.user.id
- self.config_builder(self.user_id)
- self.search_get = request.GET.get("search", False)
- self.pagination_handler = Pagination(request)
- self.sort_by = self._sort_by_overwrite()
- self._initial_data()
-
- def find_results(self):
- """add results and pagination to context"""
- response, _ = ElasticWrap(self.es_search).get(self.data)
- process_aggs(response)
- results = SearchProcess(response).process()
- max_hits = response["hits"]["total"]["value"]
- self.pagination_handler.validate(max_hits)
- self.context.update(
- {
- "results": results,
- "max_hits": max_hits,
- "pagination": self.pagination_handler.pagination,
- "aggs": response.get("aggregations"),
- }
- )
-
-
-class MinView(View):
- """to inherit from for minimal config vars"""
-
- @staticmethod
- def get_min_context(request):
- """build minimal vars for context"""
- return {
- "stylesheet": check_stylesheet(
- UserConfig(request.user.id).get_value("stylesheet")
- ),
- "version": settings.TA_VERSION,
- "ta_update": ReleaseVersion().get_update(),
- }
-
-
-class HomeView(ArchivistResultsView):
- """resolves to /
- handle home page and video search post functionality
- """
-
- view_origin = "home"
- es_search = "ta_video/_search"
-
- def get(self, request):
- """handle get requests"""
- self.initiate_vars(request)
- self._update_view_data()
- self.find_results()
- self.match_progress()
-
- return render(request, "home/home.html", self.context)
-
- def _update_view_data(self):
- """update view specific data dict"""
- self.data["sort"].extend(
- [
- {"channel.channel_name.keyword": {"order": "asc"}},
- {"title.keyword": {"order": "asc"}},
- ]
- )
-
- if self.context["hide_watched"]:
- self.data["query"] = {"term": {"player.watched": {"value": False}}}
- if self.search_get:
- del self.data["sort"]
- query = {
- "multi_match": {
- "query": self.search_get,
- "fields": ["title", "channel.channel_name", "tags"],
- "type": "cross_fields",
- "operator": "and",
- }
- }
- self.data["query"] = query
-
-
-class LoginView(MinView):
- """resolves to /login/
- Greeting and login page
- """
-
- SEC_IN_DAY = 60 * 60 * 24
-
- def get(self, request):
- """handle get requests"""
- context = self.get_min_context(request)
- context.update(
- {
- "form": CustomAuthForm(),
- "form_error": bool(request.GET.get("failed")),
- }
- )
-
- return render(request, "home/login.html", context)
-
- def post(self, request):
- """handle login post request"""
- form = AuthenticationForm(data=request.POST)
- if form.is_valid():
- remember_me = request.POST.get("remember_me") or False
- if remember_me == "on":
- request.session.set_expiry(self.SEC_IN_DAY * 365)
- else:
- request.session.set_expiry(self.SEC_IN_DAY * 2)
- print(f"expire session in {request.session.get_expiry_age()} secs")
-
- next_url = request.POST.get("next") or "home"
- user = form.get_user()
- login(request, user)
- return redirect(next_url)
-
- return redirect("/login?failed=true")
-
-
-class AboutView(MinView):
- """resolves to /about/
- show helpful how to information
- """
-
- def get(self, request):
- """handle http get"""
- context = self.get_min_context(request)
- context.update({"title": "About"})
- return render(request, "home/about.html", context)
-
-
-@method_decorator(user_passes_test(check_admin), name="dispatch")
-class DownloadView(ArchivistResultsView):
- """resolves to /download/
- handle the download queue
- """
-
- view_origin = "downloads"
- es_search = "ta_download/_search"
-
- def get(self, request):
- """handle get request"""
- self.initiate_vars(request)
- filter_view = self._update_view_data(request)
- self.find_results()
- self.context.update(
- {
- "title": "Downloads",
- "add_form": AddToQueueForm(),
- "channel_agg_list": self._get_channel_agg(filter_view),
- }
- )
- return render(request, "home/downloads.html", self.context)
-
- def _update_view_data(self, request):
- """update downloads view specific data dict"""
- if self.context["show_ignored_only"]:
- filter_view = "ignore"
- else:
- filter_view = "pending"
-
- must_list = [{"term": {"status": {"value": filter_view}}}]
-
- channel_filter = request.GET.get("channel", False)
- if channel_filter:
- must_list.append(
- {"term": {"channel_id": {"value": channel_filter}}}
- )
-
- channel = PendingInteract(channel_filter).get_channel()
- self.context.update(
- {
- "channel_filter_id": channel.get("channel_id"),
- "channel_filter_name": channel.get("channel_name"),
- }
- )
-
- self.data.update(
- {
- "query": {"bool": {"must": must_list}},
- "sort": [
- {"auto_start": {"order": "desc"}},
- {"timestamp": {"order": "asc"}},
- ],
- }
- )
-
- return filter_view
-
- def _get_channel_agg(self, filter_view):
- """get pending channel with count"""
- data = {
- "size": 0,
- "query": {"term": {"status": {"value": filter_view}}},
- "aggs": {
- "channel_downloads": {
- "multi_terms": {
- "size": 30,
- "terms": [
- {"field": "channel_name.keyword"},
- {"field": "channel_id"},
- ],
- "order": {"_count": "desc"},
- }
- }
- },
- }
- response, _ = ElasticWrap(self.es_search).get(data=data)
- buckets = response["aggregations"]["channel_downloads"]["buckets"]
-
- buckets_sorted = []
- for i in buckets:
- bucket = {
- "name": i["key"][0],
- "id": i["key"][1],
- "count": i["doc_count"],
- }
- buckets_sorted.append(bucket)
-
- return buckets_sorted
-
-
-class ChannelIdBaseView(ArchivistResultsView):
- """base class for all channel-id views"""
-
- def get_channel_meta(self, channel_id):
- """get metadata for channel"""
- path = f"ta_channel/_doc/{channel_id}"
- response, _ = ElasticWrap(path).get()
- channel_info = SearchProcess(response).process()
- if not channel_info:
- raise Http404
-
- return channel_info
-
- def channel_pages(self, channel_id):
- """get additional context for channel pages"""
- self.channel_has_pending(channel_id)
- self.channel_has_streams(channel_id)
- self.channel_has_shorts(channel_id)
- self.channel_has_playlist(channel_id)
-
- def channel_has_pending(self, channel_id):
- """check if channel has pending videos in queue"""
- path = "ta_download/_search"
- data = {
- "size": 1,
- "query": {
- "bool": {
- "must": [
- {"term": {"status": {"value": "pending"}}},
- {"term": {"channel_id": {"value": channel_id}}},
- ]
- }
- },
- "_source": False,
- }
- response, _ = ElasticWrap(path).get(data=data)
-
- self.context.update({"has_pending": bool(response["hits"]["hits"])})
-
- def channel_has_streams(self, channel_id):
- """check if channel has streams videos"""
- data = self.get_type_data("streams", channel_id)
- response, _ = ElasticWrap("ta_video/_search").get(data=data)
-
- self.context.update({"has_streams": bool(response["hits"]["hits"])})
-
- def channel_has_shorts(self, channel_id):
- """check if channel has shorts videos"""
- data = self.get_type_data("shorts", channel_id)
- response, _ = ElasticWrap("ta_video/_search").get(data=data)
-
- self.context.update({"has_shorts": bool(response["hits"]["hits"])})
-
- @staticmethod
- def get_type_data(vid_type, channel):
- """build data query for vid_type"""
- return {
- "size": 1,
- "query": {
- "bool": {
- "must": [
- {"term": {"vid_type": {"value": vid_type}}},
- {"term": {"channel.channel_id": {"value": channel}}},
- ]
- }
- },
- "_source": False,
- }
-
- def channel_has_playlist(self, channel_id):
- """check if channel has any playlist indexed"""
- path = "ta_playlist/_search"
- data = {
- "size": 1,
- "query": {"term": {"playlist_channel_id": {"value": channel_id}}},
- "_source": False,
- }
- response, _ = ElasticWrap(path).get(data=data)
- self.context.update({"has_playlists": bool(response["hits"]["hits"])})
-
-
-class ChannelIdView(ChannelIdBaseView):
- """resolves to /channel//
- display single channel page from channel_id
- """
-
- view_origin = "home"
- es_search = "ta_video/_search"
- video_types = [VideoTypeEnum.VIDEOS]
-
- def get(self, request, channel_id):
- """get request"""
- self.initiate_vars(request)
- self._update_view_data(channel_id)
- self.find_results()
- self.match_progress()
- self.channel_pages(channel_id)
-
- if self.context["results"]:
- channel_info = self.context["results"][0]["channel"]
- channel_name = channel_info["channel_name"]
- else:
- # fall back channel lookup if no videos found
- es_path = f"ta_channel/_doc/{channel_id}"
- channel_info = self.single_lookup(es_path)
- channel_name = channel_info["channel_name"]
-
- self.context.update(
- {
- "title": f"Channel: {channel_name}",
- "channel_info": channel_info,
- }
- )
-
- return render(request, "home/channel_id.html", self.context)
-
- def _update_view_data(self, channel_id):
- """update view specific data dict"""
- vid_type_terms = []
- for t in self.video_types:
- if t and isinstance(t, enum.Enum):
- vid_type_terms.append(t.value)
- else:
- print(
- "Invalid value passed into video_types on "
- + f"ChannelIdView: {t}"
- )
- self.data["query"] = {
- "bool": {
- "must": [
- {"term": {"channel.channel_id": {"value": channel_id}}},
- {"terms": {"vid_type": vid_type_terms}},
- ]
- }
- }
- self.data["aggs"] = {
- "total_items": {"value_count": {"field": "youtube_id"}},
- "total_size": {"sum": {"field": "media_size"}},
- "total_duration": {"sum": {"field": "player.duration"}},
- }
- self.data["sort"].append({"title.keyword": {"order": "asc"}})
-
- if self.context["hide_watched"]:
- to_append = {"term": {"player.watched": {"value": False}}}
- self.data["query"]["bool"]["must"].append(to_append)
-
-
-class ChannelIdLiveView(ChannelIdView):
- """resolves to /channel//streams/
- display single channel page from channel_id
- """
-
- video_types = [VideoTypeEnum.STREAMS]
-
-
-class ChannelIdShortsView(ChannelIdView):
- """resolves to /channel//shorts/
- display single channel page from channel_id
- """
-
- video_types = [VideoTypeEnum.SHORTS]
-
-
-class ChannelIdAboutView(ChannelIdBaseView):
- """resolves to /channel//about/
- show metadata, handle per channel conf
- """
-
- view_origin = "channel"
-
- def get(self, request, channel_id):
- """handle get request"""
- self.initiate_vars(request)
- self.channel_pages(channel_id)
-
- response, _ = ElasticWrap(f"ta_channel/_doc/{channel_id}").get()
- channel_info = SearchProcess(response).process()
- reindex = ReindexProgress(
- request_type="channel", request_id=channel_id
- ).get_progress()
-
- self.context.update(
- {
- "title": "Channel: About " + channel_info["channel_name"],
- "channel_info": channel_info,
- "channel_overwrite_form": ChannelOverwriteForm,
- "reindex": reindex.get("state"),
- }
- )
-
- return render(request, "home/channel_id_about.html", self.context)
-
- @method_decorator(user_passes_test(check_admin), name="dispatch")
- @staticmethod
- def post(request, channel_id):
- """handle post request"""
- print(f"handle post from {channel_id}")
- channel_overwrite_form = ChannelOverwriteForm(request.POST)
- if channel_overwrite_form.is_valid():
- overwrites = channel_overwrite_form.cleaned_data
- print(f"{channel_id}: set overwrites {overwrites}")
- channel_overwrites(channel_id, overwrites=overwrites)
- if overwrites.get("index_playlists") == "1":
- index_channel_playlists.delay(channel_id)
-
- sleep(1)
- return redirect("channel_id_about", channel_id, permanent=True)
-
-
-class ChannelIdPlaylistView(ChannelIdBaseView):
- """resolves to /channel//playlist/
- show all playlists of channel
- """
-
- view_origin = "playlist"
- es_search = "ta_playlist/_search"
-
- def get(self, request, channel_id):
- """handle get request"""
- self.initiate_vars(request)
- self._update_view_data(channel_id)
- self.find_results()
- self.channel_pages(channel_id)
-
- channel_info = self.get_channel_meta(channel_id)
- channel_name = channel_info["channel_name"]
- self.context.update(
- {
- "title": "Channel: Playlists " + channel_name,
- "channel_info": channel_info,
- }
- )
-
- return render(request, "home/channel_id_playlist.html", self.context)
-
- def _update_view_data(self, channel_id):
- """update view specific data dict"""
- self.data["sort"] = [{"playlist_name.keyword": {"order": "asc"}}]
- must_list = [{"match": {"playlist_channel_id": channel_id}}]
-
- if self.context["show_subed_only"]:
- must_list.append({"match": {"playlist_subscribed": True}})
-
- self.data["query"] = {"bool": {"must": must_list}}
-
-
-class ChannelView(ArchivistResultsView):
- """resolves to /channel/
- handle functionality for channel overview page, subscribe to channel,
- search as you type for channel name
- """
-
- view_origin = "channel"
- es_search = "ta_channel/_search"
-
- def get(self, request):
- """handle get request"""
- self.initiate_vars(request)
- self._update_view_data()
- self.find_results()
- self.context.update(
- {
- "title": "Channels",
- "subscribe_form": SubscribeToChannelForm(),
- }
- )
-
- return render(request, "home/channel.html", self.context)
-
- def _update_view_data(self):
- """update view data dict"""
- self.data["sort"] = [{"channel_name.keyword": {"order": "asc"}}]
- if self.context["show_subed_only"]:
- self.data["query"] = {
- "term": {"channel_subscribed": {"value": True}}
- }
-
- @method_decorator(user_passes_test(check_admin), name="dispatch")
- @staticmethod
- def post(request):
- """handle http post requests"""
- subscribe_form = SubscribeToChannelForm(data=request.POST)
- if subscribe_form.is_valid():
- url_str = request.POST.get("subscribe")
- print(url_str)
- subscribe_to.delay(url_str, expected_type="channel")
-
- sleep(1)
- return redirect("channel", permanent=True)
-
-
-class PlaylistIdView(ArchivistResultsView):
- """resolves to /playlist/
- show all videos in a playlist
- """
-
- view_origin = "home"
- es_search = "ta_video/_search"
-
- def get(self, request, playlist_id):
- """handle get request"""
- self.initiate_vars(request)
- playlist_info, channel_info = self._get_info(playlist_id)
- if not playlist_info:
- raise Http404
-
- playlist_name = playlist_info["playlist_name"]
- self._update_view_data(playlist_id, playlist_info)
- self.find_results()
- self.match_progress()
- reindex = ReindexProgress(
- request_type="playlist", request_id=playlist_id
- ).get_progress()
-
- self.context.update(
- {
- "title": "Playlist: " + playlist_name,
- "playlist_info": playlist_info,
- "playlist_name": playlist_name,
- "channel_info": channel_info,
- "reindex": reindex.get("state"),
- }
- )
- return render(request, "home/playlist_id.html", self.context)
-
- def _get_info(self, playlist_id):
- """return additional metadata"""
- # playlist details
- es_path = f"ta_playlist/_doc/{playlist_id}"
- playlist_info = self.single_lookup(es_path)
- channel_info = None
- if playlist_info["playlist_type"] != "custom":
- # channel details
- channel_id = playlist_info["playlist_channel_id"]
- es_path = f"ta_channel/_doc/{channel_id}"
- channel_info = self.single_lookup(es_path)
- return playlist_info, channel_info
-
- def _update_view_data(self, playlist_id, playlist_info):
- """update view specific data dict"""
- sort = {
- i["youtube_id"]: i["idx"]
- for i in playlist_info["playlist_entries"]
- }
- script = (
- "if(params.scores.containsKey(doc['youtube_id'].value)) "
- + "{return params.scores[doc['youtube_id'].value];} "
- + "return 100000;"
- )
- self.data.update(
- {
- "query": {
- "bool": {
- "must": [{"match": {"playlist.keyword": playlist_id}}]
- }
- },
- "sort": [
- {
- "_script": {
- "type": "number",
- "script": {
- "lang": "painless",
- "source": script,
- "params": {"scores": sort},
- },
- "order": "asc",
- }
- }
- ],
- }
- )
- if self.context["hide_watched"]:
- to_append = {"term": {"player.watched": {"value": False}}}
- self.data["query"]["bool"]["must"].append(to_append)
-
-
-class PlaylistView(ArchivistResultsView):
- """resolves to /playlist/
- show all playlists indexed
- """
-
- view_origin = "playlist"
- es_search = "ta_playlist/_search"
-
- def get(self, request):
- """handle get request"""
- self.initiate_vars(request)
- self._update_view_data()
- self.find_results()
- self.context.update(
- {
- "title": "Playlists",
- "subscribe_form": SubscribeToPlaylistForm(),
- "create_form": CreatePlaylistForm(),
- }
- )
-
- return render(request, "home/playlist.html", self.context)
-
- def _update_view_data(self):
- """update view specific data dict"""
- self.data["sort"] = [{"playlist_name.keyword": {"order": "asc"}}]
- if self.context["show_subed_only"]:
- self.data["query"] = {
- "term": {"playlist_subscribed": {"value": True}}
- }
- if self.search_get:
- self.data["query"] = {
- "bool": {
- "should": [
- {
- "multi_match": {
- "query": self.search_get,
- "fields": [
- "playlist_channel_id",
- "playlist_channel",
- "playlist_name",
- ],
- }
- }
- ],
- "minimum_should_match": 1,
- }
- }
-
- @method_decorator(user_passes_test(check_admin), name="dispatch")
- @staticmethod
- def post(request):
- """handle post from subscribe or create form"""
- if request.POST.get("create") is not None:
- create_form = CreatePlaylistForm(data=request.POST)
- if create_form.is_valid():
- name = request.POST.get("create")
- playlist_id = f"TA_playlist_{uuid.uuid4()}"
- YoutubePlaylist(playlist_id).create(name)
- else:
- subscribe_form = SubscribeToPlaylistForm(data=request.POST)
- if subscribe_form.is_valid():
- url_str = request.POST.get("subscribe")
- print(url_str)
- subscribe_to.delay(url_str, expected_type="playlist")
-
- sleep(1)
- return redirect("playlist")
-
-
-class VideoView(MinView):
- """resolves to /video//
- display details about a single video
- """
-
- def get(self, request, video_id):
- """get single video"""
- config_handler = AppConfig()
- response, _ = ElasticWrap(f"ta_video/_doc/{video_id}").get()
- video_data = SearchProcess(response).process()
- if not video_data:
- raise Http404
-
- try:
- rating = video_data["stats"]["average_rating"]
- video_data["stats"]["average_rating"] = self.star_creator(rating)
- except KeyError:
- video_data["stats"]["average_rating"] = False
-
- if "playlist" in video_data.keys():
- playlists = video_data["playlist"]
- playlist_nav = self.build_playlists(video_id, playlists)
- else:
- playlist_nav = False
-
- reindex = ReindexProgress(
- request_type="video", request_id=video_id
- ).get_progress()
-
- context = self.get_min_context(request)
- context.update(
- {
- "video": video_data,
- "playlist_nav": playlist_nav,
- "title": video_data.get("title"),
- "cast": EnvironmentSettings.ENABLE_CAST,
- "config": config_handler.config,
- "position": time_parser(request.GET.get("t")),
- "reindex": reindex.get("state"),
- }
- )
- return render(request, "home/video.html", context)
-
- @staticmethod
- def build_playlists(video_id, playlists):
- """build playlist nav if available"""
- all_navs = []
- for playlist_id in playlists:
- playlist = YoutubePlaylist(playlist_id)
- playlist.get_from_es()
- playlist.build_nav(video_id)
- if playlist.nav:
- all_navs.append(playlist.nav)
-
- return all_navs
-
- @staticmethod
- def star_creator(rating):
- """convert rating float to stars"""
- if not rating:
- return False
-
- stars = []
- for _ in range(1, 6):
- if rating >= 0.75:
- stars.append("full")
- elif 0.25 < rating < 0.75:
- stars.append("half")
- else:
- stars.append("empty")
- rating = rating - 1
- return stars
-
-
-class SearchView(ArchivistResultsView):
- """resolves to /search/
- handle cross index search interface
- """
-
- view_origin = "home"
- es_search = ""
-
- def get(self, request):
- """handle get request"""
- self.initiate_vars(request)
- all_styles = self.get_all_view_styles()
- self.context.update({"all_styles": all_styles})
- self.context.update(
- {
- "search_form": MultiSearchForm(initial=all_styles),
- "version": settings.TA_VERSION,
- }
- )
-
- return render(request, "home/search.html", self.context)
-
-
-class SettingsView(MinView):
- """resolves to /settings/
- handle the settings dashboard
- """
-
- def get(self, request):
- """read and display the dashboard"""
- context = self.get_min_context(request)
- context.update({"title": "Settings Dashboard"})
-
- return render(request, "home/settings.html", context)
-
-
-class SettingsUserView(MinView):
- """resolves to /settings/user/
- handle the settings sub-page for user settings,
- display current settings,
- take post request from the form to update settings
- """
-
- def get(self, request):
- """read and display current settings"""
- context = self.get_min_context(request)
- context.update(
- {
- "title": "User Settings",
- "page_size": UserConfig(request.user.id).get_value(
- "page_size"
- ),
- "user_form": UserSettingsForm(),
- }
- )
-
- return render(request, "home/settings_user.html", context)
-
- def post(self, request):
- """handle form post to update settings"""
- user_form = UserSettingsForm(request.POST)
- config_handler = UserConfig(request.user.id)
- if user_form.is_valid():
- user_form_post = user_form.cleaned_data
- if user_form_post.get("stylesheet"):
- config_handler.set_value(
- "stylesheet", user_form_post.get("stylesheet")
- )
- if user_form_post.get("page_size"):
- config_handler.set_value(
- "page_size", user_form_post.get("page_size")
- )
-
- sleep(1)
- return redirect("settings_user", permanent=True)
-
-
-@method_decorator(user_passes_test(check_admin), name="dispatch")
-class SettingsApplicationView(MinView):
- """resolves to /settings/application/
- handle the settings sub-page for application configuration,
- display current settings,
- take post request from the form to update settings
- """
-
- def get(self, request):
- """read and display current application settings"""
- context = self.get_min_context(request)
- context.update(
- {
- "title": "Application Settings",
- "config": AppConfig().config,
- "api_token": self.get_token(request),
- "app_form": ApplicationSettingsForm(),
- "snapshots": ElasticSnapshot().get_snapshot_stats(),
- }
- )
-
- return render(request, "home/settings_application.html", context)
-
- @staticmethod
- def get_token(request):
- """get existing or create new token of user"""
- # pylint: disable=no-member
- token = Token.objects.get_or_create(user=request.user)[0]
- return token
-
- def post(self, request):
- """handle form post to update settings"""
- config_handler = AppConfig()
-
- app_form = ApplicationSettingsForm(request.POST)
- if app_form.is_valid():
- app_form_post = app_form.cleaned_data
- if app_form_post:
- print(app_form_post)
- updated = config_handler.update_config(app_form_post)
- self.post_process_updated(updated, config_handler.config)
-
- sleep(1)
- return redirect("settings_application", permanent=True)
-
- def post_process_updated(self, updated, config):
- """apply changes for config"""
- if not updated:
- return
-
- for config_value, updated_value in updated:
- if config_value == "cookie_import":
- self.process_cookie(config, updated_value)
- if config_value == "enable_snapshot":
- ElasticSnapshot().setup()
-
- def process_cookie(self, config, updated_value):
- """import and validate cookie"""
- handler = CookieHandler(config)
- if updated_value:
- try:
- handler.import_cookie()
- except FileNotFoundError:
- print("cookie: import failed, file not found")
- handler.revoke()
- self._fail_message("Cookie file not found.")
- return
-
- valid = handler.validate()
- if not valid:
- handler.revoke()
- self._fail_message("Failed to validate cookie file.")
- else:
- handler.revoke()
-
- @staticmethod
- def _fail_message(message_line):
- """notify our failure"""
- key = "message:setting"
- message = {
- "status": key,
- "group": "setting:application",
- "level": "error",
- "title": "Cookie import failed",
- "messages": [message_line],
- "id": "0000",
- }
- RedisArchivist().set_message(key, message=message, expire=True)
-
-
-@method_decorator(user_passes_test(check_admin), name="dispatch")
-class SettingsSchedulingView(MinView):
- """resolves to /settings/scheduling/
- handle the settings sub-page for scheduling settings,
- display current settings,
- take post request from the form to update settings
- """
-
- def get(self, request):
- """read and display current settings"""
- context = self.get_context(request, SchedulerSettingsForm())
-
- return render(request, "home/settings_scheduling.html", context)
-
- def post(self, request):
- """handle form post to update settings"""
- scheduler_form = SchedulerSettingsForm(request.POST)
- notification_form = NotificationSettingsForm(request.POST)
-
- if notification_form.is_valid():
- notification_form_post = notification_form.cleaned_data
- print(notification_form_post)
- if any(notification_form_post.values()):
- task_name = notification_form_post.get("task")
- url = notification_form_post.get("notification_url")
- Notifications(task_name).add_url(url)
-
- if scheduler_form.is_valid():
- scheduler_form_post = scheduler_form.cleaned_data
- if any(scheduler_form_post.values()):
- print(scheduler_form_post)
- ScheduleBuilder().update_schedule_conf(scheduler_form_post)
- else:
- self.fail_message()
- context = self.get_context(request, scheduler_form)
- return render(request, "home/settings_scheduling.html", context)
-
- sleep(1)
- return redirect("settings_scheduling", permanent=True)
-
- def get_context(self, request, scheduler_form):
- """get context"""
- context = self.get_min_context(request)
- all_tasks = CustomPeriodicTask.objects.all()
- context.update(
- {
- "title": "Scheduling Settings",
- "scheduler_form": scheduler_form,
- "notification_form": NotificationSettingsForm(),
- "notifications": get_all_notifications(),
- }
- )
- for task in all_tasks:
- context.update({task.name: task})
-
- return context
-
- @staticmethod
- def fail_message():
- """send failure message"""
- mess_dict = {
- "group": "setting:schedule",
- "level": "error",
- "title": "Scheduler update failed.",
- "messages": ["Invalid schedule input"],
- "id": "0000",
- }
- RedisArchivist().set_message("message:setting", mess_dict, expire=True)
-
-
-@method_decorator(user_passes_test(check_admin), name="dispatch")
-class SettingsActionsView(MinView):
- """resolves to /settings/actions/
- handle the settings actions sub-page
- """
-
- def get(self, request):
- """read and display current settings"""
- context = self.get_min_context(request)
- context.update(
- {
- "title": "Actions",
- "available_backups": ElasticBackup().get_all_backup_files(),
- }
- )
-
- return render(request, "home/settings_actions.html", context)
diff --git a/tubearchivist/static/.jshintrc b/tubearchivist/static/.jshintrc
deleted file mode 100644
index 8ab34857..00000000
--- a/tubearchivist/static/.jshintrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "esversion": 6
-}
\ No newline at end of file
diff --git a/tubearchivist/static/cast-videos.js b/tubearchivist/static/cast-videos.js
deleted file mode 100644
index 881b55fc..00000000
--- a/tubearchivist/static/cast-videos.js
+++ /dev/null
@@ -1,157 +0,0 @@
-'use strict';
-
-/* global cast chrome getVideoPlayerVideoId postVideoProgress setProgressBar getVideoPlayer getVideoPlayerWatchStatus watchedThreshold isWatched getVideoData getURL getVideoPlayerCurrentTime */
-
-function initializeCastApi() {
- cast.framework.CastContext.getInstance().setOptions({
- receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
- autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
- });
-
- let player = new cast.framework.RemotePlayer();
- let playerController = new cast.framework.RemotePlayerController(player);
-
- // Add event listerner to check if a connection to a cast device is initiated
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
- function () {
- castConnectionChange(player);
- }
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
- function () {
- castVideoProgress(player);
- }
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
- function () {
- castVideoPaused(player);
- }
- );
-}
-
-function castConnectionChange(player) {
- // If cast connection is initialized start cast
- if (player.isConnected) {
- // console.log("Cast Connected.");
- castStart();
- } else if (!player.isConnected) {
- // console.log("Cast Disconnected.");
- }
-}
-
-function castVideoProgress(player) {
- let videoId = getVideoPlayerVideoId();
- if (player.mediaInfo.contentId.includes(videoId)) {
- let currentTime = player.currentTime;
- let duration = player.duration;
- if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- postVideoProgress(videoId, currentTime);
- setProgressBar(videoId, currentTime, duration);
- if (!getVideoPlayerWatchStatus()) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- isWatched(videoId);
- }
- }
- }
- }
-}
-
-function castVideoPaused(player) {
- let videoId = getVideoPlayerVideoId();
- let currentTime = player.currentTime;
- let duration = player.duration;
- if (player.mediaInfo != null) {
- if (player.mediaInfo.contentId.includes(videoId)) {
- if (currentTime !== 0 && duration !== 0) {
- postVideoProgress(videoId, currentTime);
- }
- }
- }
-}
-
-function castStart() {
- let castSession = cast.framework.CastContext.getInstance().getCurrentSession();
- // Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page
- if (!castSession.getMediaSession()) {
- let videoId = getVideoPlayerVideoId();
- let videoData = getVideoData(videoId);
- let contentId = getURL() + videoData.data.media_url;
- let contentTitle = videoData.data.title;
- let contentImage = getURL() + videoData.data.vid_thumb_url;
-
- let contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
- let contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
- let contentActiveSubtitle = [];
- // Check if a subtitle is turned on.
- for (let i = 0; i < getVideoPlayer().textTracks.length; i++) {
- if (getVideoPlayer().textTracks[i].mode === 'showing') {
- contentActiveSubtitle = [i + 1];
- }
- }
- let contentSubtitles = [];
- let videoSubtitles = videoData.data.subtitles; // Array of subtitles
- if (typeof videoSubtitles !== 'undefined' && videoData.config.downloads.subtitle) {
- for (let i = 0; i < videoSubtitles.length; i++) {
- let subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
- subtitle.trackContentId = videoSubtitles[i].media_url;
- subtitle.trackContentType = 'text/vtt';
- subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
- subtitle.name = videoSubtitles[i].name;
- subtitle.language = videoSubtitles[i].lang;
- subtitle.customData = null;
- contentSubtitles.push(subtitle);
- }
- }
-
- let mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
- // mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
- mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
- mediaInfo.metadata.title = contentTitle.replace('&', '&'); // Set the video title
- mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
- // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
- mediaInfo.tracks = contentSubtitles;
-
- let request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
- // request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
- request.currentTime = shiftCurrentTime(contentCurrentTime); // Set video start position based on the browser video position
- request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
- // request.autoplay = false; // Set content to auto play, true by default
- castSession.loadMedia(request).then(
- function () {
- castSuccessful();
- },
- function (error) {
- castFailed(error.code);
- }
- ); // Send request to cast device
- }
-}
-
-function shiftCurrentTime(contentCurrentTime) {
- // Shift media back 3 seconds to prevent missing some of the content
- if (contentCurrentTime > 5) {
- return contentCurrentTime - 3;
- } else {
- return 0;
- }
-}
-
-function castSuccessful() {
- // console.log('Cast Successful.');
- getVideoPlayer().pause(); // Pause browser video on successful cast
-}
-
-function castFailed(errorCode) {
- console.log('Error code: ' + errorCode);
-}
-
-window['__onGCastApiAvailable'] = function (isAvailable) {
- if (isAvailable) {
- initializeCastApi();
- }
-};
diff --git a/tubearchivist/static/css/dark.css b/tubearchivist/static/css/dark.css
deleted file mode 100644
index 55c981e3..00000000
--- a/tubearchivist/static/css/dark.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --main-bg: #00202f;
- --highlight-bg: #00293b;
- --highlight-error: #990202;
- --highlight-error-light: #c44343;
- --highlight-bg-transparent: #00293baf;
- --main-font: #eeeeee;
- --accent-font-dark: #259485;
- --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");
-}
diff --git a/tubearchivist/static/css/light.css b/tubearchivist/static/css/light.css
deleted file mode 100644
index bf9cf787..00000000
--- a/tubearchivist/static/css/light.css
+++ /dev/null
@@ -1,14 +0,0 @@
-:root {
- --main-bg: #eeeeee;
- --highlight-bg: #d9e0d9;
- --highlight-error: #990202;
- --highlight-error-light: #c44343;
- --highlight-bg-transparent: #00293baf;
- --main-font: #00202f;
- --accent-font-dark: #259485;
- --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");
-}
diff --git a/tubearchivist/static/css/matrix.css b/tubearchivist/static/css/matrix.css
deleted file mode 100644
index 934424d3..00000000
--- a/tubearchivist/static/css/matrix.css
+++ /dev/null
@@ -1,67 +0,0 @@
-: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: brightness(0) saturate(100%) invert(45%) sepia(100%) saturate(3710%) hue-rotate(96deg) brightness(100%) contrast(102%);
- --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);
-}
-
-.top-banner img {
- 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);
-}
diff --git a/tubearchivist/static/css/midnight.css b/tubearchivist/static/css/midnight.css
deleted file mode 100644
index e0e36b55..00000000
--- a/tubearchivist/static/css/midnight.css
+++ /dev/null
@@ -1,14 +0,0 @@
-: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");
-}
diff --git a/tubearchivist/static/css/style.css b/tubearchivist/static/css/style.css
deleted file mode 100644
index 67eba904..00000000
--- a/tubearchivist/static/css/style.css
+++ /dev/null
@@ -1,1374 +0,0 @@
-@font-face {
-font-family: 'Sen-Bold';
- src: url('../font/Sen-Bold.woff');
- font-family: 'Sen-Bold';
-}
-
-@font-face {
-font-family: 'Sen-Regular';
- src: url('../font/Sen-Regular.woff');
- font-family: 'Sen-Regular';
-}
-
-* {
- margin: 0;
- padding: 0;
-}
-
-html {
- height: 100%;
- scrollbar-color: var(--accent-font-dark) #0000;
-}
-
-::-webkit-scrollbar {
- width: 7px;
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--accent-font-dark);
- border-radius: 10px;
-}
-
-body {
- background-color: var(--main-bg);
- min-height: 100%;
- display: grid;
- grid-template-rows: 1fr auto;
-}
-
-a {
- font-family: Sen-Regular, sans-serif;
- text-decoration: none;
- color: var(--accent-font-light);
-}
-
-h1 {
- font-family: Sen-Bold, sans-serif;
- font-size: 2.3em;
- color: var(--accent-font-light);
-}
-
-h2 {
- font-size: 1.2em;
- margin-bottom: 10px;
- font-family: Sen-Bold, sans-serif;
- color: var(--accent-font-dark);
-}
-
-h3 {
- font-size: 1.1em;
- margin-bottom: 7px;
- font-family: Sen-Regular, sans-serif;
- color: var(--accent-font-light);
-}
-
-h4 {
- font-size: 0.7em;
- margin-bottom: 7px;
- font-family: Sen-Regular, sans-serif;
- color: var(--accent-font-light);
-}
-
-p, i, li {
- font-family: Sen-Regular, sans-serif;
- margin-bottom: 10px;
- color: var(--main-font);
-}
-
-ul {
- margin-left: 20px;
-}
-
-td, th, span, label {
- font-family: Sen-Regular, sans-serif;
- color: var(--main-font);
- text-align: left;
-}
-
-select, input {
- padding: 5px;
- margin: 5px;
- border-radius: 0;
- color: var(--main-bg);
- background-color: var(--accent-font-light);
-}
-
-select {
- border: none;
-}
-
-input {
- border: solid 1px var(--main-font);
-}
-
-textarea {
- width: 100%;
-}
-
-button {
- border-radius: 0;
- padding: 5px 13px;
- border: none;
- cursor: pointer;
- background-color: var(--accent-font-dark);
- color: #ffffff;
-}
-
-button:hover {
- background-color: var(--accent-font-light);
- transform: scale(1.05);
- color: var(--main-bg);
-}
-
-.button-box {
- padding: 5px 0;
-}
-
-.unsubscribe {
- background-color: var(--accent-font-light);
-}
-
-.unsubscribe:hover {
- background-color: var(--accent-font-dark);
-}
-
-.boxed-content {
- max-width: 1000px;
- width: 80%;
- margin: 0 auto;
-}
-
-.boxed-content.boxed-4 {
- max-width: 1200px;
- width: 80%;
-}
-
-.boxed-content.boxed-5,
-.boxed-content.boxed-6,
-.boxed-content.boxed-7 {
- max-width: unset;
- width: 85%;
-}
-
-.round-img img {
- border-radius: 50%;
-}
-
-.settings-current {
- color: var(--accent-font-light);
-}
-
-.top-banner {
- background-image: var(--banner);
- background-repeat: no-repeat;
- background-size: contain;
- height: 10vh;
- min-height: 80px;
- max-height: 120px;
- background-position: center center;
-}
-
-.footer {
- margin: 0;
- padding: 20px 0;
- background-color: var(--highlight-bg);
- grid-row-start: 2;
- grid-row-end: 3;
-}
-
-.footer a {
- text-decoration: underline;
-}
-
-.footer .boxed-content {
- text-align: center;
-}
-
-/* toggle on-off */
-.toggle {
- display: flex;
- align-items: center;
-}
-
-.toggleBox > input[type="checkbox"] {
- position: relative;
- width: 70px;
- height: 30px;
- background-color: var(--accent-font-light);
- border-color: var(--accent-font-light);
- appearance: none;
- border-radius: 15px;
- transition: 0.4s;
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
- cursor: pointer;
-}
-
-.toggleBox > input:checked[type="checkbox"] {
- background-color: var(--accent-font-dark);
- border-color: var(--accent-font-dark);
-}
-
-.toggleBox > input[type="checkbox"]::before {
- z-index: 2;
- position: absolute;
- content: "";
- left: 0;
- top: 0;
- width: 30px;
- height: 30px;
- background-color: white;
- border-radius: 50%;
- transform: scale(1.1);
- transition: 0.4s;
-}
-
-.toggleBox > input:checked[type="checkbox"]::before {
- left: 40px;
-}
-
-.toggleBox {
- margin-left: 10px;
- position: relative;
- display: inline;
-}
-
-.toggleBox > label {
- position: absolute;
- color: var(--main-font);
- pointer-events: none;
-}
-
-.toggleBox > .onbtn {
- right: 70%;
- top: 45%;
- transform: translate(50%,-50%);
- font-family: Sen-Regular, sans-serif;
-
-}
-
-.toggleBox > .ofbtn {
- left: 37%;
- top: 45%;
- transform: translate(50%,-50%);
- font-family: Sen-Regular, sans-serif;
- color: var(--main-font);
-}
-
-/* delete button */
-.delete-confirm {
- display: none;
-}
-
-.delete-confirm button {
- margin: 3px 0;
-}
-
-.danger-button {
- background-color: var(--highlight-error);
-}
-
-.danger-button:hover {
- background-color: var(--highlight-error-light);
-}
-
-/* navigation */
-.top-nav {
- display: block;
- padding: 5px 0;
- position: relative;
-}
-
-.nav-items {
- width: 100%;
- display: flex;
- justify-content: center;
-}
-
-.nav-item {
- font-size: 1.3em;
- padding: 10px 20px;
- margin: 0 10px;
- border-bottom: 2px solid;
- color: var(--accent-font-dark);
-}
-
-.nav-icons {
- width: auto;
- display: inline-flex;
- position: absolute;
- top: 50%;
- right: 0;
- transform: translate(0,-50%);
-}
-
-.nav-icons img {
- width: 40px;
- padding: 0 5px;
- filter: var(--img-filter);
-}
-
-#castbutton {
- float: right;
- width: 40px;
- padding: 0 5px;
- --disconnected-color: var(--accent-font-dark);
- --connected-color: var(--accent-font-light);
-}
-
-.alert-hover:hover {
- filter: var(--img-filter-error);
-}
-
-/* top of page */
-.title-bar {
- padding-top: 30px;
-}
-
-.sort {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.padding-box {
- padding: 30px 0;
-}
-
-.two-col {
- display: flex;
-}
-
-.two-col > div {
- width: 50%;
-}
-
-.view-controls {
- display: grid;
- grid-template-columns: 1fr auto auto;
- border-bottom: 2px solid;
- border-color: var(--accent-font-dark);
- margin: 15px 0;
-}
-
-.view-icons,
-.grid-count {
- display: flex;
- justify-content: end;
- align-items: center;
-}
-
-.view-icons img {
- width: 30px;
- margin: 5px 10px;
- cursor: pointer;
- filter: var(--img-filter);
-}
-
-.grid-count img {
- width: 15px;
- margin: 5px;
- cursor: pointer;
- filter: var(--img-filter);
-}
-
-.video-popup-menu {
- border-top: 2px solid;
- border-color: var(--accent-font-dark);
- margin: 5px 0;
- padding-top: 10px;
-}
-
-#hidden-form {
- display: none;
-}
-
-#hidden-form2 {
- display: none;
- margin-top: 10px;
-}
-
-#hidden-form button, #hidden-form2 button {
- margin-right: 1rem;
-}
-
-#text-reveal {
- height: 0;
- overflow: hidden;
-}
-
-#text-expand {
- overflow: hidden;
- display: -webkit-inline-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 4;
-}
-
-
-/* video player */
-.player-wrapper {
- background-color: var(--highlight-bg);
- margin: 20px 0;
-}
-
-.video-player {
- display: grid;
- align-content: space-evenly;
- height: 100vh;
- position: relative; /* needed for modal */
-}
-
-#notifications {
- position: relative;
-}
-
-.notifications {
- text-align: center;
- width: 80%;
- margin: auto;
-}
-
-.sponsorblock {
- text-align: center;
- width: 80%;
- margin: auto;
-}
-
-.video-player video,
-.video-main video {
- max-height: 80vh;
- width: 90%;
- max-width: 1500px;
- margin: 0 auto;
- display: block;
-}
-
-.player-title img {
- width: 30px;
- margin: 10px 10px 10px 0;
-}
-
-/* fix for safari full screen not scaling full */
-video:-webkit-full-screen {
- max-height: unset !important;
- max-width: unset !important;
-}
-
-/* video list */
-.video-list {
- display: grid;
- grid-gap: 1rem;
- margin-top: 1rem;
-}
-
-.video-list.grid.grid-3 {
- grid-template-columns: 1fr 1fr 1fr;
-}
-
-.video-list.grid.grid-4 {
- grid-template-columns: 1fr 1fr 1fr 1fr;
-}
-
-.video-list.grid.grid-5 {
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
-}
-
-.video-list.grid.grid-6 {
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
-}
-
-.video-list.grid.grid-7 {
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
-}
-
-.video-list.list {
- grid-template-columns: unset;
-}
-
-.video-item {
- overflow: hidden;
-}
-
-.video-item:hover .video-tags {
- opacity: 1;
-}
-
-.video-item.list {
- display: grid;
- grid-template-columns: 26% auto;
- background-color: var(--highlight-bg);
- align-items: center;
-}
-
-.video-progress-bar,
-.notification-progress-bar {
- position: absolute;
- background-color: var(--accent-font-dark);
- height: 7px;
- left: 0;
- bottom: 3px;
-}
-
-.video-thumb img {
- width: 100%;
- position: relative;
-}
-
-.video-tags {
- position: absolute;
- top: 5px;
- left: 0;
- padding: 5px;
- opacity: 0;
- transition: 300ms ease-in-out;
-}
-
-.video-tags span {
- background-color: var(--accent-font-light);
- padding: 5px;
-}
-
-.video-play img {
- width: 40px;
- filter: var(--img-filter);
-}
-
-.video-thumb-wrap {
- position: relative;
-}
-
-.video-thumb-wrap:hover > .video-play {
- opacity: 1;
- padding: 15px;
-}
-
-.video-play {
- opacity: 0;
- transition: all 0.3s ease-in-out;
- position: absolute;
- top: 50%;
- right: 50%;
- transform: translate(50%,-50%);
- background-color: var(--highlight-bg);
- border-radius: 50%;
- padding: 8px;
-}
-
-.video-desc.grid {
- padding: 10px;
- height: 100%;
- background-color: var(--highlight-bg);
-}
-
-.video-desc.list {
- padding: 10px;
- height: 100%;
- display: flex;
- flex-wrap: wrap;
- align-content: center;
-}
-
-.video-desc > div {
- width: 100%;
-}
-
-.video-desc img {
- width: 20px;
- margin-right: 10px;
-}
-
-.video-popup-menu img.move-video-button {
- width: 24px;
- cursor: pointer;
- filter: var(--img-filter);
-}
-
-.video-desc a {
- text-decoration: none;
- text-align: left;
-}
-
-.video-desc h3,
-.player-title h3 {
- font-size: 0.9em;
- text-transform: uppercase;
-}
-
-.player-stats {
- float: right;
- display: flex;
- align-items: center;
- margin-top: 10px;
-}
-
-.player-stats span {
- margin: 0 5px;
-}
-
-.video-desc-player {
- margin-bottom: 8px;
- display: flex;
- align-items: center;
-}
-
-.video-desc-details {
- display: flex;
- justify-content: space-between;
-}
-
-.watch-button,
-.dot-button,
-.close-button {
- cursor: pointer;
- filter: var(--img-filter);
-}
-
-.video-more {
- text-decoration: underline;
- text-align: right;
-}
-
-
-/* pagination */
-.pagination {
- padding: 30px 0;
- margin-left: auto;
- margin-right: auto;
- text-align: center;
-}
-
-.pagination-item {
- padding: 5px;
- border: 1px solid;
-}
-
-
-/* info box */
-.title-split {
- display: grid;
- grid-template-columns: 1fr 1fr;
-}
-
-.title-split img {
- width: 40px;
- filter: var(--img-filter);
- cursor: pointer;
- margin: 0 10px;
-}
-
-.title-split-form {
- padding-top: 30px;
- display: flex;
-}
-
-.info-box {
- display: grid;
- grid-gap: 1rem;
- margin-top: 1rem;
-}
-
-.description-box,
-.comments-section {
- margin-top: 1rem;
- padding: 15px;
- background-color: var(--highlight-bg);
-}
-
-.info-box-4 {
- grid-template-columns: 1fr 1fr 1fr 1fr;
-}
-
-.info-box-3 {
- grid-template-columns: 1fr 1fr 1fr;
-}
-
-.info-box-2 {
- grid-template-columns: 1fr 1fr;
-}
-
-.info-box-1 {
- grid-template-columns: 1fr;
-}
-
-.info-box img {
- width: 80px;
- margin: 0 15px;
-}
-
-.info-box-item {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- padding: 15px;
- background-color: var(--highlight-bg);
-}
-
-.info-box-item p {
- width: 100%;
-}
-
-.video-popup-menu img {
- width: 12px;
- cursor: pointer;
- filter: var(--img-filter);
-}
-
-
-.video-popup-menu-close-button {
- cursor: pointer;
- filter: var(--img-filter);
- float:right;
-}
-
-.description-text {
- width: 100%;
-}
-
-.description-text br {
- margin-bottom: 10px;
-}
-
-.overwrite-form {
- display: grid;
- grid-template-columns: 1fr 1fr;
- width: 100%;
-}
-
-.overwrite-form button {
- width: 200px;
-}
-
-.overwrite-form-item {
- margin-bottom: 1rem;
-}
-
-.overwrite-form-item input {
- width: 90%;
-}
-
-.hidden-overwrite {
- display: none;
-}
-
-/* login */
-.login-page {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- text-align: center;
- align-content: center;
-}
-
-.login-page > * {
- width: 100%;
-}
-
-.login-page img {
- width: 100%;
- max-width: 200px;
- max-height: 200px;
- margin-bottom: 40px;
- content: var(--logo);
-}
-
-.login-page form {
- margin: 30px 0;
-}
-
-.login-page input {
- min-width: 200px;
-}
-
-#id_remember_me {
- min-width: unset;
-}
-
-.login-page button,
-.login-page .danger-zone {
- width: 210px;
- margin-top: 5px;
-}
-
-.login-links a {
- text-decoration: underline;
- margin: 30px 0;
- padding: 20px;
-}
-
-.footer-colors {
- grid-row-start: 2;
- grid-row-end: 3;
- display: flex;
-}
-
-.footer-colors div {
- padding: 20px 0;
- width: 33.33%;
-}
-
-.col-1 {
- background-color: var(--highlight-bg);
-}
-
-.col-2 {
- background-color: var(--accent-font-dark);
-}
-
-.col-3 {
- background-color: var(--accent-font-light);
-}
-
-/* video page */
-.video-main {
- margin: 1rem 0;
- position: relative; /* needed for modal */
-}
-
-.video-modal {
- position: absolute;
- z-index: 1;
- top: 20%;
- width: 100%;
- text-align: center;
-}
-
-.video-modal-text {
- background: rgba(0,0,0,.5);
- color: #eeeeee;
- font-size: 1.3em;
- display: none;
-}
-
-.video-main video {
- max-height: 70vh;
- margin-bottom: 1rem;
-}
-
-.video-info-watched {
- display: flex;
- align-items: center;
-}
-
-.video-info-watched img {
- width: 20px;
- margin-left: 5px;
-}
-
-.thumb-icon {
- display: flex;
-}
-
-.video-tag-box {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
-}
-
-.video-tag {
- padding: 5px 10px;
- margin: 5px;
- border: 1px solid var(--accent-font-light);
-}
-
-.thumb-icon img,
-.rating-stars img {
- width: 20px;
- margin: 0 5px;
- filter: var(--img-filter);
-}
-
-.dislike {
- transform: rotate(180deg);
-}
-
-.playlist-wrap {
- background-color: var(--highlight-bg);
- margin: 1rem 0;
- padding: 1rem;
-}
-
-.playlist-wrap > a > h3 {
- text-align: center;
-}
-
-.playlist-nav {
- display: grid;
- grid-template-columns: 1fr 1fr;
- margin-bottom: 10px;
-}
-
-.playlist-nav-item {
- display: flex;
- justify-content: space-between;
-}
-
-.playlist-nav-item img {
- width: 200px;
-}
-
-.playlist-desc {
- padding: 5px;
- width: 100%;
-}
-
-.comment-box {
- padding-bottom: 1rem;
- overflow: hidden;
-}
-
-.comment-box h3 {
- line-break: anywhere;
-}
-
-.comments-replies {
- display: none;
- padding-left: 1rem;
- border-left: 1px solid var(--accent-font-light);
- margin-top: 1rem;
-}
-
-.comment-highlight {
- background-color: var(--main-font);
- padding: 3px;
- color: var(--accent-font-dark);
- font-family: Sen-bold, sans-serif;
- width: fit-content;
-}
-
-.comment-meta {
- display: flex;
-}
-
-.space-carrot {
- margin: 0 5px;
-}
-
-.comment-like img {
- width: 20px;
- margin-left: 5px;
- filter: var(--img-filter-error);
-}
-
-/* multi search page */
-.multi-search-box {
- padding-right: 20px;
-}
-
-.multi-search-box input {
- width: 100%;
-}
-
-.multi-search-result, #multi-search-results-placeholder {
- padding: 1rem 0;
-}
-
-#multi-search-results-placeholder span {
- font-family: monospace;
- color: var(--accent-font-dark);
- background-color: var(--highlight-bg);
-}
-
-#multi-search-results-placeholder span.value {
- color: var(--accent-font-light);
-}
-
-#multi-search-results-placeholder ul {
- margin-top: 10px;
-}
-
-/* channel overview page */
-.channel-list.list {
- display: block;
-}
-
-.channel-list.grid {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 1rem;
-}
-
-.channel-item.list {
- padding-bottom: 1rem;
-}
-
-.channel-item.grid > .info-box {
- display: block;
-}
-
-.channel-banner img {
- margin-top: 1rem;
- width: 100%;
-}
-
-.channel-banner.grid {
- overflow: hidden;
-}
-
-.channel-banner.list img {
- width: 100%;
-}
-
-.channel-banner.grid img {
- width: 250%;
- transform: translateX(-30%);
-}
-
-.info-box-item.child-page-nav {
- justify-content: center;
-}
-
-.info-box-item.child-page-nav a {
- padding: 0 1rem;
-}
-
-.info-box-item.child-page-nav a:hover {
- text-decoration: underline;
-}
-
-/* playlist overview page */
-.playlist-list.list {
- display: grid;
- grid-template-columns: unset;
- gap: 1rem;
-}
-
-.playlist-list.grid {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 1rem;
-}
-
-.playlist-item {
- overflow: hidden;
-}
-
-.playlist-item.list {
- display: flex;
-}
-
-.playlist-thumbnail img {
- width: 100%;
-}
-
-.playlist-desc.grid {
- padding: 10px;
- height: 100%;
- background-color: var(--highlight-bg);
-}
-
-.playlist-desc.list {
- width: 100%;
- padding: 10px;
- height: unset;
- background-color: var(--highlight-bg);
- display: flex;
- flex-wrap: wrap;
- align-content: center;
-}
-
-.playlist-desc.list > a,
-.playlist-desc.list > p {
- width: 100%;
-}
-
-/* download page */
-.icon-text {
- background-color: var(--highlight-bg);
- text-align: center;
- padding: 15px;
-}
-
-.icon-text img {
- filter: var(--img-filter);
- cursor: pointer;
-}
-
-.task-control-icons {
- display: flex;
- justify-content: center;
-}
-
-.task-control-icons img {
- width: 30px;
- cursor: pointer;
- margin: 5px;
-}
-
-#stop-icon {
- filter: var(--img-filter);
-}
-
-#kill-icon {
- filter: var(--img-filter-error);
-}
-
-.title-split {
- display: flex;
- justify-content: space-between;
-}
-
-/* status message */
-.notification {
- position: relative;
- background-color: var(--highlight-bg);
- text-align: center;
- padding: 30px 0 15px 0;
- margin: 1rem 0;
-}
-
-.notification.info {
- background-color: var(--highlight-bg);
-}
-
-.notification.error {
- background-color: var(--highlight-error);
-}
-
-.notification.error h3 {
- color: #fff;
-}
-
-/* settings */
-.settings-group {
- background-color: var(--highlight-bg);
- padding: 20px;
- margin: 20px 0;
-}
-
-.settings-item {
- margin-top: 25px;
-}
-
-.settings-item input {
- min-width: 300px;
-}
-
-.settings-item .agg-channel-table {
- width: 100%;
-}
-
-.settings-item .agg-channel-right-align {
- white-space: nowrap;
- text-align: right;
-}
-
-.danger-zone {
- background-color: var(--highlight-error);
- color: #fff;
- padding: 3px;
-}
-
-.backup-grid-row {
- display: grid;
- grid-template-columns: 10% 10% 10% auto;
- align-items: center;
- padding: 5px 10px;
- border-bottom: solid 1px;
- border-color: var(--main-font);
-}
-
-.backup-grid-row > span {
- margin-left: 10px;
-}
-
-/* about */
-.about-section {
- padding: 20px 0;
-}
-
-.about-section ol {
- margin-left: 20px;
-}
-
-.about-section ul {
- margin-top: 15px;
-}
-
-.about-section li {
- margin-bottom: 10px;
-}
-
-.about-icon img {
- margin-left: 5px;
- width: 20px;
- cursor: unset;
-}
-
-/* animation */
-.rotate-img {
- animation: rotation 4s infinite linear;
-}
-
-.bounce-img {
- animation: bounce 1.5s infinite ease-in-out alternate;
-}
-
-.pulse-img {
- animation: pulse 1.5s infinite ease-in-out alternate;
-}
-
-@keyframes rotation {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(359deg);
- }
-}
-
-@keyframes bounce {
- 0% {
- transform: translateY(-5%);
- }
- 100% {
- transform: translateY(5%);
- scale: 1.15;
- }
-}
-
-@keyframes pulse {
- 0% {
- scale: 1;
- }
- 100% {
- scale: 1.15;
- }
-}
-
-/* tablet */
-@media screen and (max-width: 1000px), screen and (max-height: 850px) {
- .boxed-content,
- .boxed-content.boxed-4,
- .boxed-content.boxed-5,
- .boxed-content.boxed-6,
- .boxed-content.boxed-7 {
- width: 90%;
- }
- .video-list.grid.grid-3,
- .video-list.grid.grid-4,
- .video-list.grid.grid-5,
- .video-list.grid.grid-6,
- .video-list.grid.grid-7,
- .channel-list.grid,
- .playlist-list.grid {
- grid-template-columns: 1fr 1fr;
- }
- .video-item.list,
- .playlist-item.list {
- display: grid;
- grid-template-columns: 35% auto;
- }
- .two-col {
- display: block;
- }
- .two-col > div {
- width: 100%;
- }
- .top-nav {
- flex-wrap: wrap-reverse;
- display: flex;
- }
- .nav-icons {
- width: 100%;
- justify-content: center;
- position: unset;
- transform: unset;
- }
- .grid-count {
- display: none;
- }
- .video-player {
- height: unset;
- padding: 20px 0
- }
- .video-player video {
- width: 90%;
- }
-}
-
-/* phone */
-@media screen and (max-width: 600px) {
- * {
- word-wrap: anywhere;
- }
- .video-list.grid.grid-3,
- .video-list.grid.grid-4,
- .video-list.grid.grid-5,
- .video-list.grid.grid-6,
- .video-list.grid.grid-7,
- .channel-list.grid,
- .video-item.list,
- .playlist-list.list,
- .playlist-list.grid,
- .info-box-2,
- .info-box-3,
- .info-box-4,
- .overwrite-form {
- grid-template-columns: 1fr;
- }
- .playlist-item.list {
- display: block;
- }
- .video-desc.grid {
- height: unset;
- display: flex;
- flex-wrap: wrap-reverse;
- }
- .boxed-content {
- width: 95%;
- }
- .footer {
- text-align: center;
- }
- .footer .boxed-content span {
- width: 100%;
- display: block;
- }
- .toggle {
- flex-wrap: wrap;
- }
- .nav-items {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
- .nav-item {
- padding: 5px 0;
- margin: 15px;
- text-align: center;
- }
- .view-controls.three {
- grid-template-columns: unset;
- justify-content: center;
- }
- .sort {
- display: block;
- }
- .sort select {
- margin: unset;
- }
- .description-box {
- display: block;
- }
- .backup-grid-row {
- display: flex;
- flex-wrap: wrap;
- padding: 10px 0;
- justify-content: center;
- }
- .backup-grid-row span {
- padding: 5px 0;
- }
- .playlist-nav {
- display: block;
- grid-template-columns: unset;
- }
- .playlist-nav-item {
- display: block;
- justify-content: unset;
- }
- .playlist-nav-item img {
- width: 100%;
- }
- .td, th, span, label {
- text-align: unset;
- }
-}
diff --git a/tubearchivist/static/favicon/android-chrome-192x192.png b/tubearchivist/static/favicon/android-chrome-192x192.png
deleted file mode 100644
index ef0ebfe3..00000000
Binary files a/tubearchivist/static/favicon/android-chrome-192x192.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/android-chrome-512x512.png b/tubearchivist/static/favicon/android-chrome-512x512.png
deleted file mode 100644
index 4d8b11eb..00000000
Binary files a/tubearchivist/static/favicon/android-chrome-512x512.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-114x114-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-114x114-precomposed.png
deleted file mode 100644
index 04090832..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-114x114-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-114x114.png b/tubearchivist/static/favicon/apple-touch-icon-114x114.png
deleted file mode 100644
index d4ff60af..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-114x114.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-120x120-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-120x120-precomposed.png
deleted file mode 100644
index 249857c0..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-120x120-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-120x120.png b/tubearchivist/static/favicon/apple-touch-icon-120x120.png
deleted file mode 100644
index 569d7c5a..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-120x120.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-144x144-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-144x144-precomposed.png
deleted file mode 100644
index 8d0571d3..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-144x144-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-144x144.png b/tubearchivist/static/favicon/apple-touch-icon-144x144.png
deleted file mode 100644
index 50d0937f..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-144x144.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-152x152-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-152x152-precomposed.png
deleted file mode 100644
index 5ac04bdf..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-152x152-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-152x152.png b/tubearchivist/static/favicon/apple-touch-icon-152x152.png
deleted file mode 100644
index 0692048a..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-152x152.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-180x180-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-180x180-precomposed.png
deleted file mode 100644
index cbf3158e..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-180x180-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-180x180.png b/tubearchivist/static/favicon/apple-touch-icon-180x180.png
deleted file mode 100644
index d7158782..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-180x180.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-57x57-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-57x57-precomposed.png
deleted file mode 100644
index 5785c2d4..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-57x57-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-57x57.png b/tubearchivist/static/favicon/apple-touch-icon-57x57.png
deleted file mode 100644
index 0b7823f3..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-57x57.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-60x60-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-60x60-precomposed.png
deleted file mode 100644
index a85f028b..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-60x60-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-60x60.png b/tubearchivist/static/favicon/apple-touch-icon-60x60.png
deleted file mode 100644
index 6ed808ea..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-60x60.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-72x72-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-72x72-precomposed.png
deleted file mode 100644
index 594c277e..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-72x72-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-72x72.png b/tubearchivist/static/favicon/apple-touch-icon-72x72.png
deleted file mode 100644
index bc1ae8ec..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-72x72.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-76x76-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-76x76-precomposed.png
deleted file mode 100644
index 631de948..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-76x76-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-76x76.png b/tubearchivist/static/favicon/apple-touch-icon-76x76.png
deleted file mode 100644
index 9c00af94..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-76x76.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon-precomposed.png b/tubearchivist/static/favicon/apple-touch-icon-precomposed.png
deleted file mode 100644
index cbf3158e..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon-precomposed.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/apple-touch-icon.png b/tubearchivist/static/favicon/apple-touch-icon.png
deleted file mode 100644
index d7158782..00000000
Binary files a/tubearchivist/static/favicon/apple-touch-icon.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/browserconfig.xml b/tubearchivist/static/favicon/browserconfig.xml
deleted file mode 100644
index e465c1bc..00000000
--- a/tubearchivist/static/favicon/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- #01202e
-
-
-
diff --git a/tubearchivist/static/favicon/favicon-16x16.png b/tubearchivist/static/favicon/favicon-16x16.png
deleted file mode 100644
index 20362724..00000000
Binary files a/tubearchivist/static/favicon/favicon-16x16.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/favicon-32x32.png b/tubearchivist/static/favicon/favicon-32x32.png
deleted file mode 100644
index 678a2ff8..00000000
Binary files a/tubearchivist/static/favicon/favicon-32x32.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/favicon.ico b/tubearchivist/static/favicon/favicon.ico
deleted file mode 100644
index 0b639a44..00000000
Binary files a/tubearchivist/static/favicon/favicon.ico and /dev/null differ
diff --git a/tubearchivist/static/favicon/mstile-150x150.png b/tubearchivist/static/favicon/mstile-150x150.png
deleted file mode 100644
index 24a0b256..00000000
Binary files a/tubearchivist/static/favicon/mstile-150x150.png and /dev/null differ
diff --git a/tubearchivist/static/favicon/safari-pinned-tab.svg b/tubearchivist/static/favicon/safari-pinned-tab.svg
deleted file mode 100644
index f9f2ea03..00000000
--- a/tubearchivist/static/favicon/safari-pinned-tab.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-Created by potrace 1.14, written by Peter Selinger 2001-2017
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/favicon/site.webmanifest b/tubearchivist/static/favicon/site.webmanifest
deleted file mode 100644
index f538ba53..00000000
--- a/tubearchivist/static/favicon/site.webmanifest
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "TubeArchivist",
- "short_name": "TubeArchivist",
- "icons": [
- {
- "src": "/static/favicon/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/static/favicon/android-chrome-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "theme_color": "#01202e",
- "background_color": "#01202e",
- "display": "standalone"
-}
diff --git a/tubearchivist/static/font/OFL_License.txt b/tubearchivist/static/font/OFL_License.txt
deleted file mode 100644
index 2fde9c97..00000000
--- a/tubearchivist/static/font/OFL_License.txt
+++ /dev/null
@@ -1,94 +0,0 @@
-Copyright (c) 2015, Kosal Sen, Philatype (),
-with Reserved Font Name Sen.
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/tubearchivist/static/font/Sen-Bold.woff b/tubearchivist/static/font/Sen-Bold.woff
deleted file mode 100644
index fb6c66f5..00000000
Binary files a/tubearchivist/static/font/Sen-Bold.woff and /dev/null differ
diff --git a/tubearchivist/static/font/Sen-Regular.woff b/tubearchivist/static/font/Sen-Regular.woff
deleted file mode 100644
index eb7471e0..00000000
Binary files a/tubearchivist/static/font/Sen-Regular.woff and /dev/null differ
diff --git a/tubearchivist/static/img/banner-tube-archivist-dark.png b/tubearchivist/static/img/banner-tube-archivist-dark.png
deleted file mode 100644
index ad2cd5ed..00000000
Binary files a/tubearchivist/static/img/banner-tube-archivist-dark.png and /dev/null differ
diff --git a/tubearchivist/static/img/banner-tube-archivist-light.png b/tubearchivist/static/img/banner-tube-archivist-light.png
deleted file mode 100644
index 69f70277..00000000
Binary files a/tubearchivist/static/img/banner-tube-archivist-light.png and /dev/null differ
diff --git a/tubearchivist/static/img/default-channel-art.jpg b/tubearchivist/static/img/default-channel-art.jpg
deleted file mode 100644
index 45a4b000..00000000
Binary files a/tubearchivist/static/img/default-channel-art.jpg and /dev/null differ
diff --git a/tubearchivist/static/img/default-channel-banner.jpg b/tubearchivist/static/img/default-channel-banner.jpg
deleted file mode 100644
index 578db6bc..00000000
Binary files a/tubearchivist/static/img/default-channel-banner.jpg and /dev/null differ
diff --git a/tubearchivist/static/img/default-channel-icon.jpg b/tubearchivist/static/img/default-channel-icon.jpg
deleted file mode 100644
index 33577a27..00000000
Binary files a/tubearchivist/static/img/default-channel-icon.jpg and /dev/null differ
diff --git a/tubearchivist/static/img/default-playlist-thumb.jpg b/tubearchivist/static/img/default-playlist-thumb.jpg
deleted file mode 100644
index 6bbaa227..00000000
Binary files a/tubearchivist/static/img/default-playlist-thumb.jpg and /dev/null differ
diff --git a/tubearchivist/static/img/default-video-thumb.jpg b/tubearchivist/static/img/default-video-thumb.jpg
deleted file mode 100644
index 2325bc9c..00000000
Binary files a/tubearchivist/static/img/default-video-thumb.jpg and /dev/null differ
diff --git a/tubearchivist/static/img/icon-add.svg b/tubearchivist/static/img/icon-add.svg
deleted file mode 100644
index 5c7be5fe..00000000
--- a/tubearchivist/static/img/icon-add.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-arrow-bottom.svg b/tubearchivist/static/img/icon-arrow-bottom.svg
deleted file mode 100644
index e829250d..00000000
--- a/tubearchivist/static/img/icon-arrow-bottom.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-arrow-down.svg b/tubearchivist/static/img/icon-arrow-down.svg
deleted file mode 100644
index 0d7adba6..00000000
--- a/tubearchivist/static/img/icon-arrow-down.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/static/img/icon-arrow-top.svg b/tubearchivist/static/img/icon-arrow-top.svg
deleted file mode 100644
index bb4b80ca..00000000
--- a/tubearchivist/static/img/icon-arrow-top.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/static/img/icon-arrow-up.svg b/tubearchivist/static/img/icon-arrow-up.svg
deleted file mode 100644
index 71c96522..00000000
--- a/tubearchivist/static/img/icon-arrow-up.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/static/img/icon-close.svg b/tubearchivist/static/img/icon-close.svg
deleted file mode 100644
index 63bf8d40..00000000
--- a/tubearchivist/static/img/icon-close.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-dot-menu.svg b/tubearchivist/static/img/icon-dot-menu.svg
deleted file mode 100644
index 9aa4411b..00000000
--- a/tubearchivist/static/img/icon-dot-menu.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/static/img/icon-download.svg b/tubearchivist/static/img/icon-download.svg
deleted file mode 100644
index e829250d..00000000
--- a/tubearchivist/static/img/icon-download.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-exit.svg b/tubearchivist/static/img/icon-exit.svg
deleted file mode 100644
index 3aff8e70..00000000
--- a/tubearchivist/static/img/icon-exit.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-eye.svg b/tubearchivist/static/img/icon-eye.svg
deleted file mode 100644
index 92ba61c8..00000000
--- a/tubearchivist/static/img/icon-eye.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-gear.svg b/tubearchivist/static/img/icon-gear.svg
deleted file mode 100644
index 5d8e9f01..00000000
--- a/tubearchivist/static/img/icon-gear.svg
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-gridview.svg b/tubearchivist/static/img/icon-gridview.svg
deleted file mode 100644
index 570696bc..00000000
--- a/tubearchivist/static/img/icon-gridview.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-heart.svg b/tubearchivist/static/img/icon-heart.svg
deleted file mode 100644
index 8a26e163..00000000
--- a/tubearchivist/static/img/icon-heart.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-listview.svg b/tubearchivist/static/img/icon-listview.svg
deleted file mode 100644
index d56b8dfe..00000000
--- a/tubearchivist/static/img/icon-listview.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-play.svg b/tubearchivist/static/img/icon-play.svg
deleted file mode 100644
index 3992440a..00000000
--- a/tubearchivist/static/img/icon-play.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-remove.svg b/tubearchivist/static/img/icon-remove.svg
deleted file mode 100644
index f73c5a61..00000000
--- a/tubearchivist/static/img/icon-remove.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/tubearchivist/static/img/icon-rescan.svg b/tubearchivist/static/img/icon-rescan.svg
deleted file mode 100644
index f8047ef6..00000000
--- a/tubearchivist/static/img/icon-rescan.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-search.svg b/tubearchivist/static/img/icon-search.svg
deleted file mode 100644
index 8a38b1cc..00000000
--- a/tubearchivist/static/img/icon-search.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-seen.svg b/tubearchivist/static/img/icon-seen.svg
deleted file mode 100644
index 153a7b55..00000000
--- a/tubearchivist/static/img/icon-seen.svg
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-sort.svg b/tubearchivist/static/img/icon-sort.svg
deleted file mode 100644
index ee257a87..00000000
--- a/tubearchivist/static/img/icon-sort.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-star-empty.svg b/tubearchivist/static/img/icon-star-empty.svg
deleted file mode 100644
index 8246a230..00000000
--- a/tubearchivist/static/img/icon-star-empty.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-star-full.svg b/tubearchivist/static/img/icon-star-full.svg
deleted file mode 100644
index 82761e11..00000000
--- a/tubearchivist/static/img/icon-star-full.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-star-half.svg b/tubearchivist/static/img/icon-star-half.svg
deleted file mode 100644
index 73de5e98..00000000
--- a/tubearchivist/static/img/icon-star-half.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-stop.svg b/tubearchivist/static/img/icon-stop.svg
deleted file mode 100644
index b806c20f..00000000
--- a/tubearchivist/static/img/icon-stop.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-substract.svg b/tubearchivist/static/img/icon-substract.svg
deleted file mode 100644
index 1571fdda..00000000
--- a/tubearchivist/static/img/icon-substract.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-thumb.svg b/tubearchivist/static/img/icon-thumb.svg
deleted file mode 100644
index a31320e9..00000000
--- a/tubearchivist/static/img/icon-thumb.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/icon-unseen.svg b/tubearchivist/static/img/icon-unseen.svg
deleted file mode 100644
index 43b96960..00000000
--- a/tubearchivist/static/img/icon-unseen.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
diff --git a/tubearchivist/static/img/logo-tube-archivist-dark.png b/tubearchivist/static/img/logo-tube-archivist-dark.png
deleted file mode 100644
index 08e51852..00000000
Binary files a/tubearchivist/static/img/logo-tube-archivist-dark.png and /dev/null differ
diff --git a/tubearchivist/static/img/logo-tube-archivist-light.png b/tubearchivist/static/img/logo-tube-archivist-light.png
deleted file mode 100644
index 61b0672a..00000000
Binary files a/tubearchivist/static/img/logo-tube-archivist-light.png and /dev/null differ
diff --git a/tubearchivist/static/progress.js b/tubearchivist/static/progress.js
deleted file mode 100644
index 25de8c76..00000000
--- a/tubearchivist/static/progress.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Handle multi channel notifications
- *
- */
-
-'use strict';
-
-/* globals apiRequest animate */
-
-checkMessages();
-
-// start to look for messages
-function checkMessages() {
- let notifications = document.getElementById('notifications');
- if (notifications && notifications.childNodes.length === 0) {
- let dataOrigin = notifications.getAttribute('data');
- getMessages(dataOrigin);
- }
-}
-
-function getMessages(dataOrigin) {
- let apiEndpoint = '/api/notification/';
- let responseData = apiRequest(apiEndpoint, 'GET');
- let messages = buildMessage(responseData, dataOrigin);
- if (messages.length > 0) {
- // restart itself
- setTimeout(() => getMessages(dataOrigin), 500);
- }
-}
-
-function buildMessage(responseData, dataOrigin) {
- // filter relevant messages
- let messages;
- if (dataOrigin) {
- messages = responseData.filter(function (value) {
- return dataOrigin.split(' ').includes(value.group.split(':')[0]);
- }, dataOrigin);
- } else {
- messages = responseData;
- }
-
- let notifications = document.getElementById('notifications');
- let currentNotifications = notifications.childElementCount;
-
- for (let i = 0; i < messages.length; i++) {
- const messageData = messages[i];
- if (!document.getElementById(messageData.id)) {
- let messageBox = buildPlainBox(messageData);
- notifications.appendChild(messageBox);
- }
- updateMessageBox(messageData);
- if (messageData.group.startsWith('download:')) {
- animateIcons(messageData.group);
- }
- }
- clearNotifications(responseData);
- if (currentNotifications > 0 && messages.length === 0) {
- location.replace(location.href);
- }
- return messages;
-}
-
-function buildPlainBox(messageData) {
- let messageBox = document.createElement('div');
- messageBox.classList.add(messageData.level, 'notification');
- messageBox.id = messageData.id;
- messageBox.innerHTML = `
-
-
-
-
`;
- return messageBox;
-}
-
-function updateMessageBox(messageData) {
- let messageBox = document.getElementById(messageData.id);
- let children = messageBox.children;
- children[0].textContent = messageData.title;
- children[1].innerHTML = messageData.messages.join(' ');
- if (
- !messageBox.querySelector('#stop-icon') &&
- messageData['api_stop'] &&
- messageData.command !== 'STOP'
- ) {
- children[2].appendChild(buildStopIcon(messageData.id));
- }
- if (messageData.progress) {
- children[3].style.width = `${messageData.progress * 100 || 0}%`;
- }
-}
-
-function animateIcons(group) {
- let rescanIcon = document.getElementById('rescan-icon');
- let dlIcon = document.getElementById('download-icon');
- switch (group) {
- case 'download:scan':
- if (rescanIcon && !rescanIcon.classList.contains('rotate-img')) {
- animate('rescan-icon', 'rotate-img');
- }
- break;
-
- case 'download:run':
- if (dlIcon && !dlIcon.classList.contains('bounce-img')) {
- animate('download-icon', 'bounce-img');
- }
- break;
-
- default:
- break;
- }
-}
-
-function buildStopIcon(taskId) {
- let stopIcon = document.createElement('img');
- stopIcon.setAttribute('id', 'stop-icon');
- stopIcon.setAttribute('data', taskId);
- stopIcon.setAttribute('title', 'Stop Task');
- stopIcon.setAttribute('src', '/static/img/icon-stop.svg');
- stopIcon.setAttribute('alt', 'stop icon');
- stopIcon.setAttribute('onclick', 'stopTask(this)');
- return stopIcon;
-}
-
-function buildKillIcon(taskId) {
- let killIcon = document.createElement('img');
- killIcon.setAttribute('id', 'kill-icon');
- killIcon.setAttribute('data', taskId);
- killIcon.setAttribute('title', 'Kill Task');
- killIcon.setAttribute('src', '/static/img/icon-close.svg');
- killIcon.setAttribute('alt', 'kill icon');
- killIcon.setAttribute('onclick', 'killTask(this)');
- return killIcon;
-}
-
-function clearNotifications(responseData) {
- let allIds = Array.from(responseData, x => x.id);
- let allBoxes = document.getElementsByClassName('notification');
- for (let i = 0; i < allBoxes.length; i++) {
- const notificationBox = allBoxes[i];
- if (!allIds.includes(notificationBox.id)) {
- notificationBox.remove();
- }
- }
-}
diff --git a/tubearchivist/static/script.js b/tubearchivist/static/script.js
deleted file mode 100644
index 2ce1f6f7..00000000
--- a/tubearchivist/static/script.js
+++ /dev/null
@@ -1,1774 +0,0 @@
-'use strict';
-
-/* globals checkMessages */
-
-function sortChange(button) {
- let apiEndpoint = '/api/config/user/';
- let data = {};
- data[button.name] = button.value;
- apiRequest(apiEndpoint, 'POST', data);
- setTimeout(function () {
- location.reload();
- }, 500);
-}
-
-// Updates video watch status when passed a video id and it's current state (ex if the video was unwatched but you want to mark it as watched you will pass "unwatched")
-function updateVideoWatchStatus(input1, videoCurrentWatchStatus) {
- let videoId;
- if (videoCurrentWatchStatus) {
- videoId = input1;
- } else if (input1.getAttribute('data-id')) {
- videoId = input1.getAttribute('data-id');
- videoCurrentWatchStatus = input1.getAttribute('data-status');
- }
-
- postVideoProgress(videoId, 0); // Reset video progress on watched/unwatched;
- removeProgressBar(videoId);
-
- let watchStatusIndicator;
- let apiEndpoint = '/api/watched/';
- if (videoCurrentWatchStatus === 'watched') {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched');
- apiRequest(apiEndpoint, 'POST', { id: videoId, is_watched: false });
- } else if (videoCurrentWatchStatus === 'unwatched') {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched');
- apiRequest(apiEndpoint, 'POST', { id: videoId, is_watched: true });
- }
-
- let watchButtons = document.getElementsByClassName('watch-button');
- for (let i = 0; i < watchButtons.length; i++) {
- if (watchButtons[i].getAttribute('data-id') === videoId) {
- watchButtons[i].outerHTML = watchStatusIndicator;
- }
- }
-}
-
-// Creates a watch status indicator when passed a video id and the videos watch status
-function createWatchStatusIndicator(videoId, videoWatchStatus) {
- let seen, title;
- if (videoWatchStatus === 'watched') {
- seen = 'seen';
- title = 'Mark as unwatched';
- } else if (videoWatchStatus === 'unwatched') {
- seen = 'unseen';
- title = 'Mark as watched';
- }
- let watchStatusIndicator = ` `;
- return watchStatusIndicator;
-}
-
-// Removes the progress bar when passed a video id
-function removeProgressBar(videoId) {
- setProgressBar(videoId, 0, 1);
-}
-
-function isWatchedButton(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/watched/';
- let data = { id: youtube_id, is_watched: true };
- apiRequest(apiEndpoint, 'POST', data);
- setTimeout(function () {
- location.reload();
- }, 1000);
-}
-function isUnwatchedButton(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/watched/';
- let data = { id: youtube_id, is_watched: false };
- apiRequest(apiEndpoint, 'POST', data);
- setTimeout(function () {
- location.reload();
- }, 1000);
-}
-
-function subscribeStatus(subscribeButton) {
- let id = subscribeButton.getAttribute('data-id');
- let type = subscribeButton.getAttribute('data-type');
- let subscribe = Boolean(subscribeButton.getAttribute('data-subscribe'));
- let apiEndpoint;
- let data;
- if (type === 'channel') {
- apiEndpoint = '/api/channel/';
- data = { data: [{ channel_id: id, channel_subscribed: subscribe }] };
- } else if (type === 'playlist') {
- apiEndpoint = '/api/playlist/';
- data = { data: [{ playlist_id: id, playlist_subscribed: subscribe }] };
- }
- apiRequest(apiEndpoint, 'POST', data);
- let message = document.createElement('span');
- if (subscribe) {
- message.innerText = 'You are subscribed.';
- } else {
- message.innerText = 'You are unsubscribed.';
- }
- subscribeButton.replaceWith(message);
-}
-
-function changeView(image) {
- let sourcePage = image.getAttribute('data-origin');
- let newView = image.getAttribute('data-value');
- 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 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);
-}
-
-function toggleCheckbox(checkbox) {
- // pass checkbox id as key and checkbox.checked as value
- 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);
- }, 500);
-}
-
-// start reindex task
-function reindex(button) {
- let apiEndpoint = '/api/refresh/';
- if (button.getAttribute('data-extract-videos')) {
- apiEndpoint += '?extract_videos=true';
- }
- let type = button.getAttribute('data-type');
- let id = button.getAttribute('data-id');
-
- let data = {};
- data[type] = [id];
-
- apiRequest(apiEndpoint, 'POST', data);
- let message = document.createElement('p');
- message.innerText = 'Reindex scheduled';
- document.getElementById('reindex-button').replaceWith(message);
- setTimeout(function () {
- checkMessages();
- }, 500);
-}
-
-// download page buttons
-function rescanPending() {
- let apiEndpoint = '/api/task-name/update_subscribed/';
- apiRequest(apiEndpoint, 'POST');
- animate('rescan-icon', 'rotate-img');
- setTimeout(function () {
- checkMessages();
- }, 500);
-}
-
-function dlPending() {
- let apiEndpoint = '/api/task-name/download_pending/';
- apiRequest(apiEndpoint, 'POST');
- animate('download-icon', 'bounce-img');
- setTimeout(function () {
- checkMessages();
- }, 500);
-}
-
-function addToQueue(autostart = false) {
- let textArea = document.getElementById('id_vid_url');
- if (textArea.value === '') {
- return;
- }
- let toPost = { data: [{ youtube_id: textArea.value, status: 'pending' }] };
- let apiEndpoint = '/api/download/';
- if (autostart) {
- apiEndpoint = `${apiEndpoint}?autostart=true`;
- }
- apiRequest(apiEndpoint, 'POST', toPost);
- textArea.value = '';
- setTimeout(function () {
- checkMessages();
- }, 500);
- showForm();
-}
-
-//shows the video sub menu popup
-function showAddToPlaylistMenu(input1) {
- let dataId, playlists, form_code, buttonId;
- dataId = input1.getAttribute('data-id');
- buttonId = input1.getAttribute('id');
- playlists = getCustomPlaylists();
-
- //hide the invoking button
- input1.style.visibility = 'hidden';
-
- //show the form
- form_code =
- '';
- input1.parentNode.parentNode.innerHTML += form_code;
-}
-
-//handles user action of adding a video to a custom playlist
-function addToCustomPlaylist(input, video_id, playlist_id) {
- let apiEndpoint = '/api/playlist/' + playlist_id + '/';
- let data = { action: 'create', video_id: video_id };
- apiRequest(apiEndpoint, 'POST', data);
-
- //mark the item added in the ui
- input.firstChild.src = '/static/img/icon-seen.svg';
-}
-
-function removeDotMenu(input1, button_id) {
- //show the menu button
- document.getElementById(button_id).style.visibility = 'visible';
-
- //remove the form
- input1.parentNode.remove();
-}
-
-//shows the video sub menu popup on custom playlist page
-function showCustomPlaylistMenu(input1, playlist_id, current_page, last_page) {
- let dataId, form_code, buttonId;
- dataId = input1.getAttribute('data-id');
- buttonId = input1.getAttribute('id');
-
- //hide the invoking button
- input1.style.visibility = 'hidden';
-
- //show the form
- form_code =
- '';
- input1.parentNode.parentNode.innerHTML += form_code;
-}
-
-//process custom playlist form actions
-function moveCustomPlaylistVideo(input1, playlist_id, current_page, last_page) {
- let dataId, dataContext;
- dataId = input1.getAttribute('data-id');
- dataContext = input1.getAttribute('data-context');
-
- let apiEndpoint = '/api/playlist/' + playlist_id + '/';
- let data = { action: dataContext, video_id: dataId };
- apiRequest(apiEndpoint, 'POST', data);
-
- let itemDom = input1.parentElement.parentElement.parentElement;
- let listDom = itemDom.parentElement;
-
- if (dataContext === 'up') {
- let sibling = itemDom.previousElementSibling;
- if (sibling !== null) {
- sibling.before(itemDom);
- } else if (current_page > 1) {
- itemDom.remove();
- }
- } else if (dataContext === 'down') {
- let sibling = itemDom.nextElementSibling;
- if (sibling !== null) {
- sibling.after(itemDom);
- } else if (current_page !== last_page) {
- itemDom.remove();
- }
- } else if (dataContext === 'top') {
- let sibling = listDom.firstElementChild;
- if (sibling !== null) {
- sibling.before(itemDom);
- }
- if (current_page > 1) {
- itemDom.remove();
- }
- } else if (dataContext === 'bottom') {
- let sibling = listDom.lastElementChild;
- if (sibling !== null) {
- sibling.after(itemDom);
- }
- if (current_page !== last_page) {
- itemDom.remove();
- }
- } else if (dataContext === 'remove') {
- itemDom.remove();
- }
-}
-
-function toIgnore(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/download/' + youtube_id + '/';
- apiRequest(apiEndpoint, 'POST', { status: 'ignore' });
- document.getElementById('dl-' + youtube_id).remove();
-}
-
-function downloadNow(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/download/' + youtube_id + '/';
- apiRequest(apiEndpoint, 'POST', { status: 'priority' });
- document.getElementById(youtube_id).remove();
- setTimeout(function () {
- checkMessages();
- }, 500);
-}
-
-function forgetIgnore(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/download/' + youtube_id + '/';
- apiRequest(apiEndpoint, 'DELETE');
- document.getElementById('dl-' + youtube_id).remove();
-}
-
-function addSingle(button) {
- let youtube_id = button.getAttribute('data-id');
- let apiEndpoint = '/api/download/' + youtube_id + '/';
- apiRequest(apiEndpoint, 'POST', { status: 'pending' });
- document.getElementById('dl-' + youtube_id).remove();
- setTimeout(function () {
- checkMessages();
- }, 500);
-}
-
-function deleteQueue(button) {
- let to_delete = button.getAttribute('data-id');
- let apiEndpoint = '/api/download/?filter=' + to_delete;
- apiRequest(apiEndpoint, 'DELETE');
- // clear button
- let message = document.createElement('p');
- message.innerText = 'deleting download queue: ' + to_delete;
- document.getElementById(button.id).replaceWith(message);
-}
-
-function stopTask(icon) {
- let taskId = icon.getAttribute('data');
- let apiEndpoint = `/api/task-id/${taskId}/`;
- apiRequest(apiEndpoint, 'POST', { command: 'stop' });
- icon.remove();
-}
-
-function killTask(icon) {
- let taskId = icon.getAttribute('data');
- let apiEndpoint = `/api/task-id/${taskId}/`;
- apiRequest(apiEndpoint, 'POST', { command: 'kill' });
- icon.remove();
-}
-
-// settings page buttons
-function manualImport() {
- let apiEndpoint = '/api/task-name/manual_import/';
- apiRequest(apiEndpoint, 'POST');
- // clear button
- let message = document.createElement('p');
- message.innerText = 'processing import';
- let toReplace = document.getElementById('manual-import');
- toReplace.innerHTML = '';
- toReplace.appendChild(message);
- setTimeout(function () {
- location.replace('#notifications');
- checkMessages();
- }, 200);
-}
-
-function reEmbed() {
- let apiEndpoint = '/api/task-name/resync_thumbs/';
- apiRequest(apiEndpoint, 'POST');
- // clear button
- let message = document.createElement('p');
- message.innerText = 'processing thumbnails';
- let toReplace = document.getElementById('re-embed');
- toReplace.innerHTML = '';
- toReplace.appendChild(message);
- setTimeout(function () {
- location.replace('#notifications');
- checkMessages();
- }, 200);
-}
-
-function dbBackup() {
- let apiEndpoint = '/api/backup/';
- apiRequest(apiEndpoint, 'POST');
- // clear button
- let message = document.createElement('p');
- message.innerText = 'backing up archive';
- let toReplace = document.getElementById('db-backup');
- toReplace.innerHTML = '';
- toReplace.appendChild(message);
- setTimeout(function () {
- location.replace('#notifications');
- checkMessages();
- }, 200);
-}
-
-function dbRestore(button) {
- let fileName = button.getAttribute('data-id');
- let apiEndpoint = `/api/backup/${fileName}/`;
- apiRequest(apiEndpoint, 'POST');
- // clear backup row
- let message = document.createElement('p');
- message.innerText = 'restoring from backup';
- let toReplace = document.getElementById(fileName);
- toReplace.innerHTML = '';
- toReplace.appendChild(message);
- setTimeout(function () {
- location.replace('#notifications');
- checkMessages();
- }, 200);
-}
-
-function fsRescan() {
- let apiEndpoint = '/api/task-name/rescan_filesystem/';
- apiRequest(apiEndpoint, 'POST');
- // clear button
- let message = document.createElement('p');
- message.innerText = 'File system scan in progress';
- let toReplace = document.getElementById('fs-rescan');
- toReplace.innerHTML = '';
- toReplace.appendChild(message);
- setTimeout(function () {
- location.replace('#notifications');
- checkMessages();
- }, 200);
-}
-
-function resetToken() {
- let apiEndpoint = '/api/token/';
- let result = apiRequest(apiEndpoint, 'DELETE');
- if (result && result.success) {
- let message = document.createElement('p');
- message.innerText = 'Token revoked';
- document.getElementById('text-reveal').replaceWith(message);
- } else {
- console.error('unable to revoke token');
- }
-}
-
-// restore from snapshot
-function restoreSnapshot(snapshotId) {
- console.log('restore ' + snapshotId);
- let apiEndpoint = '/api/snapshot/' + snapshotId + '/';
- apiRequest(apiEndpoint, 'POST');
- let message = document.createElement('p');
- message.innerText = 'Snapshot restore started';
- document.getElementById(snapshotId).parentElement.replaceWith(message);
-}
-
-function createSnapshot() {
- console.log('create snapshot now');
- let apiEndpoint = '/api/snapshot/';
- apiRequest(apiEndpoint, 'POST');
- let message = document.createElement('span');
- message.innerText = 'Snapshot in progress';
- document.getElementById('createButton').replaceWith(message);
-}
-
-function deleteNotificationUrl(button) {
- console.log('delete notification url');
- let apiEndpoint = '/api/schedule/notification/';
- let data = {
- task_name: button.dataset.task,
- url: button.dataset.url,
- };
- apiRequest(apiEndpoint, 'DELETE', data);
- button.parentElement.remove();
-}
-
-function deleteSchedule(button) {
- console.log('delete schedule');
- let apiEndpoint = '/api/schedule/';
- let data = { task_name: button.dataset.schedule };
- apiRequest(apiEndpoint, 'DELETE', data);
- let message = document.createElement('span');
- message.innerText = 'False';
- message.classList.add('settings-current');
- button.parentElement.replaceWith(message);
-}
-
-// delete from file system
-function deleteConfirm() {
- let to_show = document.getElementById('delete-button');
- document.getElementById('delete-item').style.display = 'none';
- to_show.style.display = 'block';
-}
-
-function deleteVideo(button) {
- let to_delete = button.getAttribute('data-id');
- let to_ignore = button.getAttribute('data-ignore');
- let to_redirect = button.getAttribute('data-redirect');
- let apiDeleteEndpoint = '/api/video/' + to_delete + '/';
- apiRequest(apiDeleteEndpoint, 'DELETE');
-
- if (to_ignore !== null) {
- let apiIgnoreEndpoint = '/api/download/' + to_delete + '/';
- apiRequest(apiIgnoreEndpoint, 'POST', { status: 'ignore-force' });
- }
-
- setTimeout(function () {
- let redirect = '/channel/' + to_redirect;
- window.location.replace(redirect);
- }, 1000);
-}
-
-function deleteChannel(button) {
- let to_delete = button.getAttribute('data-id');
- let apiEndpoint = '/api/channel/' + to_delete + '/';
- apiRequest(apiEndpoint, 'DELETE');
- setTimeout(function () {
- window.location.replace('/channel/');
- }, 1000);
-}
-
-function deletePlaylist(button) {
- let playlist_id = button.getAttribute('data-id');
- let playlist_action = button.getAttribute('data-action');
- let apiEndpoint = `/api/playlist/${playlist_id}/`;
- if (playlist_action === 'delete-videos') {
- apiEndpoint += '?delete-videos=true';
- }
- apiRequest(apiEndpoint, 'DELETE');
- setTimeout(function () {
- window.location.replace('/playlist/');
- }, 1000);
-}
-
-function cancelDelete() {
- document.getElementById('delete-button').style.display = 'none';
- document.getElementById('delete-item').style.display = 'block';
-}
-
-// get seconds from hh:mm:ss.ms timestamp
-function getSeconds(timestamp) {
- let elements = timestamp.split(':', 3);
- let secs = parseInt(elements[0]) * 60 * 60 + parseInt(elements[1]) * 60 + parseFloat(elements[2]);
- return secs;
-}
-
-// player
-let sponsorBlock = [];
-function createPlayer(button) {
- let videoId = button.getAttribute('data-id');
- let videoPosition = button.getAttribute('data-position');
- let videoData = getVideoData(videoId);
-
- let sponsorBlockElements = '';
- if (videoData.data.sponsorblock && videoData.data.sponsorblock.is_enabled) {
- sponsorBlock = videoData.data.sponsorblock;
- if (sponsorBlock.segments.length === 0) {
- sponsorBlockElements = `
-
- `;
- } else {
- if (sponsorBlock.has_unlocked) {
- sponsorBlockElements = `
-
- `;
- }
- }
- } else {
- sponsorBlock = null;
- }
- let videoProgress;
- if (videoPosition) {
- videoProgress = getSeconds(videoPosition);
- } else {
- videoProgress = getVideoProgress(videoId).position;
- }
- let videoName = videoData.data.title;
-
- let videoTag = createVideoTag(videoData, videoProgress, true);
-
- let playlist = '';
- let videoPlaylists = videoData.data.playlist; // Array of playlists the video is in
- if (typeof videoPlaylists !== 'undefined') {
- let subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed
- if (subbedPlaylists.length !== 0) {
- let playlistData = getPlaylistData(subbedPlaylists[0]); // Playlist data for first subscribed playlist
- let playlistId = playlistData.playlist_id;
- let playlistName = playlistData.playlist_name;
- playlist = ``;
- }
- }
-
- let videoViews = formatNumbers(videoData.data.stats.view_count);
-
- let channelId = videoData.data.channel.channel_id;
- let channelName = videoData.data.channel.channel_name;
-
- removePlayer();
-
- // If cast integration is enabled create cast button
- let castButton = '';
- if (videoData.config.enable_cast) {
- castButton = ` `;
- }
-
- // Watched indicator
- let watchStatusIndicator;
- if (videoData.data.player.watched) {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched');
- } else {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched');
- }
-
- let playerStats = `${videoViews} `;
- if (videoData.data.stats.like_count) {
- let likes = formatNumbers(videoData.data.stats.like_count);
- playerStats += `
| ${likes} `;
- }
- if (videoData.data.stats.dislike_count && videoData.config.downloads.integrate_ryd) {
- let dislikes = formatNumbers(videoData.data.stats.dislike_count);
- playerStats += `
| ${dislikes} `;
- }
- playerStats += '
';
-
- const markup = `
-
-
- ${videoTag}
-
- ${sponsorBlockElements}
-
-
- ${watchStatusIndicator}
- ${castButton}
- ${playerStats}
-
-
${videoName}
-
-
- `;
- const divPlayer = document.getElementById('player');
- divPlayer.innerHTML = markup;
- recordTextTrackChanges();
-}
-
-// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
-function insertVideoTag(videoData, videoProgress) {
- let videoTag = createVideoTag(videoData, videoProgress);
- let videoMain = document.querySelector('.video-main');
- videoMain.innerHTML += videoTag;
-}
-
-// Generates a video tag with subtitles when passed videoData and videoProgress.
-function createVideoTag(videoData, videoProgress, autoplay = false) {
- let videoId = videoData.data.youtube_id;
- let videoUrl = videoData.data.media_url;
- let videoThumbUrl = videoData.data.vid_thumb_url;
- let subtitles = '';
- let videoSubtitles = videoData.data.subtitles; // Array of subtitles
- if (typeof videoSubtitles !== 'undefined' && videoData.config.downloads.subtitle) {
- for (let i = 0; i < videoSubtitles.length; i++) {
- let label = videoSubtitles[i].name;
- if (videoSubtitles[i].source === 'auto') {
- label += ' - auto';
- }
- subtitles += ``;
- }
- }
-
- let videoTag = `
-
-
- ${subtitles}
-
- `;
- 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');
- return videoElement;
-}
-
-// Gets the video source tag
-function getVideoPlayerVideoSource() {
- let videoPlayerVideoSource = document.getElementById('video-source');
- return videoPlayerVideoSource;
-}
-
-// Gets the current progress of the video currently in the player
-function getVideoPlayerCurrentTime() {
- let videoElement = getVideoPlayer();
- if (videoElement != null) {
- return videoElement.currentTime;
- }
-}
-
-// Gets the video id of the video currently in the player
-function getVideoPlayerVideoId() {
- let videoPlayerVideoSource = getVideoPlayerVideoSource();
- if (videoPlayerVideoSource != null) {
- return videoPlayerVideoSource.getAttribute('videoid');
- }
-}
-
-// Gets the duration of the video currently in the player
-function getVideoPlayerDuration() {
- let videoElement = getVideoPlayer();
- if (videoElement != null) {
- return videoElement.duration;
- }
-}
-
-// Gets current watch status of video based on watch button
-function getVideoPlayerWatchStatus() {
- let videoId = getVideoPlayerVideoId();
- let watched = false;
-
- let watchButtons = document.getElementsByClassName('watch-button');
- for (let i = 0; i < watchButtons.length; i++) {
- if (
- watchButtons[i].getAttribute('data-id') === videoId &&
- watchButtons[i].getAttribute('data-status') === 'watched'
- ) {
- watched = true;
- }
- }
- return watched;
-}
-
-// Runs on video playback, marks video as watched if video gets to 90% or higher, sends position to api, SB skipping
-function onVideoProgress() {
- let videoId = getVideoPlayerVideoId();
- let currentTime = getVideoPlayerCurrentTime();
- let duration = getVideoPlayerDuration();
- let videoElement = getVideoPlayer();
- let notificationsElement = document.getElementById('notifications');
- if (sponsorBlock && sponsorBlock.segments) {
- for (let i in sponsorBlock.segments) {
- if (
- currentTime >= sponsorBlock.segments[i].segment[0] &&
- currentTime <= sponsorBlock.segments[i].segment[0] + 0.3
- ) {
- videoElement.currentTime = sponsorBlock.segments[i].segment[1];
- let notificationElement = document.getElementById(
- 'notification-' + sponsorBlock.segments[i].UUID
- );
- if (!notificationElement) {
- notificationsElement.innerHTML += ``;
- }
- }
- if (currentTime > sponsorBlock.segments[i].segment[1] + 10) {
- let notificationsElementUUID = document.getElementById(
- 'notification-' + sponsorBlock.segments[i].UUID
- );
- if (notificationsElementUUID) {
- notificationsElementUUID.outerHTML = '';
- }
- }
- }
- }
- if (currentTime < 10) return;
- if ((currentTime % 10).toFixed(1) <= 0.2) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- postVideoProgress(videoId, currentTime);
- if (!getVideoPlayerWatchStatus()) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- updateVideoWatchStatus(videoId, 'unwatched');
- }
- }
- }
-}
-
-// Runs on video end, marks video as watched
-function onVideoEnded() {
- let videoId = getVideoPlayerVideoId();
- if (!getVideoPlayerWatchStatus()) {
- // Check if video is already marked as watched
- updateVideoWatchStatus(videoId, 'unwatched');
- }
- for (let i in sponsorBlock.segments) {
- let notificationsElementUUID = document.getElementById(
- 'notification-' + sponsorBlock.segments[i].UUID
- );
- if (notificationsElementUUID) {
- notificationsElementUUID.outerHTML = '';
- }
- }
-}
-
-function watchedThreshold(currentTime, duration) {
- let watched = false;
- if (duration <= 1800) {
- // If video is less than 30 min
- if (currentTime / duration >= 0.9) {
- // Mark as watched at 90%
- watched = true;
- }
- } else {
- // If video is more than 30 min
- if (currentTime >= duration - 120) {
- // Mark as watched if there is two minutes left
- watched = true;
- }
- }
- return watched;
-}
-
-// Runs on video pause. Sends current position.
-function onVideoPause() {
- let videoId = getVideoPlayerVideoId();
- let currentTime = getVideoPlayerCurrentTime();
- if (currentTime < 10) return;
- postVideoProgress(videoId, currentTime);
-}
-
-// Format numbers for frontend
-function formatNumbers(number) {
- let numberUnformatted = parseFloat(number);
- let numberFormatted;
- if (numberUnformatted > 999999999) {
- numberFormatted = (numberUnformatted / 1000000000).toFixed(1).toString() + 'B';
- } else if (numberUnformatted > 999999) {
- numberFormatted = (numberUnformatted / 1000000).toFixed(1).toString() + 'M';
- } else if (numberUnformatted > 999) {
- numberFormatted = (numberUnformatted / 1000).toFixed(1).toString() + 'K';
- } else {
- numberFormatted = numberUnformatted;
- }
- return numberFormatted;
-}
-
-// Formats times in seconds for frontend
-function formatTime(time) {
- let hoursUnformatted = time / 3600;
- let minutesUnformatted = (time % 3600) / 60;
- let secondsUnformatted = time % 60;
-
- let hoursFormatted = Math.trunc(hoursUnformatted);
- let minutesFormatted;
- if (minutesUnformatted < 10 && hoursFormatted > 0) {
- minutesFormatted = '0' + Math.trunc(minutesUnformatted);
- } else {
- minutesFormatted = Math.trunc(minutesUnformatted);
- }
- let secondsFormatted;
- if (secondsUnformatted < 10) {
- secondsFormatted = '0' + Math.trunc(secondsUnformatted);
- } else {
- secondsFormatted = Math.trunc(secondsUnformatted);
- }
-
- let timeUnformatted = '';
- if (hoursFormatted > 0) {
- timeUnformatted = hoursFormatted + ':';
- }
- let timeFormatted = timeUnformatted.concat(minutesFormatted, ':', secondsFormatted);
- return timeFormatted;
-}
-
-// Gets video data when passed video ID
-function getVideoData(videoId) {
- let apiEndpoint = '/api/video/' + videoId + '/';
- let videoData = apiRequest(apiEndpoint, 'GET');
- return videoData;
-}
-
-// Gets channel data when passed channel ID
-function getChannelData(channelId) {
- let apiEndpoint = '/api/channel/' + channelId + '/';
- let channelData = apiRequest(apiEndpoint, 'GET');
- return channelData.data;
-}
-
-// Gets playlist data when passed playlist ID
-function getPlaylistData(playlistId) {
- let apiEndpoint = '/api/playlist/' + playlistId + '/';
- let playlistData = apiRequest(apiEndpoint, 'GET');
- return playlistData.data;
-}
-
-// Gets custom playlists
-function getCustomPlaylists() {
- let apiEndpoint = '/api/playlist/?playlist_type=custom';
- let playlistData = apiRequest(apiEndpoint, 'GET');
- return playlistData.data;
-}
-
-// Get video progress data when passed video ID
-function getVideoProgress(videoId) {
- let apiEndpoint = '/api/video/' + videoId + '/progress/';
- let videoProgress = apiRequest(apiEndpoint, 'GET');
- return videoProgress;
-}
-
-// Given an array of playlist ids it returns an array of subbed playlist ids from that list
-function getSubbedPlaylists(videoPlaylists) {
- let subbedPlaylists = [];
- for (let i = 0; i < videoPlaylists.length; i++) {
- if (getPlaylistData(videoPlaylists[i]).playlist_subscribed) {
- subbedPlaylists.push(videoPlaylists[i]);
- }
- }
- return subbedPlaylists;
-}
-
-// Send video position when given video id and progress in seconds
-function postVideoProgress(videoId, videoProgress) {
- let apiEndpoint = '/api/video/' + videoId + '/progress/';
- let duartion = getVideoPlayerDuration();
- if (!isNaN(videoProgress) && duartion !== 'undefined') {
- let data = {
- position: videoProgress,
- };
- if (videoProgress === 0) {
- apiRequest(apiEndpoint, 'DELETE');
- // console.log("Deleting Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
- } else if (!getVideoPlayerWatchStatus()) {
- apiRequest(apiEndpoint, 'POST', data);
- // console.log("Saving Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
- }
- }
-}
-
-// Send sponsor segment when given video id and and timestamps
-function postSponsorSegment(videoId, startTime, endTime) {
- let apiEndpoint = '/api/video/' + videoId + '/sponsor/';
- let data = {
- segment: {
- startTime: startTime,
- endTime: endTime,
- },
- };
- apiRequest(apiEndpoint, 'POST', data);
-}
-
-// Send sponsor segment when given video id and and timestamps
-function postSponsorSegmentVote(videoId, uuid, vote) {
- let apiEndpoint = '/api/video/' + videoId + '/sponsor/';
- let data = {
- vote: {
- uuid: uuid,
- yourVote: vote,
- },
- };
- apiRequest(apiEndpoint, 'POST', data);
-}
-
-function handleCookieValidate() {
- document.getElementById('cookieButton').remove();
- let cookieMessageElement = document.getElementById('cookieMessage');
- cookieMessageElement.innerHTML = `Processing. `;
- let response = postCookieValidate();
- if (response.cookie_validated === true) {
- cookieMessageElement.innerHTML = `The cookie file is valid. `;
- } else {
- cookieMessageElement.innerHTML = `Warning, the cookie file is invalid. `;
- }
-}
-
-// Check youtube cookie settings
-function postCookieValidate() {
- let apiEndpoint = '/api/cookie/';
- return apiRequest(apiEndpoint, 'POST');
-}
-
-// Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE")
-function apiRequest(apiEndpoint, method, data) {
- const xhttp = new XMLHttpRequest();
- let sessionToken = getCookie('sessionid');
- xhttp.open(method, apiEndpoint, false);
- xhttp.setRequestHeader('X-CSRFToken', getCookie('csrftoken')); // Used for video progress POST requests
- xhttp.setRequestHeader('Authorization', 'Token ' + sessionToken);
- xhttp.setRequestHeader('Content-Type', 'application/json');
- xhttp.send(JSON.stringify(data));
- if (xhttp.status === 404) {
- return false;
- } else {
- return JSON.parse(xhttp.responseText);
- }
-}
-
-// Gets origin URL
-function getURL() {
- return window.location.origin;
-}
-
-function removePlayer() {
- let currentTime = getVideoPlayerCurrentTime();
- let duration = getVideoPlayerDuration();
- let videoId = getVideoPlayerVideoId();
- postVideoProgress(videoId, currentTime);
- setProgressBar(videoId, currentTime, duration);
- let playerElement = document.getElementById('player');
- if (playerElement.hasChildNodes()) {
- let youtubeId = playerElement.childNodes[1].getAttribute('data-id');
- let playedStatus = document.createDocumentFragment();
- let playedBox = document.getElementById(youtubeId);
- if (playedBox) {
- playedStatus.appendChild(playedBox);
- }
- playerElement.innerHTML = '';
- // append played status
- let videoInfo = document.getElementById('video-info-' + youtubeId);
- if (videoInfo) {
- videoInfo.insertBefore(playedStatus, videoInfo.firstChild);
- }
- }
-}
-
-// Sets the progress bar when passed a video id, video progress and video duration
-function setProgressBar(videoId, currentTime, duration) {
- let progressBarWidth = (currentTime / duration) * 100 + '%';
- let progressBars = document.getElementsByClassName('video-progress-bar');
- for (let i = 0; i < progressBars.length; i++) {
- if (progressBars[i].id === 'progress-' + videoId) {
- if (!getVideoPlayerWatchStatus()) {
- progressBars[i].style.width = progressBarWidth;
- } else {
- progressBars[i].style.width = '0%';
- }
- }
- }
-
- // progressBar = document.getElementById("progress-" + videoId);
-}
-
-// multi search form
-let searchTimeout = null;
-let searchHttpRequest = null;
-function searchMulti(query) {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(function () {
- if (query.length > 0) {
- if (searchHttpRequest) {
- searchHttpRequest.abort();
- }
- searchHttpRequest = new XMLHttpRequest();
- searchHttpRequest.onreadystatechange = function () {
- if (searchHttpRequest.readyState === 4) {
- const response = JSON.parse(searchHttpRequest.response);
- populateMultiSearchResults(response.results, response.queryType);
- }
- };
- searchHttpRequest.open('GET', `/api/search/?query=${query}`, true);
- searchHttpRequest.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
- searchHttpRequest.setRequestHeader('Content-type', 'application/json');
- searchHttpRequest.send();
- } else {
- if (searchHttpRequest) {
- searchHttpRequest.abort();
- searchHttpRequest = null;
- }
- // show the placeholder container and hide the results container
- document.getElementById('multi-search-results').style.display = 'none';
- document.getElementById('multi-search-results-placeholder').style.display = 'block';
- }
- }, 500);
-}
-
-function getViewDefaults(view) {
- let defaultView = document.getElementById('id_' + view).value;
- return defaultView;
-}
-
-function populateMultiSearchResults(allResults, queryType) {
- // show the results container and hide the placeholder container
- document.getElementById('multi-search-results').style.display = 'block';
- document.getElementById('multi-search-results-placeholder').style.display = 'none';
- // videos
- let defaultVideo = getViewDefaults('home');
- let allVideos = allResults.video_results;
- let videoBox = document.getElementById('video-results');
- videoBox.innerHTML = '';
- videoBox.parentElement.style.display = 'block';
- if (allVideos.length > 0) {
- for (let index = 0; index < allVideos.length; index++) {
- const video = allVideos[index];
- const videoDiv = createVideo(video, defaultVideo);
- videoBox.appendChild(videoDiv);
- }
- } else {
- if (queryType === 'simple' || queryType === 'video') {
- videoBox.innerHTML = 'No videos found.
';
- } else {
- videoBox.parentElement.style.display = 'none';
- }
- }
- // channels
- let defaultChannel = getViewDefaults('channel');
- let allChannels = allResults.channel_results;
- let channelBox = document.getElementById('channel-results');
- channelBox.innerHTML = '';
- channelBox.parentElement.style.display = 'block';
- if (allChannels.length > 0) {
- for (let index = 0; index < allChannels.length; index++) {
- const channel = allChannels[index];
- const channelDiv = createChannel(channel, defaultChannel);
- channelBox.appendChild(channelDiv);
- }
- } else {
- if (queryType === 'simple' || queryType === 'channel') {
- channelBox.innerHTML = 'No channels found.
';
- } else {
- channelBox.parentElement.style.display = 'none';
- }
- }
- // playlists
- let defaultPlaylist = getViewDefaults('playlist');
- let allPlaylists = allResults.playlist_results;
- let playlistBox = document.getElementById('playlist-results');
- playlistBox.innerHTML = '';
- playlistBox.parentElement.style.display = 'block';
- if (allPlaylists.length > 0) {
- for (let index = 0; index < allPlaylists.length; index++) {
- const playlist = allPlaylists[index];
- const playlistDiv = createPlaylist(playlist, defaultPlaylist);
- playlistBox.appendChild(playlistDiv);
- }
- } else {
- if (queryType === 'simple' || queryType === 'playlist') {
- playlistBox.innerHTML = 'No playlists found.
';
- } else {
- playlistBox.parentElement.style.display = 'none';
- }
- }
- // fulltext
- let allFullText = allResults.fulltext_results;
- let fullTextBox = document.getElementById('fulltext-results');
- fullTextBox.innerHTML = '';
- fullTextBox.parentElement.style.display = 'block';
- if (allFullText.length > 0) {
- for (let i = 0; i < allFullText.length; i++) {
- const fullText = allFullText[i];
- if ('subtitle_line' in fullText) {
- const fullTextDiv = createFulltext(fullText);
- fullTextBox.appendChild(fullTextDiv);
- }
- }
- } else {
- if (queryType === 'simple' || queryType === 'full') {
- fullTextBox.innerHTML = 'No fulltext items found.
';
- } else {
- fullTextBox.parentElement.style.display = 'none';
- }
- }
-}
-
-function createVideo(video, viewStyle) {
- // create video item div from template
- const videoId = video.youtube_id;
- // const mediaUrl = video.media_url;
- // const thumbUrl = '/cache/' + video.vid_thumb_url;
- const videoTitle = video.title;
- const videoPublished = video.published;
- const videoDuration = video.player.duration_str;
- let watchStatusIndicator;
- if (video.player.watched) {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched');
- } else {
- watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched');
- }
- const channelId = video.channel.channel_id;
- const channelName = video.channel.channel_name;
- // build markup
- const markup = `
-
-
-
-
-
-
-
-
-
-
-
-
- ${watchStatusIndicator}
- ${videoPublished} | ${videoDuration}
-
-
-
- `;
- const videoDiv = document.createElement('div');
- videoDiv.setAttribute('class', 'video-item ' + viewStyle);
- videoDiv.innerHTML = markup;
- return videoDiv;
-}
-
-function createChannel(channel, viewStyle) {
- // create channel item div from template
- const channelId = channel.channel_id;
- const channelName = channel.channel_name;
- const channelSubs = channel.channel_subs;
- const channelLastRefresh = channel.channel_last_refresh;
- let button;
- if (channel.channel_subscribed) {
- button = `Unsubscribe `;
- } else {
- button = `Subscribe `;
- }
- // build markup
- const markup = `
-
-
-
-
-
-
-
Subscribers: ${channelSubs}
-
-
-
-
-
Last refreshed: ${channelLastRefresh}
- ${button}
-
-
-
- `;
- const channelDiv = document.createElement('div');
- channelDiv.setAttribute('class', 'channel-item ' + viewStyle);
- channelDiv.innerHTML = markup;
- return channelDiv;
-}
-
-function createPlaylist(playlist, viewStyle) {
- // create playlist item div from template
- const playlistId = playlist.playlist_id;
- const playlistName = playlist.playlist_name;
- const playlistChannelId = playlist.playlist_channel_id;
- const playlistChannel = playlist.playlist_channel;
- const playlistLastRefresh = playlist.playlist_last_refresh;
- let button;
- if (playlist.playlist_subscribed) {
- button = `Unsubscribe `;
- } else {
- button = `Subscribe `;
- }
- const markup = `
-
-
- `;
- const playlistDiv = document.createElement('div');
- playlistDiv.setAttribute('class', 'playlist-item ' + viewStyle);
- playlistDiv.innerHTML = markup;
- return playlistDiv;
-}
-
-function createFulltext(fullText) {
- const videoId = fullText.youtube_id;
- const subtitle_start = fullText.subtitle_start.split('.')[0];
- const subtitle_end = fullText.subtitle_end.split('.')[0];
- const markup = `
-
-
-
-
-
-
-
-
-
-
-
-
-
${subtitle_start} - ${subtitle_end}
-
${fullText.subtitle_line}
-
Score: ${fullText._score}
-
- `;
- const fullTextDiv = document.createElement('div');
- fullTextDiv.setAttribute('class', 'video-item list');
- fullTextDiv.innerHTML = markup;
- return fullTextDiv;
-}
-
-function getComments(videoId) {
- let apiEndpoint = '/api/video/' + videoId + '/comment/';
- let response = apiRequest(apiEndpoint, 'GET');
- let allComments = response.data;
-
- writeComments(allComments);
-}
-
-function writeComments(allComments) {
- let commentsListBox = document.getElementById('comments-list');
- for (let i = 0; i < allComments.length; i++) {
- const rootComment = allComments[i];
-
- let commentBox = createCommentBox(rootComment, true);
-
- // add replies to commentBox
- if (rootComment.comment_replies) {
- let commentReplyBox = document.createElement('div');
- commentReplyBox.setAttribute('class', 'comments-replies');
- commentReplyBox.setAttribute('id', rootComment.comment_id + '-replies');
- let totalReplies = rootComment.comment_replies.length;
- if (totalReplies > 0) {
- let replyButton = createReplyButton(rootComment.comment_id + '-replies', totalReplies);
- commentBox.appendChild(replyButton);
- }
- for (let j = 0; j < totalReplies; j++) {
- const commentReply = rootComment.comment_replies[j];
- let commentReplyDiv = createCommentBox(commentReply, false);
- commentReplyBox.appendChild(commentReplyDiv);
- }
- if (totalReplies > 0) {
- commentBox.appendChild(commentReplyBox);
- }
- }
- commentsListBox.appendChild(commentBox);
- }
-}
-
-function createReplyButton(replyId, totalReplies) {
- let replyButton = document.createElement('button');
- replyButton.innerHTML = `▼ ${totalReplies} replies`;
- replyButton.setAttribute('data-id', replyId);
- replyButton.setAttribute('onclick', 'toggleCommentReplies(this)');
- return replyButton;
-}
-
-function toggleCommentReplies(button) {
- let commentReplyId = button.getAttribute('data-id');
- let state = document.getElementById(commentReplyId).style.display;
-
- if (state === 'none' || state === '') {
- document.getElementById(commentReplyId).style.display = 'block';
- button.querySelector('#toggle-icon').innerHTML = '▲';
- } else {
- document.getElementById(commentReplyId).style.display = 'none';
- button.querySelector('#toggle-icon').innerHTML = '▼';
- }
-}
-
-function createCommentBox(comment, isRoot) {
- let commentBox = document.createElement('div');
- commentBox.setAttribute('class', 'comment-box');
-
- let commentClass;
- if (isRoot) {
- commentClass = 'root-comment';
- } else {
- commentClass = 'reply-comment';
- }
-
- commentBox.classList.add = commentClass;
-
- let commentAuthor = document.createElement('h3');
- commentAuthor.innerText = comment.comment_author;
- if (comment.comment_author_is_uploader) {
- commentAuthor.setAttribute('class', 'comment-highlight');
- }
- commentBox.appendChild(commentAuthor);
-
- let commentText = document.createElement('p');
- commentText.innerText = comment.comment_text;
- commentBox.appendChild(commentText);
-
- const spacer = '| ';
- let commentMeta = document.createElement('div');
- commentMeta.setAttribute('class', 'comment-meta');
-
- commentMeta.innerHTML = `${comment.comment_time_text} `;
-
- if (comment.comment_likecount > 0) {
- let numberFormatted = formatNumbers(comment.comment_likecount);
- commentMeta.innerHTML += `${spacer} ${numberFormatted} `;
- }
-
- if (comment.comment_is_favorited) {
- commentMeta.innerHTML += `${spacer}`;
- }
-
- commentBox.appendChild(commentMeta);
-
- return commentBox;
-}
-
-function getSimilarVideos(videoId) {
- let apiEndpoint = '/api/video/' + videoId + '/similar/';
- let response = apiRequest(apiEndpoint, 'GET');
- if (!response) {
- populateEmpty();
- return;
- }
- let allSimilar = response.data;
- if (allSimilar.length > 0) {
- populateSimilar(allSimilar);
- }
-}
-
-function populateSimilar(allSimilar) {
- let similarBox = document.getElementById('similar-videos');
- for (let i = 0; i < allSimilar.length; i++) {
- const similarRaw = allSimilar[i];
- let similarDiv = createVideo(similarRaw, 'grid');
- similarBox.appendChild(similarDiv);
- }
-}
-
-function populateEmpty() {
- let similarBox = document.getElementById('similar-videos');
- let emptyMessage = document.createElement('p');
- emptyMessage.innerText = 'No similar videos found.';
- similarBox.appendChild(emptyMessage);
-}
-
-// generic
-
-function getCookie(c_name) {
- if (document.cookie.length > 0) {
- let c_start = document.cookie.indexOf(c_name + '=');
- if (c_start !== -1) {
- c_start = c_start + c_name.length + 1;
- let c_end = document.cookie.indexOf(';', c_start);
- if (c_end === -1) c_end = document.cookie.length;
- return unescape(document.cookie.substring(c_start, c_end));
- }
- }
- return '';
-}
-
-// animations
-
-function textReveal(button) {
- let revealBox = button.parentElement.parentElement;
- let textBox = revealBox.querySelector('#text-reveal');
- let textBoxHeight = textBox.style.height;
- if (textBoxHeight === 'unset') {
- textBox.style.height = '0px';
- button.innerText = 'Show';
- } else {
- textBox.style.height = 'unset';
- button.innerText = 'Hide';
- }
-}
-
-function textExpand() {
- let textBox = document.getElementById('text-expand');
- let button = document.getElementById('text-expand-button');
- let style = window.getComputedStyle(textBox);
- if (style.webkitLineClamp === 'none') {
- textBox.style['-webkit-line-clamp'] = '4';
- button.innerText = 'Show more';
- } else {
- textBox.style['-webkit-line-clamp'] = 'unset';
- button.innerText = 'Show less';
- }
-}
-
-// hide "show more" button if all text is already visible
-function textExpandButtonVisibilityUpdate() {
- let textBox = document.getElementById('text-expand');
- let button = document.getElementById('text-expand-button');
- if (!textBox || !button) return;
-
- let styles = window.getComputedStyle(textBox);
- let textBoxLineClamp = styles.webkitLineClamp;
- if (textBoxLineClamp === 'unset') return; // text box is in revealed state
-
- if (textBox.offsetHeight < textBox.scrollHeight || textBox.offsetWidth < textBox.scrollWidth) {
- // the element has an overflow, show read more button
- button.style.display = 'inline-block';
- } else {
- // the element doesn't have overflow
- button.style.display = 'none';
- }
-}
-
-document.addEventListener('readystatechange', textExpandButtonVisibilityUpdate);
-window.addEventListener('resize', textExpandButtonVisibilityUpdate);
-
-function showForm(id) {
- let id2 = id === undefined ? 'hidden-form' : id;
- let formElement = document.getElementById(id2);
- let displayStyle = formElement.style.display;
- if (displayStyle === '') {
- formElement.style.display = 'block';
- } else {
- formElement.style.display = '';
- }
- animate('animate-icon', 'pulse-img');
-}
-
-function channelFilterDownload(value) {
- if (value === 'all') {
- window.location = '/downloads/';
- } else {
- window.location.search = '?channel=' + value;
- }
-}
-
-function showOverwrite() {
- let overwriteDiv = document.getElementById('overwrite-form');
- if (overwriteDiv.classList.contains('hidden-overwrite')) {
- overwriteDiv.classList.remove('hidden-overwrite');
- } else {
- overwriteDiv.classList.add('hidden-overwrite');
- }
-}
-
-function animate(elementId, animationClass) {
- let toAnimate = document.getElementById(elementId);
- if (toAnimate.className !== animationClass) {
- toAnimate.className = animationClass;
- } else {
- toAnimate.classList.remove(animationClass);
- }
-}
-
-// keep track of changes to the subtitles list made with the native UI
-// needed so that when toggling subtitles with the shortcut we go to the last selected one, not the first one
-addEventListener('DOMContentLoaded', recordTextTrackChanges);
-
-let lastSeenTextTrack = 0;
-function recordTextTrackChanges() {
- let player = getVideoPlayer();
- if (player == null) {
- return;
- }
- player.textTracks.addEventListener('change', () => {
- let active = [...player.textTracks].findIndex(x => x.mode === 'showing');
- if (active !== -1) {
- lastSeenTextTrack = active;
- }
- });
-}
-
-// keyboard shortcuts for the video player
-// need useCapture so we can prevent events from reaching the player
-document.addEventListener('keydown', doShortcut, true);
-
-let modalHideTimeout = -1;
-function showModal(html, duration) {
- let modal = document.querySelector('.video-modal-text');
- modal.innerHTML = html;
- modal.style.display = 'initial';
- clearTimeout(modalHideTimeout);
- modalHideTimeout = setTimeout(() => {
- modal.style.display = 'none';
- }, duration);
-}
-
-let videoSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
-function doShortcut(e) {
- if (!(e.target instanceof HTMLElement)) {
- return;
- }
- let target = e.target;
- let targetName = target.nodeName.toLowerCase();
- if (
- targetName === 'textarea' ||
- targetName === 'input' ||
- targetName === 'select' ||
- target.isContentEditable
- ) {
- return;
- }
- if (e.altKey || e.ctrlKey || e.metaKey) {
- return;
- }
- let player = getVideoPlayer();
- if (player == null) {
- // not on the video page
- return;
- }
- switch (e.key) {
- case 'c': {
- // toggle captions
- let tracks = [...player.textTracks];
- if (tracks.length === 0) {
- break;
- }
- let active = tracks.find(x => x.mode === 'showing');
- if (active != null) {
- active.mode = 'disabled';
- } else {
- tracks[lastSeenTextTrack].mode = 'showing';
- }
- break;
- }
- case 'm': {
- player.muted = !player.muted;
- break;
- }
- case 'f': {
- e.preventDefault();
- if (document.fullscreenElement === null) {
- player.requestFullscreen().catch(e => {
- console.error(e);
- showModal('Unable to enter fullscreen', 3000);
- });
- } else {
- document.exitFullscreen().catch(e => {
- console.error(e);
- showModal('Unable to exit fullscreen', 3000);
- });
- }
- break;
- }
- case 'ArrowLeft': {
- e.preventDefault();
- showModal('- 5 seconds', 500);
- player.currentTime -= 5;
- break;
- }
- case 'ArrowRight': {
- e.preventDefault();
- showModal('+ 5 seconds', 500);
- player.currentTime += 5;
- break;
- }
- case '<':
- case '>': {
- // change speed
- let currentSpeedIdx = videoSpeeds.findIndex(s => s >= player.playbackRate);
- if (currentSpeedIdx === -1) {
- // handle the case where the user manually set the speed above our max speed
- currentSpeedIdx = videoSpeeds.length - 1;
- }
- let newSpeedIdx =
- e.key === '<'
- ? Math.max(0, currentSpeedIdx - 1)
- : Math.min(videoSpeeds.length - 1, currentSpeedIdx + 1);
- let newSpeed = videoSpeeds[newSpeedIdx];
- player.playbackRate = newSpeed;
- showModal(newSpeed + 'x', 500);
- break;
- }
- case ' ': {
- e.preventDefault();
- if (player.paused) {
- player.play();
- } else {
- player.pause();
- }
- break;
- }
- case '?': {
- showModal(
- `
-
- Show help ?
- Toggle mute m
- Toggle fullscreen f
- Toggle subtitles (if available) c
- Increase speed >
- Decrease speed <
- Back 5 seconds ←
- Forward 5 seconds →
- `,
- 3000
- );
- break;
- }
- }
-}
diff --git a/tubearchivist/static/stats.js b/tubearchivist/static/stats.js
deleted file mode 100644
index 70812e7b..00000000
--- a/tubearchivist/static/stats.js
+++ /dev/null
@@ -1,414 +0,0 @@
-// build stats for settings page
-
-'use strict';
-
-/* globals apiRequest */
-
-function primaryStats() {
- let apiVideoEndpoint = '/api/stats/video/';
- let responseData = apiRequest(apiVideoEndpoint, 'GET');
-
- let activeBox = document.getElementById('activeBox');
- clearLoading(activeBox);
-
- let totalTile = buildTotalVideoTile(responseData);
- activeBox.appendChild(totalTile);
- let activeTile = buildActiveVideoTile(responseData);
- activeBox.appendChild(activeTile);
- let inActiveTile = buildInActiveVideoTile(responseData);
- activeBox.appendChild(inActiveTile);
-
- let videoTypeBox = document.getElementById('videoTypeBox');
- clearLoading(videoTypeBox);
-
- let videosTypeTile = buildVideosTypeTile(responseData);
- videoTypeBox.appendChild(videosTypeTile);
- let shortsTypeTile = buildShortsTypeTile(responseData);
- videoTypeBox.appendChild(shortsTypeTile);
- let streamsTypeTile = buildStreamsTypeTile(responseData);
- videoTypeBox.appendChild(streamsTypeTile);
-}
-
-function secondaryStats() {
- let apiChannelEndpoint = '/api/stats/channel/';
- let channelResponseData = apiRequest(apiChannelEndpoint, 'GET');
- let secondaryBox = document.getElementById('secondaryBox');
- clearLoading(secondaryBox);
- let channelTile = buildChannelTile(channelResponseData);
- secondaryBox.appendChild(channelTile);
-
- let apiPlaylistEndpoint = '/api/stats/playlist/';
- let playlistResponseData = apiRequest(apiPlaylistEndpoint, 'GET');
- let playlistTile = buildPlaylistTile(playlistResponseData);
- secondaryBox.appendChild(playlistTile);
-
- let apiDownloadEndpoint = '/api/stats/download/';
- let downloadResponseData = apiRequest(apiDownloadEndpoint, 'GET');
- let downloadTile = buildDownloadTile(downloadResponseData);
- secondaryBox.appendChild(downloadTile);
-}
-
-function buildTotalVideoTile(responseData) {
- const totalCount = responseData.doc_count || 0;
- const totalSize = humanFileSize(responseData.media_size || 0);
- const content = {
- Videos: `${totalCount}`,
- 'Media Size': `${totalSize}`,
- Duration: responseData.duration_str,
- };
- const tile = buildTile('All: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildActiveVideoTile(responseData) {
- const activeCount = responseData?.active_true?.doc_count || 0;
- const activeSize = humanFileSize(responseData?.active_true?.media_size || 0);
- const duration = responseData?.active_true?.duration_str || 'NA';
- const content = {
- Videos: `${activeCount}`,
- 'Media Size': `${activeSize}`,
- Duration: duration,
- };
- const tile = buildTile('Active: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildInActiveVideoTile(responseData) {
- const inActiveCount = responseData?.active_false?.doc_count || 0;
- const inActiveSize = humanFileSize(responseData?.active_false?.media_size || 0);
- const duration = responseData?.active_false?.duration_str || 'NA';
- const content = {
- Videos: `${inActiveCount}`,
- 'Media Size': `${inActiveSize}`,
- Duration: duration,
- };
- const tile = buildTile('Inactive: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildVideosTypeTile(responseData) {
- const videosCount = responseData?.type_videos?.doc_count || 0;
- const videosSize = humanFileSize(responseData?.type_videos?.media_size || 0);
- const duration = responseData?.type_videos?.duration_str || 'NA';
- const content = {
- Videos: `${videosCount}`,
- 'Media Size': `${videosSize}`,
- Duration: duration,
- };
- const tile = buildTile('Regular Videos: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildShortsTypeTile(responseData) {
- const shortsCount = responseData?.type_shorts?.doc_count || 0;
- const shortsSize = humanFileSize(responseData?.type_shorts?.media_size || 0);
- const duration = responseData?.type_shorts?.duration_str || 'NA';
- const content = {
- Videos: `${shortsCount}`,
- 'Media Size': `${shortsSize}`,
- Duration: duration,
- };
- const tile = buildTile('Shorts: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildStreamsTypeTile(responseData) {
- const streamsCount = responseData?.type_streams?.doc_count || 0;
- const streamsSize = humanFileSize(responseData?.type_streams?.media_size || 0);
- const duration = responseData?.type_streams?.duration_str || 'NA';
- const content = {
- Videos: `${streamsCount}`,
- 'Media Size': `${streamsSize}`,
- Duration: duration,
- };
- const tile = buildTile('Streams: ');
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
- return tile;
-}
-
-function buildChannelTile(responseData) {
- let tile = buildTile('Channels: ');
- const total = responseData.doc_count || 0;
- const subscribed = responseData.subscribed_true || 0;
- const active = responseData.active_true || 0;
- const content = {
- Subscribed: subscribed,
- Active: active,
- Total: total,
- };
- const table = buildTileContenTable(content, 3);
- tile.appendChild(table);
-
- return tile;
-}
-
-function buildPlaylistTile(responseData) {
- let tile = buildTile('Playlists: ');
- const total = responseData.doc_count || 0;
- const subscribed = responseData.subscribed_true || 0;
- const active = responseData.active_true || 0;
- const content = {
- Subscribed: subscribed,
- Active: active,
- Total: total,
- };
- const table = buildTileContenTable(content, 2);
- tile.appendChild(table);
-
- return tile;
-}
-
-function buildDownloadTile(responseData) {
- const pendingTotal = responseData.pending || 0;
- let tile = buildTile(`Downloads Pending: ${pendingTotal}`);
- const pendingVideos = responseData.pending_videos || 0;
- const pendingShorts = responseData.pending_shorts || 0;
- const pendingStreams = responseData.pending_streams || 0;
- const content = {
- Videos: pendingVideos,
- Shorts: pendingShorts,
- Streams: pendingStreams,
- };
- const table = buildTileContenTable(content, 3);
- tile.appendChild(table);
-
- return tile;
-}
-
-function watchStats() {
- let apiEndpoint = '/api/stats/watch/';
- let responseData = apiRequest(apiEndpoint, 'GET');
- let watchBox = document.getElementById('watchBox');
- clearLoading(watchBox);
-
- let watchedTile = buildWatchTile('watched', responseData.watched);
- watchBox.appendChild(watchedTile);
-
- let unwatchedTile = buildWatchTile('unwatched', responseData.unwatched);
- watchBox.appendChild(unwatchedTile);
-}
-
-function buildWatchTile(title, watchDetail) {
- const items = watchDetail?.items ?? 0;
- const duration = watchDetail?.duration ?? 0;
- const duration_str = watchDetail?.duration_str ?? '0s';
- const hasProgess = !!watchDetail?.progress;
- const progress = (Number(watchDetail?.progress) * 100).toFixed(2) ?? '0';
-
- let titleCapizalized = capitalizeFirstLetter(title);
-
- if (hasProgess) {
- titleCapizalized = `${progress}% ` + titleCapizalized;
- }
-
- let tile = buildTile(titleCapizalized);
-
- const content = {
- Videos: items,
- Seconds: duration,
- Duration: duration_str,
- };
-
- const table = buildTileContenTable(content, 3);
-
- tile.appendChild(table);
-
- return tile;
-}
-
-function downloadHist() {
- let apiEndpoint = '/api/stats/downloadhist/';
- let responseData = apiRequest(apiEndpoint, 'GET');
- let histBox = document.getElementById('downHistBox');
- clearLoading(histBox);
- if (responseData.length === 0) {
- let tile = buildTile('No recent downloads');
- histBox.appendChild(tile);
- return;
- }
-
- for (let i = 0; i < responseData.length; i++) {
- const dailyStat = responseData[i];
- let tile = buildDailyStat(dailyStat);
- histBox.appendChild(tile);
- }
-}
-
-function buildDailyStat(dailyStat) {
- let tile = buildTile(dailyStat.date);
- let message = document.createElement('p');
- const isExactlyOne = dailyStat.count === 1;
-
- let text = 'Videos';
- if (isExactlyOne) {
- text = 'Video';
- }
-
- message.innerText = `+${dailyStat.count} ${text}
- ${humanFileSize(dailyStat.media_size)}`;
-
- tile.appendChild(message);
- return tile;
-}
-
-function buildChannelRow(id, name, value) {
- let tableRow = document.createElement('tr');
-
- tableRow.innerHTML = `
- ${name}
- ${value}
- `;
-
- return tableRow;
-}
-
-function addBiggestChannelByDocCount() {
- let tBody = document.getElementById('biggestChannelTableVideos');
-
- let apiEndpoint = '/api/stats/biggestchannels/?order=doc_count';
- const responseData = apiRequest(apiEndpoint, 'GET');
-
- for (let i = 0; i < responseData.length; i++) {
- const { id, name, doc_count } = responseData[i];
-
- let tableRow = buildChannelRow(id, name, doc_count);
-
- tBody.appendChild(tableRow);
- }
-}
-
-function addBiggestChannelByDuration() {
- const tBody = document.getElementById('biggestChannelTableDuration');
-
- let apiEndpoint = '/api/stats/biggestchannels/?order=duration';
- const responseData = apiRequest(apiEndpoint, 'GET');
-
- for (let i = 0; i < responseData.length; i++) {
- const { id, name, duration_str } = responseData[i];
-
- let tableRow = buildChannelRow(id, name, duration_str);
-
- tBody.appendChild(tableRow);
- }
-}
-
-function addBiggestChannelByMediaSize() {
- let tBody = document.getElementById('biggestChannelTableMediaSize');
-
- let apiEndpoint = '/api/stats/biggestchannels/?order=media_size';
- const responseData = apiRequest(apiEndpoint, 'GET');
-
- for (let i = 0; i < responseData.length; i++) {
- const { id, name, media_size } = responseData[i];
-
- let tableRow = buildChannelRow(id, name, humanFileSize(media_size));
-
- tBody.appendChild(tableRow);
- }
-}
-
-function clearLoading(dashBox) {
- dashBox.querySelector('#loading').remove();
-}
-
-function capitalizeFirstLetter(string) {
- // source: https://stackoverflow.com/a/1026087
- return string.charAt(0).toUpperCase() + string.slice(1);
-}
-
-function humanFileSize(size) {
- let i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
- return (size / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
-}
-
-function buildTile(titleText) {
- let tile = document.createElement('div');
- tile.classList.add('info-box-item');
-
- let title = document.createElement('h3');
-
- title.innerText = titleText;
- tile.appendChild(title);
-
- return tile;
-}
-
-function buildTileContenTable(content, rowsWanted) {
- let contentEntries = Object.entries(content);
-
- const nbsp = '\u00A0'; // No-Break Space https://www.compart.com/en/unicode/U+00A0
-
- // Do not add spacing rows when on mobile device
- const isMobile = window.matchMedia('(max-width: 600px)');
- if (!isMobile.matches) {
- if (contentEntries.length < rowsWanted) {
- const rowsToAdd = rowsWanted - contentEntries.length;
-
- for (let i = 0; i < rowsToAdd; i++) {
- contentEntries.push([nbsp, nbsp]);
- }
- }
- }
-
- const table = document.createElement('table');
- table.classList.add('agg-channel-table');
- const tableBody = document.createElement('tbody');
-
- for (const [key, value] of contentEntries) {
- const row = document.createElement('tr');
-
- const leftCell = document.createElement('td');
- leftCell.classList.add('agg-channel-name');
-
- // Do not add ":" when its a spacing entry
- const keyText = key === nbsp ? key : `${key}: `;
- const leftText = document.createTextNode(keyText);
- leftCell.appendChild(leftText);
-
- const rightCell = document.createElement('td');
- rightCell.classList.add('agg-channel-right-align');
-
- const rightText = document.createTextNode(value);
- rightCell.appendChild(rightText);
-
- row.appendChild(leftCell);
- row.appendChild(rightCell);
-
- tableBody.appendChild(row);
- }
-
- table.appendChild(tableBody);
-
- return table;
-}
-
-function biggestChannel() {
- addBiggestChannelByDocCount();
- addBiggestChannelByDuration();
- addBiggestChannelByMediaSize();
-}
-
-async function buildStats() {
- primaryStats();
- secondaryStats();
- watchStats();
- downloadHist();
- biggestChannel();
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- window.requestAnimationFrame(() => {
- buildStats();
- });
-});