mirror of
https://github.com/tubearchivist/tubearchivist.git
synced 2024-12-23 02:10:14 +00:00
Move user configuration from Redis to ES (#533)
* ES Client must bootstrap itself to be the source of config If this is not done a cyclic loop is created between the config loader and the ES client. This lays the ground work for ES being the source of all app config. * auto_download is not used anymore * Add UserConfig class that encapsulates user config storage This class will allow the rest of the code to 'not care' about how user properties are stored. This requires the addition of a ta_users index in ES. * Create migration task for user config transfer * Replace getters and setters for each property Strongly type the user configuration Migrate missed sponsorblock ID * Other DB settings will be another PR
This commit is contained in:
parent
dc41e5062d
commit
85b56300b3
@ -16,6 +16,7 @@ from home.src.ta.config import AppConfig, ReleaseVersion
|
|||||||
from home.src.ta.helper import clear_dl_cache
|
from home.src.ta.helper import clear_dl_cache
|
||||||
from home.src.ta.ta_redis import RedisArchivist
|
from home.src.ta.ta_redis import RedisArchivist
|
||||||
from home.src.ta.task_manager import TaskManager
|
from home.src.ta.task_manager import TaskManager
|
||||||
|
from home.src.ta.users import UserConfig
|
||||||
|
|
||||||
TOPIC = """
|
TOPIC = """
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ class Command(BaseCommand):
|
|||||||
self._mig_snapshot_check()
|
self._mig_snapshot_check()
|
||||||
self._mig_set_streams()
|
self._mig_set_streams()
|
||||||
self._mig_set_autostart()
|
self._mig_set_autostart()
|
||||||
|
self._mig_move_users_to_es()
|
||||||
|
|
||||||
def _sync_redis_state(self):
|
def _sync_redis_state(self):
|
||||||
"""make sure redis gets new config.json values"""
|
"""make sure redis gets new config.json values"""
|
||||||
@ -219,3 +221,99 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(response)
|
self.stdout.write(response)
|
||||||
sleep(60)
|
sleep(60)
|
||||||
raise CommandError(message)
|
raise CommandError(message)
|
||||||
|
|
||||||
|
def _mig_move_users_to_es(self): # noqa: C901
|
||||||
|
"""migration: update from 0.4.1 to 0.5.0 move user config to ES"""
|
||||||
|
self.stdout.write("[MIGRATION] move user configuration to ES")
|
||||||
|
redis = RedisArchivist()
|
||||||
|
|
||||||
|
# 1: Find all users in Redis
|
||||||
|
users = {i.split(":")[0] for i in redis.list_keys("[0-9]*:")}
|
||||||
|
if not users:
|
||||||
|
self.stdout.write(" no users needed migrating to ES")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2: Write all Redis user settings to ES
|
||||||
|
# 3: Remove user settings from Redis
|
||||||
|
try:
|
||||||
|
for user in users:
|
||||||
|
new_conf = UserConfig(user)
|
||||||
|
|
||||||
|
colors_key = f"{user}:colors"
|
||||||
|
colors = redis.get_message(colors_key).get("status")
|
||||||
|
if colors:
|
||||||
|
new_conf.set_value("colors", colors)
|
||||||
|
redis.del_message(colors_key)
|
||||||
|
|
||||||
|
sort_by_key = f"{user}:sort_by"
|
||||||
|
sort_by = redis.get_message(sort_by_key).get("status")
|
||||||
|
if sort_by:
|
||||||
|
new_conf.set_value("sort_by", sort_by)
|
||||||
|
redis.del_message(sort_by_key)
|
||||||
|
|
||||||
|
page_size_key = f"{user}:page_size"
|
||||||
|
page_size = redis.get_message(page_size_key).get("status")
|
||||||
|
if page_size:
|
||||||
|
new_conf.set_value("page_size", page_size)
|
||||||
|
redis.del_message(page_size_key)
|
||||||
|
|
||||||
|
sort_order_key = f"{user}:sort_order"
|
||||||
|
sort_order = redis.get_message(sort_order_key).get("status")
|
||||||
|
if sort_order:
|
||||||
|
new_conf.set_value("sort_order", sort_order)
|
||||||
|
redis.del_message(sort_order_key)
|
||||||
|
|
||||||
|
grid_items_key = f"{user}:grid_items"
|
||||||
|
grid_items = redis.get_message(grid_items_key).get("status")
|
||||||
|
if grid_items:
|
||||||
|
new_conf.set_value("grid_items", grid_items)
|
||||||
|
redis.del_message(grid_items_key)
|
||||||
|
|
||||||
|
hide_watch_key = f"{user}:hide_watched"
|
||||||
|
hide_watch = redis.get_message(hide_watch_key).get("status")
|
||||||
|
if hide_watch:
|
||||||
|
new_conf.set_value("hide_watched", hide_watch)
|
||||||
|
redis.del_message(hide_watch_key)
|
||||||
|
|
||||||
|
ignore_only_key = f"{user}:show_ignored_only"
|
||||||
|
ignore_only = redis.get_message(ignore_only_key).get("status")
|
||||||
|
if ignore_only:
|
||||||
|
new_conf.set_value("show_ignored_only", ignore_only)
|
||||||
|
redis.del_message(ignore_only_key)
|
||||||
|
|
||||||
|
subed_only_key = f"{user}:show_subed_only"
|
||||||
|
subed_only = redis.get_message(subed_only_key).get("status")
|
||||||
|
if subed_only:
|
||||||
|
new_conf.set_value("show_subed_only", subed_only)
|
||||||
|
redis.del_message(subed_only_key)
|
||||||
|
|
||||||
|
sb_id_key = f"{user}:id_sb_id"
|
||||||
|
sb_id = redis.get_message(sb_id_key).get("status")
|
||||||
|
if sb_id:
|
||||||
|
new_conf.set_value("sb_id_id", sb_id)
|
||||||
|
redis.del_message(sb_id_key)
|
||||||
|
|
||||||
|
for view in ["channel", "playlist", "home", "downloads"]:
|
||||||
|
view_key = f"{user}:view:{view}"
|
||||||
|
view_style = redis.get_message(view_key).get("status")
|
||||||
|
if view_style:
|
||||||
|
new_conf.set_value(f"view_style_{view}", view_style)
|
||||||
|
redis.del_message(view_key)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f" ✓ Settings for user '{user}' migrated to ES"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
message = " 🗙 user migration to ES failed"
|
||||||
|
self.stdout.write(self.style.ERROR(message))
|
||||||
|
self.stdout.write(self.style.ERROR(e))
|
||||||
|
sleep(60)
|
||||||
|
raise CommandError(message)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
" ✓ Settings for all users migrated to ES"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -1,18 +1,5 @@
|
|||||||
{
|
{
|
||||||
"archive": {
|
|
||||||
"sort_by": "published",
|
|
||||||
"sort_order": "desc",
|
|
||||||
"page_size": 12
|
|
||||||
},
|
|
||||||
"default_view": {
|
|
||||||
"home": "grid",
|
|
||||||
"channel": "list",
|
|
||||||
"downloads": "list",
|
|
||||||
"playlist": "grid",
|
|
||||||
"grid_items": 3
|
|
||||||
},
|
|
||||||
"subscriptions": {
|
"subscriptions": {
|
||||||
"auto_download": false,
|
|
||||||
"channel_size": 50,
|
"channel_size": 50,
|
||||||
"live_channel_size": 50,
|
"live_channel_size": 50,
|
||||||
"shorts_channel_size": 50,
|
"shorts_channel_size": 50,
|
||||||
@ -41,7 +28,6 @@
|
|||||||
"app_root": "/app",
|
"app_root": "/app",
|
||||||
"cache_dir": "/cache",
|
"cache_dir": "/cache",
|
||||||
"videos": "/youtube",
|
"videos": "/youtube",
|
||||||
"colors": "dark",
|
|
||||||
"enable_cast": false,
|
"enable_cast": false,
|
||||||
"enable_snapshot": true
|
"enable_snapshot": true
|
||||||
},
|
},
|
||||||
|
@ -417,7 +417,7 @@ class VideoDownloader:
|
|||||||
"lang": "painless",
|
"lang": "painless",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response, _ = ElasticWrap(path, config=self.config).post(data=data)
|
response, _ = ElasticWrap(path).post(data=data)
|
||||||
updated = response.get("updated")
|
updated = response.get("updated")
|
||||||
if updated:
|
if updated:
|
||||||
print(f"[download] reset auto start on {updated} videos.")
|
print(f"[download] reset auto start on {updated} videos.")
|
||||||
|
@ -6,9 +6,9 @@ functionality:
|
|||||||
# pylint: disable=missing-timeout
|
# pylint: disable=missing-timeout
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from home.src.ta.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticWrap:
|
class ElasticWrap:
|
||||||
@ -16,21 +16,13 @@ class ElasticWrap:
|
|||||||
returns response json and status code tuple
|
returns response json and status code tuple
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path, config=False):
|
ES_URL: str = str(os.environ.get("ES_URL"))
|
||||||
self.url = False
|
ES_PASS: str = str(os.environ.get("ELASTIC_PASSWORD"))
|
||||||
self.auth = False
|
ES_USER: str = str(os.environ.get("ELASTIC_USER") or "elastic")
|
||||||
self.path = path
|
|
||||||
self.config = config
|
|
||||||
self._get_config()
|
|
||||||
|
|
||||||
def _get_config(self):
|
def __init__(self, path):
|
||||||
"""add config if not passed"""
|
self.url = f"{self.ES_URL}/{path}"
|
||||||
if not self.config:
|
self.auth = (self.ES_USER, self.ES_PASS)
|
||||||
self.config = AppConfig().config
|
|
||||||
|
|
||||||
es_url = self.config["application"]["es_url"]
|
|
||||||
self.auth = self.config["application"]["es_auth"]
|
|
||||||
self.url = f"{es_url}/{self.path}"
|
|
||||||
|
|
||||||
def get(self, data=False, timeout=10, print_error=True):
|
def get(self, data=False, timeout=10, print_error=True):
|
||||||
"""get data from es"""
|
"""get data from es"""
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
{
|
{
|
||||||
"index_config": [{
|
"index_config": [{
|
||||||
|
"index_name": "config",
|
||||||
|
"expected_map": {
|
||||||
|
"config": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expected_set": {
|
||||||
|
"number_of_replicas": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
"index_name": "channel",
|
"index_name": "channel",
|
||||||
"expected_map": {
|
"expected_map": {
|
||||||
"channel_id": {
|
"channel_id": {
|
||||||
|
@ -4,7 +4,7 @@ Functionality:
|
|||||||
- called via user input
|
- called via user input
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from home.src.ta.ta_redis import RedisArchivist
|
from home.src.ta.users import UserConfig
|
||||||
from home.tasks import run_restore_backup
|
from home.tasks import run_restore_backup
|
||||||
|
|
||||||
|
|
||||||
@ -41,10 +41,8 @@ class PostData:
|
|||||||
|
|
||||||
def _change_view(self):
|
def _change_view(self):
|
||||||
"""process view changes in home, channel, and downloads"""
|
"""process view changes in home, channel, and downloads"""
|
||||||
origin, new_view = self.exec_val.split(":")
|
view, setting = self.exec_val.split(":")
|
||||||
key = f"{self.current_user}:view:{origin}"
|
UserConfig(self.current_user).set_value(f"view_style_{view}", setting)
|
||||||
print(f"change view: {key} to {new_view}")
|
|
||||||
RedisArchivist().set_message(key, {"status": new_view})
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _change_grid(self):
|
def _change_grid(self):
|
||||||
@ -52,48 +50,38 @@ class PostData:
|
|||||||
grid_items = int(self.exec_val)
|
grid_items = int(self.exec_val)
|
||||||
grid_items = max(grid_items, 3)
|
grid_items = max(grid_items, 3)
|
||||||
grid_items = min(grid_items, 7)
|
grid_items = min(grid_items, 7)
|
||||||
|
UserConfig(self.current_user).set_value("grid_items", grid_items)
|
||||||
key = f"{self.current_user}:grid_items"
|
|
||||||
print(f"change grid items: {grid_items}")
|
|
||||||
RedisArchivist().set_message(key, {"status": grid_items})
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _sort_order(self):
|
def _sort_order(self):
|
||||||
"""change the sort between published to downloaded"""
|
"""change the sort between published to downloaded"""
|
||||||
sort_order = {"status": self.exec_val}
|
|
||||||
if self.exec_val in ["asc", "desc"]:
|
if self.exec_val in ["asc", "desc"]:
|
||||||
RedisArchivist().set_message(
|
UserConfig(self.current_user).set_value(
|
||||||
f"{self.current_user}:sort_order", sort_order
|
"sort_order", self.exec_val
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
RedisArchivist().set_message(
|
UserConfig(self.current_user).set_value("sort_by", self.exec_val)
|
||||||
f"{self.current_user}:sort_by", sort_order
|
|
||||||
)
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _hide_watched(self):
|
def _hide_watched(self):
|
||||||
"""toggle if to show watched vids or not"""
|
"""toggle if to show watched vids or not"""
|
||||||
key = f"{self.current_user}:hide_watched"
|
UserConfig(self.current_user).set_value(
|
||||||
message = {"status": bool(int(self.exec_val))}
|
"hide_watched", bool(int(self.exec_val))
|
||||||
print(f"toggle {key}: {message}")
|
)
|
||||||
RedisArchivist().set_message(key, message)
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _show_subed_only(self):
|
def _show_subed_only(self):
|
||||||
"""show or hide subscribed channels only on channels page"""
|
"""show or hide subscribed channels only on channels page"""
|
||||||
key = f"{self.current_user}:show_subed_only"
|
UserConfig(self.current_user).set_value(
|
||||||
message = {"status": bool(int(self.exec_val))}
|
"show_subed_only", bool(int(self.exec_val))
|
||||||
print(f"toggle {key}: {message}")
|
)
|
||||||
RedisArchivist().set_message(key, message)
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _show_ignored_only(self):
|
def _show_ignored_only(self):
|
||||||
"""switch view on /downloads/ to show ignored only"""
|
"""switch view on /downloads/ to show ignored only"""
|
||||||
show_value = self.exec_val
|
UserConfig(self.current_user).set_value(
|
||||||
key = f"{self.current_user}:show_ignored_only"
|
"show_ignored_only", bool(int(self.exec_val))
|
||||||
value = {"status": show_value}
|
)
|
||||||
print(f"Filter download view ignored only: {show_value}")
|
|
||||||
RedisArchivist().set_message(key, value)
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def _db_restore(self):
|
def _db_restore(self):
|
||||||
|
@ -11,23 +11,21 @@ from datetime import datetime
|
|||||||
|
|
||||||
from home.src.download.thumbnails import ThumbManager
|
from home.src.download.thumbnails import ThumbManager
|
||||||
from home.src.es.connect import ElasticWrap
|
from home.src.es.connect import ElasticWrap
|
||||||
from home.src.ta.config import AppConfig
|
|
||||||
from home.src.ta.helper import get_duration_str
|
from home.src.ta.helper import get_duration_str
|
||||||
|
|
||||||
|
|
||||||
class SearchHandler:
|
class SearchHandler:
|
||||||
"""search elastic search"""
|
"""search elastic search"""
|
||||||
|
|
||||||
def __init__(self, path, config, data=False):
|
def __init__(self, path, data=False):
|
||||||
self.max_hits = None
|
self.max_hits = None
|
||||||
self.aggs = None
|
self.aggs = None
|
||||||
self.path = path
|
self.path = path
|
||||||
self.config = config
|
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
"""get the data"""
|
"""get the data"""
|
||||||
response, _ = ElasticWrap(self.path, config=self.config).get(self.data)
|
response, _ = ElasticWrap(self.path).get(self.data)
|
||||||
|
|
||||||
if "hits" in response.keys():
|
if "hits" in response.keys():
|
||||||
self.max_hits = response["hits"]["total"]["value"]
|
self.max_hits = response["hits"]["total"]["value"]
|
||||||
@ -109,12 +107,10 @@ class SearchHandler:
|
|||||||
class SearchForm:
|
class SearchForm:
|
||||||
"""build query from search form data"""
|
"""build query from search form data"""
|
||||||
|
|
||||||
CONFIG = AppConfig().config
|
|
||||||
|
|
||||||
def multi_search(self, search_query):
|
def multi_search(self, search_query):
|
||||||
"""searching through index"""
|
"""searching through index"""
|
||||||
path, query, query_type = SearchParser(search_query).run()
|
path, query, query_type = SearchParser(search_query).run()
|
||||||
look_up = SearchHandler(path, config=self.CONFIG, data=query)
|
look_up = SearchHandler(path, data=query)
|
||||||
search_results = look_up.get_data()
|
search_results = look_up.get_data()
|
||||||
all_results = self.build_results(search_results)
|
all_results = self.build_results(search_results)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import math
|
|||||||
from home.src.download.yt_dlp_base import YtWrap
|
from home.src.download.yt_dlp_base import YtWrap
|
||||||
from home.src.es.connect import ElasticWrap
|
from home.src.es.connect import ElasticWrap
|
||||||
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.users import UserConfig
|
||||||
|
|
||||||
|
|
||||||
class YouTubeItem:
|
class YouTubeItem:
|
||||||
@ -100,13 +100,7 @@ class Pagination:
|
|||||||
|
|
||||||
def get_page_size(self):
|
def get_page_size(self):
|
||||||
"""get default or user modified page_size"""
|
"""get default or user modified page_size"""
|
||||||
key = f"{self.request.user.id}:page_size"
|
return UserConfig(self.request.user.id).get_value("page_size")
|
||||||
page_size = RedisArchivist().get_message(key)["status"]
|
|
||||||
if not page_size:
|
|
||||||
config = AppConfig().config
|
|
||||||
page_size = config["archive"]["page_size"]
|
|
||||||
|
|
||||||
return page_size
|
|
||||||
|
|
||||||
def first_guess(self):
|
def first_guess(self):
|
||||||
"""build first guess before api call"""
|
"""build first guess before api call"""
|
||||||
|
@ -18,7 +18,7 @@ from home.src.index.subtitle import YoutubeSubtitle
|
|||||||
from home.src.index.video_constants import VideoTypeEnum
|
from home.src.index.video_constants import VideoTypeEnum
|
||||||
from home.src.index.video_streams import MediaStreamExtractor
|
from home.src.index.video_streams import MediaStreamExtractor
|
||||||
from home.src.ta.helper import get_duration_sec, get_duration_str, randomizor
|
from home.src.ta.helper import get_duration_sec, get_duration_str, randomizor
|
||||||
from home.src.ta.ta_redis import RedisArchivist
|
from home.src.ta.users import UserConfig
|
||||||
from ryd_client import ryd_client
|
from ryd_client import ryd_client
|
||||||
|
|
||||||
|
|
||||||
@ -32,17 +32,16 @@ class SponsorBlock:
|
|||||||
self.user_agent = f"{settings.TA_UPSTREAM} {settings.TA_VERSION}"
|
self.user_agent = f"{settings.TA_UPSTREAM} {settings.TA_VERSION}"
|
||||||
self.last_refresh = int(datetime.now().timestamp())
|
self.last_refresh = int(datetime.now().timestamp())
|
||||||
|
|
||||||
def get_sb_id(self):
|
def get_sb_id(self) -> str:
|
||||||
"""get sponsorblock userid or generate if needed"""
|
"""get sponsorblock for the userid or generate if needed"""
|
||||||
if not self.user_id:
|
if not self.user_id:
|
||||||
print("missing request user id")
|
raise ValueError("missing request user id")
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
key = f"{self.user_id}:id_sponsorblock"
|
user = UserConfig(self.user_id)
|
||||||
sb_id = RedisArchivist().get_message(key)
|
sb_id = user.get_value("sponsorblock_id")
|
||||||
if not sb_id["status"]:
|
if not sb_id:
|
||||||
sb_id = {"status": randomizor(32)}
|
sb_id = randomizor(32)
|
||||||
RedisArchivist().set_message(key, sb_id)
|
user.set_value("sponsorblock_id", sb_id)
|
||||||
|
|
||||||
return sb_id
|
return sb_id
|
||||||
|
|
||||||
@ -88,7 +87,7 @@ class SponsorBlock:
|
|||||||
|
|
||||||
def post_timestamps(self, youtube_id, start_time, end_time):
|
def post_timestamps(self, youtube_id, start_time, end_time):
|
||||||
"""post timestamps to api"""
|
"""post timestamps to api"""
|
||||||
user_id = self.get_sb_id().get("status")
|
user_id = self.get_sb_id()
|
||||||
data = {
|
data = {
|
||||||
"videoID": youtube_id,
|
"videoID": youtube_id,
|
||||||
"startTime": start_time,
|
"startTime": start_time,
|
||||||
@ -105,7 +104,7 @@ class SponsorBlock:
|
|||||||
|
|
||||||
def vote_on_segment(self, uuid, vote):
|
def vote_on_segment(self, uuid, vote):
|
||||||
"""send vote on existing segment"""
|
"""send vote on existing segment"""
|
||||||
user_id = self.get_sb_id().get("status")
|
user_id = self.get_sb_id()
|
||||||
data = {
|
data = {
|
||||||
"UUID": uuid,
|
"UUID": uuid,
|
||||||
"userID": user_id,
|
"userID": user_id,
|
||||||
|
@ -17,12 +17,10 @@ from home.src.ta.ta_redis import RedisArchivist
|
|||||||
|
|
||||||
|
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
"""handle user settings and application variables"""
|
"""handle application variables"""
|
||||||
|
|
||||||
def __init__(self, user_id=False):
|
def __init__(self):
|
||||||
self.user_id = user_id
|
|
||||||
self.config = self.get_config()
|
self.config = self.get_config()
|
||||||
self.colors = self.get_colors()
|
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
"""get config from default file or redis if changed"""
|
"""get config from default file or redis if changed"""
|
||||||
@ -30,12 +28,6 @@ class AppConfig:
|
|||||||
if not config:
|
if not config:
|
||||||
config = self.get_config_file()
|
config = self.get_config_file()
|
||||||
|
|
||||||
if self.user_id:
|
|
||||||
key = f"{self.user_id}:page_size"
|
|
||||||
page_size = RedisArchivist().get_message(key)["status"]
|
|
||||||
if page_size:
|
|
||||||
config["archive"]["page_size"] = page_size
|
|
||||||
|
|
||||||
config["application"].update(self.get_config_env())
|
config["application"].update(self.get_config_env())
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@ -50,14 +42,12 @@ class AppConfig:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_config_env():
|
def get_config_env():
|
||||||
"""read environment application variables"""
|
"""read environment application variables.
|
||||||
es_pass = os.environ.get("ELASTIC_PASSWORD")
|
|
||||||
es_user = os.environ.get("ELASTIC_USER", default="elastic")
|
Connection to ES is managed in ElasticWrap and the
|
||||||
|
connection to Redis is managed in RedisArchivist."""
|
||||||
|
|
||||||
application = {
|
application = {
|
||||||
"REDIS_HOST": os.environ.get("REDIS_HOST"),
|
|
||||||
"es_url": os.environ.get("ES_URL"),
|
|
||||||
"es_auth": (es_user, es_pass),
|
|
||||||
"HOST_UID": int(os.environ.get("HOST_UID", False)),
|
"HOST_UID": int(os.environ.get("HOST_UID", False)),
|
||||||
"HOST_GID": int(os.environ.get("HOST_GID", False)),
|
"HOST_GID": int(os.environ.get("HOST_GID", False)),
|
||||||
"enable_cast": bool(os.environ.get("ENABLE_CAST")),
|
"enable_cast": bool(os.environ.get("ENABLE_CAST")),
|
||||||
@ -103,30 +93,6 @@ class AppConfig:
|
|||||||
RedisArchivist().set_message("config", self.config, save=True)
|
RedisArchivist().set_message("config", self.config, save=True)
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_user_config(form_post, user_id):
|
|
||||||
"""set values in redis for user settings"""
|
|
||||||
for key, value in form_post.items():
|
|
||||||
if not value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
message = {"status": value}
|
|
||||||
redis_key = f"{user_id}:{key}"
|
|
||||||
RedisArchivist().set_message(redis_key, message, save=True)
|
|
||||||
|
|
||||||
def get_colors(self):
|
|
||||||
"""overwrite config if user has set custom values"""
|
|
||||||
colors = False
|
|
||||||
if self.user_id:
|
|
||||||
col_dict = RedisArchivist().get_message(f"{self.user_id}:colors")
|
|
||||||
colors = col_dict["status"]
|
|
||||||
|
|
||||||
if not colors:
|
|
||||||
colors = self.config["application"]["colors"]
|
|
||||||
|
|
||||||
self.config["application"]["colors"] = colors
|
|
||||||
return colors
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_rand_daily():
|
def _build_rand_daily():
|
||||||
"""build random daily schedule per installation"""
|
"""build random daily schedule per installation"""
|
||||||
|
104
tubearchivist/home/src/ta/users.py
Normal file
104
tubearchivist/home/src/ta/users.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"""
|
||||||
|
Functionality:
|
||||||
|
- read and write user config backed by ES
|
||||||
|
- encapsulate persistence of user properties
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from home.src.es.connect import ElasticWrap
|
||||||
|
|
||||||
|
|
||||||
|
class UserConfigType(TypedDict, total=False):
|
||||||
|
"""describes the user configuration"""
|
||||||
|
|
||||||
|
colors: str
|
||||||
|
page_size: int
|
||||||
|
sort_by: str
|
||||||
|
sort_order: str
|
||||||
|
view_style_home: str
|
||||||
|
view_style_channel: str
|
||||||
|
view_style_downloads: str
|
||||||
|
view_style_playlist: str
|
||||||
|
grid_items: int
|
||||||
|
hide_watched: bool
|
||||||
|
show_ignored_only: bool
|
||||||
|
show_subed_only: bool
|
||||||
|
sponsorblock_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserConfig:
|
||||||
|
"""Handle settings for an individual user
|
||||||
|
|
||||||
|
Create getters and setters for usage in the application.
|
||||||
|
Although tedious it helps prevents everything caring about how properties
|
||||||
|
are persisted. Plus it allows us to save anytime any value is set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_DEFAULT_USER_SETTINGS = UserConfigType(
|
||||||
|
colors="dark",
|
||||||
|
page_size=12,
|
||||||
|
sort_by="published",
|
||||||
|
sort_order="desc",
|
||||||
|
view_style_home="grid",
|
||||||
|
view_style_channel="list",
|
||||||
|
view_style_downloads="list",
|
||||||
|
view_style_playlist="grid",
|
||||||
|
grid_items=3,
|
||||||
|
hide_watched=False,
|
||||||
|
show_ignored_only=False,
|
||||||
|
show_subed_only=False,
|
||||||
|
sponsorblock_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, user_id: str):
|
||||||
|
self._user_id: str = user_id
|
||||||
|
self._config: UserConfigType = self._get_config()
|
||||||
|
|
||||||
|
def get_value(self, key: str):
|
||||||
|
"""Get the given key from the users configuration
|
||||||
|
|
||||||
|
Throws a KeyError if the requested Key is not a permitted value"""
|
||||||
|
if key not in self._DEFAULT_USER_SETTINGS:
|
||||||
|
raise KeyError(f"Unable to read config for unknown key '{key}'")
|
||||||
|
|
||||||
|
return self._config.get(key) or self._DEFAULT_USER_SETTINGS.get(key)
|
||||||
|
|
||||||
|
def set_value(self, key: str, value: str | bool | int):
|
||||||
|
"""Set or replace a configuration value for the user
|
||||||
|
|
||||||
|
Throws a KeyError if the requested Key is not a permitted value"""
|
||||||
|
if not self._user_id:
|
||||||
|
raise ValueError("Unable to persist config for null user_id")
|
||||||
|
|
||||||
|
if key not in self._DEFAULT_USER_SETTINGS:
|
||||||
|
raise KeyError(f"Unable to persist config for unknown key '{key}'")
|
||||||
|
|
||||||
|
old = self.get_value(key)
|
||||||
|
self._config[key] = value
|
||||||
|
|
||||||
|
# Upsert this property (creating a record if not exists)
|
||||||
|
es_payload = {"doc": {"config": {key: value}}, "doc_as_upsert": True}
|
||||||
|
es_document_path = f"ta_config/_update/user_{self._user_id}"
|
||||||
|
response, status = ElasticWrap(es_document_path).post(es_payload)
|
||||||
|
if status < 200 or status > 299:
|
||||||
|
raise ValueError(f"Failed storing user value {status}: {response}")
|
||||||
|
|
||||||
|
print(f"User {self._user_id} value '{key}' change: {old} > {value}")
|
||||||
|
|
||||||
|
def _get_config(self) -> UserConfigType:
|
||||||
|
"""get config from ES or load from the application defaults"""
|
||||||
|
if not self._user_id:
|
||||||
|
# this is for a non logged-in user so use all the defaults
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Does this user have configuration stored in ES
|
||||||
|
es_document_path = f"ta_config/_doc/user_{self._user_id}"
|
||||||
|
response, status = ElasticWrap(es_document_path).get(print_error=False)
|
||||||
|
if status == 200 and "_source" in response.keys():
|
||||||
|
source = response.get("_source")
|
||||||
|
if "config" in source.keys():
|
||||||
|
return source.get("config")
|
||||||
|
|
||||||
|
# There is no config in ES
|
||||||
|
return {}
|
@ -9,7 +9,7 @@
|
|||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<h2>Color scheme</h2>
|
<h2>Color scheme</h2>
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<p>Current color scheme: <span class="settings-current">{{ config.application.colors }}</span></p>
|
<p>Current color scheme: <span class="settings-current">{{ colors }}</span></p>
|
||||||
<i>Select your preferred color scheme between dark and light mode.</i><br>
|
<i>Select your preferred color scheme between dark and light mode.</i><br>
|
||||||
{{ user_form.colors }}
|
{{ user_form.colors }}
|
||||||
</div>
|
</div>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<h2>Archive View</h2>
|
<h2>Archive View</h2>
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
<p>Current page size: <span class="settings-current">{{ config.archive.page_size }}</span></p>
|
<p>Current page size: <span class="settings-current">{{ page_size }}</span></p>
|
||||||
<i>Result of videos showing in archive page</i><br>
|
<i>Result of videos showing in archive page</i><br>
|
||||||
{{ user_form.page_size }}
|
{{ user_form.page_size }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +41,7 @@ from home.src.index.video_constants import VideoTypeEnum
|
|||||||
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
|
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
|
||||||
from home.src.ta.helper import time_parser
|
from home.src.ta.helper import time_parser
|
||||||
from home.src.ta.ta_redis import RedisArchivist
|
from home.src.ta.ta_redis import RedisArchivist
|
||||||
|
from home.src.ta.users import UserConfig
|
||||||
from home.tasks import index_channel_playlists, subscribe_to
|
from home.tasks import index_channel_playlists, subscribe_to
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
@ -52,93 +53,38 @@ class ArchivistViewConfig(View):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.view_origin = view_origin
|
self.view_origin = view_origin
|
||||||
self.user_id = False
|
self.user_id = False
|
||||||
self.user_conf = False
|
self.user_conf: UserConfig = False
|
||||||
self.default_conf = False
|
self.default_conf = False
|
||||||
self.context = False
|
self.context = False
|
||||||
|
|
||||||
def _get_sort_by(self):
|
|
||||||
"""return sort_by config var"""
|
|
||||||
messag_key = f"{self.user_id}:sort_by"
|
|
||||||
sort_by = self.user_conf.get_message(messag_key)["status"]
|
|
||||||
if not sort_by:
|
|
||||||
sort_by = self.default_conf["archive"]["sort_by"]
|
|
||||||
|
|
||||||
return sort_by
|
|
||||||
|
|
||||||
def _get_sort_order(self):
|
|
||||||
"""return sort_order config var"""
|
|
||||||
sort_order_key = f"{self.user_id}:sort_order"
|
|
||||||
sort_order = self.user_conf.get_message(sort_order_key)["status"]
|
|
||||||
if not sort_order:
|
|
||||||
sort_order = self.default_conf["archive"]["sort_order"]
|
|
||||||
|
|
||||||
return sort_order
|
|
||||||
|
|
||||||
def _get_view_style(self):
|
|
||||||
"""return view_style config var"""
|
|
||||||
view_key = f"{self.user_id}:view:{self.view_origin}"
|
|
||||||
view_style = self.user_conf.get_message(view_key)["status"]
|
|
||||||
if not view_style:
|
|
||||||
view_style = self.default_conf["default_view"][self.view_origin]
|
|
||||||
|
|
||||||
return view_style
|
|
||||||
|
|
||||||
def _get_grid_items(self):
|
|
||||||
"""return items per row to show in grid view"""
|
|
||||||
grid_key = f"{self.user_id}:grid_items"
|
|
||||||
grid_items = self.user_conf.get_message(grid_key)["status"]
|
|
||||||
if not grid_items:
|
|
||||||
grid_items = self.default_conf["default_view"]["grid_items"]
|
|
||||||
|
|
||||||
return grid_items
|
|
||||||
|
|
||||||
def get_all_view_styles(self):
|
def get_all_view_styles(self):
|
||||||
"""get dict of all view stiles for search form"""
|
"""get dict of all view styles for search form"""
|
||||||
all_keys = ["channel", "playlist", "home"]
|
|
||||||
all_styles = {}
|
all_styles = {}
|
||||||
for view_origin in all_keys:
|
for view_origin in ["channel", "playlist", "home", "downloads"]:
|
||||||
view_key = f"{self.user_id}:view:{view_origin}"
|
all_styles[view_origin] = self.user_conf.get_value(
|
||||||
view_style = self.user_conf.get_message(view_key)["status"]
|
f"view_style_{view_origin}"
|
||||||
if not view_style:
|
)
|
||||||
view_style = self.default_conf["default_view"][view_origin]
|
|
||||||
all_styles[view_origin] = view_style
|
|
||||||
|
|
||||||
return all_styles
|
return all_styles
|
||||||
|
|
||||||
def _get_hide_watched(self):
|
|
||||||
hide_watched_key = f"{self.user_id}:hide_watched"
|
|
||||||
hide_watched = self.user_conf.get_message(hide_watched_key)["status"]
|
|
||||||
|
|
||||||
return hide_watched
|
|
||||||
|
|
||||||
def _get_show_ignore_only(self):
|
|
||||||
ignored_key = f"{self.user_id}:show_ignored_only"
|
|
||||||
show_ignored_only = self.user_conf.get_message(ignored_key)["status"]
|
|
||||||
|
|
||||||
return show_ignored_only
|
|
||||||
|
|
||||||
def _get_show_subed_only(self):
|
|
||||||
sub_only_key = f"{self.user_id}:show_subed_only"
|
|
||||||
show_subed_only = self.user_conf.get_message(sub_only_key)["status"]
|
|
||||||
|
|
||||||
return show_subed_only
|
|
||||||
|
|
||||||
def config_builder(self, user_id):
|
def config_builder(self, user_id):
|
||||||
"""build default context for every view"""
|
"""build default context for every view"""
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.user_conf = RedisArchivist()
|
self.user_conf = UserConfig(self.user_id)
|
||||||
self.default_conf = AppConfig(self.user_id).config
|
self.default_conf = AppConfig().config
|
||||||
|
|
||||||
self.context = {
|
self.context = {
|
||||||
"colors": self.default_conf["application"]["colors"],
|
"colors": self.user_conf.get_value("colors"),
|
||||||
"cast": self.default_conf["application"]["enable_cast"],
|
"cast": self.default_conf["application"]["enable_cast"],
|
||||||
"sort_by": self._get_sort_by(),
|
"sort_by": self.user_conf.get_value("sort_by"),
|
||||||
"sort_order": self._get_sort_order(),
|
"sort_order": self.user_conf.get_value("sort_order"),
|
||||||
"view_style": self._get_view_style(),
|
"view_style": self.user_conf.get_value(
|
||||||
"grid_items": self._get_grid_items(),
|
f"view_style_{self.view_origin}"
|
||||||
"hide_watched": self._get_hide_watched(),
|
),
|
||||||
"show_ignored_only": self._get_show_ignore_only(),
|
"grid_items": self.user_conf.get_value("grid_items"),
|
||||||
"show_subed_only": self._get_show_subed_only(),
|
"hide_watched": self.user_conf.get_value("hide_watched"),
|
||||||
|
"show_ignored_only": self.user_conf.get_value("show_ignored_only"),
|
||||||
|
"show_subed_only": self.user_conf.get_value("show_subed_only"),
|
||||||
"version": settings.TA_VERSION,
|
"version": settings.TA_VERSION,
|
||||||
"ta_update": ReleaseVersion().get_update(),
|
"ta_update": ReleaseVersion().get_update(),
|
||||||
}
|
}
|
||||||
@ -212,13 +158,11 @@ class ArchivistResultsView(ArchivistViewConfig):
|
|||||||
"""get all videos in progress"""
|
"""get all videos in progress"""
|
||||||
ids = [{"match": {"youtube_id": i.get("youtube_id")}} for i in results]
|
ids = [{"match": {"youtube_id": i.get("youtube_id")}} for i in results]
|
||||||
data = {
|
data = {
|
||||||
"size": self.default_conf["archive"]["page_size"],
|
"size": UserConfig(self.user_id).get_value("page_size"),
|
||||||
"query": {"bool": {"should": ids}},
|
"query": {"bool": {"should": ids}},
|
||||||
"sort": [{"published": {"order": "desc"}}],
|
"sort": [{"published": {"order": "desc"}}],
|
||||||
}
|
}
|
||||||
search = SearchHandler(
|
search = SearchHandler("ta_video/_search", data=data)
|
||||||
"ta_video/_search", self.default_conf, data=data
|
|
||||||
)
|
|
||||||
videos = search.get_data()
|
videos = search.get_data()
|
||||||
if not videos:
|
if not videos:
|
||||||
return False
|
return False
|
||||||
@ -236,7 +180,7 @@ class ArchivistResultsView(ArchivistViewConfig):
|
|||||||
|
|
||||||
def single_lookup(self, es_path):
|
def single_lookup(self, es_path):
|
||||||
"""retrieve a single item from url"""
|
"""retrieve a single item from url"""
|
||||||
search = SearchHandler(es_path, config=self.default_conf)
|
search = SearchHandler(es_path)
|
||||||
result = search.get_data()[0]["source"]
|
result = search.get_data()[0]["source"]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -251,9 +195,7 @@ class ArchivistResultsView(ArchivistViewConfig):
|
|||||||
|
|
||||||
def find_results(self):
|
def find_results(self):
|
||||||
"""add results and pagination to context"""
|
"""add results and pagination to context"""
|
||||||
search = SearchHandler(
|
search = SearchHandler(self.es_search, data=self.data)
|
||||||
self.es_search, config=self.default_conf, data=self.data
|
|
||||||
)
|
|
||||||
self.context["results"] = search.get_data()
|
self.context["results"] = search.get_data()
|
||||||
self.pagination_handler.validate(search.max_hits)
|
self.pagination_handler.validate(search.max_hits)
|
||||||
self.context["max_hits"] = search.max_hits
|
self.context["max_hits"] = search.max_hits
|
||||||
@ -268,7 +210,7 @@ class MinView(View):
|
|||||||
def get_min_context(request):
|
def get_min_context(request):
|
||||||
"""build minimal vars for context"""
|
"""build minimal vars for context"""
|
||||||
return {
|
return {
|
||||||
"colors": AppConfig(request.user.id).colors,
|
"colors": UserConfig(request.user.id).get_value("colors"),
|
||||||
"version": settings.TA_VERSION,
|
"version": settings.TA_VERSION,
|
||||||
"ta_update": ReleaseVersion().get_update(),
|
"ta_update": ReleaseVersion().get_update(),
|
||||||
}
|
}
|
||||||
@ -892,8 +834,8 @@ class VideoView(MinView):
|
|||||||
|
|
||||||
def get(self, request, video_id):
|
def get(self, request, video_id):
|
||||||
"""get single video"""
|
"""get single video"""
|
||||||
config_handler = AppConfig(request.user.id)
|
config_handler = AppConfig()
|
||||||
look_up = SearchHandler(f"ta_video/_doc/{video_id}", config=False)
|
look_up = SearchHandler(f"ta_video/_doc/{video_id}")
|
||||||
video_data = look_up.get_data()[0]["source"]
|
video_data = look_up.get_data()[0]["source"]
|
||||||
try:
|
try:
|
||||||
rating = video_data["stats"]["average_rating"]
|
rating = video_data["stats"]["average_rating"]
|
||||||
@ -1005,7 +947,9 @@ class SettingsUserView(MinView):
|
|||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"title": "User Settings",
|
"title": "User Settings",
|
||||||
"config": AppConfig(request.user.id).config,
|
"page_size": UserConfig(request.user.id).get_value(
|
||||||
|
"page_size"
|
||||||
|
),
|
||||||
"user_form": UserSettingsForm(),
|
"user_form": UserSettingsForm(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1015,10 +959,17 @@ class SettingsUserView(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)
|
user_form = UserSettingsForm(request.POST)
|
||||||
|
config_handler = UserConfig(request.user.id)
|
||||||
if user_form.is_valid():
|
if user_form.is_valid():
|
||||||
user_form_post = user_form.cleaned_data
|
user_form_post = user_form.cleaned_data
|
||||||
if any(user_form_post.values()):
|
if user_form_post.get("colors"):
|
||||||
AppConfig().set_user_config(user_form_post, request.user.id)
|
config_handler.set_value(
|
||||||
|
"colors", user_form_post.get("colors")
|
||||||
|
)
|
||||||
|
if user_form_post.get("page_size"):
|
||||||
|
config_handler.set_value(
|
||||||
|
"page_size", user_form_post.get("page_size")
|
||||||
|
)
|
||||||
|
|
||||||
sleep(1)
|
sleep(1)
|
||||||
return redirect("settings_user", permanent=True)
|
return redirect("settings_user", permanent=True)
|
||||||
@ -1037,7 +988,7 @@ class SettingsApplicationView(MinView):
|
|||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"title": "Application Settings",
|
"title": "Application Settings",
|
||||||
"config": AppConfig(request.user.id).config,
|
"config": AppConfig().config,
|
||||||
"api_token": self.get_token(request),
|
"api_token": self.get_token(request),
|
||||||
"app_form": ApplicationSettingsForm(),
|
"app_form": ApplicationSettingsForm(),
|
||||||
"snapshots": ElasticSnapshot().get_snapshot_stats(),
|
"snapshots": ElasticSnapshot().get_snapshot_stats(),
|
||||||
@ -1126,7 +1077,7 @@ class SettingsSchedulingView(MinView):
|
|||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"title": "Scheduling Settings",
|
"title": "Scheduling Settings",
|
||||||
"config": AppConfig(request.user.id).config,
|
"config": AppConfig().config,
|
||||||
"scheduler_form": SchedulerSettingsForm(),
|
"scheduler_form": SchedulerSettingsForm(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user