mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Work on provider manager and UI
This commit is contained in:
parent
c8b2ef77e6
commit
45d50a1bff
@ -1,21 +1,20 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Union, Iterable
|
from typing import List, Dict, Union, Iterable, Optional
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
from YtManagerApp.models import VideoProviderConfig, Video, Subscription
|
from YtManagerApp.models import VideoProviderConfig, Video, Subscription
|
||||||
from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError
|
from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError, VideoProviderState
|
||||||
import json
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
log = logging.getLogger("VideoProviderManager")
|
log = logging.getLogger("VideoProviderManager")
|
||||||
|
|
||||||
VideoProviderInfo = namedtuple('VideoProviderInfo', ['id', 'name', 'is_configured', 'description'])
|
|
||||||
|
|
||||||
|
|
||||||
class VideoProviderManager(object):
|
class VideoProviderManager(object):
|
||||||
def __init__(self, registered_providers: List[VideoProvider]):
|
def __init__(self, registered_providers: List[VideoProvider]):
|
||||||
self._registered_providers: Dict[str, VideoProvider] = {}
|
self._registered_providers: Dict[str, VideoProvider] = {}
|
||||||
self._configured_providers: Dict[str, VideoProvider] = {}
|
|
||||||
self._pending_configs: Dict[str, VideoProviderConfig] = {}
|
self._pending_configs: Dict[str, VideoProviderConfig] = {}
|
||||||
|
|
||||||
for rp in registered_providers:
|
for rp in registered_providers:
|
||||||
self.register_provider(rp)
|
self.register_provider(rp)
|
||||||
self._load()
|
self._load()
|
||||||
@ -39,6 +38,28 @@ class VideoProviderManager(object):
|
|||||||
self._configure(provider, self._pending_configs[provider.id])
|
self._configure(provider, self._pending_configs[provider.id])
|
||||||
del self._pending_configs[provider.id]
|
del self._pending_configs[provider.id]
|
||||||
|
|
||||||
|
def configure_provider(self, provider_id: str, config: Optional[Dict[str, any]]):
|
||||||
|
provider = self.get(provider_id)
|
||||||
|
|
||||||
|
if config is not None:
|
||||||
|
provider.configure(config)
|
||||||
|
with transaction.atomic():
|
||||||
|
cfg, _ = VideoProviderConfig.objects.get_or_create(provider_id=provider_id)
|
||||||
|
cfg.settings = json.dumps(config)
|
||||||
|
cfg.save()
|
||||||
|
provider.state = VideoProviderState.OK
|
||||||
|
|
||||||
|
else:
|
||||||
|
provider.unconfigure()
|
||||||
|
VideoProviderConfig.objects.filter(provider_id=provider_id).delete()
|
||||||
|
provider.state = VideoProviderState.NOT_CONFIGURED
|
||||||
|
|
||||||
|
def get_provider_config(self, provider_id: str):
|
||||||
|
cfg = VideoProviderConfig.objects.filter(provider_id=provider_id).first()
|
||||||
|
if cfg is not None:
|
||||||
|
return json.loads(cfg.settings)
|
||||||
|
return None
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
# Loads configuration from database
|
# Loads configuration from database
|
||||||
for config in VideoProviderConfig.objects.all():
|
for config in VideoProviderConfig.objects.all():
|
||||||
@ -56,8 +77,8 @@ class VideoProviderManager(object):
|
|||||||
def _configure(self, provider, config):
|
def _configure(self, provider, config):
|
||||||
settings = json.loads(config.settings)
|
settings = json.loads(config.settings)
|
||||||
provider.configure(settings)
|
provider.configure(settings)
|
||||||
|
provider.state = VideoProviderState.OK
|
||||||
log.info(f"Configured video provider {provider.id}")
|
log.info(f"Configured video provider {provider.id}")
|
||||||
self._configured_providers[provider.id] = provider
|
|
||||||
|
|
||||||
def get(self, item: Union[str, Subscription, Video]):
|
def get(self, item: Union[str, Subscription, Video]):
|
||||||
"""
|
"""
|
||||||
@ -79,12 +100,13 @@ class VideoProviderManager(object):
|
|||||||
:param url:
|
:param url:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for provider in self._configured_providers.values():
|
for provider in self._registered_providers.values():
|
||||||
try:
|
if provider.state == VideoProviderState.OK:
|
||||||
provider.validate_subscription_url(url)
|
try:
|
||||||
return
|
provider.validate_subscription_url(url)
|
||||||
except InvalidURLError:
|
return
|
||||||
pass
|
except InvalidURLError:
|
||||||
|
pass
|
||||||
|
|
||||||
raise InvalidURLError("The given URL is not valid for any of the supported sites!")
|
raise InvalidURLError("The given URL is not valid for any of the supported sites!")
|
||||||
|
|
||||||
@ -94,23 +116,16 @@ class VideoProviderManager(object):
|
|||||||
:param url:
|
:param url:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for provider in self._configured_providers.values():
|
for provider in self._registered_providers.values():
|
||||||
try:
|
if provider.state == VideoProviderState.OK:
|
||||||
provider.validate_subscription_url(url)
|
try:
|
||||||
# Found the right provider
|
provider.validate_subscription_url(url)
|
||||||
return provider.fetch_subscription(url)
|
# Found the right provider
|
||||||
except InvalidURLError:
|
return provider.fetch_subscription(url)
|
||||||
pass
|
except InvalidURLError:
|
||||||
|
pass
|
||||||
|
|
||||||
raise InvalidURLError("The given URL is not valid for any of the supported sites!")
|
raise InvalidURLError("The given URL is not valid for any of the supported sites!")
|
||||||
|
|
||||||
def get_available_providers(self) -> Iterable[VideoProviderInfo]:
|
def get_available_providers(self) -> Iterable[VideoProvider]:
|
||||||
"""
|
return self._registered_providers.values()
|
||||||
Gets a list of available providers and some basic information about them.
|
|
||||||
:return: List of dictionary entries
|
|
||||||
"""
|
|
||||||
for key, provider in self._registered_providers.items():
|
|
||||||
yield VideoProviderInfo(id=key,
|
|
||||||
name=provider.name,
|
|
||||||
description=provider.description,
|
|
||||||
is_configured=(key in self._configured_providers))
|
|
||||||
|
@ -20,6 +20,9 @@ class DummyVideoProvider(VideoProvider):
|
|||||||
def configure(self, configuration: Dict[str, Any]) -> None:
|
def configure(self, configuration: Dict[str, Any]) -> None:
|
||||||
print(configuration)
|
print(configuration)
|
||||||
|
|
||||||
|
def unconfigure(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def validate_configuration(self, configuration: Dict[str, Any]):
|
def validate_configuration(self, configuration: Dict[str, Any]):
|
||||||
print("Validating...")
|
print("Validating...")
|
||||||
if configuration["number_of_something"] >= 10:
|
if configuration["number_of_something"] >= 10:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from abc import abstractmethod, ABC
|
from abc import abstractmethod, ABC
|
||||||
|
from enum import Enum
|
||||||
from typing import Dict, Iterable, List, Any
|
from typing import Dict, Iterable, List, Any
|
||||||
|
|
||||||
from django.forms import Field
|
from django.forms import Field
|
||||||
@ -6,6 +7,12 @@ from django.forms import Field
|
|||||||
from YtManagerApp.models import Subscription, Video
|
from YtManagerApp.models import Subscription, Video
|
||||||
|
|
||||||
|
|
||||||
|
class VideoProviderState(Enum):
|
||||||
|
NOT_CONFIGURED = 0
|
||||||
|
OK = 1
|
||||||
|
ERROR = 2
|
||||||
|
|
||||||
|
|
||||||
class ProviderValidationError(ValueError):
|
class ProviderValidationError(ValueError):
|
||||||
"""
|
"""
|
||||||
Exception type thrown when validating configurations.
|
Exception type thrown when validating configurations.
|
||||||
@ -44,6 +51,9 @@ class VideoProvider(ABC):
|
|||||||
"""
|
"""
|
||||||
settings: Dict[str, Field] = {}
|
settings: Dict[str, Field] = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.state = VideoProviderState.NOT_CONFIGURED
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def configure(self, configuration: Dict[str, Any]) -> None:
|
def configure(self, configuration: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -53,6 +63,14 @@ class VideoProvider(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def unconfigure(self) -> None:
|
||||||
|
"""
|
||||||
|
Destroys video provider configuration
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate_configuration(self, configuration: Dict[str, Any]) -> None:
|
def validate_configuration(self, configuration: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -25,6 +25,10 @@ class YouTubeApiVideoProvider(VideoProvider):
|
|||||||
self.__api_key = configuration['api_key']
|
self.__api_key = configuration['api_key']
|
||||||
self.__api = yt.YouTube(key=self.__api_key)
|
self.__api = yt.YouTube(key=self.__api_key)
|
||||||
|
|
||||||
|
def unconfigure(self):
|
||||||
|
self.__api_key = None
|
||||||
|
self.__api = None
|
||||||
|
|
||||||
def validate_configuration(self, configuration: Dict[str, Any]):
|
def validate_configuration(self, configuration: Dict[str, Any]):
|
||||||
# TODO: implement
|
# TODO: implement
|
||||||
pass
|
pass
|
||||||
|
@ -200,3 +200,7 @@ img.muted {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 4rem;
|
line-height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-status {
|
||||||
|
font-size: 10rem;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
jQuery(function() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
{% extends 'YtManagerApp/controls/modal.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block modal_title %}
|
||||||
|
{{ provider.name }} configuration
|
||||||
|
{% endblock modal_title %}
|
||||||
|
|
||||||
|
{% block modal_content %}
|
||||||
|
<form action="{% url 'modal_provider_config' provider.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ block.super }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal_body %}
|
||||||
|
{{ form | crispy }}
|
||||||
|
{% endblock modal_body %}
|
||||||
|
|
||||||
|
{% block modal_footer %}
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save">
|
||||||
|
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||||
|
{% endblock modal_footer %}
|
@ -3,34 +3,48 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<div class="row">
|
{% if have_configured %}
|
||||||
|
<section>
|
||||||
{% for provider in providers %}
|
<h3>Configured providers</h3>
|
||||||
{% if provider.is_configured %}
|
<div class="row">
|
||||||
<div class="provider-wrapper col-2">
|
{% for provider in providers %}
|
||||||
<button class="btn btn-light ">
|
{% if provider.is_configured %}
|
||||||
<img class="provider-logo"
|
<div class="provider-wrapper col-2">
|
||||||
src="{% static provider.image_src %}"
|
<button class="btn btn-light"
|
||||||
title="{{ provider.name }}">
|
data-modal="modal"
|
||||||
</button>
|
data-modal-url="{% url 'modal_provider_config' provider.id %}">
|
||||||
</div>
|
<img class="provider-logo"
|
||||||
{% endif %}
|
src="{% static provider.image_src %}"
|
||||||
{% endfor %}
|
title="{{ provider.name }}">
|
||||||
|
{% if provider.has_error %}
|
||||||
{% if have_unconfigured %}
|
<span class="typcn typcn-warning provider-status text-warning"></span>
|
||||||
<div class="provider-wrapper col-2 dropdown">
|
{% else %}
|
||||||
<button id="btnAddProvider"
|
<span class="typcn typcn-tick provider-status text-success"></span>
|
||||||
class="btn btn-light"
|
{% endif %}
|
||||||
type="button"
|
</button>
|
||||||
data-toggle="dropdown"
|
</div>
|
||||||
aria-haspopup="true" aria-expanded="false">
|
{% endif %}
|
||||||
<span class="provider-logo typcn typcn-plus"
|
{% endfor %}
|
||||||
title="Add provider">
|
</div>
|
||||||
</span>
|
</section>
|
||||||
</button>
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% if have_unconfigured %}
|
||||||
|
<h3>Not configured</h3>
|
||||||
|
<div class="row">
|
||||||
|
{% for provider in providers %}
|
||||||
|
{% if not provider.is_configured %}
|
||||||
|
<div class="provider-wrapper col-2">
|
||||||
|
<button class="btn btn-light"
|
||||||
|
data-modal="modal"
|
||||||
|
data-modal-url="{% url 'modal_provider_config' provider.id %}">
|
||||||
|
<img class="provider-logo"
|
||||||
|
src="{% static provider.image_src %}"
|
||||||
|
title="{{ provider.name }}">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -18,6 +18,7 @@ from django.conf.urls import include
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from YtManagerApp.views.settings.provider_config import ProviderConfigView
|
||||||
from YtManagerApp.views.settings.providers_view import ProvidersView
|
from YtManagerApp.views.settings.providers_view import ProvidersView
|
||||||
from .views import first_time
|
from .views import first_time
|
||||||
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
||||||
@ -61,6 +62,8 @@ urlpatterns = [
|
|||||||
path('modal/update_subscription/<int:pk>/', UpdateSubscriptionModal.as_view(), name='modal_update_subscription'),
|
path('modal/update_subscription/<int:pk>/', UpdateSubscriptionModal.as_view(), name='modal_update_subscription'),
|
||||||
path('modal/delete_subscription/<int:pk>/', DeleteSubscriptionModal.as_view(), name='modal_delete_subscription'),
|
path('modal/delete_subscription/<int:pk>/', DeleteSubscriptionModal.as_view(), name='modal_delete_subscription'),
|
||||||
|
|
||||||
|
path('modal/provider_config/<str:provider_id>/', ProviderConfigView.as_view(), name='modal_provider_config'),
|
||||||
|
|
||||||
# Pages
|
# Pages
|
||||||
path('', index, name='home'),
|
path('', index, name='home'),
|
||||||
path('settings/', SettingsView.as_view(), name='settings'),
|
path('settings/', SettingsView.as_view(), name='settings'),
|
||||||
|
59
app/YtManagerApp/views/settings/provider_config.py
Normal file
59
app/YtManagerApp/views/settings/provider_config.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
from YtManagerApp.providers.video_provider import VideoProvider, ProviderValidationError
|
||||||
|
from YtManagerApp.services import Services
|
||||||
|
from YtManagerApp.views.controls.modal import ModalMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderConfigForm(forms.Form):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.provider_id = kwargs.pop('provider_id', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.provider_id is not None:
|
||||||
|
provider: VideoProvider = Services.videoProviderManager().get(self.provider_id)
|
||||||
|
for key, field in provider.settings.items():
|
||||||
|
self.fields[key] = field
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
provider: VideoProvider = Services.videoProviderManager().get(self.provider_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider.validate_configuration(cleaned_data)
|
||||||
|
except ProviderValidationError as ex:
|
||||||
|
raise ValidationError(ex.field_messages)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderConfigView(LoginRequiredMixin, ModalMixin, FormView):
|
||||||
|
template_name = 'YtManagerApp/controls/provider_config_modal.html'
|
||||||
|
form_class = ProviderConfigForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['provider_id'] = self.kwargs['provider_id']
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
data['provider'] = Services.videoProviderManager().get(self.kwargs['provider_id'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
cfg = Services.videoProviderManager().get_provider_config(self.kwargs['provider_id'])
|
||||||
|
if cfg is not None:
|
||||||
|
initial.update(cfg)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
Services.videoProviderManager().configure_provider(self.kwargs['provider_id'], form.cleaned_data)
|
||||||
|
except Exception as ex:
|
||||||
|
super().modal_response(form, success=False, error_msg='Configuration of provider failed! ' + str(ex))
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
@ -1,11 +1,12 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from YtManagerApp.providers.video_provider import VideoProviderState
|
||||||
from YtManagerApp.services import Services
|
from YtManagerApp.services import Services
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
VideoProviderInfoViewModel = namedtuple('VideoProviderInfoViewModel',
|
VideoProviderInfoViewModel = namedtuple('VideoProviderInfoViewModel',
|
||||||
['id', 'name', 'is_configured', 'image_src'])
|
['id', 'name', 'is_configured', 'has_error', 'image_src'])
|
||||||
|
|
||||||
|
|
||||||
class ProvidersView(LoginRequiredMixin, TemplateView):
|
class ProvidersView(LoginRequiredMixin, TemplateView):
|
||||||
@ -16,16 +17,23 @@ class ProvidersView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
providers = []
|
providers = []
|
||||||
have_unconfigured = False
|
have_unconfigured = False
|
||||||
|
have_configured = False
|
||||||
|
|
||||||
for provider in Services.videoProviderManager().get_available_providers():
|
for provider in Services.videoProviderManager().get_available_providers():
|
||||||
providers.append(VideoProviderInfoViewModel(
|
providers.append(VideoProviderInfoViewModel(
|
||||||
id=provider.id,
|
id=provider.id,
|
||||||
name=provider.name,
|
name=provider.name,
|
||||||
is_configured=provider.is_configured,
|
is_configured=provider.state != VideoProviderState.NOT_CONFIGURED,
|
||||||
|
has_error=provider.state == VideoProviderState.ERROR,
|
||||||
image_src=f"YtManagerApp/img/video_providers/{provider.id}.png"
|
image_src=f"YtManagerApp/img/video_providers/{provider.id}.png"
|
||||||
))
|
))
|
||||||
if not provider.is_configured:
|
|
||||||
|
if provider.state != VideoProviderState.NOT_CONFIGURED:
|
||||||
|
have_configured = True
|
||||||
|
if provider.state == VideoProviderState.NOT_CONFIGURED:
|
||||||
have_unconfigured = True
|
have_unconfigured = True
|
||||||
|
|
||||||
context['providers'] = sorted(providers, key=lambda x: x.name)
|
context['providers'] = sorted(providers, key=lambda x: x.name)
|
||||||
context['have_unconfigured'] = have_unconfigured
|
context['have_unconfigured'] = have_unconfigured
|
||||||
|
context['have_configured'] = have_configured
|
||||||
return context
|
return context
|
||||||
|
Loading…
Reference in New Issue
Block a user