Split settings pages, new dashbord, RC, #build
Changed: - Changed split settings page - Changed reset autostart on queue stop - Added stats dashbord - Fixed for wrong date epoch indexing
This commit is contained in:
commit
c3da3e23af
|
@ -165,14 +165,15 @@ bin/elasticsearch-service-tokens create elastic/kibana kibana
|
||||||
|
|
||||||
Example docker compose, use same version as for Elasticsearch:
|
Example docker compose, use same version as for Elasticsearch:
|
||||||
```yml
|
```yml
|
||||||
kibana:
|
services:
|
||||||
image: docker.elastic.co/kibana/kibana:0.0.0
|
kibana:
|
||||||
container_name: kibana
|
image: docker.elastic.co/kibana/kibana:0.0.0
|
||||||
environment:
|
container_name: kibana
|
||||||
- "ELASTICSEARCH_HOSTS=http://archivist-es:9200"
|
environment:
|
||||||
- "ELASTICSEARCH_SERVICEACCOUNTTOKEN=<your-token-here>"
|
- "ELASTICSEARCH_HOSTS=http://archivist-es:9200"
|
||||||
ports:
|
- "ELASTICSEARCH_SERVICEACCOUNTTOKEN=<your-token-here>"
|
||||||
- "5601:5601"
|
ports:
|
||||||
|
- "5601:5601"
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to run queries on the Elasticsearch container directly from your host with for example `curl` or something like *postman*, you might want to **publish** the port 9200 instead of just **exposing** it.
|
If you want to run queries on the Elasticsearch container directly from your host with for example `curl` or something like *postman*, you might want to **publish** the port 9200 instead of just **exposing** it.
|
||||||
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
"""aggregations"""
|
||||||
|
|
||||||
|
from home.src.es.connect import ElasticWrap
|
||||||
|
from home.src.index.video_streams import DurationConverter
|
||||||
|
|
||||||
|
|
||||||
|
class AggBase:
|
||||||
|
"""base class for aggregation calls"""
|
||||||
|
|
||||||
|
path: str = ""
|
||||||
|
data: dict = {}
|
||||||
|
name: str = ""
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""make get call"""
|
||||||
|
response, _ = ElasticWrap(self.path).get(self.data)
|
||||||
|
print(f"[agg][{self.name}] took {response.get('took')} ms to process")
|
||||||
|
|
||||||
|
return response.get("aggregations")
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""implement in subclassess"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Primary(AggBase):
|
||||||
|
"""primary aggregation for total documents indexed"""
|
||||||
|
|
||||||
|
name = "primary"
|
||||||
|
path = "ta_video,ta_channel,ta_playlist,ta_subtitle,ta_download/_search"
|
||||||
|
data = {
|
||||||
|
"size": 0,
|
||||||
|
"aggs": {
|
||||||
|
"video_type": {
|
||||||
|
"filter": {"exists": {"field": "active"}},
|
||||||
|
"aggs": {"filtered": {"terms": {"field": "vid_type"}}},
|
||||||
|
},
|
||||||
|
"channel_total": {"value_count": {"field": "channel_active"}},
|
||||||
|
"channel_sub": {"terms": {"field": "channel_subscribed"}},
|
||||||
|
"playlist_total": {"value_count": {"field": "playlist_active"}},
|
||||||
|
"playlist_sub": {"terms": {"field": "playlist_subscribed"}},
|
||||||
|
"download": {"terms": {"field": "status"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""make the call"""
|
||||||
|
aggregations = self.get()
|
||||||
|
|
||||||
|
videos = {"total": aggregations["video_type"].get("doc_count")}
|
||||||
|
videos.update(
|
||||||
|
{
|
||||||
|
i.get("key"): i.get("doc_count")
|
||||||
|
for i in aggregations["video_type"]["filtered"]["buckets"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
channels = {"total": aggregations["channel_total"].get("value")}
|
||||||
|
channels.update(
|
||||||
|
{
|
||||||
|
"sub_" + i.get("key_as_string"): i.get("doc_count")
|
||||||
|
for i in aggregations["channel_sub"]["buckets"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
playlists = {"total": aggregations["playlist_total"].get("value")}
|
||||||
|
playlists.update(
|
||||||
|
{
|
||||||
|
"sub_" + i.get("key_as_string"): i.get("doc_count")
|
||||||
|
for i in aggregations["playlist_sub"]["buckets"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
downloads = {
|
||||||
|
i.get("key"): i.get("doc_count")
|
||||||
|
for i in aggregations["download"]["buckets"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"videos": videos,
|
||||||
|
"channels": channels,
|
||||||
|
"playlists": playlists,
|
||||||
|
"downloads": downloads,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class WatchProgress(AggBase):
|
||||||
|
"""get watch progress"""
|
||||||
|
|
||||||
|
name = "watch_progress"
|
||||||
|
path = "ta_video/_search"
|
||||||
|
data = {
|
||||||
|
"size": 0,
|
||||||
|
"aggs": {
|
||||||
|
name: {
|
||||||
|
"terms": {"field": "player.watched"},
|
||||||
|
"aggs": {
|
||||||
|
"watch_docs": {
|
||||||
|
"filter": {"terms": {"player.watched": [True, False]}},
|
||||||
|
"aggs": {
|
||||||
|
"true_count": {"value_count": {"field": "_index"}},
|
||||||
|
"duration": {"sum": {"field": "player.duration"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_duration": {"sum": {"field": "player.duration"}},
|
||||||
|
"total_vids": {"value_count": {"field": "_index"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""make the call"""
|
||||||
|
aggregations = self.get()
|
||||||
|
buckets = aggregations[self.name]["buckets"]
|
||||||
|
|
||||||
|
response = {}
|
||||||
|
all_duration = int(aggregations["total_duration"].get("value"))
|
||||||
|
response.update(
|
||||||
|
{
|
||||||
|
"all": {
|
||||||
|
"duration": all_duration,
|
||||||
|
"duration_str": DurationConverter().get_str(all_duration),
|
||||||
|
"items": aggregations["total_vids"].get("value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for bucket in buckets:
|
||||||
|
response.update(self._build_bucket(bucket, all_duration))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_bucket(bucket, all_duration):
|
||||||
|
"""parse bucket"""
|
||||||
|
|
||||||
|
duration = int(bucket["watch_docs"]["duration"]["value"])
|
||||||
|
duration_str = DurationConverter().get_str(duration)
|
||||||
|
items = bucket["watch_docs"]["true_count"]["value"]
|
||||||
|
if bucket["key_as_string"] == "false":
|
||||||
|
key = "unwatched"
|
||||||
|
else:
|
||||||
|
key = "watched"
|
||||||
|
|
||||||
|
bucket_parsed = {
|
||||||
|
key: {
|
||||||
|
"duration": duration,
|
||||||
|
"duration_str": duration_str,
|
||||||
|
"progress": duration / all_duration if all_duration else 0,
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket_parsed
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadHist(AggBase):
|
||||||
|
"""get downloads histogram last week"""
|
||||||
|
|
||||||
|
name = "videos_last_week"
|
||||||
|
path = "ta_video/_search"
|
||||||
|
data = {
|
||||||
|
"size": 0,
|
||||||
|
"aggs": {
|
||||||
|
name: {
|
||||||
|
"date_histogram": {
|
||||||
|
"field": "date_downloaded",
|
||||||
|
"calendar_interval": "day",
|
||||||
|
"format": "yyyy-MM-dd",
|
||||||
|
"order": {"_key": "desc"},
|
||||||
|
},
|
||||||
|
"aggs": {
|
||||||
|
"total_videos": {"value_count": {"field": "youtube_id"}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": {"range": {"date_downloaded": {"gte": "now-7d/d"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""process query"""
|
||||||
|
aggregations = self.get()
|
||||||
|
buckets = aggregations[self.name]["buckets"]
|
||||||
|
|
||||||
|
response = [
|
||||||
|
{
|
||||||
|
"date": i.get("key_as_string"),
|
||||||
|
"count": i.get("doc_count"),
|
||||||
|
}
|
||||||
|
for i in buckets
|
||||||
|
]
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class BiggestChannel(AggBase):
|
||||||
|
"""get channel aggregations"""
|
||||||
|
|
||||||
|
name = "channel_stats"
|
||||||
|
path = "ta_video/_search"
|
||||||
|
data = {
|
||||||
|
"size": 0,
|
||||||
|
"aggs": {
|
||||||
|
name: {
|
||||||
|
"multi_terms": {
|
||||||
|
"terms": [
|
||||||
|
{"field": "channel.channel_name.keyword"},
|
||||||
|
{"field": "channel.channel_id"},
|
||||||
|
],
|
||||||
|
"order": {"doc_count": "desc"},
|
||||||
|
},
|
||||||
|
"aggs": {
|
||||||
|
"doc_count": {"value_count": {"field": "_index"}},
|
||||||
|
"duration": {"sum": {"field": "player.duration"}},
|
||||||
|
"media_size": {"sum": {"field": "media_size"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
order_choices = ["doc_count", "duration", "media_size"]
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""process aggregation, order_by validated in the view"""
|
||||||
|
|
||||||
|
aggregations = self.get()
|
||||||
|
buckets = aggregations[self.name]["buckets"]
|
||||||
|
|
||||||
|
response = [
|
||||||
|
{
|
||||||
|
"id": i["key"][1],
|
||||||
|
"name": i["key"][0].title(),
|
||||||
|
"doc_count": i["doc_count"]["value"],
|
||||||
|
"duration": i["duration"]["value"],
|
||||||
|
"duration_str": DurationConverter().get_str(
|
||||||
|
i["duration"]["value"]
|
||||||
|
),
|
||||||
|
"media_size": i["media_size"]["value"],
|
||||||
|
}
|
||||||
|
for i in buckets
|
||||||
|
]
|
||||||
|
|
||||||
|
return response
|
|
@ -136,4 +136,24 @@ urlpatterns = [
|
||||||
views.NotificationView.as_view(),
|
views.NotificationView.as_view(),
|
||||||
name="api-notification",
|
name="api-notification",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"stats/primary/",
|
||||||
|
views.StatPrimaryView.as_view(),
|
||||||
|
name="api-stats-primary",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"stats/watch/",
|
||||||
|
views.StatWatchProgress.as_view(),
|
||||||
|
name="api-stats-watch",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"stats/downloadhist/",
|
||||||
|
views.StatDownloadHist.as_view(),
|
||||||
|
name="api-stats-downloadhist",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"stats/biggestchannels/",
|
||||||
|
views.StatBiggestChannel.as_view(),
|
||||||
|
name="api-stats-biggestchannels",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""all API views"""
|
"""all API views"""
|
||||||
|
|
||||||
|
from api.src.aggs import BiggestChannel, DownloadHist, Primary, WatchProgress
|
||||||
from api.src.search_processor import SearchProcess
|
from api.src.search_processor import SearchProcess
|
||||||
from home.src.download.queue import PendingInteract
|
from home.src.download.queue import PendingInteract
|
||||||
from home.src.download.subscriptions import (
|
from home.src.download.subscriptions import (
|
||||||
|
@ -975,3 +976,58 @@ class NotificationView(ApiBaseView):
|
||||||
query = f"{query}:{filter_by}"
|
query = f"{query}:{filter_by}"
|
||||||
|
|
||||||
return Response(RedisArchivist().list_items(query))
|
return Response(RedisArchivist().list_items(query))
|
||||||
|
|
||||||
|
|
||||||
|
class StatPrimaryView(ApiBaseView):
|
||||||
|
"""resolves to /api/stats/primary/
|
||||||
|
GET: return document count
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""get stats"""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
return Response(Primary().process())
|
||||||
|
|
||||||
|
|
||||||
|
class StatWatchProgress(ApiBaseView):
|
||||||
|
"""resolves to /api/stats/watchprogress/
|
||||||
|
GET: return watch/unwatch progress stats
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""handle get request"""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
return Response(WatchProgress().process())
|
||||||
|
|
||||||
|
|
||||||
|
class StatDownloadHist(ApiBaseView):
|
||||||
|
"""resolves to /api/stats/downloadhist/
|
||||||
|
GET: return download video count histogram for last days
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""handle get request"""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
return Response(DownloadHist().process())
|
||||||
|
|
||||||
|
|
||||||
|
class StatBiggestChannel(ApiBaseView):
|
||||||
|
"""resolves to /api/stats/biggestchannels/
|
||||||
|
GET: return biggest channels
|
||||||
|
param: order
|
||||||
|
"""
|
||||||
|
|
||||||
|
order_choices = ["doc_count", "duration", "media_size"]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""handle get request"""
|
||||||
|
|
||||||
|
order = request.GET.get("order", False)
|
||||||
|
if order and order not in self.order_choices:
|
||||||
|
message = {"message": f"invalid order parameter {order}"}
|
||||||
|
return Response(message, status=400)
|
||||||
|
|
||||||
|
return Response(BiggestChannel().process())
|
||||||
|
|
|
@ -12,7 +12,6 @@ from home.src.index.channel import YoutubeChannel
|
||||||
from home.src.index.playlist import YoutubePlaylist
|
from home.src.index.playlist import YoutubePlaylist
|
||||||
from home.src.index.video_constants import VideoTypeEnum
|
from home.src.index.video_constants import VideoTypeEnum
|
||||||
from home.src.ta.config import AppConfig
|
from home.src.ta.config import AppConfig
|
||||||
from home.src.ta.ta_redis import RedisArchivist
|
|
||||||
from home.src.ta.urlparser import Parser
|
from home.src.ta.urlparser import Parser
|
||||||
|
|
||||||
|
|
||||||
|
@ -197,16 +196,13 @@ class PlaylistSubscription:
|
||||||
thumb = ThumbManager(playlist_id, item_type="playlist")
|
thumb = ThumbManager(playlist_id, item_type="playlist")
|
||||||
thumb.download_playlist_thumb(url)
|
thumb.download_playlist_thumb(url)
|
||||||
|
|
||||||
# notify
|
if self.task:
|
||||||
message = {
|
self.task.send_progress(
|
||||||
"status": "message:subplaylist",
|
message_lines=[
|
||||||
"level": "info",
|
f"Processing {idx + 1} of {len(new_playlists)}"
|
||||||
"title": "Subscribing to Playlists",
|
],
|
||||||
"message": f"Processing {idx + 1} of {len(new_playlists)}",
|
progress=(idx + 1) / len(new_playlists),
|
||||||
}
|
)
|
||||||
RedisArchivist().set_message(
|
|
||||||
"message:subplaylist", message=message, expire=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def channel_validate(channel_id):
|
def channel_validate(channel_id):
|
||||||
|
|
|
@ -163,6 +163,7 @@ class VideoDownloader:
|
||||||
while True:
|
while True:
|
||||||
video_data = self._get_next(auto_only)
|
video_data = self._get_next(auto_only)
|
||||||
if self.task.is_stopped() or not video_data:
|
if self.task.is_stopped() or not video_data:
|
||||||
|
self._reset_auto()
|
||||||
break
|
break
|
||||||
|
|
||||||
youtube_id = video_data.get("youtube_id")
|
youtube_id = video_data.get("youtube_id")
|
||||||
|
@ -405,3 +406,18 @@ class VideoDownloader:
|
||||||
self.channels.add(channel_id)
|
self.channels.add(channel_id)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _reset_auto(self):
|
||||||
|
"""reset autostart to defaults after queue stop"""
|
||||||
|
path = "ta_download/_update_by_query"
|
||||||
|
data = {
|
||||||
|
"query": {"term": {"auto_start": {"value": True}}},
|
||||||
|
"script": {
|
||||||
|
"source": "ctx._source.auto_start = false",
|
||||||
|
"lang": "painless",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, _ = ElasticWrap(path, config=self.config).post(data=data)
|
||||||
|
updated = response.get("updated")
|
||||||
|
if updated:
|
||||||
|
print(f"[download] reset auto start on {updated} videos.")
|
||||||
|
|
|
@ -37,7 +37,8 @@
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
"channel_last_refresh": {
|
"channel_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"channel_tags": {
|
"channel_tags": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -90,7 +91,8 @@
|
||||||
"index": false
|
"index": false
|
||||||
},
|
},
|
||||||
"date_downloaded": {
|
"date_downloaded": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -129,7 +131,8 @@
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
"channel_last_refresh": {
|
"channel_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"channel_tags": {
|
"channel_tags": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -196,7 +199,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vid_last_refresh": {
|
"vid_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"youtube_id": {
|
"youtube_id": {
|
||||||
"type": "keyword"
|
"type": "keyword"
|
||||||
|
@ -289,7 +293,8 @@
|
||||||
"sponsorblock": {
|
"sponsorblock": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"last_refresh": {
|
"last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"has_unlocked": {
|
"has_unlocked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
@ -341,7 +346,8 @@
|
||||||
"index_name": "download",
|
"index_name": "download",
|
||||||
"expected_map": {
|
"expected_map": {
|
||||||
"timestamp": {
|
"timestamp": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"channel_id": {
|
"channel_id": {
|
||||||
"type": "keyword"
|
"type": "keyword"
|
||||||
|
@ -439,7 +445,8 @@
|
||||||
"type": "keyword"
|
"type": "keyword"
|
||||||
},
|
},
|
||||||
"playlist_last_refresh": {
|
"playlist_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"expected_set": {
|
"expected_set": {
|
||||||
|
@ -493,7 +500,8 @@
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
"subtitle_last_refresh": {
|
"subtitle_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"subtitle_index": {
|
"subtitle_index": {
|
||||||
"type" : "long"
|
"type" : "long"
|
||||||
|
@ -528,7 +536,8 @@
|
||||||
"type": "keyword"
|
"type": "keyword"
|
||||||
},
|
},
|
||||||
"comment_last_refresh": {
|
"comment_last_refresh": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"comment_channel_id": {
|
"comment_channel_id": {
|
||||||
"type": "keyword"
|
"type": "keyword"
|
||||||
|
@ -542,7 +551,8 @@
|
||||||
"type" : "text"
|
"type" : "text"
|
||||||
},
|
},
|
||||||
"comment_timestamp": {
|
"comment_timestamp": {
|
||||||
"type": "date"
|
"type": "date",
|
||||||
|
"format": "epoch_second"
|
||||||
},
|
},
|
||||||
"comment_time_text": {
|
"comment_time_text": {
|
||||||
"type" : "text"
|
"type" : "text"
|
||||||
|
|
|
@ -205,10 +205,11 @@ class ScheduleBuilder:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(f"failed: {key} {value}")
|
print(f"failed: {key} {value}")
|
||||||
mess_dict = {
|
mess_dict = {
|
||||||
"status": self.MSG,
|
"group": "setting:schedule",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"title": "Scheduler update failed.",
|
"title": "Scheduler update failed.",
|
||||||
"message": "Invalid schedule input",
|
"messages": ["Invalid schedule input"],
|
||||||
|
"id": "0000",
|
||||||
}
|
}
|
||||||
RedisArchivist().set_message(
|
RedisArchivist().set_message(
|
||||||
self.MSG, mess_dict, expire=True
|
self.MSG, mess_dict, expire=True
|
||||||
|
@ -227,10 +228,11 @@ class ScheduleBuilder:
|
||||||
|
|
||||||
RedisArchivist().set_message("config", redis_config, save=True)
|
RedisArchivist().set_message("config", redis_config, save=True)
|
||||||
mess_dict = {
|
mess_dict = {
|
||||||
"status": self.MSG,
|
"group": "setting:schedule",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"title": "Scheduler changed.",
|
"title": "Scheduler changed.",
|
||||||
"message": "Please restart container for changes to take effect",
|
"messages": ["Restart container for changes to take effect"],
|
||||||
|
"id": "0000",
|
||||||
}
|
}
|
||||||
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
|
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
|
||||||
|
|
||||||
|
@ -344,6 +346,7 @@ class ReleaseVersion:
|
||||||
|
|
||||||
def get_remote_version(self):
|
def get_remote_version(self):
|
||||||
"""read version from remote"""
|
"""read version from remote"""
|
||||||
|
sleep(randint(0, 60))
|
||||||
self.response = requests.get(self.REMOTE_URL, timeout=20).json()
|
self.response = requests.get(self.REMOTE_URL, timeout=20).json()
|
||||||
remote_version_str = self.response["release_version"]
|
remote_version_str = self.response["release_version"]
|
||||||
self.remote_version = self._parse_version(remote_version_str)
|
self.remote_version = self._parse_version(remote_version_str)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{# Base file for all of the settings pages to ensure a common menu #}
|
||||||
|
{% extends "home/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="boxed-content">
|
||||||
|
<div class="info-box-item child-page-nav">
|
||||||
|
<a href="{% url 'settings' %}"><h3>Dashboard</h3></a>
|
||||||
|
<a href="{% url 'settings_user' %}"><h3>User</h3></a>
|
||||||
|
<a href="{% url 'settings_application' %}"><h3>Application</h3></a>
|
||||||
|
<a href="{% url 'settings_scheduling' %}"><h3>Scheduling</h3></a>
|
||||||
|
<a href="{% url 'settings_actions' %}"><h3>Actions</h3></a>
|
||||||
|
</div>
|
||||||
|
<div id="notifications" data=""></div>
|
||||||
|
{% block settings_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="{% static 'progress.js' %}"></script>
|
||||||
|
{% endblock content %}
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="channel-banner">
|
<div class="channel-banner">
|
||||||
<a href="/channel/{{ channel_info.channel_id }}/"><img src="/cache/channels/{{ channel_info.channel_id }}_banner.jpg" alt="channel_banner"></a>
|
<a href="/channel/{{ channel_info.channel_id }}/"><img src="/cache/channels/{{ channel_info.channel_id }}_banner.jpg" alt="channel_banner"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box-item channel-nav">
|
<div class="info-box-item child-page-nav">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
{% if has_streams %}
|
{% if has_streams %}
|
||||||
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="channel-banner">
|
<div class="channel-banner">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box-item channel-nav">
|
<div class="info-box-item child-page-nav">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
{% if has_streams %}
|
{% if has_streams %}
|
||||||
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="channel-banner">
|
<div class="channel-banner">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box-item channel-nav">
|
<div class="info-box-item child-page-nav">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
{% if has_streams %}
|
{% if has_streams %}
|
||||||
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
<a href="{% url 'channel_id_live' channel_info.channel_id %}"><h3>Streams</h3></a>
|
||||||
|
|
|
@ -1,441 +1,42 @@
|
||||||
{% extends "home/base.html" %}
|
{% extends "home/base_settings.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block settings_content %}
|
||||||
<div class="boxed-content">
|
<div class="title-bar">
|
||||||
<div id="notifications" data="setting reindex"></div>
|
<h1>Your Archive</h1>
|
||||||
<div class="title-bar">
|
|
||||||
<h1>User Configurations</h1>
|
|
||||||
</div>
|
|
||||||
<form action="/settings/" method="POST" name="user-update">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Color scheme</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current color scheme: <span class="settings-current">{{ config.application.colors }}</span></p>
|
|
||||||
<i>Select your preferred color scheme between dark and light mode.</i><br>
|
|
||||||
{{ user_form.colors }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Archive View</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current page size: <span class="settings-current">{{ config.archive.page_size }}</span></p>
|
|
||||||
<i>Result of videos showing in archive page</i><br>
|
|
||||||
{{ user_form.page_size }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" name="user-settings">Update User Configurations</button>
|
|
||||||
</form>
|
|
||||||
<div class="title-bar">
|
|
||||||
<h1>Application Configurations</h1>
|
|
||||||
</div>
|
|
||||||
<form action="/settings/" method="POST" name="application-update">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="subscriptions">Subscriptions</h2>
|
|
||||||
<p>Disable shorts or streams by setting their page size to 0 (zero).</p>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>YouTube page size: <span class="settings-current">{{ config.subscriptions.channel_size }}</span></p>
|
|
||||||
<i>Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
|
||||||
{{ app_form.subscriptions_channel_size }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>YouTube Live page size: <span class="settings-current">{{ config.subscriptions.live_channel_size }}</span></p>
|
|
||||||
<i>Live Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
|
||||||
{{ app_form.subscriptions_live_channel_size }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>YouTube Shorts page size: <span class="settings-current">{{ config.subscriptions.shorts_channel_size }}</span></p>
|
|
||||||
<i>Shorts Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
|
||||||
{{ app_form.subscriptions_shorts_channel_size }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Auto start download from your subscriptions: <span class="settings-current">{{ config.subscriptions.auto_start}}</span></p>
|
|
||||||
<i>Enable this will automatically start and prioritize videos from your subscriptions.</i><br>
|
|
||||||
{{ app_form.subscriptions_auto_start }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="downloads">Downloads</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current download speed limit in KB/s: <span class="settings-current">{{ config.downloads.limit_speed }}</span></p>
|
|
||||||
<i>Limit download speed. 0 (zero) to deactivate, e.g. 1000 (1MB/s). Speeds are in KB/s. Setting takes effect on new download jobs or application restart.</i><br>
|
|
||||||
{{ app_form.downloads_limit_speed }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current throttled rate limit in KB/s: <span class="settings-current">{{ config.downloads.throttledratelimit }}</span></p>
|
|
||||||
<i>Download will restart if speeds drop below specified amount. 0 (zero) to deactivate, e.g. 100. Speeds are in KB/s.</i><br>
|
|
||||||
{{ app_form.downloads_throttledratelimit }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current scraping sleep interval: <span class="settings-current">{{ config.downloads.sleep_interval }}</p>
|
|
||||||
<i>Seconds to sleep between calls to YouTube. Might be necessary to avoid throttling. Recommended 3.</i><br>
|
|
||||||
{{ app_form.downloads_sleep_interval }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p><span class="danger-zone">Danger Zone</span>: Current auto delete watched videos: <span class="settings-current">{{ config.downloads.autodelete_days }}</span></p>
|
|
||||||
<i>Auto delete watched videos after x days, 0 (zero) to deactivate:</i><br>
|
|
||||||
{{ app_form.downloads_autodelete_days }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="format">Download Format</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Limit video and audio quality format for yt-dlp.<br>
|
|
||||||
Currently: <span class="settings-current">{{ config.downloads.format }}</span>
|
|
||||||
</p>
|
|
||||||
<p>Example configurations:</p>
|
|
||||||
<ul>
|
|
||||||
<li><span class="settings-current">bestvideo[height<=720]+bestaudio/best[height<=720]</span>: best audio and max video height of 720p.</li>
|
|
||||||
<li><span class="settings-current">bestvideo[height<=1080]+bestaudio/best[height<=1080]</span>: best audio and max video height of 1080p.</li>
|
|
||||||
<li><span class="settings-current">bestvideo[height<=1080][vcodec*=avc1]+bestaudio[acodec*=mp4a]/mp4</span>: Max 1080p video height with iOS compatible video and audio codecs.</li>
|
|
||||||
<li><span class="settings-current">0</span>: deactivate and download the best quality possible as decided by yt-dlp.</li>
|
|
||||||
</ul>
|
|
||||||
<i>Make sure your custom format gets merged into a single file. Check out the <a href="https://github.com/yt-dlp/yt-dlp#format-selection" target="_blank">documentation</a> for valid configurations.</i><br>
|
|
||||||
{{ app_form.downloads_format }}
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Force sort order to have precedence over all yt-dlp fields.<br>
|
|
||||||
Currently: <span class="settings-current">{{ config.downloads.format_sort }}</span>
|
|
||||||
</p>
|
|
||||||
<p>Example configurations:</p>
|
|
||||||
<ul>
|
|
||||||
<li><span class="settings-current">res,codec:av1</span>: prefer AV1 over all other video codecs.</li>
|
|
||||||
<li><span class="settings-current">0</span>: deactivate and keep the default as decided by yt-dlp.</li>
|
|
||||||
</ul>
|
|
||||||
<i>Not all codecs are supported by all browsers. The default value ensures best compatibility. Check out the <a href="https://github.com/yt-dlp/yt-dlp#sorting-formats" target="_blank">documentation</a> for valid configurations.</i><br>
|
|
||||||
{{ app_form.downloads_format_sort }}
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Prefer translated metadata language: <span class="settings-current">{{ config.downloads.extractor_lang }}</span></p>
|
|
||||||
<i>This will change the language this video gets indexed as. That will only be available if the uploader provides translations. Add as two letter ISO language code, check the <a href="https://github.com/yt-dlp/yt-dlp#youtube" target="_blank">documentation</a> which languages are available.</i><br>
|
|
||||||
{{ app_form.downloads_extractor_lang}}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current metadata embed setting: <span class="settings-current">{{ config.downloads.add_metadata }}</span></p>
|
|
||||||
<i>Metadata is not embedded into the downloaded files by default.</i><br>
|
|
||||||
{{ app_form.downloads_add_metadata }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current thumbnail embed setting: <span class="settings-current">{{ config.downloads.add_thumbnail }}</span></p>
|
|
||||||
<i>Embed thumbnail into the mediafile.</i><br>
|
|
||||||
{{ app_form.downloads_add_thumbnail }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="format">Subtitles</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Subtitles download setting: <span class="settings-current">{{ config.downloads.subtitle }}</span><br>
|
|
||||||
<i>Choose which subtitles to download, add comma separated language codes,<br>
|
|
||||||
e.g. <span class="settings-current">en, de, zh-Hans</span></i><br>
|
|
||||||
{{ app_form.downloads_subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Subtitle source settings: <span class="settings-current">{{ config.downloads.subtitle_source }}</span></p>
|
|
||||||
<i>Download only user generated, or also less accurate auto generated subtitles.</i><br>
|
|
||||||
{{ app_form.downloads_subtitle_source }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Index and make subtitles searchable: <span class="settings-current">{{ config.downloads.subtitle_index }}</span></p>
|
|
||||||
<i>Store subtitle lines in Elasticsearch. Not recommended for low-end hardware.</i><br>
|
|
||||||
{{ app_form.downloads_subtitle_index }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="comments">Comments</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Download and index comments: <span class="settings-current">{{ config.downloads.comment_max }}</span><br>
|
|
||||||
<i>Follow the yt-dlp max_comments documentation, <a href="https://github.com/yt-dlp/yt-dlp#youtube" target="_blank">max-comments,max-parents,max-replies,max-replies-per-thread</a>:</i><br>
|
|
||||||
<p>Example configurations:</p>
|
|
||||||
<ul>
|
|
||||||
<li><span class="settings-current">all,100,all,30</span>: Get 100 max-parents and 30 max-replies-per-thread.</li>
|
|
||||||
<li><span class="settings-current">1000,all,all,50</span>: Get a total of 1000 comments over all, 50 replies per thread.</li>
|
|
||||||
</ul>
|
|
||||||
{{ app_form.downloads_comment_max }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Selected comment sort method: <span class="settings-current">{{ config.downloads.comment_sort }}</span><br>
|
|
||||||
<i>Select how many comments and threads to download:</i><br>
|
|
||||||
{{ app_form.downloads_comment_sort }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="format">Cookie</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Import YouTube cookie: <span class="settings-current">{{ config.downloads.cookie_import }}</span><br></p>
|
|
||||||
<p>For automatic cookie import use <b>Tube Archivist Companion</b> <a href="https://github.com/tubearchivist/browser-extension" target="_blank">browser extension</a>.</p>
|
|
||||||
<i>For manual cookie import, place your cookie file named <span class="settings-current">cookies.google.txt</span> in <span class="settings-current">cache/import</span> before enabling. Instructions in the <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki.</a></i><br>
|
|
||||||
{{ app_form.downloads_cookie_import }}<br>
|
|
||||||
{% if config.downloads.cookie_import %}
|
|
||||||
<div id="cookieMessage">
|
|
||||||
<button onclick="handleCookieValidate()" type="button" id="cookieButton">Validate Cookie File</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="integrations">Integrations</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>API token: <button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button></p>
|
|
||||||
<div id="text-reveal" class="description-text">
|
|
||||||
<p>{{ api_token }}</p>
|
|
||||||
<button class="danger-button" type="button" onclick="resetToken()">Revoke</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Integrate with <a href="https://returnyoutubedislike.com/" target="_blank">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p>
|
|
||||||
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
|
|
||||||
{{ app_form.downloads_integrate_ryd }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Integrate with <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a> to get sponsored timestamps: <span class="settings-current">{{ config.downloads.integrate_sponsorblock }}</span></p>
|
|
||||||
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
|
|
||||||
{{ app_form.downloads_integrate_sponsorblock }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2 id="snapshots">Snapshots</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current system snapshot: <span class="settings-current">{{ config.application.enable_snapshot }}</span></p>
|
|
||||||
<i>Automatically create daily deduplicated snapshots of the index, stored in Elasticsearch. Read first before activating: <a target="_blank" href="https://docs.tubearchivist.com/settings/#snapshots">Wiki</a>.</i><br>
|
|
||||||
{{ app_form.application_enable_snapshot }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{% if snapshots %}
|
|
||||||
<p>Create next snapshot: <span class="settings-current">{{ snapshots.next_exec_str }}</span>, snapshots expire after <span class="settings-current">{{ snapshots.expire_after }}</span>. <button onclick="createSnapshot()" id="createButton">Create snapshot now</button></p>
|
|
||||||
<br>
|
|
||||||
{% for snapshot in snapshots.snapshots %}
|
|
||||||
<p><button id="{{ snapshot.id }}" onclick="restoreSnapshot(id)">Restore</button> Snapshot created on: <span class="settings-current">{{ snapshot.start_date }}</span>, took <span class="settings-current">{{ snapshot.duration_s }}s</span> to create. State: <i>{{ snapshot.state }}</i></p>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" name="application-settings">Update Application Configurations</button>
|
|
||||||
</form>
|
|
||||||
<div class="title-bar">
|
|
||||||
<h1>Scheduler Setup</h1>
|
|
||||||
<div class="settings-group">
|
|
||||||
<p>Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week.</p>
|
|
||||||
<p>Examples:</p>
|
|
||||||
<ul>
|
|
||||||
<li><span class="settings-current">0 15 *</span>: Run task every day at 15:00 in the afternoon.</li>
|
|
||||||
<li><span class="settings-current">30 8 */2</span>: Run task every second day of the week (Sun, Tue, Thu, Sat) at 08:30 in the morning.</li>
|
|
||||||
<li><span class="settings-current">auto</span>: Sensible default.</li>
|
|
||||||
<li><span class="settings-current">0</span>: (zero), deactivate that task.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Note:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Changes in the scheduler settings require a container restart to take effect.</li>
|
|
||||||
<li>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.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form action="/settings/" method="POST" name="scheduler-update">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Rescan Subscriptions</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current rescan schedule: <span class="settings-current">
|
|
||||||
{% if config.scheduler.update_subscribed %}
|
|
||||||
{% for key, value in config.scheduler.update_subscribed.items %}
|
|
||||||
{{ value }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
False
|
|
||||||
{% endif %}
|
|
||||||
</span></p>
|
|
||||||
<p>Become a sponsor and join <a href="https://members.tubearchivist.com/" target="_blank">members.tubearchivist.com</a> to get access to <span class="settings-current">real time</span> notifications for new videos uploaded by your favorite channels.</p>
|
|
||||||
<p>Periodically rescan your subscriptions:</p>
|
|
||||||
{{ scheduler_form.update_subscribed }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Send notification on task completed:</p>
|
|
||||||
{% if config.scheduler.update_subscribed_notify %}
|
|
||||||
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
|
||||||
<div id="text-reveal" class="description-text">
|
|
||||||
<p>{{ config.scheduler.update_subscribed_notify|linebreaks }}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.update_subscribed_notify }}</span></p>
|
|
||||||
{% endif %}
|
|
||||||
{{ scheduler_form.update_subscribed_notify }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Start download</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current Download schedule: <span class="settings-current">
|
|
||||||
{% if config.scheduler.download_pending %}
|
|
||||||
{% for key, value in config.scheduler.download_pending.items %}
|
|
||||||
{{ value }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
False
|
|
||||||
{% endif %}
|
|
||||||
</span></p>
|
|
||||||
<p>Automatic video download schedule:</p>
|
|
||||||
{{ scheduler_form.download_pending }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Send notification on task completed:</p>
|
|
||||||
{% if config.scheduler.download_pending_notify %}
|
|
||||||
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
|
||||||
<div id="text-reveal" class="description-text">
|
|
||||||
<p>{{ config.scheduler.download_pending_notify|linebreaks }}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.download_pending_notify }}</span></p>
|
|
||||||
{% endif %}
|
|
||||||
{{ scheduler_form.download_pending_notify }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Refresh Metadata</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current Metadata refresh schedule: <span class="settings-current">
|
|
||||||
{% if config.scheduler.check_reindex %}
|
|
||||||
{% for key, value in config.scheduler.check_reindex.items %}
|
|
||||||
{{ value }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
False
|
|
||||||
{% endif %}
|
|
||||||
</span></p>
|
|
||||||
<p>Daily schedule to refresh metadata from YouTube:</p>
|
|
||||||
{{ scheduler_form.check_reindex }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current refresh for metadata older than x days: <span class="settings-current">{{ config.scheduler.check_reindex_days }}</span></p>
|
|
||||||
<p>Refresh older than x days, recommended 90:</p>
|
|
||||||
{{ scheduler_form.check_reindex_days }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Send notification on task completed:</p>
|
|
||||||
{% if config.scheduler.check_reindex_notify %}
|
|
||||||
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
|
||||||
<div id="text-reveal" class="description-text">
|
|
||||||
<p>{{ config.scheduler.check_reindex_notify|linebreaks }}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.check_reindex_notify }}</span></p>
|
|
||||||
{% endif %}
|
|
||||||
{{ scheduler_form.check_reindex_notify }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Thumbnail check</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current thumbnail check schedule: <span class="settings-current">
|
|
||||||
{% if config.scheduler.thumbnail_check %}
|
|
||||||
{% for key, value in config.scheduler.thumbnail_check.items %}
|
|
||||||
{{ value }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
False
|
|
||||||
{% endif %}
|
|
||||||
</span></p>
|
|
||||||
<p>Periodically check and cleanup thumbnails:</p>
|
|
||||||
{{ scheduler_form.thumbnail_check }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>ZIP file index backup</h2>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p><i>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.</i></p>
|
|
||||||
<p>Current index backup schedule: <span class="settings-current">
|
|
||||||
{% if config.scheduler.run_backup %}
|
|
||||||
{% for key, value in config.scheduler.run_backup.items %}
|
|
||||||
{{ value }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
False
|
|
||||||
{% endif %}
|
|
||||||
</span></p>
|
|
||||||
<p>Automatically backup metadata to a zip file:</p>
|
|
||||||
{{ scheduler_form.run_backup }}
|
|
||||||
</div>
|
|
||||||
<div class="settings-item">
|
|
||||||
<p>Current backup files to keep: <span class="settings-current">{{ config.scheduler.run_backup_rotate }}</span></p>
|
|
||||||
<p>Max auto backups to keep:</p>
|
|
||||||
{{ scheduler_form.run_backup_rotate }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" name="scheduler-settings">Update Scheduler Settings</button>
|
|
||||||
</form>
|
|
||||||
<div class="title-bar">
|
|
||||||
<h1>Actions</h1>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Delete download queue</h2>
|
|
||||||
<p>Delete your pending or previously ignored videos from your download queue.<p>
|
|
||||||
<button onclick="deleteQueue(this)" id="ignore-button" data-id="ignore" title="Delete all previously ignored videos from the queue">Delete all ignored</button>
|
|
||||||
<button onclick="deleteQueue(this)" id="pending-button" data-id="pending" title="Delete all pending videos from the queue">Delete all queued</button>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Manual media files import.</h2>
|
|
||||||
<p>Add files to the <span class="settings-current">cache/import</span> folder. Make sure to follow the instructions in the Github <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki</a>.</p>
|
|
||||||
<div id="manual-import">
|
|
||||||
<button onclick="manualImport()">Start import</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Embed thumbnails into media file.</h2>
|
|
||||||
<p>Set extracted youtube thumbnail as cover art of the media file.</p>
|
|
||||||
<div id="re-embed">
|
|
||||||
<button onclick="reEmbed()">Start process</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>ZIP file index backup</h2>
|
|
||||||
<p>Export your database to a zip file stored at <span class="settings-current">cache/backup</span>.</p>
|
|
||||||
<p><i>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.</i></p>
|
|
||||||
<div id="db-backup">
|
|
||||||
<button onclick="dbBackup()">Start backup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Restore from backup</h2>
|
|
||||||
<p><span class="danger-zone">Danger Zone</span>: This will replace your existing index with the backup.</p>
|
|
||||||
<p>Restore from available backup files from <span class="settings-current">cache/backup</span>.</p>
|
|
||||||
{% if available_backups %}
|
|
||||||
<div class="backup-grid-row">
|
|
||||||
<span></span>
|
|
||||||
<span>Timestamp</span>
|
|
||||||
<span>Source</span>
|
|
||||||
<span>Filename</span>
|
|
||||||
</div>
|
|
||||||
{% for backup in available_backups %}
|
|
||||||
<div class="backup-grid-row" id="{{ backup.filename }}">
|
|
||||||
<button onclick="dbRestore(this)" data-id="{{ backup.filename }}">Restore</button>
|
|
||||||
<span>{{ backup.timestamp }}</span>
|
|
||||||
<span>{{ backup.reason }}</span>
|
|
||||||
<span>{{ backup.filename }}</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p>No backups found.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>Rescan filesystem</h2>
|
|
||||||
<p><span class="danger-zone">Danger Zone</span>: This will delete the metadata of deleted videos from the filesystem.</p>
|
|
||||||
<p>Rescan your media folder looking for missing videos and clean up index. More infos on the Github <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki</a>.</p>
|
|
||||||
<div id="fs-rescan">
|
|
||||||
<button onclick="fsRescan()">Rescan filesystem</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if request.user.is_superuser %}
|
|
||||||
<div class="title-bar">
|
|
||||||
<h1>Users</h1>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h2>User Management</h2>
|
|
||||||
<p>Access the admin interface for basic user management functionality like adding and deleting users, changing passwords and more.</p>
|
|
||||||
<a href="/admin/"><button>Admin Interface</button></a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="{% static 'progress.js' %}"></script>
|
<div class="settings-item">
|
||||||
{% endblock content %}
|
<h2>Main overview</h2>
|
||||||
|
<div id="primaryBox" class="info-box info-box-4">
|
||||||
|
<p id="loading">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<h2>Watch Progress</h2>
|
||||||
|
<div id="watchBox" class="info-box info-box-3">
|
||||||
|
<p id="loading">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<h2>Download History</h2>
|
||||||
|
<div id="downHistBox" class="info-box info-box-4">
|
||||||
|
<p id="loading">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<h2>Biggest Channels</h2>
|
||||||
|
<div class="info-box description-box">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Videos</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Media Size</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="biggestChannelTable"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="{% static 'stats.js' %}"></script>
|
||||||
|
{% endblock settings_content %}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
{% extends "home/base_settings.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block settings_content %}
|
||||||
|
<div class="title-bar">
|
||||||
|
<h1>Actions</h1>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Delete download queue</h2>
|
||||||
|
<p>Delete your pending or previously ignored videos from your download queue.<p>
|
||||||
|
<button onclick="deleteQueue(this)" id="ignore-button" data-id="ignore" title="Delete all previously ignored videos from the queue">Delete all ignored</button>
|
||||||
|
<button onclick="deleteQueue(this)" id="pending-button" data-id="pending" title="Delete all pending videos from the queue">Delete all queued</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Manual media files import.</h2>
|
||||||
|
<p>Add files to the <span class="settings-current">cache/import</span> folder. Make sure to follow the instructions in the Github <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki</a>.</p>
|
||||||
|
<div id="manual-import">
|
||||||
|
<button onclick="manualImport()">Start import</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Embed thumbnails into media file.</h2>
|
||||||
|
<p>Set extracted youtube thumbnail as cover art of the media file.</p>
|
||||||
|
<div id="re-embed">
|
||||||
|
<button onclick="reEmbed()">Start process</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>ZIP file index backup</h2>
|
||||||
|
<p>Export your database to a zip file stored at <span class="settings-current">cache/backup</span>.</p>
|
||||||
|
<p><i>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.</i></p>
|
||||||
|
<div id="db-backup">
|
||||||
|
<button onclick="dbBackup()">Start backup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Restore from backup</h2>
|
||||||
|
<p><span class="danger-zone">Danger Zone</span>: This will replace your existing index with the backup.</p>
|
||||||
|
<p>Restore from available backup files from <span class="settings-current">cache/backup</span>.</p>
|
||||||
|
{% if available_backups %}
|
||||||
|
<div class="backup-grid-row">
|
||||||
|
<span></span>
|
||||||
|
<span>Timestamp</span>
|
||||||
|
<span>Source</span>
|
||||||
|
<span>Filename</span>
|
||||||
|
</div>
|
||||||
|
{% for backup in available_backups %}
|
||||||
|
<div class="backup-grid-row" id="{{ backup.filename }}">
|
||||||
|
<button onclick="dbRestore(this)" data-id="{{ backup.filename }}">Restore</button>
|
||||||
|
<span>{{ backup.timestamp }}</span>
|
||||||
|
<span>{{ backup.reason }}</span>
|
||||||
|
<span>{{ backup.filename }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>No backups found.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Rescan filesystem</h2>
|
||||||
|
<p><span class="danger-zone">Danger Zone</span>: This will delete the metadata of deleted videos from the filesystem.</p>
|
||||||
|
<p>Rescan your media folder looking for missing videos and clean up index. More infos on the Github <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki</a>.</p>
|
||||||
|
<div id="fs-rescan">
|
||||||
|
<button onclick="fsRescan()">Rescan filesystem</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock settings_content %}
|
|
@ -0,0 +1,192 @@
|
||||||
|
{% extends "home/base_settings.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block settings_content %}
|
||||||
|
<div class="title-bar">
|
||||||
|
<h1>Application Configurations</h1>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'settings_application' %}" method="POST" name="application-update">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="subscriptions">Subscriptions</h2>
|
||||||
|
<p>Disable shorts or streams by setting their page size to 0 (zero).</p>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>YouTube page size: <span class="settings-current">{{ config.subscriptions.channel_size }}</span></p>
|
||||||
|
<i>Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
||||||
|
{{ app_form.subscriptions_channel_size }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>YouTube Live page size: <span class="settings-current">{{ config.subscriptions.live_channel_size }}</span></p>
|
||||||
|
<i>Live Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
||||||
|
{{ app_form.subscriptions_live_channel_size }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>YouTube Shorts page size: <span class="settings-current">{{ config.subscriptions.shorts_channel_size }}</span></p>
|
||||||
|
<i>Shorts Videos to scan to find new items for the <b>Rescan subscriptions</b> task, max recommended 50.</i><br>
|
||||||
|
{{ app_form.subscriptions_shorts_channel_size }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Auto start download from your subscriptions: <span class="settings-current">{{ config.subscriptions.auto_start}}</span></p>
|
||||||
|
<i>Enable this will automatically start and prioritize videos from your subscriptions.</i><br>
|
||||||
|
{{ app_form.subscriptions_auto_start }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="downloads">Downloads</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current download speed limit in KB/s: <span class="settings-current">{{ config.downloads.limit_speed }}</span></p>
|
||||||
|
<i>Limit download speed. 0 (zero) to deactivate, e.g. 1000 (1MB/s). Speeds are in KB/s. Setting takes effect on new download jobs or application restart.</i><br>
|
||||||
|
{{ app_form.downloads_limit_speed }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current throttled rate limit in KB/s: <span class="settings-current">{{ config.downloads.throttledratelimit }}</span></p>
|
||||||
|
<i>Download will restart if speeds drop below specified amount. 0 (zero) to deactivate, e.g. 100. Speeds are in KB/s.</i><br>
|
||||||
|
{{ app_form.downloads_throttledratelimit }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current scraping sleep interval: <span class="settings-current">{{ config.downloads.sleep_interval }}</p>
|
||||||
|
<i>Seconds to sleep between calls to YouTube. Might be necessary to avoid throttling. Recommended 3.</i><br>
|
||||||
|
{{ app_form.downloads_sleep_interval }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p><span class="danger-zone">Danger Zone</span>: Current auto delete watched videos: <span class="settings-current">{{ config.downloads.autodelete_days }}</span></p>
|
||||||
|
<i>Auto delete watched videos after x days, 0 (zero) to deactivate:</i><br>
|
||||||
|
{{ app_form.downloads_autodelete_days }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="format">Download Format</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Limit video and audio quality format for yt-dlp.<br>
|
||||||
|
Currently: <span class="settings-current">{{ config.downloads.format }}</span>
|
||||||
|
</p>
|
||||||
|
<p>Example configurations:</p>
|
||||||
|
<ul>
|
||||||
|
<li><span class="settings-current">bestvideo[height<=720]+bestaudio/best[height<=720]</span>: best audio and max video height of 720p.</li>
|
||||||
|
<li><span class="settings-current">bestvideo[height<=1080]+bestaudio/best[height<=1080]</span>: best audio and max video height of 1080p.</li>
|
||||||
|
<li><span class="settings-current">bestvideo[height<=1080][vcodec*=avc1]+bestaudio[acodec*=mp4a]/mp4</span>: Max 1080p video height with iOS compatible video and audio codecs.</li>
|
||||||
|
<li><span class="settings-current">0</span>: deactivate and download the best quality possible as decided by yt-dlp.</li>
|
||||||
|
</ul>
|
||||||
|
<i>Make sure your custom format gets merged into a single file. Check out the <a href="https://github.com/yt-dlp/yt-dlp#format-selection" target="_blank">documentation</a> for valid configurations.</i><br>
|
||||||
|
{{ app_form.downloads_format }}
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Force sort order to have precedence over all yt-dlp fields.<br>
|
||||||
|
Currently: <span class="settings-current">{{ config.downloads.format_sort }}</span>
|
||||||
|
</p>
|
||||||
|
<p>Example configurations:</p>
|
||||||
|
<ul>
|
||||||
|
<li><span class="settings-current">res,codec:av1</span>: prefer AV1 over all other video codecs.</li>
|
||||||
|
<li><span class="settings-current">0</span>: deactivate and keep the default as decided by yt-dlp.</li>
|
||||||
|
</ul>
|
||||||
|
<i>Not all codecs are supported by all browsers. The default value ensures best compatibility. Check out the <a href="https://github.com/yt-dlp/yt-dlp#sorting-formats" target="_blank">documentation</a> for valid configurations.</i><br>
|
||||||
|
{{ app_form.downloads_format_sort }}
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Prefer translated metadata language: <span class="settings-current">{{ config.downloads.extractor_lang }}</span></p>
|
||||||
|
<i>This will change the language this video gets indexed as. That will only be available if the uploader provides translations. Add as two letter ISO language code, check the <a href="https://github.com/yt-dlp/yt-dlp#youtube" target="_blank">documentation</a> which languages are available.</i><br>
|
||||||
|
{{ app_form.downloads_extractor_lang}}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current metadata embed setting: <span class="settings-current">{{ config.downloads.add_metadata }}</span></p>
|
||||||
|
<i>Metadata is not embedded into the downloaded files by default.</i><br>
|
||||||
|
{{ app_form.downloads_add_metadata }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current thumbnail embed setting: <span class="settings-current">{{ config.downloads.add_thumbnail }}</span></p>
|
||||||
|
<i>Embed thumbnail into the mediafile.</i><br>
|
||||||
|
{{ app_form.downloads_add_thumbnail }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="format">Subtitles</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Subtitles download setting: <span class="settings-current">{{ config.downloads.subtitle }}</span><br>
|
||||||
|
<i>Choose which subtitles to download, add comma separated language codes,<br>
|
||||||
|
e.g. <span class="settings-current">en, de, zh-Hans</span></i><br>
|
||||||
|
{{ app_form.downloads_subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Subtitle source settings: <span class="settings-current">{{ config.downloads.subtitle_source }}</span></p>
|
||||||
|
<i>Download only user generated, or also less accurate auto generated subtitles.</i><br>
|
||||||
|
{{ app_form.downloads_subtitle_source }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Index and make subtitles searchable: <span class="settings-current">{{ config.downloads.subtitle_index }}</span></p>
|
||||||
|
<i>Store subtitle lines in Elasticsearch. Not recommended for low-end hardware.</i><br>
|
||||||
|
{{ app_form.downloads_subtitle_index }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="comments">Comments</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Download and index comments: <span class="settings-current">{{ config.downloads.comment_max }}</span><br>
|
||||||
|
<i>Follow the yt-dlp max_comments documentation, <a href="https://github.com/yt-dlp/yt-dlp#youtube" target="_blank">max-comments,max-parents,max-replies,max-replies-per-thread</a>:</i><br>
|
||||||
|
<p>Example configurations:</p>
|
||||||
|
<ul>
|
||||||
|
<li><span class="settings-current">all,100,all,30</span>: Get 100 max-parents and 30 max-replies-per-thread.</li>
|
||||||
|
<li><span class="settings-current">1000,all,all,50</span>: Get a total of 1000 comments over all, 50 replies per thread.</li>
|
||||||
|
</ul>
|
||||||
|
{{ app_form.downloads_comment_max }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Selected comment sort method: <span class="settings-current">{{ config.downloads.comment_sort }}</span><br>
|
||||||
|
<i>Select how many comments and threads to download:</i><br>
|
||||||
|
{{ app_form.downloads_comment_sort }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="format">Cookie</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Import YouTube cookie: <span class="settings-current">{{ config.downloads.cookie_import }}</span><br></p>
|
||||||
|
<p>For automatic cookie import use <b>Tube Archivist Companion</b> <a href="https://github.com/tubearchivist/browser-extension" target="_blank">browser extension</a>.</p>
|
||||||
|
<i>For manual cookie import, place your cookie file named <span class="settings-current">cookies.google.txt</span> in <span class="settings-current">cache/import</span> before enabling. Instructions in the <a href="https://docs.tubearchivist.com/settings/" target="_blank">Wiki.</a></i><br>
|
||||||
|
{{ app_form.downloads_cookie_import }}<br>
|
||||||
|
{% if config.downloads.cookie_import %}
|
||||||
|
<div id="cookieMessage">
|
||||||
|
<button onclick="handleCookieValidate()" type="button" id="cookieButton">Validate Cookie File</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="integrations">Integrations</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>API token: <button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button></p>
|
||||||
|
<div id="text-reveal" class="description-text">
|
||||||
|
<p>{{ api_token }}</p>
|
||||||
|
<button class="danger-button" type="button" onclick="resetToken()">Revoke</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Integrate with <a href="https://returnyoutubedislike.com/" target="_blank">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p>
|
||||||
|
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
|
||||||
|
{{ app_form.downloads_integrate_ryd }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Integrate with <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a> to get sponsored timestamps: <span class="settings-current">{{ config.downloads.integrate_sponsorblock }}</span></p>
|
||||||
|
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
|
||||||
|
{{ app_form.downloads_integrate_sponsorblock }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2 id="snapshots">Snapshots</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current system snapshot: <span class="settings-current">{{ config.application.enable_snapshot }}</span></p>
|
||||||
|
<i>Automatically create daily deduplicated snapshots of the index, stored in Elasticsearch. Read first before activating: <a target="_blank" href="https://docs.tubearchivist.com/settings/#snapshots">Wiki</a>.</i><br>
|
||||||
|
{{ app_form.application_enable_snapshot }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if snapshots %}
|
||||||
|
<p>Create next snapshot: <span class="settings-current">{{ snapshots.next_exec_str }}</span>, snapshots expire after <span class="settings-current">{{ snapshots.expire_after }}</span>. <button onclick="createSnapshot()" id="createButton">Create snapshot now</button></p>
|
||||||
|
<br>
|
||||||
|
{% for snapshot in snapshots.snapshots %}
|
||||||
|
<p><button id="{{ snapshot.id }}" onclick="restoreSnapshot(id)">Restore</button> Snapshot created on: <span class="settings-current">{{ snapshot.start_date }}</span>, took <span class="settings-current">{{ snapshot.duration_s }}s</span> to create. State: <i>{{ snapshot.state }}</i></p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="application-settings">Update Application Configurations</button>
|
||||||
|
</form>
|
||||||
|
{% endblock settings_content %}
|
|
@ -0,0 +1,154 @@
|
||||||
|
{% extends "home/base_settings.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block settings_content %}
|
||||||
|
<div class="title-bar">
|
||||||
|
<h1>Scheduler Setup</h1>
|
||||||
|
<div class="settings-group">
|
||||||
|
<p>Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week.</p>
|
||||||
|
<p>Examples:</p>
|
||||||
|
<ul>
|
||||||
|
<li><span class="settings-current">0 15 *</span>: Run task every day at 15:00 in the afternoon.</li>
|
||||||
|
<li><span class="settings-current">30 8 */2</span>: Run task every second day of the week (Sun, Tue, Thu, Sat) at 08:30 in the morning.</li>
|
||||||
|
<li><span class="settings-current">auto</span>: Sensible default.</li>
|
||||||
|
<li><span class="settings-current">0</span>: (zero), deactivate that task.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Note:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Changes in the scheduler settings require a container restart to take effect.</li>
|
||||||
|
<li>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.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'settings_scheduling' %}" method="POST" name="scheduler-update">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Rescan Subscriptions</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current rescan schedule: <span class="settings-current">
|
||||||
|
{% if config.scheduler.update_subscribed %}
|
||||||
|
{% for key, value in config.scheduler.update_subscribed.items %}
|
||||||
|
{{ value }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
False
|
||||||
|
{% endif %}
|
||||||
|
</span></p>
|
||||||
|
<p>Become a sponsor and join <a href="https://members.tubearchivist.com/" target="_blank">members.tubearchivist.com</a> to get access to <span class="settings-current">real time</span> notifications for new videos uploaded by your favorite channels.</p>
|
||||||
|
<p>Periodically rescan your subscriptions:</p>
|
||||||
|
{{ scheduler_form.update_subscribed }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Send notification on task completed:</p>
|
||||||
|
{% if config.scheduler.update_subscribed_notify %}
|
||||||
|
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
||||||
|
<div id="text-reveal" class="description-text">
|
||||||
|
<p>{{ config.scheduler.update_subscribed_notify|linebreaks }}</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.update_subscribed_notify }}</span></p>
|
||||||
|
{% endif %}
|
||||||
|
{{ scheduler_form.update_subscribed_notify }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Start download</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current Download schedule: <span class="settings-current">
|
||||||
|
{% if config.scheduler.download_pending %}
|
||||||
|
{% for key, value in config.scheduler.download_pending.items %}
|
||||||
|
{{ value }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
False
|
||||||
|
{% endif %}
|
||||||
|
</span></p>
|
||||||
|
<p>Automatic video download schedule:</p>
|
||||||
|
{{ scheduler_form.download_pending }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Send notification on task completed:</p>
|
||||||
|
{% if config.scheduler.download_pending_notify %}
|
||||||
|
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
||||||
|
<div id="text-reveal" class="description-text">
|
||||||
|
<p>{{ config.scheduler.download_pending_notify|linebreaks }}</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.download_pending_notify }}</span></p>
|
||||||
|
{% endif %}
|
||||||
|
{{ scheduler_form.download_pending_notify }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Refresh Metadata</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current Metadata refresh schedule: <span class="settings-current">
|
||||||
|
{% if config.scheduler.check_reindex %}
|
||||||
|
{% for key, value in config.scheduler.check_reindex.items %}
|
||||||
|
{{ value }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
False
|
||||||
|
{% endif %}
|
||||||
|
</span></p>
|
||||||
|
<p>Daily schedule to refresh metadata from YouTube:</p>
|
||||||
|
{{ scheduler_form.check_reindex }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current refresh for metadata older than x days: <span class="settings-current">{{ config.scheduler.check_reindex_days }}</span></p>
|
||||||
|
<p>Refresh older than x days, recommended 90:</p>
|
||||||
|
{{ scheduler_form.check_reindex_days }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Send notification on task completed:</p>
|
||||||
|
{% if config.scheduler.check_reindex_notify %}
|
||||||
|
<p><button type="button" onclick="textReveal(this)" id="text-reveal-button">Show</button> stored notification links</p>
|
||||||
|
<div id="text-reveal" class="description-text">
|
||||||
|
<p>{{ config.scheduler.check_reindex_notify|linebreaks }}</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>Current notification urls: <span class="settings-current">{{ config.scheduler.check_reindex_notify }}</span></p>
|
||||||
|
{% endif %}
|
||||||
|
{{ scheduler_form.check_reindex_notify }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Thumbnail check</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current thumbnail check schedule: <span class="settings-current">
|
||||||
|
{% if config.scheduler.thumbnail_check %}
|
||||||
|
{% for key, value in config.scheduler.thumbnail_check.items %}
|
||||||
|
{{ value }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
False
|
||||||
|
{% endif %}
|
||||||
|
</span></p>
|
||||||
|
<p>Periodically check and cleanup thumbnails:</p>
|
||||||
|
{{ scheduler_form.thumbnail_check }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>ZIP file index backup</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p><i>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.</i></p>
|
||||||
|
<p>Current index backup schedule: <span class="settings-current">
|
||||||
|
{% if config.scheduler.run_backup %}
|
||||||
|
{% for key, value in config.scheduler.run_backup.items %}
|
||||||
|
{{ value }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
False
|
||||||
|
{% endif %}
|
||||||
|
</span></p>
|
||||||
|
<p>Automatically backup metadata to a zip file:</p>
|
||||||
|
{{ scheduler_form.run_backup }}
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current backup files to keep: <span class="settings-current">{{ config.scheduler.run_backup_rotate }}</span></p>
|
||||||
|
<p>Max auto backups to keep:</p>
|
||||||
|
{{ scheduler_form.run_backup_rotate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="scheduler-settings">Update Scheduler Settings</button>
|
||||||
|
</form>
|
||||||
|
{% endblock settings_content %}
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends "home/base_settings.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block settings_content %}
|
||||||
|
<div class="title-bar">
|
||||||
|
<h1>User Configurations</h1>
|
||||||
|
</div>
|
||||||
|
<form action="{% url 'settings_user' %}" method="POST" name="user-update">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Color scheme</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current color scheme: <span class="settings-current">{{ config.application.colors }}</span></p>
|
||||||
|
<i>Select your preferred color scheme between dark and light mode.</i><br>
|
||||||
|
{{ user_form.colors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>Archive View</h2>
|
||||||
|
<div class="settings-item">
|
||||||
|
<p>Current page size: <span class="settings-current">{{ config.archive.page_size }}</span></p>
|
||||||
|
<i>Result of videos showing in archive page</i><br>
|
||||||
|
{{ user_form.page_size }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="user-settings">Update User Configurations</button>
|
||||||
|
</form>
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
<div class="title-bar">
|
||||||
|
<h1>Users</h1>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<h2>User Management</h2>
|
||||||
|
<p>Access the admin interface for basic user management functionality like adding and deleting users, changing passwords and more.</p>
|
||||||
|
<a href="/admin/"><button>Admin Interface</button></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock settings_content %}
|
|
@ -26,6 +26,26 @@ urlpatterns = [
|
||||||
login_required(views.SettingsView.as_view()),
|
login_required(views.SettingsView.as_view()),
|
||||||
name="settings",
|
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("process/", login_required(views.process), name="process"),
|
path("process/", login_required(views.process), name="process"),
|
||||||
path(
|
path(
|
||||||
"channel/",
|
"channel/",
|
||||||
|
|
|
@ -981,7 +981,21 @@ class SearchView(ArchivistResultsView):
|
||||||
|
|
||||||
class SettingsView(MinView):
|
class SettingsView(MinView):
|
||||||
"""resolves to /settings/
|
"""resolves to /settings/
|
||||||
handle the settings page, display current 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
|
take post request from the form to update settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -990,18 +1004,47 @@ class SettingsView(MinView):
|
||||||
context = self.get_min_context(request)
|
context = self.get_min_context(request)
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"title": "Settings",
|
"title": "User Settings",
|
||||||
|
"config": AppConfig(request.user.id).config,
|
||||||
|
"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)
|
||||||
|
if user_form.is_valid():
|
||||||
|
user_form_post = user_form.cleaned_data
|
||||||
|
if any(user_form_post.values()):
|
||||||
|
AppConfig().set_user_config(user_form_post, request.user.id)
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
return redirect("settings_user", permanent=True)
|
||||||
|
|
||||||
|
|
||||||
|
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(request.user.id).config,
|
"config": AppConfig(request.user.id).config,
|
||||||
"api_token": self.get_token(request),
|
"api_token": self.get_token(request),
|
||||||
"available_backups": ElasticBackup().get_all_backup_files(),
|
|
||||||
"user_form": UserSettingsForm(),
|
|
||||||
"app_form": ApplicationSettingsForm(),
|
"app_form": ApplicationSettingsForm(),
|
||||||
"scheduler_form": SchedulerSettingsForm(),
|
|
||||||
"snapshots": ElasticSnapshot().get_snapshot_stats(),
|
"snapshots": ElasticSnapshot().get_snapshot_stats(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(request, "home/settings.html", context)
|
return render(request, "home/settings_application.html", context)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_token(request):
|
def get_token(request):
|
||||||
|
@ -1012,12 +1055,7 @@ class SettingsView(MinView):
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""handle form post to update settings"""
|
"""handle form post to update settings"""
|
||||||
user_form = UserSettingsForm(request.POST)
|
|
||||||
config_handler = AppConfig()
|
config_handler = AppConfig()
|
||||||
if user_form.is_valid():
|
|
||||||
user_form_post = user_form.cleaned_data
|
|
||||||
if any(user_form_post.values()):
|
|
||||||
config_handler.set_user_config(user_form_post, request.user.id)
|
|
||||||
|
|
||||||
app_form = ApplicationSettingsForm(request.POST)
|
app_form = ApplicationSettingsForm(request.POST)
|
||||||
if app_form.is_valid():
|
if app_form.is_valid():
|
||||||
|
@ -1027,15 +1065,8 @@ class SettingsView(MinView):
|
||||||
updated = config_handler.update_config(app_form_post)
|
updated = config_handler.update_config(app_form_post)
|
||||||
self.post_process_updated(updated, config_handler.config)
|
self.post_process_updated(updated, config_handler.config)
|
||||||
|
|
||||||
scheduler_form = SchedulerSettingsForm(request.POST)
|
|
||||||
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)
|
|
||||||
|
|
||||||
sleep(1)
|
sleep(1)
|
||||||
return redirect("settings", permanent=True)
|
return redirect("settings_application", permanent=True)
|
||||||
|
|
||||||
def post_process_updated(self, updated, config):
|
def post_process_updated(self, updated, config):
|
||||||
"""apply changes for config"""
|
"""apply changes for config"""
|
||||||
|
@ -1073,13 +1104,66 @@ class SettingsView(MinView):
|
||||||
key = "message:setting"
|
key = "message:setting"
|
||||||
message = {
|
message = {
|
||||||
"status": key,
|
"status": key,
|
||||||
|
"group": "setting:application",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"title": "Cookie import failed",
|
"title": "Cookie import failed",
|
||||||
"message": message_line,
|
"messages": [message_line],
|
||||||
|
"id": "0000",
|
||||||
}
|
}
|
||||||
RedisArchivist().set_message(key, message=message, expire=True)
|
RedisArchivist().set_message(key, message=message, expire=True)
|
||||||
|
|
||||||
|
|
||||||
|
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_min_context(request)
|
||||||
|
context.update(
|
||||||
|
{
|
||||||
|
"title": "Scheduling Settings",
|
||||||
|
"config": AppConfig(request.user.id).config,
|
||||||
|
"scheduler_form": SchedulerSettingsForm(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(request, "home/settings_scheduling.html", context)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""handle form post to update settings"""
|
||||||
|
scheduler_form = SchedulerSettingsForm(request.POST)
|
||||||
|
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)
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
return redirect("settings_scheduling", permanent=True)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def process(request):
|
def process(request):
|
||||||
"""handle all the buttons calls via POST ajax"""
|
"""handle all the buttons calls via POST ajax"""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
|
@ -82,6 +82,7 @@ ul {
|
||||||
td, th, span, label {
|
td, th, span, label {
|
||||||
font-family: Sen-Regular, sans-serif;
|
font-family: Sen-Regular, sans-serif;
|
||||||
color: var(--main-font);
|
color: var(--main-font);
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
select, input {
|
select, input {
|
||||||
|
@ -643,6 +644,10 @@ video:-webkit-full-screen {
|
||||||
background-color: var(--highlight-bg);
|
background-color: var(--highlight-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box-4 {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.info-box-3 {
|
.info-box-3 {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
@ -651,6 +656,10 @@ video:-webkit-full-screen {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box-1 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.info-box img {
|
.info-box img {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
margin: 0 15px;
|
margin: 0 15px;
|
||||||
|
@ -960,15 +969,15 @@ video:-webkit-full-screen {
|
||||||
transform: translateX(-30%);
|
transform: translateX(-30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box-item.channel-nav {
|
.info-box-item.child-page-nav {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box-item.channel-nav a {
|
.info-box-item.child-page-nav a {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box-item.channel-nav a:hover {
|
.info-box-item.child-page-nav a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,15 @@ function getMessages(dataOrigin) {
|
||||||
|
|
||||||
function buildMessage(responseData, dataOrigin) {
|
function buildMessage(responseData, dataOrigin) {
|
||||||
// filter relevant messages
|
// filter relevant messages
|
||||||
let messages = responseData.filter(function (value) {
|
let messages;
|
||||||
return dataOrigin.split(' ').includes(value.group.split(':')[0]);
|
if (dataOrigin) {
|
||||||
}, dataOrigin);
|
messages = responseData.filter(function (value) {
|
||||||
|
return dataOrigin.split(' ').includes(value.group.split(':')[0]);
|
||||||
|
}, dataOrigin);
|
||||||
|
} else {
|
||||||
|
messages = responseData;
|
||||||
|
}
|
||||||
|
|
||||||
let notifications = document.getElementById('notifications');
|
let notifications = document.getElementById('notifications');
|
||||||
let currentNotifications = notifications.childElementCount;
|
let currentNotifications = notifications.childElementCount;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
// build stats for settings page
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* globals apiRequest */
|
||||||
|
|
||||||
|
function primaryStats() {
|
||||||
|
let apiEndpoint = '/api/stats/primary/';
|
||||||
|
let responseData = apiRequest(apiEndpoint, 'GET');
|
||||||
|
let primaryBox = document.getElementById('primaryBox');
|
||||||
|
clearLoading(primaryBox);
|
||||||
|
let videoTile = buildVideoTile(responseData);
|
||||||
|
primaryBox.appendChild(videoTile);
|
||||||
|
let channelTile = buildChannelTile(responseData);
|
||||||
|
primaryBox.appendChild(channelTile);
|
||||||
|
let playlistTile = buildPlaylistTile(responseData);
|
||||||
|
primaryBox.appendChild(playlistTile);
|
||||||
|
let downloadTile = buildDownloadTile(responseData);
|
||||||
|
primaryBox.appendChild(downloadTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLoading(dashBox) {
|
||||||
|
dashBox.querySelector('#loading').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 buildVideoTile(responseData) {
|
||||||
|
let tile = buildTile(`Total Videos: ${responseData.videos.total || 0}`);
|
||||||
|
let message = document.createElement('p');
|
||||||
|
message.innerHTML = `
|
||||||
|
videos: ${responseData.videos.videos || 0}<br>
|
||||||
|
shorts: ${responseData.videos.shorts || 0}<br>
|
||||||
|
streams: ${responseData.videos.streams || 0}<br>
|
||||||
|
`;
|
||||||
|
tile.appendChild(message);
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChannelTile(responseData) {
|
||||||
|
let tile = buildTile(`Total Channels: ${responseData.channels.total || 0}`);
|
||||||
|
let message = document.createElement('p');
|
||||||
|
message.innerHTML = `subscribed: ${responseData.channels.sub_true || 0}`;
|
||||||
|
tile.appendChild(message);
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlaylistTile(responseData) {
|
||||||
|
let tile = buildTile(`Total Playlists: ${responseData.playlists.total || 0}`);
|
||||||
|
let message = document.createElement('p');
|
||||||
|
message.innerHTML = `subscribed: ${responseData.playlists.sub_true || 0}`;
|
||||||
|
tile.appendChild(message);
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDownloadTile(responseData) {
|
||||||
|
let tile = buildTile('Downloads');
|
||||||
|
let message = document.createElement('p');
|
||||||
|
message.innerHTML = `
|
||||||
|
pending: ${responseData.downloads.pending || 0}<br>
|
||||||
|
ignored: ${responseData.downloads.ignore || 0}<br>
|
||||||
|
`;
|
||||||
|
tile.appendChild(message);
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchStats() {
|
||||||
|
let apiEndpoint = '/api/stats/watch/';
|
||||||
|
let responseData = apiRequest(apiEndpoint, 'GET');
|
||||||
|
let watchBox = document.getElementById('watchBox');
|
||||||
|
clearLoading(watchBox);
|
||||||
|
|
||||||
|
for (const property in responseData) {
|
||||||
|
let tile = buildWatchTile(property, responseData[property]);
|
||||||
|
watchBox.appendChild(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWatchTile(title, watchDetail) {
|
||||||
|
let tile = buildTile(`Total ${title}`);
|
||||||
|
let message = document.createElement('p');
|
||||||
|
message.innerHTML = `
|
||||||
|
${watchDetail.items} Videos<br>
|
||||||
|
${watchDetail.duration} Seconds<br>
|
||||||
|
${watchDetail.duration_str} Playback
|
||||||
|
`;
|
||||||
|
if (watchDetail.progress) {
|
||||||
|
message.innerHTML += `<br>${Number(watchDetail.progress * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
tile.appendChild(message);
|
||||||
|
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');
|
||||||
|
message.innerText = `new videos: ${dailyStat.count}`;
|
||||||
|
tile.appendChild(message);
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function biggestChannel() {
|
||||||
|
let apiEndpoint = '/api/stats/biggestchannels/';
|
||||||
|
let responseData = apiRequest(apiEndpoint, 'GET');
|
||||||
|
let tBody = document.getElementById('biggestChannelTable');
|
||||||
|
for (let i = 0; i < responseData.length; i++) {
|
||||||
|
const channelData = responseData[i];
|
||||||
|
let tableRow = buildChannelRow(channelData);
|
||||||
|
tBody.appendChild(tableRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChannelRow(channelData) {
|
||||||
|
let tableRow = document.createElement('tr');
|
||||||
|
tableRow.innerHTML = `
|
||||||
|
<td><a href="/channel/${channelData.id}/">${channelData.name}</a></td>
|
||||||
|
<td>${channelData.doc_count}</td>
|
||||||
|
<td>${channelData.duration_str}</td>
|
||||||
|
<td>${humanFileSize(channelData.media_size)}</td>
|
||||||
|
`;
|
||||||
|
return tableRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildStats() {
|
||||||
|
primaryStats();
|
||||||
|
watchStats();
|
||||||
|
downloadHist();
|
||||||
|
biggestChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
buildStats();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue