mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
commit
620860923b
@ -4,19 +4,22 @@ from threading import Lock
|
|||||||
|
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
from YtManagerApp.management.notification_manager import OPERATION_ID_SYNCHRONIZE
|
||||||
from YtManagerApp.scheduler import scheduler
|
from YtManagerApp.scheduler import scheduler
|
||||||
from YtManagerApp.management.appconfig import appconfig
|
from YtManagerApp.management.appconfig import appconfig
|
||||||
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription
|
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription
|
||||||
from YtManagerApp.models import *
|
from YtManagerApp.models import *
|
||||||
from YtManagerApp.utils import youtube
|
from YtManagerApp.utils import youtube
|
||||||
|
|
||||||
|
from YtManagerApp.management import notification_manager
|
||||||
|
|
||||||
log = logging.getLogger('sync')
|
log = logging.getLogger('sync')
|
||||||
__lock = Lock()
|
__lock = Lock()
|
||||||
|
|
||||||
_ENABLE_UPDATE_STATS = True
|
_ENABLE_UPDATE_STATS = True
|
||||||
|
|
||||||
|
|
||||||
def __check_new_videos_sub(subscription: Subscription, yt_api: youtube.YoutubeAPI):
|
def __check_new_videos_sub(subscription: Subscription, yt_api: youtube.YoutubeAPI, progress_callback=None):
|
||||||
# Get list of videos
|
# Get list of videos
|
||||||
for item in yt_api.playlist_items(subscription.playlist_id):
|
for item in yt_api.playlist_items(subscription.playlist_id):
|
||||||
results = Video.objects.filter(video_id=item.resource_video_id, subscription=subscription)
|
results = Video.objects.filter(video_id=item.resource_video_id, subscription=subscription)
|
||||||
@ -98,6 +101,10 @@ def __fetch_thumbnails():
|
|||||||
__fetch_thumbnails_obj(Video.objects.filter(icon_best__istartswith='http'), 'video', 'video_id')
|
__fetch_thumbnails_obj(Video.objects.filter(icon_best__istartswith='http'), 'video', 'video_id')
|
||||||
|
|
||||||
|
|
||||||
|
def __compute_progress(stage, stage_count, items, total_items):
|
||||||
|
stage_percent = float(stage) / stage_count
|
||||||
|
|
||||||
|
|
||||||
def synchronize():
|
def synchronize():
|
||||||
if not __lock.acquire(blocking=False):
|
if not __lock.acquire(blocking=False):
|
||||||
# Synchronize already running in another thread
|
# Synchronize already running in another thread
|
||||||
@ -106,6 +113,12 @@ def synchronize():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log.info("Running scheduled synchronization... ")
|
log.info("Running scheduled synchronization... ")
|
||||||
|
notification_manager.notify_status_operation_progress(
|
||||||
|
OPERATION_ID_SYNCHRONIZE,
|
||||||
|
'Running scheduled synchronization: checking for new videos...',
|
||||||
|
0.1,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
# Sync subscribed playlists/channels
|
# Sync subscribed playlists/channels
|
||||||
log.info("Sync - checking videos")
|
log.info("Sync - checking videos")
|
||||||
@ -114,13 +127,32 @@ def synchronize():
|
|||||||
__check_new_videos_sub(subscription, yt_api)
|
__check_new_videos_sub(subscription, yt_api)
|
||||||
__detect_deleted(subscription)
|
__detect_deleted(subscription)
|
||||||
|
|
||||||
|
notification_manager.notify_status_operation_progress(
|
||||||
|
OPERATION_ID_SYNCHRONIZE,
|
||||||
|
'Running scheduled synchronization: enqueueing videos to download...',
|
||||||
|
0.5,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
log.info("Sync - checking for videos to download")
|
log.info("Sync - checking for videos to download")
|
||||||
downloader_process_all()
|
downloader_process_all()
|
||||||
|
|
||||||
|
notification_manager.notify_status_operation_progress(
|
||||||
|
OPERATION_ID_SYNCHRONIZE,
|
||||||
|
'Running scheduled synchronization: fetching thumbnails...',
|
||||||
|
0.7,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
log.info("Sync - fetching missing thumbnails")
|
log.info("Sync - fetching missing thumbnails")
|
||||||
__fetch_thumbnails()
|
__fetch_thumbnails()
|
||||||
|
|
||||||
log.info("Synchronization finished.")
|
log.info("Synchronization finished.")
|
||||||
|
notification_manager.notify_status_operation_ended(
|
||||||
|
OPERATION_ID_SYNCHRONIZE,
|
||||||
|
'Synchronization finished.',
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
__lock.release()
|
__lock.release()
|
||||||
@ -130,6 +162,8 @@ def synchronize_subscription(subscription: Subscription):
|
|||||||
__lock.acquire()
|
__lock.acquire()
|
||||||
try:
|
try:
|
||||||
log.info("Running synchronization for single subscription %d [%s]", subscription.id, subscription.name)
|
log.info("Running synchronization for single subscription %d [%s]", subscription.id, subscription.name)
|
||||||
|
notification_manager.notify_status_update(f'Synchronization started for subscription <strong>{subscription.name}</strong>.')
|
||||||
|
|
||||||
yt_api = youtube.YoutubeAPI.build_public()
|
yt_api = youtube.YoutubeAPI.build_public()
|
||||||
|
|
||||||
log.info("Sync - checking videos")
|
log.info("Sync - checking videos")
|
||||||
@ -143,6 +177,7 @@ def synchronize_subscription(subscription: Subscription):
|
|||||||
__fetch_thumbnails()
|
__fetch_thumbnails()
|
||||||
|
|
||||||
log.info("Synchronization finished for subscription %d [%s].", subscription.id, subscription.name)
|
log.info("Synchronization finished for subscription %d [%s].", subscription.id, subscription.name)
|
||||||
|
notification_manager.notify_status_update(f'Synchronization finished for subscription <strong>{subscription.name}</strong>.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
__lock.release()
|
__lock.release()
|
||||||
|
95
app/YtManagerApp/management/notification_manager.py
Normal file
95
app/YtManagerApp/management/notification_manager.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from typing import Dict, Deque, Any, Optional
|
||||||
|
from collections import deque
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from YtManagerApp.utils.algorithms import bisect_left
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
# Clients will request updates at most every few seconds, so a retention period of 60 seconds should be more than
|
||||||
|
# enough. I gave it 15 minutes so that if for some reason the connection fails (internet drops) and then comes back a
|
||||||
|
# few minutes later, the client will still get the updates
|
||||||
|
__RETENTION_PERIOD = 15 * 60
|
||||||
|
__NOTIFICATIONS: Deque[Dict] = deque()
|
||||||
|
__NEXT_ID = 0
|
||||||
|
__LOCK = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
OPERATION_ID_SYNCHRONIZE = 1
|
||||||
|
|
||||||
|
# Messages enum
|
||||||
|
class Messages:
|
||||||
|
STATUS_UPDATE = 'st-up'
|
||||||
|
STATUS_OPERATION_PROGRESS = 'st-op-prog'
|
||||||
|
STATUS_OPERATION_END = 'st-op-end'
|
||||||
|
|
||||||
|
|
||||||
|
def __add_notification(message, user: User=None, **kwargs):
|
||||||
|
global __NEXT_ID
|
||||||
|
|
||||||
|
__LOCK.acquire()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# add notification
|
||||||
|
notification = {
|
||||||
|
'time': datetime.now(),
|
||||||
|
'msg': message,
|
||||||
|
'id': __NEXT_ID,
|
||||||
|
'uid': user and user.id,
|
||||||
|
}
|
||||||
|
notification.update(kwargs)
|
||||||
|
__NOTIFICATIONS.append(notification)
|
||||||
|
__NEXT_ID += 1
|
||||||
|
|
||||||
|
# trim old notifications
|
||||||
|
oldest = __NOTIFICATIONS[0]
|
||||||
|
while len(__NOTIFICATIONS) > 0 and oldest['time'] + timedelta(seconds=__RETENTION_PERIOD) < datetime.now():
|
||||||
|
__NOTIFICATIONS.popleft()
|
||||||
|
oldest = __NOTIFICATIONS[0]
|
||||||
|
|
||||||
|
finally:
|
||||||
|
__LOCK.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_notifications(user: User, last_received_id: Optional[int]):
|
||||||
|
|
||||||
|
__LOCK.acquire()
|
||||||
|
|
||||||
|
try:
|
||||||
|
first_index = 0
|
||||||
|
if last_received_id is not None:
|
||||||
|
first_index = bisect_left(__NOTIFICATIONS,
|
||||||
|
{'id': last_received_id},
|
||||||
|
key=lambda item: item['id'])
|
||||||
|
|
||||||
|
for i in range(first_index, len(__NOTIFICATIONS)):
|
||||||
|
item = __NOTIFICATIONS[i]
|
||||||
|
if item['uid'] is None or item['uid'] == user.id:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
finally:
|
||||||
|
__LOCK.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_notification_id():
|
||||||
|
return __NEXT_ID
|
||||||
|
|
||||||
|
|
||||||
|
def notify_status_update(status_message: str, user: User=None):
|
||||||
|
__add_notification(Messages.STATUS_UPDATE,
|
||||||
|
user=user,
|
||||||
|
status=status_message)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_status_operation_progress(op_id: Any, status_message: str, progress_percent: float, user: User=None):
|
||||||
|
__add_notification(Messages.STATUS_OPERATION_PROGRESS,
|
||||||
|
user=user,
|
||||||
|
operation=op_id,
|
||||||
|
status=status_message,
|
||||||
|
progress=progress_percent)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_status_operation_ended(op_id: Any, status_message: str, user: User=None):
|
||||||
|
__add_notification(Messages.STATUS_OPERATION_END,
|
||||||
|
user=user,
|
||||||
|
operation=op_id,
|
||||||
|
status=status_message)
|
@ -126,4 +126,7 @@
|
|||||||
padding: 0.15rem 0.4rem;
|
padding: 0.15rem 0.4rem;
|
||||||
font-size: 14pt; }
|
font-size: 14pt; }
|
||||||
|
|
||||||
|
.status-timestamp {
|
||||||
|
margin-right: 0.25rem; }
|
||||||
|
|
||||||
/*# sourceMappingURL=style.css.map */
|
/*# sourceMappingURL=style.css.map */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "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",
|
"mappings": "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",
|
||||||
"sources": ["style.scss"],
|
"sources": ["style.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "style.css"
|
"file": "style.css"
|
||||||
|
@ -157,3 +157,7 @@ $accent-color: #007bff;
|
|||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-timestamp {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
@ -9,6 +9,8 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{% static 'YtManagerApp/import/jstree/dist/jstree.min.js' %}"></script>
|
<script src="{% static 'YtManagerApp/import/jstree/dist/jstree.min.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
let LAST_NOTIFICATION_ID = {{ current_notification_id }};
|
||||||
|
|
||||||
{% include 'YtManagerApp/js/index.js' %}
|
{% include 'YtManagerApp/js/index.js' %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="video-gallery container-fluid">
|
<div class="video-gallery container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for video in videos %}
|
{% for video in videos %}
|
||||||
<div class="card-wrapper col-12 col-sm-6 col-lg-4 col-xl-3 d-flex align-items-stretch">
|
<div class="card-wrapper d-flex align-items-stretch" style="width: 18rem;">
|
||||||
<div class="card mx-auto">
|
<div class="card mx-auto">
|
||||||
<a href="{% url 'video' video.id %}">
|
<a href="{% url 'video' video.id %}">
|
||||||
<img class="card-img-top" src="{{ video.icon_best }}" alt="Thumbnail">
|
<img class="card-img-top" src="{{ video.icon_best }}" alt="Thumbnail">
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
function zeroFill(number, width) {
|
||||||
|
width -= number.toString().length;
|
||||||
|
if ( width > 0 ) {
|
||||||
|
return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number;
|
||||||
|
}
|
||||||
|
return number + ""; // always return a string
|
||||||
|
}
|
||||||
|
|
||||||
class AjaxModal
|
class AjaxModal
|
||||||
{
|
{
|
||||||
constructor(url)
|
constructor(url)
|
||||||
|
@ -175,6 +175,70 @@ function videos_Submit(e)
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Notifications
|
||||||
|
///
|
||||||
|
const NOTIFICATION_INTERVAL = 1000;
|
||||||
|
const STATUS_UPDATE = 'st-up';
|
||||||
|
const STATUS_OPERATION_PROGRESS = 'st-op-prog';
|
||||||
|
const STATUS_OPERATION_END = 'st-op-end';
|
||||||
|
const OPERATION_LIST = {};
|
||||||
|
|
||||||
|
function notifications_update_progress_bar() {
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
var percent = 0;
|
||||||
|
|
||||||
|
for(op in OPERATION_LIST) {
|
||||||
|
count++;
|
||||||
|
percent += OPERATION_LIST[op];
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = $('#status-progress');
|
||||||
|
if (count > 0) {
|
||||||
|
progress.removeClass('invisible');
|
||||||
|
let bar = progress.find('.progress-bar');
|
||||||
|
bar.width(percent * 100 + '%');
|
||||||
|
bar.text(count + ' operations in progress');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
progress.addClass('invisible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_and_process_notifications()
|
||||||
|
{
|
||||||
|
$.get("{% url 'ajax_get_notifications' 12345 %}".replace("12345", LAST_NOTIFICATION_ID))
|
||||||
|
.done(function(data) {
|
||||||
|
for (let entry of data)
|
||||||
|
{
|
||||||
|
LAST_NOTIFICATION_ID = entry.id;
|
||||||
|
let dt = new Date(entry.time);
|
||||||
|
|
||||||
|
// Status update
|
||||||
|
if (entry.msg === STATUS_UPDATE) {
|
||||||
|
let txt = `<span class="status-timestamp">${dt.getHours()}:${zeroFill(dt.getMinutes(), 2)}</span>${entry.status}`;
|
||||||
|
$('#status-message').html(txt);
|
||||||
|
}
|
||||||
|
else if (entry.msg === STATUS_OPERATION_PROGRESS) {
|
||||||
|
let txt = `<span class="status-timestamp">${dt.getHours()}:${zeroFill(dt.getMinutes(), 2)}</span>${entry.status}`;
|
||||||
|
$('#status-message').html(txt);
|
||||||
|
|
||||||
|
OPERATION_LIST[entry.operation] = entry.progress;
|
||||||
|
notifications_update_progress_bar();
|
||||||
|
}
|
||||||
|
else if (entry.msg === STATUS_OPERATION_END) {
|
||||||
|
let txt = `<span class="status-timestamp">${dt.getHours()}:${dt.getMinutes()}</span>${entry.status}`;
|
||||||
|
$('#status-message').html(txt);
|
||||||
|
|
||||||
|
delete OPERATION_LIST[entry.operation];
|
||||||
|
notifications_update_progress_bar();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Initialization
|
/// Initialization
|
||||||
///
|
///
|
||||||
@ -214,4 +278,7 @@ $(document).ready(function ()
|
|||||||
filters_form.find('select[name=results_per_page]').on('change', videos_ResetPageAndReloadWithTimer);
|
filters_form.find('select[name=results_per_page]').on('change', videos_ResetPageAndReloadWithTimer);
|
||||||
|
|
||||||
videos_Reload();
|
videos_Reload();
|
||||||
|
|
||||||
|
// Notification manager
|
||||||
|
setInterval(get_and_process_notifications, NOTIFICATION_INTERVAL);
|
||||||
});
|
});
|
||||||
|
@ -65,11 +65,15 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer id="main_footer" class="footer bg-light">
|
<footer id="main_footer" class="footer bg-light row">
|
||||||
<span class="ml-auto text-muted">Last synchronized: just now</span>
|
|
||||||
<button id="btn_sync_now" class="btn btn-sm btn-light" title="Synchronize now!">
|
<button id="btn_sync_now" class="btn btn-sm btn-light" title="Synchronize now!">
|
||||||
<span class="typcn typcn-arrow-sync" aria-hidden="true"></span>
|
<span class="typcn typcn-arrow-sync" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<span id="status-message" class="text-muted"></span>
|
||||||
|
<div id="status-progress" class="progress my-2 ml-auto invisible" style="width: 15rem">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{% static 'YtManagerApp/import/jquery/jquery-3.3.1.min.js' %}"></script>
|
<script src="{% static 'YtManagerApp/import/jquery/jquery-3.3.1.min.js' %}"></script>
|
||||||
|
@ -18,14 +18,15 @@ from django.conf.urls import include
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from YtManagerApp.views.video import VideoDetailView, video_detail_view
|
|
||||||
from .views import first_time
|
from .views import first_time
|
||||||
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
||||||
MarkVideoUnwatchedView
|
MarkVideoUnwatchedView
|
||||||
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, ImportSubscriptionsModal
|
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
|
||||||
|
from .views.notifications import ajax_get_notifications
|
||||||
from .views.settings import SettingsView, AdminSettingsView
|
from .views.settings import SettingsView, AdminSettingsView
|
||||||
|
from .views.video import VideoDetailView, video_detail_view
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Authentication URLs
|
# Authentication URLs
|
||||||
@ -44,6 +45,8 @@ urlpatterns = [
|
|||||||
path('ajax/get_tree/', ajax_get_tree, name='ajax_get_tree'),
|
path('ajax/get_tree/', ajax_get_tree, name='ajax_get_tree'),
|
||||||
path('ajax/get_videos/', ajax_get_videos, name='ajax_get_videos'),
|
path('ajax/get_videos/', ajax_get_videos, name='ajax_get_videos'),
|
||||||
|
|
||||||
|
path('ajax/get_notifications/<int:last_id>', ajax_get_notifications, name='ajax_get_notifications'),
|
||||||
|
|
||||||
# Modals
|
# Modals
|
||||||
path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'),
|
path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'),
|
||||||
path('modal/create_folder/<int:parent_id>/', CreateFolderModal.as_view(), name='modal_create_folder'),
|
path('modal/create_folder/<int:parent_id>/', CreateFolderModal.as_view(), name='modal_create_folder'),
|
||||||
|
57
app/YtManagerApp/utils/algorithms.py
Normal file
57
app/YtManagerApp/utils/algorithms.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Bisection algorithms.
|
||||||
|
|
||||||
|
These algorithms are taken from Python's standard library, and modified so they take a 'key' parameter (similar to how
|
||||||
|
`sorted` works).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def bisect_right(a, x, lo=0, hi=None, key=None):
|
||||||
|
"""Return the index where to insert item x in list a, assuming a is sorted.
|
||||||
|
|
||||||
|
The return value i is such that all e in a[:i] have e <= x, and all e in
|
||||||
|
a[i:] have e > x. So if x already appears in the list, a.insert(x) will
|
||||||
|
insert just after the rightmost x already there.
|
||||||
|
|
||||||
|
Optional args lo (default 0) and hi (default len(a)) bound the
|
||||||
|
slice of a to be searched.
|
||||||
|
"""
|
||||||
|
if key is None:
|
||||||
|
key = lambda x: x
|
||||||
|
|
||||||
|
if lo < 0:
|
||||||
|
raise ValueError('lo must be non-negative')
|
||||||
|
if hi is None:
|
||||||
|
hi = len(a)
|
||||||
|
while lo < hi:
|
||||||
|
mid = (lo+hi)//2
|
||||||
|
if key(x) < key(a[mid]): hi = mid
|
||||||
|
else: lo = mid+1
|
||||||
|
return lo
|
||||||
|
|
||||||
|
|
||||||
|
def bisect_left(a, x, lo=0, hi=None, key=None):
|
||||||
|
"""Return the index where to insert item x in list a, assuming a is sorted.
|
||||||
|
|
||||||
|
The return value i is such that all e in a[:i] have e < x, and all e in
|
||||||
|
a[i:] have e >= x. So if x already appears in the list, a.insert(x) will
|
||||||
|
insert just before the leftmost x already there.
|
||||||
|
|
||||||
|
Optional args lo (default 0) and hi (default len(a)) bound the
|
||||||
|
slice of a to be searched.
|
||||||
|
"""
|
||||||
|
if key is None:
|
||||||
|
key = lambda x: x
|
||||||
|
|
||||||
|
if lo < 0:
|
||||||
|
raise ValueError('lo must be non-negative')
|
||||||
|
if hi is None:
|
||||||
|
hi = len(a)
|
||||||
|
while lo < hi:
|
||||||
|
mid = (lo+hi)//2
|
||||||
|
if key(a[mid]) < key(x): lo = mid+1
|
||||||
|
else: hi = mid
|
||||||
|
return lo
|
||||||
|
|
||||||
|
|
||||||
|
# Create aliases
|
||||||
|
bisect = bisect_right
|
@ -15,6 +15,7 @@ from YtManagerApp.management.appconfig import appconfig
|
|||||||
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
|
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
|
||||||
from YtManagerApp.utils import youtube, subscription_file_parser
|
from YtManagerApp.utils import youtube, subscription_file_parser
|
||||||
from YtManagerApp.views.controls.modal import ModalMixin
|
from YtManagerApp.views.controls.modal import ModalMixin
|
||||||
|
from YtManagerApp.management.notification_manager import get_current_notification_id
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -121,6 +122,7 @@ def index(request: HttpRequest):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
context.update({
|
context.update({
|
||||||
'filter_form': VideoFilterForm(),
|
'filter_form': VideoFilterForm(),
|
||||||
|
'current_notification_id': get_current_notification_id(),
|
||||||
})
|
})
|
||||||
return render(request, 'YtManagerApp/index.html', context)
|
return render(request, 'YtManagerApp/index.html', context)
|
||||||
else:
|
else:
|
||||||
|
12
app/YtManagerApp/views/notifications.py
Normal file
12
app/YtManagerApp/views/notifications.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpRequest, JsonResponse
|
||||||
|
|
||||||
|
from YtManagerApp.management.notification_manager import get_notifications
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def ajax_get_notifications(request: HttpRequest, last_id: int):
|
||||||
|
user = request.user
|
||||||
|
notifications = get_notifications(user, last_id)
|
||||||
|
notifications = list(notifications)
|
||||||
|
return JsonResponse(notifications, safe=False)
|
Loading…
Reference in New Issue
Block a user