diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 4009c67..dd56b21 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,22 +2,24 @@
-
-
-
-
+
+
+
-
+
+
+
-
-
-
+
+
+
+
@@ -29,7 +31,7 @@
-
+
@@ -71,14 +73,14 @@
-
+
-
+
@@ -88,26 +90,26 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -117,15 +119,15 @@
-
+
-
+
-
+
-
+
-
+
@@ -133,86 +135,52 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -220,8 +188,8 @@
-
-
+
+
@@ -232,17 +200,32 @@
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -250,8 +233,56 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -265,17 +296,14 @@
-
+
- folder
- modal
- folderEditDialog
subscriptionEditDialog_
folder_edit_dialog
folderEditDialog_
@@ -303,6 +331,9 @@
modal_edit_folder
modal_update_folder
modal_delete_folder
+ ajax_index_get_videos
+ ajax_index_get_tree
+ self.helper
loading
@@ -315,6 +346,8 @@
modal_
videos-wrapper
videos-loading
+ ajax_get_videos
+ ajax_get_tree
@@ -323,16 +356,6 @@
@@ -389,10 +422,10 @@
-
-
-
-
+
+
+
+
@@ -496,14 +529,6 @@
-
-
-
-
-
-
-
-
@@ -525,6 +550,14 @@
+
+
+
+
+
+
+
+
@@ -556,11 +589,6 @@
-
-
-
-
-
@@ -568,6 +596,11 @@
+
+
+
+
+
@@ -581,7 +614,7 @@
-
+
@@ -700,22 +733,22 @@
-
+
-
+
-
+
-
+
@@ -725,7 +758,7 @@
-
+
@@ -771,90 +804,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -921,13 +870,6 @@
-
-
-
-
-
-
-
@@ -955,36 +897,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1000,13 +912,6 @@
-
-
-
-
-
-
-
@@ -1014,48 +919,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1066,91 +933,17 @@
-
+
-
-
-
-
-
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -1161,5 +954,265 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YtManagerApp/management/folders.py b/YtManagerApp/management/folders.py
deleted file mode 100644
index a5af081..0000000
--- a/YtManagerApp/management/folders.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from YtManagerApp.models import SubscriptionFolder, Subscription
-from typing import Callable, Union, Any, Optional
-from django.contrib.auth.models import User
-import logging
-from django.db.models.functions import Lower
-
-
-def traverse_tree(root_folder_id: Optional[int], user: User, visit_func: Callable[[Union[SubscriptionFolder, Subscription]], Any]):
- data_collected = []
-
- def collect(data):
- if data is not None:
- data_collected.append(data)
-
- # Visit root
- if root_folder_id is not None:
- root_folder = SubscriptionFolder.objects.get(id = root_folder_id)
- collect(visit_func(root_folder))
-
- queue = [root_folder_id]
- visited = []
-
- while len(queue) > 0:
- folder_id = queue.pop()
-
- if folder_id in visited:
- logging.error('Found folder tree cycle for folder id %d.', folder_id)
- continue
- visited.append(folder_id)
-
- for folder in SubscriptionFolder.objects.filter(parent_id=folder_id, user=user).order_by(Lower('name')):
- collect(visit_func(folder))
- queue.append(folder.id)
-
- for subscription in Subscription.objects.filter(parent_folder_id=folder_id, user=user).order_by(Lower('name')):
- collect(visit_func(subscription))
-
- return data_collected
diff --git a/YtManagerApp/management/management.py b/YtManagerApp/management/management.py
index 7533f62..6862518 100644
--- a/YtManagerApp/management/management.py
+++ b/YtManagerApp/management/management.py
@@ -112,53 +112,3 @@ class SubscriptionManager(object):
sub.save()
- @staticmethod
- def list_videos(fid: int):
- sub = Subscription.objects.get(id=fid)
- return Video.objects.filter(subscription=sub).order_by('playlist_index')
-
- @staticmethod
- def __get_or_create_channel(url_type, url_id, yt_api: YoutubeAPI):
-
- channel: Channel = None
- info_channel: YoutubeChannelInfo = None
-
- if url_type == 'user':
- channel = Channel.find_by_username(url_id)
- if not channel:
- info_channel = yt_api.get_channel_info_by_username(url_id)
- channel = Channel.find_by_channel_id(info_channel.getId())
-
- elif url_type == 'channel_id':
- channel = Channel.find_by_channel_id(url_id)
- if not channel:
- info_channel = yt_api.get_channel_info(url_id)
-
- elif url_type == 'channel_custom':
- channel = Channel.find_by_custom_url(url_id)
- if not channel:
- found_channel_id = yt_api.search_channel(url_id)
- channel = Channel.find_by_channel_id(found_channel_id)
- if not channel:
- info_channel = yt_api.get_channel_info(found_channel_id)
-
- # Store information about the channel
- if info_channel:
- if not channel:
- channel = Channel()
- if url_type == 'user':
- channel.username = url_id
- SubscriptionManager.__update_channel(channel, info_channel)
-
- return channel
-
- @staticmethod
- def __update_channel(channel: Channel, yt_info: YoutubeChannelInfo):
- channel.channel_id = yt_info.getId()
- channel.custom_url = yt_info.getCustomUrl()
- channel.name = yt_info.getTitle()
- channel.description = yt_info.getDescription()
- channel.icon_default = yt_info.getDefaultThumbnailUrl()
- channel.icon_best = yt_info.getBestThumbnailUrl()
- channel.upload_playlist_id = yt_info.getUploadsPlaylist()
- channel.save()
diff --git a/YtManagerApp/management/videos.py b/YtManagerApp/management/videos.py
index 9dce364..303c0cf 100644
--- a/YtManagerApp/management/videos.py
+++ b/YtManagerApp/management/videos.py
@@ -1,9 +1,8 @@
-from YtManagerApp.models import Subscription, Video
+from YtManagerApp.models import Subscription, Video, SubscriptionFolder
from YtManagerApp.utils.youtube import YoutubePlaylistItem
from typing import Optional
import re
from django.db.models import Q
-from YtManagerApp.management.folders import traverse_tree
from django.contrib.auth.models import User
@@ -57,7 +56,7 @@ def get_videos(user: User,
if isinstance(node, Subscription):
return node.id
return None
- filter_kwargs['subscription_id__in'] = traverse_tree(folder_id, user, visit)
+ filter_kwargs['subscription_id__in'] = SubscriptionFolder.traverse(folder_id, user, visit)
# Only watched
if only_watched is not None:
diff --git a/YtManagerApp/models.py b/YtManagerApp/models.py
index 81cc1dc..2939b1a 100644
--- a/YtManagerApp/models.py
+++ b/YtManagerApp/models.py
@@ -1,6 +1,11 @@
-from django.db import models
+import logging
+from typing import Callable, Union, Any, Optional
+
from django.contrib.auth.models import User
+from django.contrib.auth.models import User
+from django.db import models
from django.db.models.functions import Lower
+from YtManagerApp.utils.youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistInfo
# help_text = user shown text
# verbose_name = user shown name
@@ -26,7 +31,7 @@ class UserSettings(models.Model):
@staticmethod
def find_by_user(user: User):
- result = UserSettings.objects.filter2(user=user)
+ result = UserSettings.objects.filter(user=user)
if len(result) > 0:
return result.first()
return None
@@ -74,6 +79,9 @@ class SubscriptionFolder(models.Model):
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False)
+ class Meta:
+ ordering = [Lower('parent__name'), Lower('name')]
+
def __str__(self):
s = ""
current = self
@@ -82,8 +90,53 @@ class SubscriptionFolder(models.Model):
current = current.parent
return s[:-3]
- class Meta:
- ordering = [Lower('parent__name'), Lower('name')]
+ def delete_folder(self, keep_subscriptions: bool):
+ if keep_subscriptions:
+
+ def visit(node: Union["SubscriptionFolder", "Subscription"]):
+ if isinstance(node, Subscription):
+ node.parent_folder = None
+ node.save()
+
+ SubscriptionFolder.traverse(self.id, self.user, visit)
+
+ self.delete()
+
+ @staticmethod
+ def traverse(root_folder_id: Optional[int],
+ user: User,
+ visit_func: Callable[[Union["SubscriptionFolder", "Subscription"]], Any]):
+
+ data_collected = []
+
+ def collect(data):
+ if data is not None:
+ data_collected.append(data)
+
+ # Visit root
+ if root_folder_id is not None:
+ root_folder = SubscriptionFolder.objects.get(id=root_folder_id)
+ collect(visit_func(root_folder))
+
+ queue = [root_folder_id]
+ visited = []
+
+ while len(queue) > 0:
+ folder_id = queue.pop()
+
+ if folder_id in visited:
+ logging.error('Found folder tree cycle for folder id %d.', folder_id)
+ continue
+ visited.append(folder_id)
+
+ for folder in SubscriptionFolder.objects.filter(parent_id=folder_id, user=user).order_by(Lower('name')):
+ collect(visit_func(folder))
+ queue.append(folder.id)
+
+ for subscription in Subscription.objects.filter(parent_folder_id=folder_id, user=user).order_by(Lower('name')):
+ collect(visit_func(subscription))
+
+ return data_collected
class Channel(models.Model):
@@ -96,6 +149,9 @@ class Channel(models.Model):
icon_best = models.TextField()
upload_playlist_id = models.TextField()
+ def __str__(self):
+ return self.name
+
@staticmethod
def find_by_channel_id(channel_id):
result = Channel.objects.filter(channel_id=channel_id)
@@ -117,25 +173,95 @@ class Channel(models.Model):
return result.first()
return None
- def __str__(self):
- return self.name
+ def fill(self, yt_channel_info: YoutubeChannelInfo):
+ self.channel_id = yt_channel_info.getId()
+ self.custom_url = yt_channel_info.getCustomUrl()
+ self.name = yt_channel_info.getTitle()
+ self.description = yt_channel_info.getDescription()
+ self.icon_default = yt_channel_info.getDefaultThumbnailUrl()
+ self.icon_best = yt_channel_info.getBestThumbnailUrl()
+ self.upload_playlist_id = yt_channel_info.getUploadsPlaylist()
+ self.save()
+
+ @staticmethod
+ def get_or_create(url_type: str, url_id: str, yt_api: YoutubeAPI):
+ channel: Channel = None
+ info_channel: YoutubeChannelInfo = None
+
+ if url_type == 'user':
+ channel = Channel.find_by_username(url_id)
+ if not channel:
+ info_channel = yt_api.get_channel_info_by_username(url_id)
+ channel = Channel.find_by_channel_id(info_channel.getId())
+
+ elif url_type == 'channel_id':
+ channel = Channel.find_by_channel_id(url_id)
+ if not channel:
+ info_channel = yt_api.get_channel_info(url_id)
+
+ elif url_type == 'channel_custom':
+ channel = Channel.find_by_custom_url(url_id)
+ if not channel:
+ found_channel_id = yt_api.search_channel(url_id)
+ channel = Channel.find_by_channel_id(found_channel_id)
+ if not channel:
+ info_channel = yt_api.get_channel_info(found_channel_id)
+
+ # If we downloaded information about the channel, store information
+ # about the channel here.
+ if info_channel:
+ if not channel:
+ channel = Channel()
+ if url_type == 'user':
+ channel.username = url_id
+ channel.fill(info_channel)
+
+ return channel
class Subscription(models.Model):
- name = models.TextField(null=False)
- parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.SET_NULL, null=True, blank=True)
- playlist_id = models.TextField(null=False, unique=True)
+ name = models.CharField(null=False, max_length=1024)
+ parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True)
+ playlist_id = models.CharField(null=False, max_length=128)
description = models.TextField()
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
- icon_default = models.TextField()
- icon_best = models.TextField()
+ icon_default = models.CharField(max_length=1024)
+ icon_best = models.CharField(max_length=1024)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# overrides
- auto_download = models.BooleanField(null=True)
- download_limit = models.IntegerField(null=True)
- download_order = models.TextField(null=True)
- manager_delete_after_watched = models.BooleanField(null=True)
+ auto_download = models.BooleanField(null=True, blank=True)
+ download_limit = models.IntegerField(null=True, blank=True)
+ download_order = models.CharField(null=True, blank=True, max_length=128)
+ manager_delete_after_watched = models.BooleanField(null=True, blank=True)
+
+ def fill_from_playlist(self, info_playlist: YoutubePlaylistInfo):
+ self.name = info_playlist.getTitle()
+ self.playlist_id = info_playlist.getId()
+ self.description = info_playlist.getDescription()
+ self.icon_default = info_playlist.getDefaultThumbnailUrl()
+ self.icon_best = info_playlist.getBestThumbnailUrl()
+
+ def copy_from_channel(self):
+ # No point in storing info about the 'uploads from X' playlist
+ self.name = self.channel.name
+ self.playlist_id = self.channel.upload_playlist_id
+ self.description = self.channel.description
+ self.icon_default = self.channel.icon_default
+ self.icon_best = self.channel.icon_best
+
+ def fetch_from_url(self, url, yt_api: YoutubeAPI):
+ url_type, url_id = yt_api.parse_channel_url(url)
+ if url_type == 'playlist_id':
+ info_playlist = yt_api.get_playlist_info(url_id)
+ self.channel = Channel.get_or_create('channel_id', info_playlist.getChannelId(), yt_api)
+ self.fill_from_playlist(info_playlist)
+ else:
+ self.channel = Channel.get_or_create(url_type, url_id, yt_api)
+ self.copy_from_channel()
+
+ def delete_subscription(self, keep_downloaded_videos: bool):
+ self.delete()
def __str__(self):
return self.name
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css b/YtManagerApp/static/YtManagerApp/css/style.css
index f95552a..74e3624 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.css
+++ b/YtManagerApp/static/YtManagerApp/css/style.css
@@ -1,16 +1,39 @@
-/* Some material font helpers */
-.material-folder::before {
- content: "\e2c7"; }
-
-.material-person::before {
- content: "\e7fd"; }
-
/* Loading animation */
.loading-dual-ring {
display: inline-block;
width: 64px;
height: 64px; }
+ .loading-dual-ring:after {
+ content: " ";
+ display: block;
+ width: 46px;
+ height: 46px;
+ margin: 1px;
+ border-radius: 50%;
+ border: 5px solid #007bff;
+ border-color: #007bff transparent #007bff transparent;
+ animation: loading-dual-ring 1.2s linear infinite; }
+.loading-dual-ring-small {
+ display: inline-block;
+ width: 32px;
+ height: 32px; }
+ .loading-dual-ring-small:after {
+ content: " ";
+ display: block;
+ width: 23px;
+ height: 23px;
+ margin: 1px;
+ border-radius: 50%;
+ border: 2.5px solid #007bff;
+ border-color: #007bff transparent #007bff transparent;
+ animation: loading-dual-ring 1.2s linear infinite; }
+
+@keyframes loading-dual-ring {
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(360deg); } }
.loading-dual-ring-center-screen {
position: fixed;
top: 50%;
@@ -18,17 +41,6 @@
margin-top: -32px;
margin-left: -32px; }
-.loading-dual-ring:after {
- content: " ";
- display: block;
- width: 46px;
- height: 46px;
- margin: 1px;
- border-radius: 50%;
- border: 5px solid #007bff;
- border-color: #007bff transparent #007bff transparent;
- animation: loading-dual-ring 1.2s linear infinite; }
-
.black-overlay {
position: fixed;
/* Sit on top of the page content */
@@ -49,11 +61,6 @@
cursor: pointer;
/* Add a pointer on hover */ }
-@keyframes loading-dual-ring {
- 0% {
- transform: rotate(0deg); }
- 100% {
- transform: rotate(360deg); } }
.video-gallery .card-wrapper {
padding: 0.4rem;
margin-bottom: .5rem; }
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css.map b/YtManagerApp/static/YtManagerApp/css/style.css.map
index 8a24164..f183181 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.css.map
+++ b/YtManagerApp/static/YtManagerApp/css/style.css.map
@@ -1,6 +1,6 @@
{
"version": 3,
-"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAGjD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAK7B,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAKjC,8BAAgB;EACZ,KAAK,EAAE,OAAO;AAElB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC",
+"mappings": "AAoBA,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AASjC,8BAAgB;EACZ,KAAK,EAAE,OAAO;AAElB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC",
"sources": ["style.scss"],
"names": [],
"file": "style.css"
diff --git a/YtManagerApp/static/YtManagerApp/css/style.scss b/YtManagerApp/static/YtManagerApp/css/style.scss
index ef655dd..7166be0 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.scss
+++ b/YtManagerApp/static/YtManagerApp/css/style.scss
@@ -1,19 +1,39 @@
$accent-color: #007bff;
-/* Some material font helpers */
-.material-folder::before {
- content: "\e2c7";
-}
+@mixin loading-dual-ring($scale : 1) {
+ display: inline-block;
+ width: $scale * 64px;
+ height: $scale * 64px;
-.material-person::before {
- content: "\e7fd";
+ &:after {
+ content: " ";
+ display: block;
+ width: $scale * 46px;
+ height: $scale * 46px;
+ margin: 1px;
+ border-radius: 50%;
+ border: ($scale * 5px) solid $accent-color;
+ border-color: $accent-color transparent $accent-color transparent;
+ animation: loading-dual-ring 1.2s linear infinite;
+ }
}
/* Loading animation */
.loading-dual-ring {
- display: inline-block;
- width: 64px;
- height: 64px;
+ @include loading-dual-ring(1.0);
+}
+
+.loading-dual-ring-small {
+ @include loading-dual-ring(0.5);
+}
+
+@keyframes loading-dual-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
}
.loading-dual-ring-center-screen {
@@ -24,18 +44,6 @@ $accent-color: #007bff;
margin-left: -32px;
}
-.loading-dual-ring:after {
- content: " ";
- display: block;
- width: 46px;
- height: 46px;
- margin: 1px;
- border-radius: 50%;
- border: 5px solid $accent-color;
- border-color: $accent-color transparent $accent-color transparent;
- animation: loading-dual-ring 1.2s linear infinite;
-}
-
.black-overlay {
position: fixed; /* Sit on top of the page content */
display: none; /* Hidden by default */
@@ -50,15 +58,6 @@ $accent-color: #007bff;
cursor: pointer; /* Add a pointer on hover */
}
-@keyframes loading-dual-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
-
.video-gallery {
.card-wrapper {
padding: 0.4rem;
@@ -90,6 +89,10 @@ $accent-color: #007bff;
text-decoration: none;
}
}
+
+ .card-img-top {
+
+ }
}
.video-icon-yes {
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html b/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html
index faeca72..8b52457 100644
--- a/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html
+++ b/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html
@@ -6,16 +6,18 @@
{% endblock modal_title %}
{% block modal_content %}
-
{% endblock %}
{% block modal_body %}
- {% crispy form %}
+ Are you sure you want to delete folder "{{ object }}" and all its subfolders?
+ {{ form | crispy }}
{% endblock modal_body %}
{% block modal_footer %}
-
+
{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/modal.html b/YtManagerApp/templates/YtManagerApp/controls/modal.html
index a4948fd..51677f2 100644
--- a/YtManagerApp/templates/YtManagerApp/controls/modal.html
+++ b/YtManagerApp/templates/YtManagerApp/controls/modal.html
@@ -28,6 +28,7 @@
{% block modal_footer_wrapper %}
diff --git a/YtManagerApp/templates/YtManagerApp/controls/subscription_create_modal.html b/YtManagerApp/templates/YtManagerApp/controls/subscription_create_modal.html
new file mode 100644
index 0000000..f6ce237
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/subscription_create_modal.html
@@ -0,0 +1,21 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ New subscription
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ {% crispy form %}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/subscription_delete_modal.html b/YtManagerApp/templates/YtManagerApp/controls/subscription_delete_modal.html
new file mode 100644
index 0000000..1765894
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/subscription_delete_modal.html
@@ -0,0 +1,23 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ Delete subscription
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ Are you sure you want to delete subscription "{{ object }}"?
+ {{ form | crispy }}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html
deleted file mode 100644
index 2de4513..0000000
--- a/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/subscription_update_modal.html b/YtManagerApp/templates/YtManagerApp/controls/subscription_update_modal.html
new file mode 100644
index 0000000..6cecc2e
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/subscription_update_modal.html
@@ -0,0 +1,21 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ Edit subscription
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ {% crispy form %}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/js/common.js b/YtManagerApp/templates/YtManagerApp/js/common.js
index fdf451b..1f51ea2 100644
--- a/YtManagerApp/templates/YtManagerApp/js/common.js
+++ b/YtManagerApp/templates/YtManagerApp/js/common.js
@@ -55,6 +55,7 @@ class AjaxModal
this.modal = null;
this.form = null;
this.submitCallback = null;
+ this.modalLoadingRing = null;
}
setSubmitCallback(callback) {
@@ -84,6 +85,7 @@ class AjaxModal
this.modal = this.wrapper.find('.modal');
this.form = this.wrapper.find('form');
+ this.modalLoadingRing = this.wrapper.find('#modal-loading-ring');
let pThis = this;
this.form.submit(function(e) {
@@ -104,8 +106,15 @@ class AjaxModal
})
.fail(function() {
pThis._submitFailed();
+ })
+ .always(function() {
+ pThis.modalLoadingRing.fadeOut(100);
+ pThis.wrapper.find(":input").prop("disabled", false);
});
+ this.modalLoadingRing.fadeIn(200);
+ this.wrapper.find(":input").prop("disabled", true);
+
e.preventDefault();
}
diff --git a/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
index 5127f6b..0fc69cd 100644
--- a/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
+++ b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
@@ -236,11 +236,10 @@ function treeNode_Edit()
modal.loadAndShow();
}
else {
- //TODO:
- //let id = node.id.replace('sub', '');
- //let modal = new AjaxModal("{ url 'modal_update_subscription' 98765 }".replace('98765', id));
- //modal.setSubmitCallback(tree_Refresh);
- //modal.loadAndShow();
+ let id = node.id.replace('sub', '');
+ let modal = new AjaxModal("{% url 'modal_update_subscription' 98765 %}".replace('98765', id));
+ modal.setSubmitCallback(tree_Refresh);
+ modal.loadAndShow();
}
}
}
@@ -259,11 +258,10 @@ function treeNode_Delete()
modal.loadAndShow();
}
else {
- //TODO:
- //let id = node.id.replace('sub', '');
- //let modal = new AjaxModal("{ url 'modal_delete_subscription' 98765 }".replace('98765', id));
- //modal.setSubmitCallback(tree_Refresh);
- //modal.loadAndShow();
+ let id = node.id.replace('sub', '');
+ let modal = new AjaxModal("{% url 'modal_delete_subscription' 98765 %}".replace('98765', id));
+ modal.setSubmitCallback(tree_Refresh);
+ modal.loadAndShow();
}
}
}
@@ -274,7 +272,7 @@ function tree_Initialize()
treeWrapper.jstree({
core : {
data : {
- url : "{% url 'ajax_index_get_tree' %}"
+ url : "{% url 'ajax_get_tree' %}"
},
check_callback : tree_ValidateChange,
themes : {
@@ -350,7 +348,7 @@ function videos_Reload()
loadingDiv.fadeIn(300);
// Perform query
- $.post("{% url 'ajax_index_get_videos' %}", filterForm.serialize())
+ $.post("{% url 'ajax_get_videos' %}", filterForm.serialize())
.done(function (result) {
$("#videos-wrapper").html(result);
})
@@ -393,6 +391,11 @@ $(document).ready(function ()
// folderEditDialog = new FolderEditDialog('#folderEditDialog');
// subscriptionEditDialog = new SubscriptionEditDialog('#subscriptionEditDialog');
//
+ $("#btn_create_sub").on("click", function () {
+ let modal = new AjaxModal("{% url 'modal_create_subscription' %}");
+ modal.setSubmitCallback(tree_Refresh);
+ modal.loadAndShow();
+ });
$("#btn_create_folder").on("click", function () {
let modal = new AjaxModal("{% url 'modal_create_folder' %}");
modal.setSubmitCallback(tree_Refresh);
diff --git a/YtManagerApp/urls.py b/YtManagerApp/urls.py
index 347bbbb..ec2216b 100644
--- a/YtManagerApp/urls.py
+++ b/YtManagerApp/urls.py
@@ -19,7 +19,8 @@ from django.conf.urls.static import static
from django.urls import path
from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
-from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal
+from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal,\
+ CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal
from .views import old_views
urlpatterns = [
@@ -30,8 +31,8 @@ urlpatterns = [
path('', include('django.contrib.auth.urls')),
# Ajax
- path('ajax/index_get_tree/', ajax_get_tree, name='ajax_index_get_tree'),
- path('ajax/index_get_videos/', ajax_get_videos, name='ajax_index_get_videos'),
+ path('ajax/get_tree/', ajax_get_tree, name='ajax_get_tree'),
+ path('ajax/get_videos/', ajax_get_videos, name='ajax_get_videos'),
# Modals
path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'),
@@ -39,6 +40,11 @@ urlpatterns = [
path('modal/update_folder//', UpdateFolderModal.as_view(), name='modal_update_folder'),
path('modal/delete_folder//', DeleteFolderModal.as_view(), name='modal_delete_folder'),
+ path('modal/create_subscription/', CreateSubscriptionModal.as_view(), name='modal_create_subscription'),
+ path('modal/create_subscription//', CreateSubscriptionModal.as_view(), name='modal_create_subscription'),
+ path('modal/update_subscription//', UpdateSubscriptionModal.as_view(), name='modal_update_subscription'),
+ path('modal/delete_subscription//', DeleteSubscriptionModal.as_view(), name='modal_delete_subscription'),
+
# Index
path('', index, name='home'),
diff --git a/YtManagerApp/utils/youtube.py b/YtManagerApp/utils/youtube.py
index ec66c6c..e58910e 100644
--- a/YtManagerApp/utils/youtube.py
+++ b/YtManagerApp/utils/youtube.py
@@ -1,4 +1,5 @@
from googleapiclient.discovery import build
+from googleapiclient.errors import Error as APIError
from google_auth_oauthlib.flow import InstalledAppFlow
from django.conf import settings
import re
@@ -7,6 +8,26 @@ API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
+class YoutubeException(Exception):
+ pass
+
+
+class YoutubeInvalidURLException(YoutubeException):
+ pass
+
+
+class YoutubeChannelNotFoundException(YoutubeException):
+ pass
+
+
+class YoutubeUserNotFoundException(YoutubeException):
+ pass
+
+
+class YoutubePlaylistNotFoundException(YoutubeException):
+ pass
+
+
class YoutubeChannelInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
@@ -23,7 +44,10 @@ class YoutubeChannelInfo(object):
return self.__snippet['description']
def getCustomUrl(self):
- return self.__snippet['customUrl']
+ try:
+ return self.__snippet['customUrl']
+ except KeyError:
+ return None
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
@@ -142,7 +166,7 @@ class YoutubeAPI(object):
if match:
return 'channel_custom', match.group(1)
- raise Exception('Unrecognized URL format!')
+ raise YoutubeInvalidURLException('Unrecognized URL format!')
def get_playlist_info(self, list_id) -> YoutubePlaylistInfo:
result = self.service.playlists()\
@@ -150,7 +174,7 @@ class YoutubeAPI(object):
.execute()
if len(result['items']) <= 0:
- raise Exception("Invalid playlist ID.")
+ raise YoutubePlaylistNotFoundException("Invalid playlist ID.")
return YoutubePlaylistInfo(result['items'][0])
@@ -160,7 +184,7 @@ class YoutubeAPI(object):
.execute()
if len(result['items']) <= 0:
- raise Exception('Invalid user.')
+ raise YoutubeUserNotFoundException('Invalid user.')
return YoutubeChannelInfo(result['items'][0])
@@ -170,7 +194,7 @@ class YoutubeAPI(object):
.execute()
if len(result['items']) <= 0:
- raise Exception('Invalid channel ID.')
+ raise YoutubeChannelNotFoundException('Invalid channel ID.')
return YoutubeChannelInfo(result['items'][0])
@@ -180,7 +204,7 @@ class YoutubeAPI(object):
.execute()
if len(result['items']) <= 0:
- raise Exception('Could not find channel!')
+ raise YoutubeChannelNotFoundException('Could not find channel!')
channel_result = result['items'][0]
return channel_result['id']['channelId']
diff --git a/YtManagerApp/views/controls/modal.py b/YtManagerApp/views/controls/modal.py
index 52f80dc..448aef7 100644
--- a/YtManagerApp/views/controls/modal.py
+++ b/YtManagerApp/views/controls/modal.py
@@ -35,10 +35,12 @@ class ModalMixin(ContextMixin):
return data
- def modal_response(self, form, success=True):
+ def modal_response(self, form, success=True, error_msg=None):
result = {'success': success}
if not success:
result['errors'] = form.errors.get_json_data(escape_html=True)
+ if error_msg is not None:
+ result['errors']['__all__'] = [{'message': error_msg}]
return JsonResponse(result)
diff --git a/YtManagerApp/views/index.py b/YtManagerApp/views/index.py
index e1a5b90..b613728 100644
--- a/YtManagerApp/views/index.py
+++ b/YtManagerApp/views/index.py
@@ -2,15 +2,14 @@ from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from django import forms
from django.views.generic import CreateView, UpdateView, DeleteView
-from YtManagerApp.management.folders import traverse_tree
+from django.views.generic.edit import FormMixin
from YtManagerApp.management.videos import get_videos
from YtManagerApp.models import Subscription, SubscriptionFolder
from YtManagerApp.views.controls.modal import ModalMixin
from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout, Field
-from crispy_forms.layout import Submit
+from crispy_forms.layout import Layout, Field, Div, HTML
from django.db.models import Q
-
+from YtManagerApp.utils import youtube
class VideoFilterForm(forms.Form):
CHOICES_SORT = (
@@ -69,7 +68,7 @@ class VideoFilterForm(forms.Form):
self.helper.form_id = 'form_video_filter'
self.helper.form_class = 'form-inline'
self.helper.form_method = 'POST'
- self.helper.form_action = 'ajax_index_get_videos'
+ self.helper.form_action = 'ajax_get_videos'
self.helper.field_class = 'mr-1'
self.helper.label_class = 'ml-2 mr-1 no-asterisk'
@@ -104,7 +103,7 @@ def __tree_folder_id(fd_id):
def __tree_sub_id(sub_id):
if sub_id is None:
return '#'
- return 'folder' + str(sub_id)
+ return 'sub' + str(sub_id)
def index(request: HttpRequest):
@@ -137,7 +136,7 @@ def ajax_get_tree(request: HttpRequest):
"parent": __tree_folder_id(node.parent_folder_id)
}
- result = traverse_tree(None, request.user, visit)
+ result = SubscriptionFolder.traverse(None, request.user, visit)
return JsonResponse(result, safe=False)
@@ -220,6 +219,120 @@ class UpdateFolderModal(ModalMixin, UpdateView):
form_class = SubscriptionFolderForm
-class DeleteFolderModal(ModalMixin, DeleteView):
+class DeleteFolderForm(forms.Form):
+ keep_subscriptions = forms.BooleanField(required=False, initial=False, label="Keep subscriptions")
+
+
+class DeleteFolderModal(ModalMixin, FormMixin, DeleteView):
template_name = 'YtManagerApp/controls/folder_delete_modal.html'
model = SubscriptionFolder
+ form_class = DeleteFolderForm
+
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ form = self.get_form()
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ def form_valid(self, form):
+ self.object.delete_folder(keep_subscriptions=form.cleaned_data['keep_subscriptions'])
+ return super().form_valid(form)
+
+
+class CreateSubscriptionForm(forms.ModelForm):
+ playlist_url = forms.URLField(label='Playlist/Channel URL')
+
+ class Meta:
+ model = Subscription
+ fields = ['parent_folder']
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.helper = FormHelper()
+ self.helper.form_tag = False
+ self.helper.layout = Layout(
+ 'playlist_url',
+ 'parent_folder'
+ )
+
+ def clean_playlist_url(self):
+ playlist_url = self.cleaned_data['playlist_url']
+ try:
+ youtube.YoutubeAPI.parse_channel_url(playlist_url)
+ except youtube.YoutubeInvalidURLException:
+ raise forms.ValidationError('Invalid playlist/channel URL, or not in a recognized format.')
+ return playlist_url
+
+
+class CreateSubscriptionModal(ModalMixin, CreateView):
+ template_name = 'YtManagerApp/controls/subscription_create_modal.html'
+ form_class = CreateSubscriptionForm
+
+ def form_valid(self, form):
+ form.instance.user = self.request.user
+ api = youtube.YoutubeAPI.build_public()
+ try:
+ form.instance.fetch_from_url(form.cleaned_data['playlist_url'], api)
+ except youtube.YoutubeChannelNotFoundException:
+ return self.modal_response(form, False, 'Could not find a channel based on the given URL. Please verify that the URL is correct.')
+ except youtube.YoutubeUserNotFoundException:
+ return self.modal_response(form, False, 'Could not find an user based on the given URL. Please verify that the URL is correct.')
+ except youtube.YoutubePlaylistNotFoundException:
+ return self.modal_response(form, False, 'Could not find a playlist based on the given URL. Please verify that the URL is correct.')
+ except youtube.YoutubeException as e:
+ return self.modal_response(form, False, str(e))
+ except youtube.APIError as e:
+ return self.modal_response(form, False, 'An error occurred while communicating with the YouTube API: ' + str(e))
+
+ return super().form_valid(form)
+
+
+class UpdateSubscriptionForm(forms.ModelForm):
+ class Meta:
+ model = Subscription
+ fields = ['name', 'parent_folder', 'auto_download', 'download_limit', 'download_order', 'manager_delete_after_watched']
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.helper = FormHelper()
+ self.helper.form_tag = False
+ self.helper.layout = Layout(
+ 'name',
+ 'parent_folder',
+ HTML('
'),
+ HTML('Download configuration overloads
'),
+ 'auto_download',
+ 'download_limit',
+ 'download_order',
+ 'manager_delete_after_watched'
+ )
+
+
+class UpdateSubscriptionModal(ModalMixin, UpdateView):
+ template_name = 'YtManagerApp/controls/subscription_update_modal.html'
+ model = Subscription
+ form_class = UpdateSubscriptionForm
+
+
+class DeleteSubscriptionForm(forms.Form):
+ keep_downloaded_videos = forms.BooleanField(required=False, initial=False, label="Keep downloaded videos")
+
+
+class DeleteSubscriptionModal(ModalMixin, FormMixin, DeleteView):
+ template_name = 'YtManagerApp/controls/subscription_delete_modal.html'
+ model = Subscription
+ form_class = DeleteSubscriptionForm
+
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ form = self.get_form()
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ def form_valid(self, form):
+ self.object.delete_subscription(keep_downloaded_videos=form.cleaned_data['keep_downloaded_videos'])
+ return super().form_valid(form)