Reworked Config Schemas, added File/Folder-Picker and dedicated Int-Spinners

This commit is contained in:
Christoph Stahl 2024-10-06 02:29:18 +02:00
parent 9279a6a5a2
commit eb725c7c33
7 changed files with 229 additions and 50 deletions

48
syng/config.py Normal file
View file

@ -0,0 +1,48 @@
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
class Option(Generic[T]):
pass
@dataclass
class ConfigOption(Generic[T]):
type: Option[T]
description: str
default: T
class BoolOption(Option[bool]):
pass
class IntOption(Option[int]):
pass
class StrOption(Option[str]):
pass
class PasswordOption(Option[str]):
pass
class FolderOption(Option[str]):
pass
class FileOption(Option[str]):
pass
class ListStrOption(Option[list[str]]):
pass
@dataclass
class ChoiceOption(Option[str]):
choices: list[str]

View file

@ -32,6 +32,7 @@ from PyQt6.QtWidgets import (
QCheckBox, QCheckBox,
QComboBox, QComboBox,
QDateTimeEdit, QDateTimeEdit,
QFileDialog,
QFormLayout, QFormLayout,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
@ -40,6 +41,7 @@ from PyQt6.QtWidgets import (
QPushButton, QPushButton,
QSizePolicy, QSizePolicy,
QSpacerItem, QSpacerItem,
QSpinBox,
QTabWidget, QTabWidget,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@ -52,6 +54,16 @@ from . import resources # noqa
from .client import create_async_and_start_client, default_config from .client import create_async_and_start_client, default_config
from .sources import available_sources from .sources import available_sources
from .config import (
BoolOption,
ChoiceOption,
FileOption,
FolderOption,
IntOption,
ListStrOption,
PasswordOption,
StrOption,
)
# try: # try:
# from .server import run_server # from .server import run_server
@ -72,7 +84,6 @@ class OptionFrame(QWidget):
self.bool_options[name] = QCheckBox(self) self.bool_options[name] = QCheckBox(self)
self.bool_options[name].setChecked(value) self.bool_options[name].setChecked(value)
self.form_layout.addRow(label, self.bool_options[name]) self.form_layout.addRow(label, self.bool_options[name])
self.number_of_options += 1
def add_string_option( def add_string_option(
self, self,
@ -105,9 +116,100 @@ class OptionFrame(QWidget):
self.string_options[name].insert(value) self.string_options[name].insert(value)
self.form_layout.addRow(label, self.string_options[name]) self.form_layout.addRow(label, self.string_options[name])
self.rows[name] = (label, self.string_options[name])
if callback is not None: if callback is not None:
self.string_options[name].textChanged.connect(callback) self.string_options[name].textChanged.connect(callback)
self.number_of_options += 1
def path_setter(self, line: QLineEdit, name: Optional[str]) -> None:
if name:
line.setText(name)
def add_file_option(
self,
name: str,
description: str,
value: Optional[str] = "",
callback: Optional[Callable[..., None]] = None,
) -> None:
if value is None:
value = ""
label = QLabel(description, self)
file_layout = QHBoxLayout()
file_name_widget = QLineEdit(value, self)
file_button = QPushButton(QIcon.fromTheme("document-open"), "", self)
file_button.clicked.connect(
lambda: self.path_setter(
file_name_widget,
QFileDialog.getOpenFileName(
self, "Select File", directory=os.path.dirname(file_name_widget.text())
)[0],
)
)
if callback is not None:
file_name_widget.textChanged.connect(callback)
file_layout.addWidget(file_name_widget)
file_layout.addWidget(file_button)
self.string_options[name] = file_name_widget
self.rows[name] = (label, file_name_widget)
self.form_layout.addRow(label, file_layout)
def add_folder_option(
self,
name: str,
description: str,
value: Optional[str] = "",
callback: Optional[Callable[..., None]] = None,
) -> None:
if value is None:
value = ""
label = QLabel(description, self)
folder_layout = QHBoxLayout()
folder_name_widget = QLineEdit(value, self)
folder_button = QPushButton(QIcon.fromTheme("folder-open"), "", self)
folder_button.clicked.connect(
lambda: self.path_setter(
folder_name_widget,
QFileDialog.getExistingDirectory(
self, "Select Folder", directory=folder_name_widget.text()
),
)
)
if callback is not None:
folder_name_widget.textChanged.connect(callback)
folder_layout.addWidget(folder_name_widget)
folder_layout.addWidget(folder_button)
self.string_options[name] = folder_name_widget
self.rows[name] = (label, folder_name_widget)
self.form_layout.addRow(label, folder_layout)
def add_int_option(
self,
name: str,
description: str,
value: Optional[int] = 0,
callback: Optional[Callable[..., None]] = None,
) -> None:
if value is None:
value = 0
label = QLabel(description, self)
self.int_options[name] = QSpinBox(self)
self.int_options[name].setValue(value)
self.form_layout.addRow(label, self.int_options[name])
self.rows[name] = (label, self.int_options[name])
if callback is not None:
self.int_options[name].textChanged.connect(callback)
def del_list_element( def del_list_element(
self, self,
@ -165,6 +267,7 @@ class OptionFrame(QWidget):
container_layout = QVBoxLayout() container_layout = QVBoxLayout()
self.form_layout.addRow(label, container_layout) self.form_layout.addRow(label, container_layout)
self.rows[name] = (label, container_layout)
self.list_options[name] = [] self.list_options[name] = []
for v in value: for v in value:
@ -177,8 +280,6 @@ class OptionFrame(QWidget):
container_layout.addWidget(plus_button) container_layout.addWidget(plus_button)
self.number_of_options += 1
def add_choose_option( def add_choose_option(
self, name: str, description: str, values: list[str], value: str = "" self, name: str, description: str, values: list[str], value: str = ""
) -> None: ) -> None:
@ -186,9 +287,9 @@ class OptionFrame(QWidget):
self.choose_options[name] = QComboBox(self) self.choose_options[name] = QComboBox(self)
self.choose_options[name].addItems(values) self.choose_options[name].addItems(values)
self.choose_options[name].setCurrentText(value) self.choose_options[name].setCurrentText(str(value))
self.form_layout.addRow(label, self.choose_options[name]) self.form_layout.addRow(label, self.choose_options[name])
self.number_of_options += 1 self.rows[name] = (label, self.choose_options[name])
def add_date_time_option(self, name: str, description: str, value: str) -> None: def add_date_time_option(self, name: str, description: str, value: str) -> None:
label = QLabel(description, self) label = QLabel(description, self)
@ -213,25 +314,27 @@ class OptionFrame(QWidget):
date_time_layout.addWidget(date_time_enabled) date_time_layout.addWidget(date_time_enabled)
self.form_layout.addRow(label, date_time_layout) self.form_layout.addRow(label, date_time_layout)
self.rows[name] = (label, date_time_layout)
self.number_of_options += 1
def __init__(self, parent: Optional[QWidget] = None) -> None: def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent) super().__init__(parent)
self.form_layout = QFormLayout(self) self.form_layout = QFormLayout(self)
self.setLayout(self.form_layout) self.setLayout(self.form_layout)
self.number_of_options: int = 0
self.string_options: dict[str, QLineEdit] = {} self.string_options: dict[str, QLineEdit] = {}
self.int_options: dict[str, QSpinBox] = {}
self.choose_options: dict[str, QComboBox] = {} self.choose_options: dict[str, QComboBox] = {}
self.bool_options: dict[str, QCheckBox] = {} self.bool_options: dict[str, QCheckBox] = {}
self.list_options: dict[str, list[QLineEdit]] = {} self.list_options: dict[str, list[QLineEdit]] = {}
self.date_time_options: dict[str, tuple[QDateTimeEdit, QCheckBox]] = {} self.date_time_options: dict[str, tuple[QDateTimeEdit, QCheckBox]] = {}
self.rows: dict[str, tuple[QLabel, QWidget | QLayout]] = {}
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config: dict[str, Any] = {} config: dict[str, Any] = {}
for name, textbox in self.string_options.items(): for name, textbox in self.string_options.items():
config[name] = textbox.text().strip() config[name] = textbox.text().strip()
for name, textbox in self.int_options.items():
config[name] = textbox.value()
for name, optionmenu in self.choose_options.items(): for name, optionmenu in self.choose_options.items():
config[name] = optionmenu.currentText().strip() config[name] = optionmenu.currentText().strip()
@ -260,17 +363,25 @@ class SourceTab(OptionFrame):
super().__init__(parent) super().__init__(parent)
source = available_sources[source_name] source = available_sources[source_name]
self.vars: dict[str, str | bool | list[str]] = {} self.vars: dict[str, str | bool | list[str]] = {}
for name, (typ, desc, default) in source.config_schema.items(): for name, option in source.config_schema.items():
value = config[name] if name in config else default value = config[name] if name in config else option.default
match typ: match option.type:
case builtins.bool: case BoolOption():
self.add_bool_option(name, desc, value=value) self.add_bool_option(name, option.description, value=value)
case builtins.list: case ListStrOption():
self.add_list_option(name, desc, value=value) self.add_list_option(name, option.description, value=value)
case builtins.str: case StrOption():
self.add_string_option(name, desc, value=value) self.add_string_option(name, option.description, value=value)
case "password": case IntOption():
self.add_string_option(name, desc, value=value, is_password=True) self.add_int_option(name, option.description, value=value)
case PasswordOption():
self.add_string_option(name, option.description, value=value, is_password=True)
case FolderOption():
self.add_folder_option(name, option.description, value=value)
case FileOption():
self.add_file_option(name, option.description, value=value)
case ChoiceOption(choices):
self.add_choose_option(name, option.description, choices, value)
class GeneralConfig(OptionFrame): class GeneralConfig(OptionFrame):
@ -292,8 +403,8 @@ class GeneralConfig(OptionFrame):
str(config["waiting_room_policy"]).lower(), str(config["waiting_room_policy"]).lower(),
) )
self.add_date_time_option("last_song", "Last song ends at", config["last_song"]) self.add_date_time_option("last_song", "Last song ends at", config["last_song"])
self.add_string_option( self.add_int_option(
"preview_duration", "Preview duration in seconds", str(config["preview_duration"]) "preview_duration", "Preview duration in seconds", int(config["preview_duration"])
) )
self.add_string_option( self.add_string_option(
"key", "Key for server (if necessary)", config["key"], is_password=True "key", "Key for server (if necessary)", config["key"], is_password=True
@ -302,11 +413,6 @@ class GeneralConfig(OptionFrame):
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config = super().get_config() config = super().get_config()
try:
config["preview_duration"] = int(config["preview_duration"])
except ValueError:
config["preview_duration"] = 0
return config return config

View file

@ -4,6 +4,7 @@ import asyncio
import os import os
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
try: try:
from pymediainfo import MediaInfo from pymediainfo import MediaInfo
@ -14,6 +15,7 @@ except ImportError:
PYMEDIAINFO_AVAILABLE = False PYMEDIAINFO_AVAILABLE = False
from .source import Source from .source import Source
from ..config import ListStrOption, ConfigOption
class FileBasedSource(Source): class FileBasedSource(Source):
@ -25,8 +27,8 @@ class FileBasedSource(Source):
""" """
config_schema = Source.config_schema | { config_schema = Source.config_schema | {
"extensions": ( "extensions": ConfigOption(
list, ListStrOption(),
"List of filename extensions\n(mp3+cdg, mp4, ...)", "List of filename extensions\n(mp3+cdg, mp4, ...)",
["mp3+cdg"], ["mp3+cdg"],
) )

View file

@ -6,9 +6,11 @@ from typing import Any, Optional
from typing import Tuple from typing import Tuple
from platformdirs import user_cache_dir from platformdirs import user_cache_dir
from ..entry import Entry from ..entry import Entry
from .source import available_sources from .source import available_sources
from .filebased import FileBasedSource from .filebased import FileBasedSource
from ..config import FolderOption, ConfigOption
class FilesSource(FileBasedSource): class FilesSource(FileBasedSource):
@ -20,8 +22,8 @@ class FilesSource(FileBasedSource):
source_name = "files" source_name = "files"
config_schema = FileBasedSource.config_schema | { config_schema = FileBasedSource.config_schema | {
"dir": (str, "Directory to index", "."), "dir": ConfigOption(FolderOption(), "Directory to index", "."),
"index_file": (str, "Index file", os.path.join(user_cache_dir("syng"), "files-index")), # "index_file": ("file", "Index file", os.path.join(user_cache_dir("syng"), "files-index")),
} }
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):

View file

@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Optional, Tuple, cast
from platformdirs import user_cache_dir from platformdirs import user_cache_dir
try: try:
from minio import Minio from minio import Minio
@ -23,6 +24,7 @@ except ImportError:
from ..entry import Entry from ..entry import Entry
from .filebased import FileBasedSource from .filebased import FileBasedSource
from .source import available_sources from .source import available_sources
from ..config import BoolOption, ConfigOption, FileOption, FolderOption, PasswordOption, StrOption
class S3Source(FileBasedSource): class S3Source(FileBasedSource):
@ -40,13 +42,19 @@ class S3Source(FileBasedSource):
source_name = "s3" source_name = "s3"
config_schema = FileBasedSource.config_schema | { config_schema = FileBasedSource.config_schema | {
"endpoint": (str, "Endpoint of the s3", ""), "endpoint": ConfigOption(StrOption(), "Endpoint of the s3", ""),
"access_key": ("password", "Access Key of the s3", ""), "access_key": ConfigOption(StrOption(), "Access Key of the s3 (username)", ""),
"secret_key": ("password", "Secret Key of the s3", ""), "secret_key": ConfigOption(PasswordOption(), "Secret Key of the s3 (password)", ""),
"secure": (bool, "Use SSL", True), "secure": ConfigOption(BoolOption(), "Use SSL", True),
"bucket": (str, "Bucket of the s3", ""), "bucket": ConfigOption(StrOption(), "Bucket of the s3", ""),
"tmp_dir": (str, "Folder for\ntemporary download", user_cache_dir("syng")), "tmp_dir": ConfigOption(
"index_file": (str, "Index file", os.path.join(user_cache_dir("syng"), "s3-index")), FolderOption(), "Folder for\ntemporary download", user_cache_dir("syng")
),
"index_file": ConfigOption(
FileOption(),
"Index file",
os.path.join(user_cache_dir("syng"), "s3-index"),
),
} }
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):

View file

@ -21,9 +21,11 @@ from typing import Tuple
from typing import Type from typing import Type
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from ..log import logger from ..log import logger
from ..entry import Entry from ..entry import Entry
from ..result import Result from ..result import Result
from ..config import BoolOption, ConfigOption
# logger: logging.Logger = logging.getLogger(__name__) # logger: logging.Logger = logging.getLogger(__name__)
@ -106,8 +108,8 @@ class Source(ABC):
""" """
source_name: str = "" source_name: str = ""
config_schema: dict[str, tuple[type | list[type] | str, str, Any]] = { config_schema: dict[str, ConfigOption[Any]] = {
"enabled": (bool, "Enable this source", False) "enabled": ConfigOption(BoolOption(), "Enable this source", False)
} }
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):

View file

@ -18,9 +18,11 @@ from yt_dlp import YoutubeDL
from yt_dlp.utils import DownloadError from yt_dlp.utils import DownloadError
from platformdirs import user_cache_dir from platformdirs import user_cache_dir
from ..entry import Entry from ..entry import Entry
from ..result import Result from ..result import Result
from .source import Source, available_sources from .source import Source, available_sources
from ..config import BoolOption, ChoiceOption, FolderOption, ListStrOption, Option, ConfigOption
class YouTube: class YouTube:
@ -28,9 +30,9 @@ class YouTube:
A minimal compatibility layer for the YouTube object of pytube, implemented via yt-dlp A minimal compatibility layer for the YouTube object of pytube, implemented via yt-dlp
""" """
__cache__: dict[ __cache__: dict[str, Any] = (
str, Any {}
] = {} # TODO: this may grow fast... but atm it fixed youtubes anti bot measures ) # TODO: this may grow fast... but atm it fixed youtubes anti bot measures
def __init__(self, url: Optional[str] = None): def __init__(self, url: Optional[str] = None):
""" """
@ -182,12 +184,18 @@ class YoutubeSource(Source):
source_name = "youtube" source_name = "youtube"
config_schema = Source.config_schema | { config_schema = Source.config_schema | {
"enabled": (bool, "Enable this source", True), "enabled": ConfigOption(BoolOption(), "Enable this source", True),
"channels": (list, "A list channels\nto search in", []), "channels": ConfigOption(ListStrOption(), "A list channels\nto search in", []),
"tmp_dir": (str, "Folder for\ntemporary download", user_cache_dir("syng")), "tmp_dir": ConfigOption(
"max_res": (int, "Maximum resolution\nto download", 720), FolderOption(), "Folder for\ntemporary download", user_cache_dir("syng")
"start_streaming": ( ),
bool, "max_res": ConfigOption(
ChoiceOption(["144", "240", "360", "480", "720", "1080", "2160"]),
"Maximum resolution\nto download",
"720",
),
"start_streaming": ConfigOption(
BoolOption(),
"Start streaming if\ndownload is not complete", "Start streaming if\ndownload is not complete",
False, False,
), ),
@ -205,7 +213,10 @@ class YoutubeSource(Source):
self.channels: list[str] = config["channels"] if "channels" in config else [] self.channels: list[str] = config["channels"] if "channels" in config else []
self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng" self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng"
self.max_res: int = config["max_res"] if "max_res" in config else 720 try:
self.max_res: int = int(config["max_res"])
except (ValueError, KeyError):
self.max_res = 720
self.start_streaming: bool = ( self.start_streaming: bool = (
config["start_streaming"] if "start_streaming" in config else False config["start_streaming"] if "start_streaming" in config else False
) )