Implemented YouTube layer, as well as synchronization on schedule. TODO: fix issue where ready() is called twice. The synchronization should only run on one thread/process.

This commit is contained in:
2018-10-08 03:01:35 +03:00
parent c26732d101
commit 291da16461
19 changed files with 1958 additions and 284 deletions

View File

@ -3,4 +3,4 @@ from .models import SubscriptionFolder, Subscription, Video
admin.site.register(SubscriptionFolder)
admin.site.register(Subscription)
admin.site.register(Video)
admin.site.register(Video)

View File

@ -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()

View File

@ -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()

View File

@ -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,
),
]

View File

@ -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,
),
]

View File

@ -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

View File

@ -1,36 +1,37 @@
<div id="folder_edit_dialog" class="modal" tabindex="-1" role="dialog">
<div id="folderEditDialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 id="folder_edit_dialog_title" class="modal-title">Edit folder</h5>
<h5 id="folderEditDialog_Title" class="modal-title">Edit folder</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="folder_edit_dialog_loading" class="modal-body">
<div id="folderEditDialog_Loading" class="modal-body">
<div class="loading-dual-ring"></div>
<div id="folder_edit_dialog_error"></div>
</div>
<form id="folder_edit_dialog_form" action="{% url 'ajax_edit_folder' %}" method="post">
<div id="folderEditDialog_Error">
</div>
<form id="folderEditDialog_Form" action="{% url 'ajax_edit_folder' %}" method="post">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="folder_edit_dialog_id" name="id" value="#">
<input type="hidden" id="folderEditDialog_Id" name="id" value="#">
<div class="form-group row">
<label class="col-sm-3" for="folder_edit_dialog_name">Name</label>
<label class="col-sm-3" for="folderEditDialog_Name">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="folder_edit_dialog_name" name="name" placeholder="Folder name">
<input type="text" class="form-control" id="folderEditDialog_Name" name="name" placeholder="Folder name">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3" for="folder_edit_dialog_parent">Parent folder</label>
<label class="col-sm-3" for="folderEditDialog_Parent">Parent folder</label>
<div class="col-sm-9">
<select class="form-control" id="folder_edit_dialog_parent" name="parent">
<select class="form-control" id="folderEditDialog_Parent" name="parent">
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button id="folder_edit_dialog_submit" type="submit" class="btn btn-primary">Submit</button>
<button id="folderEditDialog_Submit" type="submit" class="btn btn-primary">Submit</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>

View File

@ -1,36 +1,42 @@
<div id="subscription_edit_dialog" class="modal" tabindex="-1" role="dialog">
<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="subscription_edit_dialog_title" class="modal-title">Edit subscription</h5>
<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">&times;</span>
</button>
</div>
<div id="subscription_edit_dialog_loading" class="modal-body">
<div id="subscriptionEditDialog_Loading" class="modal-body">
<div class="loading-dual-ring"></div>
<div id="subscription_edit_dialog_error"></div>
</div>
<form id="subscription_edit_dialog_form" action="{% url 'ajax_edit_subscription' %}" method="post">
<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="subscription_edit_dialog_id" name="id" value="#">
<input type="hidden" id="subscriptionEditDialog_Id" name="id" value="#">
<div class="form-group row">
<label class="col-sm-3" for="subscription_edit_dialog_url">Link:</label>
<label class="col-sm-3" for="subscriptionEditDialog_Url">Link:</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="subscription_edit_dialog_name" name="name" placeholder="subscription name">
<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="subscription_edit_dialog_parent">Parent subscription</label>
<label class="col-sm-3" for="subscriptionEditDialog_Name">Name:</label>
<div class="col-sm-9">
<select class="form-control" id="subscription_edit_dialog_parent" name="parent">
<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="subscription_edit_dialog_submit" type="submit" class="btn btn-primary">Submit</button>
<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>

View File

@ -12,6 +12,7 @@
</script>
{% include 'YtManagerApp/controls/folder_edit_dialog.html' %}
{% include 'YtManagerApp/controls/subscription_edit_dialog.html' %}
{% endblock %}
@ -19,7 +20,7 @@
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
<div class="btn-group btn-group-sm mr-2" role="group">
<button type="button" class="btn btn-secondary" >
<button id="btn_create_sub" type="button" class="btn btn-secondary" >
<i class="material-icons" aria-hidden="true">add</i>
</button>
<button id="btn_create_folder" type="button" class="btn btn-secondary">

View File

@ -1,79 +1,226 @@
function folderEditDialog_Show(isNew, editNode)
{
let dialog = $("#folder_edit_dialog");
dialog.find('#folder_edit_dialog_title').text(isNew ? "New folder" : "Edit folder");
dialog.find("#folder_edit_dialog_loading").show();
dialog.find("#folder_edit_dialog_error").hide();
dialog.find("#folder_edit_dialog_form").hide();
dialog.modal();
class Dialog {
constructor(modalId) {
this.modal = $(modalId);
this.title = $(modalId + "_Title");
this.form = $(modalId + "_Form");
this.error = $(modalId + "_Error");
this.loading = $(modalId + "_Loading");
this.btnSubmit = $(modalId + "_Submit");
this.setState('normal');
}
$.get("{% url 'ajax_get_folders' %}")
.done(function(folders)
{
// Populate list of folders
let selParent = dialog.find("#folder_edit_dialog_parent");
selParent.empty();
selParent.append(new Option('(None)', '#'));
setTitle(value) {
this.title.text(value);
}
let parentId = null;
if (!isNew) {
parentId = editNode.parent.replace('folder', '');
}
setState(state) {
if (state === 'loading') {
this.loading.show();
this.error.hide();
this.form.hide();
}
if (state === 'error') {
this.loading.hide();
this.error.show();
this.form.hide();
}
if (state === 'normal') {
this.loading.hide();
this.error.hide();
this.form.show();
}
}
for (let folder of folders)
{
let o = new Option(folder.text, folder.id);
if (!isNew && folder.id.toString() === parentId.toString())
o.selected = true;
setError(text) {
this.error.text(text);
}
selParent.append(o);
}
showModal() {
this.modal.modal();
}
// Show form
dialog.find("#folder_edit_dialog_loading").hide();
dialog.find("#folder_edit_dialog_form").show();
dialog.find("#folder_edit_dialog_submit").text(isNew ? "Create" : "Save");
hideModal() {
this.modal.modal('hide');
}
}
if (isNew)
{
dialog.find("#folder_edit_dialog_id").val('#');
dialog.find("#folder_edit_dialog_name").val('');
}
if (!isNew)
{
idTrimmed = editNode.id.replace('folder', '');
dialog.find("#folder_edit_dialog_id").val(idTrimmed);
dialog.find("#folder_edit_dialog_name").val(editNode.text);
}
class FolderEditDialog extends Dialog {
constructor (modalId) {
super(modalId);
this.field_Id = $(modalId + "_Id");
this.field_Name = $(modalId + "_Name");
this.field_Parent = $(modalId + "_Parent");
let pThis = this;
this.form.submit(function(e) {
pThis.submit(e);
})
.fail(function() {
let msgError = dialog.find("#folder_edit_dialog_error");
msgError.show();
msgError.text("An error occurred!");
});
}
setParentFolderOptions(folders, selectedId)
{
// Populate list of folders
this.field_Parent.empty();
this.field_Parent.append(new Option('(None)', '#'));
for (let folder of folders)
{
let o = new Option(folder.text, folder.id);
if (selectedId != null && folder.id.toString() === selectedId.toString())
o.selected = true;
this.field_Parent.append(o);
}
}
show (isNew, editNode) {
let pThis = this;
this.setTitle(isNew ? "New folder" : "Edit folder");
this.setState('loading');
this.showModal();
$.get("{% url 'ajax_get_folders' %}")
.done(function(folders)
{
let parentId = null;
if (!isNew) {
parentId = editNode.parent.replace('folder', '');
}
pThis.setParentFolderOptions(folders, parentId);
pThis.setState('normal');
pThis.btnSubmit.text(isNew ? "Create" : "Save");
if (isNew)
{
pThis.field_Id.val('#');
pThis.field_Name.val('');
}
if (!isNew)
{
let idTrimmed = editNode.id.replace('folder', '');
pThis.field_Id.val(idTrimmed);
pThis.field_Name.val(editNode.text);
}
})
.fail(function() {
pThis.setState('error');
pThis.setError('An error occurred!');
});
}
showNew() {
this.show(true, null);
}
showEdit(editNode) {
this.show(false, editNode);
}
submit(e) {
let url = this.form.attr('action');
$.post(url, this.form.serialize())
.done(tree_Refresh);
this.hideModal();
e.preventDefault();
}
}
function folderEditDialog_ShowNew()
{
folderEditDialog_Show(true, null);
class SubscriptionEditDialog extends Dialog {
constructor (modalId) {
super(modalId);
this.field_Id = $(modalId + "_Id");
this.field_Url = $(modalId + "_Url");
this.field_Name = $(modalId + "_Name");
this.field_Parent = $(modalId + "_Parent");
let pThis = this;
this.form.submit(function(e) {
pThis.submit(e);
})
}
setParentFolderOptions(folders, selectedId)
{
// Populate list of folders
this.field_Parent.empty();
this.field_Parent.append(new Option('(None)', '#'));
for (let folder of folders)
{
let o = new Option(folder.text, folder.id);
if (selectedId != null && folder.id.toString() === selectedId.toString())
o.selected = true;
this.field_Parent.append(o);
}
}
show (isNew, editNode) {
let pThis = this;
this.setTitle(isNew ? "New subscription" : "Edit subscription");
this.setState('loading');
this.showModal();
$.get("{% url 'ajax_get_folders' %}")
.done(function(folders)
{
let parentId = null;
if (!isNew) {
parentId = editNode.parent.replace('folder', '');
}
pThis.setParentFolderOptions(folders, parentId);
pThis.setState('normal');
pThis.btnSubmit.text(isNew ? "Create" : "Save");
if (isNew)
{
pThis.field_Id.val('#');
pThis.field_Url.show();
pThis.field_Url.val('');
pThis.field_Name.hide();
pThis.field_Name.val('');
}
if (!isNew)
{
let idTrimmed = editNode.id.replace('sub', '');
pThis.field_Id.val(idTrimmed);
pThis.field_Url.hide();
pThis.field_Url.val('');
pThis.field_Name.show();
pThis.field_Name.val(editNode.text);
}
})
.fail(function() {
pThis.setState('error');
pThis.setError('An error occurred!');
});
}
showNew() {
this.show(true, null);
}
showEdit(editNode) {
this.show(false, editNode);
}
submit(e) {
let url = this.form.attr('action');
$.post(url, this.form.serialize())
.done(tree_Refresh);
this.hideModal();
e.preventDefault();
}
}
function folderEditDialog_Close()
{
$("#folder_edit_dialog").modal('hide');
}
function folderEditDialog_Submit(e)
{
let form = $(this);
let url = form.attr('action');
$.post(url, form.serialize())
.done(tree_Refresh);
folderEditDialog_Close();
e.preventDefault();
}
function treeNode_Edit()
{
@ -82,10 +229,10 @@ function treeNode_Edit()
{
let node = selectedNodes[0];
if (node.type === 'folder') {
folderEditDialog_Show(false, node);
folderEditDialog.showEdit(node);
}
else {
// TODO...
subscriptionEditDialog.showEdit(node);
}
}
}
@ -106,7 +253,13 @@ function treeNode_Delete()
}
}
else {
// TODO...
let subId = node.id.toString().replace('sub', '');
if (confirm('Are you sure you want to delete subscription "' + node.text + '"?'))
{
$.post("{% url 'ajax_delete_subscription' 99999 %}".replace('99999', subId), {
csrfmiddlewaretoken: '{{ csrf_token }}'
}).done(tree_Refresh);
}
}
}
}
@ -163,12 +316,24 @@ function tree_OnSelectionChanged(e, data)
node = data.instance.get_selected(true)[0];
}
$(document).ready(function ()
///
/// Globals
///
let folderEditDialog = null;
let subscriptionEditDialog = null;
///
/// Initialization
///
$(document).ready(function ()
{
tree_Initialize();
$("#btn_create_folder").on("click", folderEditDialog_ShowNew);
folderEditDialog = new FolderEditDialog('#folderEditDialog');
subscriptionEditDialog = new SubscriptionEditDialog('#subscriptionEditDialog');
$("#btn_create_sub").on("click", function () { subscriptionEditDialog.showNew(); });
$("#btn_create_folder").on("click", function () { folderEditDialog.showNew(); });
$("#btn_edit_node").on("click", treeNode_Edit);
$("#btn_delete_node").on("click", treeNode_Delete);
$("#folder_edit_dialog_form").submit(folderEditDialog_Submit);
});

View File

@ -1,7 +1,7 @@
from django.shortcuts import render
from django.http import HttpResponse, HttpRequest, JsonResponse
from .models import SubscriptionFolder, Subscription
from .management import FolderManager
from .management import FolderManager, SubscriptionManager
def get_children_recurse(parent_id):
@ -66,6 +66,22 @@ def ajax_delete_folder(request: HttpRequest, fid):
return HttpResponse()
def ajax_edit_subscription(request: HttpRequest):
if request.method == 'POST':
sid = request.POST['id']
name = request.POST['name']
url = request.POST['url']
parent_id = request.POST['parent']
SubscriptionManager.create_or_edit(sid, url, name, parent_id)
return HttpResponse()
def ajax_delete_subscription(request: HttpRequest, sid):
SubscriptionManager.delete(sid)
return HttpResponse()
def index(request: HttpRequest):
context = {}
return render(request, 'YtManagerApp/index.html', context)

213
YtManagerApp/youtube.py Normal file
View File

@ -0,0 +1,213 @@
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from django.conf import settings
import re
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
class YoutubeChannelInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
self.__snippet = result_dict['snippet']
self.__contentDetails = result_dict['contentDetails']
def getId(self):
return self.__id
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getCustomUrl(self):
return self.__snippet['customUrl']
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
def getUploadsPlaylist(self):
return self.__contentDetails['relatedPlaylists']['uploads']
class YoutubePlaylistInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
self.__snippet = result_dict['snippet']
def getId(self):
return self.__id
def getChannelId(self):
return self.__snippet['channelId']
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
class YoutubePlaylistItem(object):
def __init__(self, result_dict):
self.__snippet = result_dict['snippet']
def getVideoId(self):
return self.__snippet['resourceId']['videoId']
def getPublishDate(self):
return self.__snippet['publishedAt']
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
def getPlaylistIndex(self):
return self.__snippet['position']
class YoutubeAPI(object):
def __init__(self, service):
self.service = service
@staticmethod
def build_public() -> 'YoutubeAPI':
service = build(API_SERVICE_NAME, API_VERSION, developerKey=settings.YOUTUBE_API_KEY)
return YoutubeAPI(service)
@staticmethod
def parse_channel_url(url):
"""
Parses given channel url, returns a tuple of the form (type, value), where type can be one of:
* channel_id
* channel_custom
* user
* playlist_id
:param url: URL to parse
:return: (type, value) tuple
"""
match = re.search(r'youtube\.com/.*[&?]list=([^?&/]+)', url)
if match:
return 'playlist_id', match.group(1)
match = re.search(r'youtube\.com/user/([^?&/]+)', url)
if match:
return 'user', match.group(1)
match = re.search(r'youtube\.com/channel/([^?&/]+)', url)
if match:
return 'channel_id', match.group(1)
match = re.search(r'youtube\.com/(?:c/)?([^?&/]+)', url)
if match:
return 'channel_custom', match.group(1)
raise Exception('Unrecognized URL format!')
def get_playlist_info(self, list_id) -> YoutubePlaylistInfo:
result = self.service.playlists()\
.list(part='snippet', id=list_id)\
.execute()
if len(result['items']) <= 0:
raise Exception("Invalid playlist ID.")
return YoutubePlaylistInfo(result['items'][0])
def get_channel_info_by_username(self, user) -> YoutubeChannelInfo:
result = self.service.channels()\
.list(part='snippet,contentDetails', forUsername=user)\
.execute()
if len(result['items']) <= 0:
raise Exception('Invalid user.')
return YoutubeChannelInfo(result['items'][0])
def get_channel_info(self, channel_id) -> YoutubeChannelInfo:
result = self.service.channels()\
.list(part='snippet,contentDetails', id=channel_id)\
.execute()
if len(result['items']) <= 0:
raise Exception('Invalid channel ID.')
return YoutubeChannelInfo(result['items'][0])
def search_channel(self, custom) -> str:
result = self.service.search()\
.list(part='id', q=custom, type='channel')\
.execute()
if len(result['items']) <= 0:
raise Exception('Could not find channel!')
channel_result = result['items'][0]
return channel_result['id']['channelId']
def list_playlist_videos(self, playlist_id):
kwargs = {
"part": "snippet",
"maxResults": 50,
"playlistId": playlist_id
}
last_page = False
while not last_page:
result = self.service.playlistItems()\
.list(**kwargs)\
.execute()
for item in result['items']:
yield YoutubePlaylistItem(item)
if 'nextPageToken' in result:
kwargs['pageToken'] = result['nextPageToken']
else:
last_page = True
# @staticmethod
# def build_oauth() -> 'YoutubeAPI':
# flow =
# credentials =
# service = build(API_SERVICE_NAME, API_VERSION, credentials)