Reworked Config Schemas, added File/Folder-Picker and dedicated Int-Spinners
This commit is contained in:
parent
9279a6a5a2
commit
eb725c7c33
7 changed files with 229 additions and 50 deletions
48
syng/config.py
Normal file
48
syng/config.py
Normal 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]
|
160
syng/gui.py
160
syng/gui.py
|
@ -32,6 +32,7 @@ from PyQt6.QtWidgets import (
|
|||
QCheckBox,
|
||||
QComboBox,
|
||||
QDateTimeEdit,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
|
@ -40,6 +41,7 @@ from PyQt6.QtWidgets import (
|
|||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QSpinBox,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
|
@ -52,6 +54,16 @@ from . import resources # noqa
|
|||
from .client import create_async_and_start_client, default_config
|
||||
|
||||
from .sources import available_sources
|
||||
from .config import (
|
||||
BoolOption,
|
||||
ChoiceOption,
|
||||
FileOption,
|
||||
FolderOption,
|
||||
IntOption,
|
||||
ListStrOption,
|
||||
PasswordOption,
|
||||
StrOption,
|
||||
)
|
||||
|
||||
# try:
|
||||
# from .server import run_server
|
||||
|
@ -72,7 +84,6 @@ class OptionFrame(QWidget):
|
|||
self.bool_options[name] = QCheckBox(self)
|
||||
self.bool_options[name].setChecked(value)
|
||||
self.form_layout.addRow(label, self.bool_options[name])
|
||||
self.number_of_options += 1
|
||||
|
||||
def add_string_option(
|
||||
self,
|
||||
|
@ -105,9 +116,100 @@ class OptionFrame(QWidget):
|
|||
|
||||
self.string_options[name].insert(value)
|
||||
self.form_layout.addRow(label, self.string_options[name])
|
||||
self.rows[name] = (label, self.string_options[name])
|
||||
if callback is not None:
|
||||
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(
|
||||
self,
|
||||
|
@ -165,6 +267,7 @@ class OptionFrame(QWidget):
|
|||
container_layout = QVBoxLayout()
|
||||
|
||||
self.form_layout.addRow(label, container_layout)
|
||||
self.rows[name] = (label, container_layout)
|
||||
|
||||
self.list_options[name] = []
|
||||
for v in value:
|
||||
|
@ -177,8 +280,6 @@ class OptionFrame(QWidget):
|
|||
|
||||
container_layout.addWidget(plus_button)
|
||||
|
||||
self.number_of_options += 1
|
||||
|
||||
def add_choose_option(
|
||||
self, name: str, description: str, values: list[str], value: str = ""
|
||||
) -> None:
|
||||
|
@ -186,9 +287,9 @@ class OptionFrame(QWidget):
|
|||
|
||||
self.choose_options[name] = QComboBox(self)
|
||||
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.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:
|
||||
label = QLabel(description, self)
|
||||
|
@ -213,25 +314,27 @@ class OptionFrame(QWidget):
|
|||
date_time_layout.addWidget(date_time_enabled)
|
||||
|
||||
self.form_layout.addRow(label, date_time_layout)
|
||||
|
||||
self.number_of_options += 1
|
||||
self.rows[name] = (label, date_time_layout)
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.form_layout = QFormLayout(self)
|
||||
self.setLayout(self.form_layout)
|
||||
self.number_of_options: int = 0
|
||||
self.string_options: dict[str, QLineEdit] = {}
|
||||
self.int_options: dict[str, QSpinBox] = {}
|
||||
self.choose_options: dict[str, QComboBox] = {}
|
||||
self.bool_options: dict[str, QCheckBox] = {}
|
||||
self.list_options: dict[str, list[QLineEdit]] = {}
|
||||
self.date_time_options: dict[str, tuple[QDateTimeEdit, QCheckBox]] = {}
|
||||
self.rows: dict[str, tuple[QLabel, QWidget | QLayout]] = {}
|
||||
|
||||
def get_config(self) -> dict[str, Any]:
|
||||
config: dict[str, Any] = {}
|
||||
for name, textbox in self.string_options.items():
|
||||
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():
|
||||
config[name] = optionmenu.currentText().strip()
|
||||
|
||||
|
@ -260,17 +363,25 @@ class SourceTab(OptionFrame):
|
|||
super().__init__(parent)
|
||||
source = available_sources[source_name]
|
||||
self.vars: dict[str, str | bool | list[str]] = {}
|
||||
for name, (typ, desc, default) in source.config_schema.items():
|
||||
value = config[name] if name in config else default
|
||||
match typ:
|
||||
case builtins.bool:
|
||||
self.add_bool_option(name, desc, value=value)
|
||||
case builtins.list:
|
||||
self.add_list_option(name, desc, value=value)
|
||||
case builtins.str:
|
||||
self.add_string_option(name, desc, value=value)
|
||||
case "password":
|
||||
self.add_string_option(name, desc, value=value, is_password=True)
|
||||
for name, option in source.config_schema.items():
|
||||
value = config[name] if name in config else option.default
|
||||
match option.type:
|
||||
case BoolOption():
|
||||
self.add_bool_option(name, option.description, value=value)
|
||||
case ListStrOption():
|
||||
self.add_list_option(name, option.description, value=value)
|
||||
case StrOption():
|
||||
self.add_string_option(name, option.description, value=value)
|
||||
case IntOption():
|
||||
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):
|
||||
|
@ -292,8 +403,8 @@ class GeneralConfig(OptionFrame):
|
|||
str(config["waiting_room_policy"]).lower(),
|
||||
)
|
||||
self.add_date_time_option("last_song", "Last song ends at", config["last_song"])
|
||||
self.add_string_option(
|
||||
"preview_duration", "Preview duration in seconds", str(config["preview_duration"])
|
||||
self.add_int_option(
|
||||
"preview_duration", "Preview duration in seconds", int(config["preview_duration"])
|
||||
)
|
||||
self.add_string_option(
|
||||
"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]:
|
||||
config = super().get_config()
|
||||
try:
|
||||
config["preview_duration"] = int(config["preview_duration"])
|
||||
except ValueError:
|
||||
config["preview_duration"] = 0
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import asyncio
|
|||
import os
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
|
||||
try:
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
|
@ -14,6 +15,7 @@ except ImportError:
|
|||
PYMEDIAINFO_AVAILABLE = False
|
||||
|
||||
from .source import Source
|
||||
from ..config import ListStrOption, ConfigOption
|
||||
|
||||
|
||||
class FileBasedSource(Source):
|
||||
|
@ -25,8 +27,8 @@ class FileBasedSource(Source):
|
|||
"""
|
||||
|
||||
config_schema = Source.config_schema | {
|
||||
"extensions": (
|
||||
list,
|
||||
"extensions": ConfigOption(
|
||||
ListStrOption(),
|
||||
"List of filename extensions\n(mp3+cdg, mp4, ...)",
|
||||
["mp3+cdg"],
|
||||
)
|
||||
|
|
|
@ -6,9 +6,11 @@ from typing import Any, Optional
|
|||
from typing import Tuple
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
|
||||
from ..entry import Entry
|
||||
from .source import available_sources
|
||||
from .filebased import FileBasedSource
|
||||
from ..config import FolderOption, ConfigOption
|
||||
|
||||
|
||||
class FilesSource(FileBasedSource):
|
||||
|
@ -20,8 +22,8 @@ class FilesSource(FileBasedSource):
|
|||
|
||||
source_name = "files"
|
||||
config_schema = FileBasedSource.config_schema | {
|
||||
"dir": (str, "Directory to index", "."),
|
||||
"index_file": (str, "Index file", os.path.join(user_cache_dir("syng"), "files-index")),
|
||||
"dir": ConfigOption(FolderOption(), "Directory to index", "."),
|
||||
# "index_file": ("file", "Index file", os.path.join(user_cache_dir("syng"), "files-index")),
|
||||
}
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
|
|
|
@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Optional, Tuple, cast
|
|||
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
|
||||
try:
|
||||
from minio import Minio
|
||||
|
||||
|
@ -23,6 +24,7 @@ except ImportError:
|
|||
from ..entry import Entry
|
||||
from .filebased import FileBasedSource
|
||||
from .source import available_sources
|
||||
from ..config import BoolOption, ConfigOption, FileOption, FolderOption, PasswordOption, StrOption
|
||||
|
||||
|
||||
class S3Source(FileBasedSource):
|
||||
|
@ -40,13 +42,19 @@ class S3Source(FileBasedSource):
|
|||
|
||||
source_name = "s3"
|
||||
config_schema = FileBasedSource.config_schema | {
|
||||
"endpoint": (str, "Endpoint of the s3", ""),
|
||||
"access_key": ("password", "Access Key of the s3", ""),
|
||||
"secret_key": ("password", "Secret Key of the s3", ""),
|
||||
"secure": (bool, "Use SSL", True),
|
||||
"bucket": (str, "Bucket of the s3", ""),
|
||||
"tmp_dir": (str, "Folder for\ntemporary download", user_cache_dir("syng")),
|
||||
"index_file": (str, "Index file", os.path.join(user_cache_dir("syng"), "s3-index")),
|
||||
"endpoint": ConfigOption(StrOption(), "Endpoint of the s3", ""),
|
||||
"access_key": ConfigOption(StrOption(), "Access Key of the s3 (username)", ""),
|
||||
"secret_key": ConfigOption(PasswordOption(), "Secret Key of the s3 (password)", ""),
|
||||
"secure": ConfigOption(BoolOption(), "Use SSL", True),
|
||||
"bucket": ConfigOption(StrOption(), "Bucket of the s3", ""),
|
||||
"tmp_dir": ConfigOption(
|
||||
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]):
|
||||
|
|
|
@ -21,9 +21,11 @@ from typing import Tuple
|
|||
from typing import Type
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
from ..log import logger
|
||||
from ..entry import Entry
|
||||
from ..result import Result
|
||||
from ..config import BoolOption, ConfigOption
|
||||
|
||||
# logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -106,8 +108,8 @@ class Source(ABC):
|
|||
"""
|
||||
|
||||
source_name: str = ""
|
||||
config_schema: dict[str, tuple[type | list[type] | str, str, Any]] = {
|
||||
"enabled": (bool, "Enable this source", False)
|
||||
config_schema: dict[str, ConfigOption[Any]] = {
|
||||
"enabled": ConfigOption(BoolOption(), "Enable this source", False)
|
||||
}
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
|
|
|
@ -18,9 +18,11 @@ from yt_dlp import YoutubeDL
|
|||
from yt_dlp.utils import DownloadError
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
|
||||
from ..entry import Entry
|
||||
from ..result import Result
|
||||
from .source import Source, available_sources
|
||||
from ..config import BoolOption, ChoiceOption, FolderOption, ListStrOption, Option, ConfigOption
|
||||
|
||||
|
||||
class YouTube:
|
||||
|
@ -28,9 +30,9 @@ class YouTube:
|
|||
A minimal compatibility layer for the YouTube object of pytube, implemented via yt-dlp
|
||||
"""
|
||||
|
||||
__cache__: dict[
|
||||
str, Any
|
||||
] = {} # TODO: this may grow fast... but atm it fixed youtubes anti bot measures
|
||||
__cache__: dict[str, Any] = (
|
||||
{}
|
||||
) # TODO: this may grow fast... but atm it fixed youtubes anti bot measures
|
||||
|
||||
def __init__(self, url: Optional[str] = None):
|
||||
"""
|
||||
|
@ -182,12 +184,18 @@ class YoutubeSource(Source):
|
|||
|
||||
source_name = "youtube"
|
||||
config_schema = Source.config_schema | {
|
||||
"enabled": (bool, "Enable this source", True),
|
||||
"channels": (list, "A list channels\nto search in", []),
|
||||
"tmp_dir": (str, "Folder for\ntemporary download", user_cache_dir("syng")),
|
||||
"max_res": (int, "Maximum resolution\nto download", 720),
|
||||
"start_streaming": (
|
||||
bool,
|
||||
"enabled": ConfigOption(BoolOption(), "Enable this source", True),
|
||||
"channels": ConfigOption(ListStrOption(), "A list channels\nto search in", []),
|
||||
"tmp_dir": ConfigOption(
|
||||
FolderOption(), "Folder for\ntemporary download", user_cache_dir("syng")
|
||||
),
|
||||
"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",
|
||||
False,
|
||||
),
|
||||
|
@ -205,7 +213,10 @@ class YoutubeSource(Source):
|
|||
|
||||
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.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 = (
|
||||
config["start_streaming"] if "start_streaming" in config else False
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue