diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml new file mode 100644 index 0000000..0f9ab3c --- /dev/null +++ b/.idea/dataSources.local.xml @@ -0,0 +1,11 @@ + + + + + + + false + *:@ + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..159fddc --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,16 @@ + + + + + sqlite.xerial + true + true + $PROJECT_DIR$/YtManager/settings.py + org.sqlite.JDBC + jdbc:sqlite:D:\Dev\youtube-channel-manager\db.sqlite3 + + + + + + \ No newline at end of file diff --git a/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd.xml b/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd.xml new file mode 100644 index 0000000..f4ef6ac --- /dev/null +++ b/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd.xml @@ -0,0 +1,741 @@ + + + + + 3.20.1 + + + 1 + 1 + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1 +
+ + 1 +
+ + 1 + integer|0s + 1 + 1 + + + 2 + text|0s + 1 + + + 3 + text|0s + + + 4 + text|0s + + + 5 + text|0s + 1 + + + 6 + text|0s + 1 + + + 7 + text|0s + 1 + + + 8 + text|0s + 1 + + + 9 + text|0s + 1 + + + 1 + channel_id + + 1 + + + 1 + username + + 1 + + + 1 + custom_url + + 1 + + + id + 1 + + + channel_id + sqlite_autoindex_YtManagerApp_channel_1 + + + username + sqlite_autoindex_YtManagerApp_channel_2 + + + custom_url + sqlite_autoindex_YtManagerApp_channel_3 + + + 1 + integer|0s + 1 + 1 + + + 2 + text|0s + 1 + + + 3 + integer|0s + + + 4 + text|0s + 1 + + + 5 + text|0s + 1 + + + 6 + text|0s + 1 + + + 7 + text|0s + 1 + + + 8 + integer|0s + 1 + + + 1 + playlist_id + + 1 + + + parent_folder_id + + + + channel_id + + + + id + 1 + + + playlist_id + sqlite_autoindex_YtManagerApp_subscription_1 + + + parent_folder_id + YtManagerApp_subscriptionfolder + id + 1 + 1 + + + channel_id + YtManagerApp_channel + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + text|0s + 1 + + + 3 + integer|0s + + + parent_id + + + + id + 1 + + + parent_id + YtManagerApp_subscriptionfolder + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + text|0s + 1 + + + 3 + text|0s + 1 + + + 4 + text|0s + + + 5 + bool|0s + 1 + + + 6 + integer|0s + 1 + + + 7 + text|0s + 1 + + + 8 + text|0s + 1 + + + 9 + text|0s + 1 + + + 10 + integer|0s + 1 + + + 11 + datetime|0s + 1 + + + subscription_id + + + + id + 1 + + + subscription_id + YtManagerApp_subscription + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + varchar(80)|0s + 1 + + + 1 + name + + 1 + + + id + 1 + + + name + sqlite_autoindex_auth_group_1 + + + 1 + integer|0s + 1 + 1 + + + 2 + integer|0s + 1 + + + 3 + integer|0s + 1 + + + group_id +permission_id + + 1 + + + group_id + + + + permission_id + + + + id + 1 + + + group_id + auth_group + id + 1 + 1 + + + permission_id + auth_permission + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + integer|0s + 1 + + + 3 + varchar(100)|0s + 1 + + + 4 + varchar(255)|0s + 1 + + + content_type_id +codename + + 1 + + + content_type_id + + + + id + 1 + + + content_type_id + django_content_type + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + varchar(128)|0s + 1 + + + 3 + datetime|0s + + + 4 + bool|0s + 1 + + + 5 + varchar(150)|0s + 1 + + + 6 + varchar(30)|0s + 1 + + + 7 + varchar(254)|0s + 1 + + + 8 + bool|0s + 1 + + + 9 + bool|0s + 1 + + + 10 + datetime|0s + 1 + + + 11 + varchar(150)|0s + 1 + + + 1 + username + + 1 + + + id + 1 + + + username + sqlite_autoindex_auth_user_1 + + + 1 + integer|0s + 1 + 1 + + + 2 + integer|0s + 1 + + + 3 + integer|0s + 1 + + + user_id +group_id + + 1 + + + user_id + + + + group_id + + + + id + 1 + + + user_id + auth_user + id + 1 + 1 + + + group_id + auth_group + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + integer|0s + 1 + + + 3 + integer|0s + 1 + + + user_id +permission_id + + 1 + + + user_id + + + + permission_id + + + + id + 1 + + + user_id + auth_user + id + 1 + 1 + + + permission_id + auth_permission + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + datetime|0s + 1 + + + 3 + text|0s + + + 4 + varchar(200)|0s + 1 + + + 5 + text|0s + 1 + + + 6 + integer|0s + + + 7 + integer|0s + 1 + + + 8 + smallint unsigned|0s + 1 + + + content_type_id + + + + user_id + + + + id + 1 + + + content_type_id + django_content_type + id + 1 + 1 + + + user_id + auth_user + id + 1 + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + varchar(100)|0s + 1 + + + 3 + varchar(100)|0s + 1 + + + app_label +model + + 1 + + + id + 1 + + + 1 + integer|0s + 1 + 1 + + + 2 + varchar(255)|0s + 1 + + + 3 + varchar(255)|0s + 1 + + + 4 + datetime|0s + 1 + + + id + 1 + + + 1 + varchar(40)|0s + 1 + + + 2 + text|0s + 1 + + + 3 + datetime|0s + 1 + + + 1 + session_key + + 1 + + + expire_date + + + + session_key + 1 + sqlite_autoindex_django_session_1 + + + 1 + text|0s + + + 2 + text|0s + + + 3 + text|0s + + + 4 + integer|0s + + + 5 + text|0s + + + 1 + + + 2 + +
+
\ No newline at end of file diff --git a/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd/storage_v2/_src_/schema/main.uQUzAA.meta b/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd/storage_v2/_src_/schema/main.uQUzAA.meta new file mode 100644 index 0000000..8dab49c --- /dev/null +++ b/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd/storage_v2/_src_/schema/main.uQUzAA.meta @@ -0,0 +1,2 @@ +#n:main +! [0, 0, null, null, -2147483648, -2147483648] diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 48da65d..8cff17d 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,20 +2,21 @@ - - - - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -262,11 +350,12 @@ - + + - + @@ -328,9 +417,34 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -353,7 +467,7 @@ - + - - - - - - - + @@ -379,13 +487,7 @@ - - - - - - - + @@ -393,68 +495,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - + + - + + + + + + + diff --git a/YtManager/settings.py b/YtManager/settings.py index af30010..503b6d0 100644 --- a/YtManager/settings.py +++ b/YtManager/settings.py @@ -22,6 +22,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0' +YOUTUBE_API_KEY = "AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8" + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,7 +33,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'YtManagerApp', + 'YtManagerApp.apps.YtManagerAppConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/YtManager/urls.py b/YtManager/urls.py index 7ef8942..dab89f9 100644 --- a/YtManager/urls.py +++ b/YtManager/urls.py @@ -24,5 +24,7 @@ urlpatterns = [ path('ajax/get_folders', views.ajax_get_folders, name='ajax_get_folders'), path('ajax/edit_folder', views.ajax_edit_folder, name='ajax_edit_folder'), path('ajax/delete_folder//', views.ajax_delete_folder, name='ajax_delete_folder'), + path('ajax/edit_subscription', views.ajax_edit_subscription, name='ajax_edit_subscription'), + path('ajax/delete_subscription//', views.ajax_delete_subscription, name='ajax_delete_subscription'), path(r'', views.index, name='home') ] diff --git a/YtManagerApp/admin.py b/YtManagerApp/admin.py index f3a7b44..bcffebb 100644 --- a/YtManagerApp/admin.py +++ b/YtManagerApp/admin.py @@ -3,4 +3,4 @@ from .models import SubscriptionFolder, Subscription, Video admin.site.register(SubscriptionFolder) admin.site.register(Subscription) -admin.site.register(Video) \ No newline at end of file +admin.site.register(Video) diff --git a/YtManagerApp/apps.py b/YtManagerApp/apps.py index 06ed906..3ac548e 100644 --- a/YtManagerApp/apps.py +++ b/YtManagerApp/apps.py @@ -1,5 +1,9 @@ from django.apps import AppConfig -class YtmanagerappConfig(AppConfig): +class YtManagerAppConfig(AppConfig): name = 'YtManagerApp' + + def ready(self): + from .management import SubscriptionManager + SubscriptionManager.start_scheduler() diff --git a/YtManagerApp/management.py b/YtManagerApp/management.py index acfeb9e..32f28fa 100644 --- a/YtManagerApp/management.py +++ b/YtManagerApp/management.py @@ -1,5 +1,7 @@ -from .models import SubscriptionFolder, Subscription, Video - +from .models import SubscriptionFolder, Subscription, Video, Channel +from .youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistItem +from apscheduler.schedulers.background import BackgroundScheduler +import os class FolderManager(object): @@ -22,7 +24,7 @@ class FolderManager(object): folder.save() @staticmethod - def __validate(folder): + def __validate(folder: SubscriptionFolder): # Make sure folder name is unique in the parent folder for dbFolder in SubscriptionFolder.objects.filter(parent_id=folder.parent_id): if dbFolder.id != folder.id and dbFolder.name == folder.name: @@ -42,3 +44,141 @@ class FolderManager(object): def delete(fid: int): folder = SubscriptionFolder.objects.get(id=fid) folder.delete() + + +class SubscriptionManager(object): + __scheduler = BackgroundScheduler() + + @staticmethod + def create_or_edit(sid, url, name, parent_id): + # Create or edit + if sid == '#': + sub = Subscription() + SubscriptionManager.create(url, parent_id, YoutubeAPI.build_public()) + else: + sub = Subscription.objects.get(id=int(sid)) + sub.name = name + + if parent_id == '#': + sub.parent_folder = None + else: + sub.parent_folder = SubscriptionFolder.objects.get(id=int(parent_id)) + + sub.save() + + @staticmethod + def create(url, parent_id, yt_api: YoutubeAPI): + sub = Subscription() + # Set parent + if parent_id == '#': + sub.parent_folder = None + else: + sub.parent_folder = SubscriptionFolder.objects.get(id=int(parent_id)) + + # Pull information about the channel and playlist + url_type, url_id = yt_api.parse_channel_url(url) + + if url_type == 'playlist_id': + info_playlist = yt_api.get_playlist_info(url_id) + channel = SubscriptionManager.__get_or_create_channel('channel_id', info_playlist.getChannelId(), yt_api) + sub.name = info_playlist.getTitle() + sub.playlist_id = info_playlist.getId() + sub.description = info_playlist.getDescription() + sub.channel = channel + sub.icon_default = info_playlist.getDefaultThumbnailUrl() + sub.icon_best = info_playlist.getBestThumbnailUrl() + + else: + channel = SubscriptionManager.__get_or_create_channel(url_type, url_id, yt_api) + # No point in getting the 'uploads' playlist info + sub.name = channel.name + sub.playlist_id = channel.upload_playlist_id + sub.description = channel.description + sub.channel = channel + sub.icon_default = channel.icon_default + sub.icon_best = channel.icon_best + + sub.save() + + @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() + + @staticmethod + def __create_video(yt_video: YoutubePlaylistItem, subscription: Subscription): + video = Video() + video.video_id = yt_video.getVideoId() + video.name = yt_video.getTitle() + video.description = yt_video.getDescription() + video.watched = False + video.downloaded_path = None + video.subscription = subscription + video.playlist_index = yt_video.getPlaylistIndex() + video.publish_date = yt_video.getPublishDate() + video.icon_default = yt_video.getDefaultThumbnailUrl() + video.icon_best = yt_video.getBestThumbnailUrl() + video.save() + + @staticmethod + def __synchronize(subscription: Subscription, yt_api: YoutubeAPI): + # Get list of videos + for video in yt_api.list_playlist_videos(subscription.playlist_id): + results = Video.objects.filter(video_id=video.getVideoId(), subscription=subscription) + if len(results) == 0: + print('New video for subscription "', subscription, '": ', video.getVideoId(), video.getTitle()) + SubscriptionManager.__create_video(video, subscription) + + @staticmethod + def __synchronize_all(): + print("Running scheduled synchronization... ") + yt_api = YoutubeAPI.build_public() + for subscription in Subscription.objects.all(): + SubscriptionManager.__synchronize(subscription, yt_api) + + @staticmethod + def start_scheduler(): + SubscriptionManager.__scheduler.add_job(SubscriptionManager.__synchronize_all, 'cron', + hour='*', minute=44, max_instances=1) + SubscriptionManager.__scheduler.start() diff --git a/YtManagerApp/migrations/0005_auto_20181007_2015.py b/YtManagerApp/migrations/0005_auto_20181007_2015.py new file mode 100644 index 0000000..d38c04b --- /dev/null +++ b/YtManagerApp/migrations/0005_auto_20181007_2015.py @@ -0,0 +1,57 @@ +# Generated by Django 2.1.2 on 2018-10-07 17:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('YtManagerApp', '0004_auto_20181005_1626'), + ] + + operations = [ + migrations.CreateModel( + name='Channel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('channel_id', models.TextField(unique=True)), + ('username', models.TextField(null=True, unique=True)), + ('custom_url', models.TextField(null=True, unique=True)), + ('name', models.TextField()), + ('description', models.TextField()), + ('icon_default', models.TextField()), + ('icon_best', models.TextField()), + ('upload_playlist_id', models.TextField()), + ], + ), + migrations.RenameField( + model_name='subscription', + old_name='url', + new_name='playlist_id', + ), + migrations.AddField( + model_name='subscription', + name='description', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='subscription', + name='icon_best', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='subscription', + name='icon_default', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='subscription', + name='channel', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='YtManagerApp.Channel'), + preserve_default=False, + ), + ] diff --git a/YtManagerApp/migrations/0006_auto_20181008_0037.py b/YtManagerApp/migrations/0006_auto_20181008_0037.py new file mode 100644 index 0000000..0eebb4c --- /dev/null +++ b/YtManagerApp/migrations/0006_auto_20181008_0037.py @@ -0,0 +1,49 @@ +# Generated by Django 2.1.2 on 2018-10-07 21:37 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('YtManagerApp', '0005_auto_20181007_2015'), + ] + + operations = [ + migrations.RenameField( + model_name='video', + old_name='ytid', + new_name='video_id', + ), + migrations.AddField( + model_name='video', + name='description', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='icon_best', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='icon_default', + field=models.TextField(default=None), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='playlist_index', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='publish_date', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/YtManagerApp/models.py b/YtManagerApp/models.py index e0fe514..a11059c 100644 --- a/YtManagerApp/models.py +++ b/YtManagerApp/models.py @@ -1,5 +1,6 @@ from django.db import models + class SubscriptionFolder(models.Model): name = models.TextField(null=False) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True) @@ -8,21 +9,65 @@ class SubscriptionFolder(models.Model): return self.name +class Channel(models.Model): + channel_id = models.TextField(null=False, unique=True) + username = models.TextField(null=True, unique=True) + custom_url = models.TextField(null=True, unique=True) + name = models.TextField() + description = models.TextField() + icon_default = models.TextField() + icon_best = models.TextField() + upload_playlist_id = models.TextField() + + @staticmethod + def find_by_channel_id(channel_id): + result = Channel.objects.filter(channel_id=channel_id) + if len(result) > 0: + return result.first() + return None + + @staticmethod + def find_by_username(username): + result = Channel.objects.filter(username=username) + if len(result) > 0: + return result.first() + return None + + @staticmethod + def find_by_custom_url(custom_url): + result = Channel.objects.filter(custom_url=custom_url) + if len(result) > 0: + return result.first() + return None + + def __str__(self): + return self.name + + class Subscription(models.Model): name = models.TextField(null=False) parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.SET_NULL, null=True, blank=True) - url = models.TextField(null=False, unique=True) - + playlist_id = models.TextField(null=False, unique=True) + description = models.TextField() + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) + icon_default = models.TextField() + icon_best = models.TextField() + def __str__(self): return self.name class Video(models.Model): + video_id = models.TextField(null=False) name = models.TextField(null=False) - ytid = models.TextField(null=False) - downloaded_path = models.TextField(null=True, blank=True) + description = models.TextField() watched = models.BooleanField(default=False, null=False) + downloaded_path = models.TextField(null=True, blank=True) subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE) + playlist_index = models.IntegerField(null=False) + publish_date = models.DateTimeField(null=False) + icon_default = models.TextField() + icon_best = models.TextField() def __str__(self): return self.name \ No newline at end of file diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html index d71897e..8cefaa1 100644 --- a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html +++ b/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html @@ -1,36 +1,37 @@ -