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()
|
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 YtManagerApp.utils.youtube import YoutubePlaylistItem
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import re
|
import re
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from YtManagerApp.management.folders import traverse_tree
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +56,7 @@ def get_videos(user: User,
|
|||||||
if isinstance(node, Subscription):
|
if isinstance(node, Subscription):
|
||||||
return node.id
|
return node.id
|
||||||
return None
|
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
|
# Only watched
|
||||||
if only_watched is not None:
|
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.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
|
from YtManagerApp.utils.youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistInfo
|
||||||
|
|
||||||
# help_text = user shown text
|
# help_text = user shown text
|
||||||
# verbose_name = user shown name
|
# verbose_name = user shown name
|
||||||
@ -26,7 +31,7 @@ class UserSettings(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_user(user: User):
|
def find_by_user(user: User):
|
||||||
result = UserSettings.objects.filter2(user=user)
|
result = UserSettings.objects.filter(user=user)
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
return result.first()
|
return result.first()
|
||||||
return None
|
return None
|
||||||
@ -74,6 +79,9 @@ class SubscriptionFolder(models.Model):
|
|||||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
|
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False)
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = [Lower('parent__name'), Lower('name')]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = ""
|
s = ""
|
||||||
current = self
|
current = self
|
||||||
@ -82,8 +90,53 @@ class SubscriptionFolder(models.Model):
|
|||||||
current = current.parent
|
current = current.parent
|
||||||
return s[:-3]
|
return s[:-3]
|
||||||
|
|
||||||
class Meta:
|
def delete_folder(self, keep_subscriptions: bool):
|
||||||
ordering = [Lower('parent__name'), Lower('name')]
|
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):
|
class Channel(models.Model):
|
||||||
@ -96,6 +149,9 @@ class Channel(models.Model):
|
|||||||
icon_best = models.TextField()
|
icon_best = models.TextField()
|
||||||
upload_playlist_id = models.TextField()
|
upload_playlist_id = models.TextField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_channel_id(channel_id):
|
def find_by_channel_id(channel_id):
|
||||||
result = Channel.objects.filter(channel_id=channel_id)
|
result = Channel.objects.filter(channel_id=channel_id)
|
||||||
@ -117,25 +173,95 @@ class Channel(models.Model):
|
|||||||
return result.first()
|
return result.first()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def fill(self, yt_channel_info: YoutubeChannelInfo):
|
||||||
return self.name
|
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):
|
class Subscription(models.Model):
|
||||||
name = models.TextField(null=False)
|
name = models.CharField(null=False, max_length=1024)
|
||||||
parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.SET_NULL, null=True, blank=True)
|
parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
playlist_id = models.TextField(null=False, unique=True)
|
playlist_id = models.CharField(null=False, max_length=128)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
||||||
icon_default = models.TextField()
|
icon_default = models.CharField(max_length=1024)
|
||||||
icon_best = models.TextField()
|
icon_best = models.CharField(max_length=1024)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
# overrides
|
# overrides
|
||||||
auto_download = models.BooleanField(null=True)
|
auto_download = models.BooleanField(null=True, blank=True)
|
||||||
download_limit = models.IntegerField(null=True)
|
download_limit = models.IntegerField(null=True, blank=True)
|
||||||
download_order = models.TextField(null=True)
|
download_order = models.CharField(null=True, blank=True, max_length=128)
|
||||||
manager_delete_after_watched = models.BooleanField(null=True)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -1,24 +1,9 @@
|
|||||||
/* Some material font helpers */
|
|
||||||
.material-folder::before {
|
|
||||||
content: "\e2c7"; }
|
|
||||||
|
|
||||||
.material-person::before {
|
|
||||||
content: "\e7fd"; }
|
|
||||||
|
|
||||||
/* Loading animation */
|
/* Loading animation */
|
||||||
.loading-dual-ring {
|
.loading-dual-ring {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px; }
|
height: 64px; }
|
||||||
|
.loading-dual-ring:after {
|
||||||
.loading-dual-ring-center-screen {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -32px;
|
|
||||||
margin-left: -32px; }
|
|
||||||
|
|
||||||
.loading-dual-ring:after {
|
|
||||||
content: " ";
|
content: " ";
|
||||||
display: block;
|
display: block;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
@ -29,6 +14,33 @@
|
|||||||
border-color: #007bff transparent #007bff transparent;
|
border-color: #007bff transparent #007bff transparent;
|
||||||
animation: loading-dual-ring 1.2s linear infinite; }
|
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%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -32px;
|
||||||
|
margin-left: -32px; }
|
||||||
|
|
||||||
.black-overlay {
|
.black-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
/* Sit on top of the page content */
|
/* Sit on top of the page content */
|
||||||
@ -49,11 +61,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* Add a pointer on hover */ }
|
/* Add a pointer on hover */ }
|
||||||
|
|
||||||
@keyframes loading-dual-ring {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg); }
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg); } }
|
|
||||||
.video-gallery .card-wrapper {
|
.video-gallery .card-wrapper {
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
margin-bottom: .5rem; }
|
margin-bottom: .5rem; }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"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"],
|
"sources": ["style.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "style.css"
|
"file": "style.css"
|
||||||
|
@ -1,19 +1,39 @@
|
|||||||
$accent-color: #007bff;
|
$accent-color: #007bff;
|
||||||
|
|
||||||
/* Some material font helpers */
|
@mixin loading-dual-ring($scale : 1) {
|
||||||
.material-folder::before {
|
display: inline-block;
|
||||||
content: "\e2c7";
|
width: $scale * 64px;
|
||||||
}
|
height: $scale * 64px;
|
||||||
|
|
||||||
.material-person::before {
|
&:after {
|
||||||
content: "\e7fd";
|
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 animation */
|
||||||
.loading-dual-ring {
|
.loading-dual-ring {
|
||||||
display: inline-block;
|
@include loading-dual-ring(1.0);
|
||||||
width: 64px;
|
}
|
||||||
height: 64px;
|
|
||||||
|
.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 {
|
.loading-dual-ring-center-screen {
|
||||||
@ -24,18 +44,6 @@ $accent-color: #007bff;
|
|||||||
margin-left: -32px;
|
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 {
|
.black-overlay {
|
||||||
position: fixed; /* Sit on top of the page content */
|
position: fixed; /* Sit on top of the page content */
|
||||||
display: none; /* Hidden by default */
|
display: none; /* Hidden by default */
|
||||||
@ -50,15 +58,6 @@ $accent-color: #007bff;
|
|||||||
cursor: pointer; /* Add a pointer on hover */
|
cursor: pointer; /* Add a pointer on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading-dual-ring {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-gallery {
|
.video-gallery {
|
||||||
.card-wrapper {
|
.card-wrapper {
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
@ -90,6 +89,10 @@ $accent-color: #007bff;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-img-top {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-icon-yes {
|
.video-icon-yes {
|
||||||
|
@ -6,16 +6,18 @@
|
|||||||
{% endblock modal_title %}
|
{% endblock modal_title %}
|
||||||
|
|
||||||
{% block modal_content %}
|
{% 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 }}
|
{{ block.super }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal_body %}
|
{% 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 %}
|
{% endblock modal_body %}
|
||||||
|
|
||||||
{% block modal_footer %}
|
{% 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">
|
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
|
||||||
{% endblock modal_footer %}
|
{% endblock modal_footer %}
|
@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
{% block modal_footer_wrapper %}
|
{% block modal_footer_wrapper %}
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<div id="modal-loading-ring" class="loading-dual-ring-small mr-auto" style="display: none;"></div>
|
||||||
{% block modal_footer %}
|
{% block modal_footer %}
|
||||||
{% endblock modal_footer %}
|
{% endblock modal_footer %}
|
||||||
</div>
|
</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.modal = null;
|
||||||
this.form = null;
|
this.form = null;
|
||||||
this.submitCallback = null;
|
this.submitCallback = null;
|
||||||
|
this.modalLoadingRing = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitCallback(callback) {
|
setSubmitCallback(callback) {
|
||||||
@ -84,6 +85,7 @@ class AjaxModal
|
|||||||
|
|
||||||
this.modal = this.wrapper.find('.modal');
|
this.modal = this.wrapper.find('.modal');
|
||||||
this.form = this.wrapper.find('form');
|
this.form = this.wrapper.find('form');
|
||||||
|
this.modalLoadingRing = this.wrapper.find('#modal-loading-ring');
|
||||||
|
|
||||||
let pThis = this;
|
let pThis = this;
|
||||||
this.form.submit(function(e) {
|
this.form.submit(function(e) {
|
||||||
@ -104,8 +106,15 @@ class AjaxModal
|
|||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
pThis._submitFailed();
|
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();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,11 +236,10 @@ function treeNode_Edit()
|
|||||||
modal.loadAndShow();
|
modal.loadAndShow();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//TODO:
|
let id = node.id.replace('sub', '');
|
||||||
//let id = node.id.replace('sub', '');
|
let modal = new AjaxModal("{% url 'modal_update_subscription' 98765 %}".replace('98765', id));
|
||||||
//let modal = new AjaxModal("{ url 'modal_update_subscription' 98765 }".replace('98765', id));
|
modal.setSubmitCallback(tree_Refresh);
|
||||||
//modal.setSubmitCallback(tree_Refresh);
|
modal.loadAndShow();
|
||||||
//modal.loadAndShow();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,11 +258,10 @@ function treeNode_Delete()
|
|||||||
modal.loadAndShow();
|
modal.loadAndShow();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//TODO:
|
let id = node.id.replace('sub', '');
|
||||||
//let id = node.id.replace('sub', '');
|
let modal = new AjaxModal("{% url 'modal_delete_subscription' 98765 %}".replace('98765', id));
|
||||||
//let modal = new AjaxModal("{ url 'modal_delete_subscription' 98765 }".replace('98765', id));
|
modal.setSubmitCallback(tree_Refresh);
|
||||||
//modal.setSubmitCallback(tree_Refresh);
|
modal.loadAndShow();
|
||||||
//modal.loadAndShow();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,7 +272,7 @@ function tree_Initialize()
|
|||||||
treeWrapper.jstree({
|
treeWrapper.jstree({
|
||||||
core : {
|
core : {
|
||||||
data : {
|
data : {
|
||||||
url : "{% url 'ajax_index_get_tree' %}"
|
url : "{% url 'ajax_get_tree' %}"
|
||||||
},
|
},
|
||||||
check_callback : tree_ValidateChange,
|
check_callback : tree_ValidateChange,
|
||||||
themes : {
|
themes : {
|
||||||
@ -350,7 +348,7 @@ function videos_Reload()
|
|||||||
loadingDiv.fadeIn(300);
|
loadingDiv.fadeIn(300);
|
||||||
|
|
||||||
// Perform query
|
// Perform query
|
||||||
$.post("{% url 'ajax_index_get_videos' %}", filterForm.serialize())
|
$.post("{% url 'ajax_get_videos' %}", filterForm.serialize())
|
||||||
.done(function (result) {
|
.done(function (result) {
|
||||||
$("#videos-wrapper").html(result);
|
$("#videos-wrapper").html(result);
|
||||||
})
|
})
|
||||||
@ -393,6 +391,11 @@ $(document).ready(function ()
|
|||||||
// folderEditDialog = new FolderEditDialog('#folderEditDialog');
|
// folderEditDialog = new FolderEditDialog('#folderEditDialog');
|
||||||
// subscriptionEditDialog = new SubscriptionEditDialog('#subscriptionEditDialog');
|
// 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 () {
|
$("#btn_create_folder").on("click", function () {
|
||||||
let modal = new AjaxModal("{% url 'modal_create_folder' %}");
|
let modal = new AjaxModal("{% url 'modal_create_folder' %}");
|
||||||
modal.setSubmitCallback(tree_Refresh);
|
modal.setSubmitCallback(tree_Refresh);
|
||||||
|
@ -19,7 +19,8 @@ from django.conf.urls.static import static
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
|
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
|
from .views import old_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -30,8 +31,8 @@ urlpatterns = [
|
|||||||
path('', include('django.contrib.auth.urls')),
|
path('', include('django.contrib.auth.urls')),
|
||||||
|
|
||||||
# Ajax
|
# Ajax
|
||||||
path('ajax/index_get_tree/', ajax_get_tree, name='ajax_index_get_tree'),
|
path('ajax/get_tree/', ajax_get_tree, name='ajax_get_tree'),
|
||||||
path('ajax/index_get_videos/', ajax_get_videos, name='ajax_index_get_videos'),
|
path('ajax/get_videos/', ajax_get_videos, name='ajax_get_videos'),
|
||||||
|
|
||||||
# Modals
|
# Modals
|
||||||
path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'),
|
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/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/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
|
# Index
|
||||||
path('', index, name='home'),
|
path('', index, name='home'),
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import Error as APIError
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import re
|
import re
|
||||||
@ -7,6 +8,26 @@ API_SERVICE_NAME = 'youtube'
|
|||||||
API_VERSION = 'v3'
|
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):
|
class YoutubeChannelInfo(object):
|
||||||
def __init__(self, result_dict):
|
def __init__(self, result_dict):
|
||||||
self.__id = result_dict['id']
|
self.__id = result_dict['id']
|
||||||
@ -23,7 +44,10 @@ class YoutubeChannelInfo(object):
|
|||||||
return self.__snippet['description']
|
return self.__snippet['description']
|
||||||
|
|
||||||
def getCustomUrl(self):
|
def getCustomUrl(self):
|
||||||
|
try:
|
||||||
return self.__snippet['customUrl']
|
return self.__snippet['customUrl']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
def getDefaultThumbnailUrl(self):
|
def getDefaultThumbnailUrl(self):
|
||||||
return self.__snippet['thumbnails']['default']['url']
|
return self.__snippet['thumbnails']['default']['url']
|
||||||
@ -142,7 +166,7 @@ class YoutubeAPI(object):
|
|||||||
if match:
|
if match:
|
||||||
return 'channel_custom', match.group(1)
|
return 'channel_custom', match.group(1)
|
||||||
|
|
||||||
raise Exception('Unrecognized URL format!')
|
raise YoutubeInvalidURLException('Unrecognized URL format!')
|
||||||
|
|
||||||
def get_playlist_info(self, list_id) -> YoutubePlaylistInfo:
|
def get_playlist_info(self, list_id) -> YoutubePlaylistInfo:
|
||||||
result = self.service.playlists()\
|
result = self.service.playlists()\
|
||||||
@ -150,7 +174,7 @@ class YoutubeAPI(object):
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if len(result['items']) <= 0:
|
if len(result['items']) <= 0:
|
||||||
raise Exception("Invalid playlist ID.")
|
raise YoutubePlaylistNotFoundException("Invalid playlist ID.")
|
||||||
|
|
||||||
return YoutubePlaylistInfo(result['items'][0])
|
return YoutubePlaylistInfo(result['items'][0])
|
||||||
|
|
||||||
@ -160,7 +184,7 @@ class YoutubeAPI(object):
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if len(result['items']) <= 0:
|
if len(result['items']) <= 0:
|
||||||
raise Exception('Invalid user.')
|
raise YoutubeUserNotFoundException('Invalid user.')
|
||||||
|
|
||||||
return YoutubeChannelInfo(result['items'][0])
|
return YoutubeChannelInfo(result['items'][0])
|
||||||
|
|
||||||
@ -170,7 +194,7 @@ class YoutubeAPI(object):
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if len(result['items']) <= 0:
|
if len(result['items']) <= 0:
|
||||||
raise Exception('Invalid channel ID.')
|
raise YoutubeChannelNotFoundException('Invalid channel ID.')
|
||||||
|
|
||||||
return YoutubeChannelInfo(result['items'][0])
|
return YoutubeChannelInfo(result['items'][0])
|
||||||
|
|
||||||
@ -180,7 +204,7 @@ class YoutubeAPI(object):
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if len(result['items']) <= 0:
|
if len(result['items']) <= 0:
|
||||||
raise Exception('Could not find channel!')
|
raise YoutubeChannelNotFoundException('Could not find channel!')
|
||||||
|
|
||||||
channel_result = result['items'][0]
|
channel_result = result['items'][0]
|
||||||
return channel_result['id']['channelId']
|
return channel_result['id']['channelId']
|
||||||
|
@ -35,10 +35,12 @@ class ModalMixin(ContextMixin):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def modal_response(self, form, success=True):
|
def modal_response(self, form, success=True, error_msg=None):
|
||||||
result = {'success': success}
|
result = {'success': success}
|
||||||
if not success:
|
if not success:
|
||||||
result['errors'] = form.errors.get_json_data(escape_html=True)
|
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)
|
return JsonResponse(result)
|
||||||
|
|
||||||
|
@ -2,15 +2,14 @@ from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.views.generic import CreateView, UpdateView, DeleteView
|
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.management.videos import get_videos
|
||||||
from YtManagerApp.models import Subscription, SubscriptionFolder
|
from YtManagerApp.models import Subscription, SubscriptionFolder
|
||||||
from YtManagerApp.views.controls.modal import ModalMixin
|
from YtManagerApp.views.controls.modal import ModalMixin
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Field
|
from crispy_forms.layout import Layout, Field, Div, HTML
|
||||||
from crispy_forms.layout import Submit
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from YtManagerApp.utils import youtube
|
||||||
|
|
||||||
class VideoFilterForm(forms.Form):
|
class VideoFilterForm(forms.Form):
|
||||||
CHOICES_SORT = (
|
CHOICES_SORT = (
|
||||||
@ -69,7 +68,7 @@ class VideoFilterForm(forms.Form):
|
|||||||
self.helper.form_id = 'form_video_filter'
|
self.helper.form_id = 'form_video_filter'
|
||||||
self.helper.form_class = 'form-inline'
|
self.helper.form_class = 'form-inline'
|
||||||
self.helper.form_method = 'POST'
|
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.field_class = 'mr-1'
|
||||||
self.helper.label_class = 'ml-2 mr-1 no-asterisk'
|
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):
|
def __tree_sub_id(sub_id):
|
||||||
if sub_id is None:
|
if sub_id is None:
|
||||||
return '#'
|
return '#'
|
||||||
return 'folder' + str(sub_id)
|
return 'sub' + str(sub_id)
|
||||||
|
|
||||||
|
|
||||||
def index(request: HttpRequest):
|
def index(request: HttpRequest):
|
||||||
@ -137,7 +136,7 @@ def ajax_get_tree(request: HttpRequest):
|
|||||||
"parent": __tree_folder_id(node.parent_folder_id)
|
"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)
|
return JsonResponse(result, safe=False)
|
||||||
|
|
||||||
|
|
||||||
@ -220,6 +219,120 @@ class UpdateFolderModal(ModalMixin, UpdateView):
|
|||||||
form_class = SubscriptionFolderForm
|
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'
|
template_name = 'YtManagerApp/controls/folder_delete_modal.html'
|
||||||
model = SubscriptionFolder
|
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