mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Finished CRUD operations for subscriptions and folders.
This commit is contained in:
parent
c3e3bfa33c
commit
a6d58dfaa6
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -6,16 +6,18 @@
|
||||
{% endblock modal_title %}
|
||||
|
||||
{% block modal_content %}
|
||||
<form action="{% url 'modal_delete_folder' form.instance.pk %}" method="post">
|
||||
<form action="{% url 'modal_delete_folder' object.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ block.super }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
{% crispy form %}
|
||||
<p>Are you sure you want to delete folder "{{ object }}" and all its subfolders?</p>
|
||||
{{ form | crispy }}
|
||||
{% endblock modal_body %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<input class="btn btn-danger" type="submit" value="Save" aria-label="Delete">
|
||||
<input class="btn btn-danger" type="submit" value="Delete" aria-label="Delete">
|
||||
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||
{% endblock modal_footer %}
|
@ -28,6 +28,7 @@
|
||||
|
||||
{% block modal_footer_wrapper %}
|
||||
<div class="modal-footer">
|
||||
<div id="modal-loading-ring" class="loading-dual-ring-small mr-auto" style="display: none;"></div>
|
||||
{% block modal_footer %}
|
||||
{% endblock modal_footer %}
|
||||
</div>
|
||||
|
@ -0,0 +1,21 @@
|
||||
{% extends 'YtManagerApp/controls/modal.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block modal_title %}
|
||||
New subscription
|
||||
{% endblock modal_title %}
|
||||
|
||||
{% block modal_content %}
|
||||
<form action="{% url 'modal_create_subscription' %}" method="post">
|
||||
{{ block.super }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
{% crispy form %}
|
||||
{% endblock modal_body %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<input class="btn btn-primary" type="submit" value="Create">
|
||||
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||
{% endblock modal_footer %}
|
@ -0,0 +1,23 @@
|
||||
{% extends 'YtManagerApp/controls/modal.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block modal_title %}
|
||||
Delete subscription
|
||||
{% endblock modal_title %}
|
||||
|
||||
{% block modal_content %}
|
||||
<form action="{% url 'modal_delete_folder' object.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ block.super }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
<p>Are you sure you want to delete subscription "{{ object }}"?</p>
|
||||
{{ form | crispy }}
|
||||
{% endblock modal_body %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<input class="btn btn-danger" type="submit" value="Delete" aria-label="Delete">
|
||||
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||
{% endblock modal_footer %}
|
@ -1,45 +0,0 @@
|
||||
<div id="subscriptionEditDialog" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="subscriptionEditDialog_Title" class="modal-title">Edit subscription</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="subscriptionEditDialog_Loading" class="modal-body">
|
||||
<div class="loading-dual-ring"></div>
|
||||
</div>
|
||||
<div id="subscriptionEditDialog_Error"></div>
|
||||
<form id="subscriptionEditDialog_Form" action="{% url 'ajax_edit_subscription' %}" method="post">
|
||||
<div class="modal-body">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="subscriptionEditDialog_Id" name="id" value="#">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3" for="subscriptionEditDialog_Url">Link:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="subscriptionEditDialog_Url" name="url" placeholder="Subscription URL (playlist, channel)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3" for="subscriptionEditDialog_Name">Name:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="subscriptionEditDialog_Name" name="name" placeholder="Subscription name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3" for="subscriptionEditDialog_Parent">Parent subscription</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" id="subscriptionEditDialog_Parent" name="parent">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="subscriptionEditDialog_Submit" type="submit" class="btn btn-primary">Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
{% extends 'YtManagerApp/controls/modal.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block modal_title %}
|
||||
Edit subscription
|
||||
{% endblock modal_title %}
|
||||
|
||||
{% block modal_content %}
|
||||
<form action="{% url 'modal_update_subscription' form.instance.pk %}" method="post">
|
||||
{{ block.super }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
{% crispy form %}
|
||||
{% endblock modal_body %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<input class="btn btn-primary" type="submit" value="Save" aria-label="Save">
|
||||
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||
{% endblock modal_footer %}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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/<int:pk>/', UpdateFolderModal.as_view(), name='modal_update_folder'),
|
||||
path('modal/delete_folder/<int:pk>/', DeleteFolderModal.as_view(), name='modal_delete_folder'),
|
||||
|
||||
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/update_subscription/<int:pk>/', UpdateSubscriptionModal.as_view(), name='modal_update_subscription'),
|
||||
path('modal/delete_subscription/<int:pk>/', DeleteSubscriptionModal.as_view(), name='modal_delete_subscription'),
|
||||
|
||||
# Index
|
||||
path('', index, name='home'),
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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('<hr>'),
|
||||
HTML('<h5>Download configuration overloads</h5>'),
|
||||
'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)
|
||||
|
Loading…
Reference in New Issue
Block a user