mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Implemented import from file functionality.
This commit is contained in:
parent
cd37b2671b
commit
59a766b0fe
@ -0,0 +1,22 @@
|
||||
{% extends 'YtManagerApp/controls/modal.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block modal_title %}
|
||||
Import subscriptions
|
||||
{% endblock modal_title %}
|
||||
|
||||
{% block modal_content %}
|
||||
<form action="{% url 'modal_import_subscriptions' %}" method="post"
|
||||
enctype="multipart/form-data">
|
||||
{{ block.super }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
{% crispy form %}
|
||||
{% endblock modal_body %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<input class="btn btn-primary" type="submit" value="Import">
|
||||
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||
{% endblock modal_footer %}
|
@ -29,19 +29,27 @@
|
||||
<div class="col-3">
|
||||
{# Tree toolbar #}
|
||||
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
|
||||
<div class="btn-group btn-group-sm mr-2" role="group">
|
||||
<button id="btn_create_sub" type="button" class="btn btn-secondary" >
|
||||
<div class="btn-group mr-2" role="group">
|
||||
<button id="btn_create_sub" type="button" class="btn btn-light"
|
||||
data-toggle="tooltip" title="Add subscription...">
|
||||
<span class="typcn typcn-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button id="btn_create_folder" type="button" class="btn btn-secondary">
|
||||
<button id="btn_create_folder" type="button" class="btn btn-light"
|
||||
data-toggle="tooltip" title="Add folder...">
|
||||
<span class="typcn typcn-folder-add" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button id="btn_import" type="button" class="btn btn-light"
|
||||
data-toggle="tooltip" title="Import from file...">
|
||||
<span class="typcn typcn-document-add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm mr-2" role="group">
|
||||
<button id="btn_edit_node" type="button" class="btn btn-secondary" >
|
||||
<div class="btn-group mr-2" role="group">
|
||||
<button id="btn_edit_node" type="button" class="btn btn-light"
|
||||
data-toggle="tooltip" title="Edit selection">
|
||||
<span class="typcn typcn-edit" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button id="btn_delete_node" type="button" class="btn btn-secondary" >
|
||||
<button id="btn_delete_node" type="button" class="btn btn-light"
|
||||
data-toggle="tooltip" title="Delete selection">
|
||||
<span class="typcn typcn-trash" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -162,6 +162,9 @@ function videos_Submit(e)
|
||||
///
|
||||
$(document).ready(function ()
|
||||
{
|
||||
// Initialize tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
tree_Initialize();
|
||||
|
||||
// Subscription toolbar
|
||||
@ -175,6 +178,11 @@ $(document).ready(function ()
|
||||
modal.setSubmitCallback(tree_Refresh);
|
||||
modal.loadAndShow();
|
||||
});
|
||||
$("#btn_import").on("click", function () {
|
||||
let modal = new AjaxModal("{% url 'modal_import_subscriptions' %}");
|
||||
modal.setSubmitCallback(tree_Refresh);
|
||||
modal.loadAndShow();
|
||||
});
|
||||
$("#btn_edit_node").on("click", treeNode_Edit);
|
||||
$("#btn_delete_node").on("click", treeNode_Delete);
|
||||
|
||||
|
@ -22,7 +22,7 @@ from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFiles
|
||||
MarkVideoUnwatchedView
|
||||
from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
|
||||
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \
|
||||
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal
|
||||
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
|
||||
from .views.settings import SettingsView
|
||||
|
||||
urlpatterns = [
|
||||
@ -50,6 +50,8 @@ urlpatterns = [
|
||||
|
||||
path('modal/create_subscription/', CreateSubscriptionModal.as_view(), name='modal_create_subscription'),
|
||||
path('modal/create_subscription/<int:parent_folder_id>/', CreateSubscriptionModal.as_view(), name='modal_create_subscription'),
|
||||
path('modal/import_subscriptions/', ImportSubscriptionsModal.as_view(), name='modal_import_subscriptions'),
|
||||
path('modal/import_subscriptions/<int:parent_folder_id>/', ImportSubscriptionsModal.as_view(), name='modal_import_subscriptions'),
|
||||
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'),
|
||||
|
||||
|
104
app/YtManagerApp/utils/subscription_file_parser.py
Normal file
104
app/YtManagerApp/utils/subscription_file_parser.py
Normal file
@ -0,0 +1,104 @@
|
||||
from typing import Iterable
|
||||
from xml.etree import ElementTree
|
||||
import re
|
||||
|
||||
|
||||
class FormatNotSupportedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SubFileParser(object):
|
||||
|
||||
def probe(self, file_handle) -> bool:
|
||||
"""
|
||||
Tests if file matches file format.
|
||||
:param file: File path
|
||||
:return: True if file matches, false otherwise
|
||||
"""
|
||||
return False
|
||||
|
||||
def parse(self, file_handle) -> Iterable[str]:
|
||||
"""
|
||||
Parses file and returns a list of subscription URLs.
|
||||
:param file:
|
||||
:return:
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class SubscriptionListFileParser(SubFileParser):
|
||||
"""
|
||||
A subscription list file is file which contains just a bunch of URLs.
|
||||
Comments are supported using # character.
|
||||
"""
|
||||
|
||||
def __is_url(self, text: str) -> bool:
|
||||
return text.startswith('http://') or text.startswith('https://')
|
||||
|
||||
def probe(self, file_handle):
|
||||
file_handle.seek(0)
|
||||
for line in file_handle:
|
||||
# Trim comments and spaces
|
||||
line = re.sub('#.*', '', line).strip()
|
||||
if len(line) > 0:
|
||||
return self.__is_url(line)
|
||||
return False
|
||||
|
||||
def parse(self, file_handle):
|
||||
file_handle.seek(0)
|
||||
for line in file_handle:
|
||||
# Trim comments and spaces
|
||||
line = re.sub('#.*', '', line).strip()
|
||||
if len(line) > 0:
|
||||
yield line
|
||||
|
||||
|
||||
class OPMLParser(SubFileParser):
|
||||
"""
|
||||
Parses OPML files (emitted by YouTube)
|
||||
"""
|
||||
def __init__(self):
|
||||
self.__cached_file = None
|
||||
self.__cached_tree: ElementTree.ElementTree = None
|
||||
|
||||
def __parse(self, file_handle):
|
||||
if file_handle == self.__cached_file:
|
||||
return self.__cached_tree
|
||||
|
||||
file_handle.seek(0)
|
||||
tree = ElementTree.parse(file_handle)
|
||||
|
||||
self.__cached_file = file_handle
|
||||
self.__cached_tree = tree
|
||||
return self.__cached_tree
|
||||
|
||||
def probe(self, file_handle):
|
||||
try:
|
||||
tree = self.__parse(file_handle)
|
||||
except ElementTree.ParseError:
|
||||
# Malformed XML
|
||||
return False
|
||||
|
||||
return tree.getroot().tag.lower() == 'opml'
|
||||
|
||||
def parse(self, file_handle):
|
||||
tree = self.__parse(file_handle)
|
||||
root = tree.getroot()
|
||||
|
||||
for node in root.iter('outline'):
|
||||
if 'xmlUrl' in node.keys():
|
||||
yield node.get('xmlUrl')
|
||||
|
||||
|
||||
PARSERS = (
|
||||
OPMLParser(),
|
||||
SubscriptionListFileParser()
|
||||
)
|
||||
|
||||
|
||||
def parse(file_handle) -> Iterable[str]:
|
||||
for parser in PARSERS:
|
||||
if parser.probe(file_handle):
|
||||
return parser.parse(file_handle)
|
||||
|
||||
raise FormatNotSupportedError('This file cannot be parsed!')
|
@ -6,14 +6,16 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import CreateView, UpdateView, DeleteView
|
||||
from django.views.generic import CreateView, UpdateView, DeleteView, FormView
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from YtManagerApp.management.videos import get_videos
|
||||
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
|
||||
from YtManagerApp.utils import youtube
|
||||
from YtManagerApp.utils import youtube, subscription_file_parser
|
||||
from YtManagerApp.views.controls.modal import ModalMixin
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class VideoFilterForm(forms.Form):
|
||||
CHOICES_SHOW_WATCHED = (
|
||||
@ -346,3 +348,101 @@ class DeleteSubscriptionModal(LoginRequiredMixin, ModalMixin, FormMixin, DeleteV
|
||||
def form_valid(self, form):
|
||||
self.object.delete_subscription(keep_downloaded_videos=form.cleaned_data['keep_downloaded_videos'])
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ImportSubscriptionsForm(forms.Form):
|
||||
TRUE_FALSE_CHOICES = (
|
||||
(None, '(default)'),
|
||||
(True, 'Yes'),
|
||||
(False, 'No')
|
||||
)
|
||||
|
||||
VIDEO_ORDER_CHOICES_WITH_EMPTY = (
|
||||
('', '(default)'),
|
||||
*VIDEO_ORDER_CHOICES,
|
||||
)
|
||||
|
||||
file = forms.FileField(label='File to import',
|
||||
help_text='Supported file types: OPML, subscription list')
|
||||
parent_folder = forms.ModelChoiceField(SubscriptionFolder.objects, required=False)
|
||||
auto_download = forms.ChoiceField(choices=TRUE_FALSE_CHOICES, required=False)
|
||||
download_limit = forms.IntegerField(required=False)
|
||||
download_order = forms.ChoiceField(choices=VIDEO_ORDER_CHOICES_WITH_EMPTY, required=False)
|
||||
delete_after_watched = forms.ChoiceField(choices=TRUE_FALSE_CHOICES, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.yt_api = youtube.YoutubeAPI.build_public()
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.layout = Layout(
|
||||
'file',
|
||||
'parent_folder',
|
||||
HTML('<hr>'),
|
||||
HTML('<h5>Download configuration overloads</h5>'),
|
||||
'auto_download',
|
||||
'download_limit',
|
||||
'download_order',
|
||||
'delete_after_watched'
|
||||
)
|
||||
|
||||
def __clean_empty_none(self, name: str):
|
||||
data = self.cleaned_data[name]
|
||||
if isinstance(data, str) and len(data) == 0:
|
||||
return None
|
||||
return data
|
||||
|
||||
def __clean_boolean(self, name: str):
|
||||
data = self.cleaned_data[name]
|
||||
if isinstance(data, str) and len(data) == 0:
|
||||
return None
|
||||
if isinstance(data, str):
|
||||
return data == 'True'
|
||||
return data
|
||||
|
||||
def clean_auto_download(self):
|
||||
return self.__clean_boolean('auto_download')
|
||||
|
||||
def clean_delete_after_watched(self):
|
||||
return self.__clean_boolean('delete_after_watched')
|
||||
|
||||
def clean_download_order(self):
|
||||
return self.__clean_empty_none('download_order')
|
||||
|
||||
|
||||
class ImportSubscriptionsModal(LoginRequiredMixin, ModalMixin, FormView):
|
||||
template_name = 'YtManagerApp/controls/subscriptions_import_modal.html'
|
||||
form_class = ImportSubscriptionsForm
|
||||
|
||||
def form_valid(self, form):
|
||||
file = form.cleaned_data['file']
|
||||
|
||||
# Parse file
|
||||
try:
|
||||
url_list = list(subscription_file_parser.parse(file))
|
||||
except subscription_file_parser.FormatNotSupportedError:
|
||||
return super().modal_response(form, success=False,
|
||||
error_msg="The file could not be parsed! "
|
||||
"Possible problems: format not supported, file is malformed.")
|
||||
|
||||
print(form.cleaned_data)
|
||||
|
||||
# Create subscriptions
|
||||
api = youtube.YoutubeAPI.build_public()
|
||||
for url in url_list:
|
||||
sub = Subscription()
|
||||
sub.user = self.request.user
|
||||
sub.parent_folder = form.cleaned_data['parent_folder']
|
||||
sub.auto_download = form.cleaned_data['auto_download']
|
||||
sub.download_limit = form.cleaned_data['download_limit']
|
||||
sub.download_order = form.cleaned_data['download_order']
|
||||
sub.delete_after_watched = form.cleaned_data['delete_after_watched']
|
||||
try:
|
||||
sub.fetch_from_url(url, api)
|
||||
except Exception as e:
|
||||
logging.error("Import subscription error - error processing URL %s: %s", url, e)
|
||||
continue
|
||||
|
||||
sub.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
Loading…
Reference in New Issue
Block a user