You've already forked amazing-ytdlp-archive
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
da333ab4f6 | ||
![]() |
f2b01033ea | ||
![]() |
49f0ea7481 | ||
![]() |
f1287a4212 | ||
![]() |
30ea647ca9 | ||
![]() |
a7c640a8cf | ||
![]() |
f6da232164 | ||
![]() |
1d5934275c | ||
![]() |
72af6b6126 | ||
![]() |
8bf8e08af3 |
@@ -1,4 +1,4 @@
|
|||||||
name: Generate release
|
name: Generate docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -22,13 +22,4 @@ jobs:
|
|||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: git.ventilaar.nl/ventilaar/ayta:latest
|
tags: git.ventilaar.nl/ventilaar/ayta:latest
|
||||||
|
|
||||||
- name: Update worker server
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: 192.168.66.109
|
|
||||||
username: root
|
|
||||||
key: ${{ secrets.SERVER_KEY }}
|
|
||||||
port: 22
|
|
||||||
script: /root/update_worker.sh
|
|
18
.gitea/workflows/workers-tasks.yaml
Normal file
18
.gitea/workflows/workers-tasks.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: Update worker server
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Update worker server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: 192.168.66.109
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.SERVER_KEY }}
|
||||||
|
port: 22
|
||||||
|
script: /root/update_worker.sh
|
@@ -25,7 +25,7 @@ def create_app(test_config=None):
|
|||||||
|
|
||||||
config['CELERY']['beat_schedule'] = {}
|
config['CELERY']['beat_schedule'] = {}
|
||||||
config['CELERY']['beat_schedule']['Renew WebSub endpoints'] = {'task': 'ayta.tasks.websub_renew_expiring', 'schedule': 4000}
|
config['CELERY']['beat_schedule']['Renew WebSub endpoints'] = {'task': 'ayta.tasks.websub_renew_expiring', 'schedule': 4000}
|
||||||
#config['CELERY']['beat_schedule']['Process WebSub data'] = {'task': 'ayta.tasks.websub_process_data', 'schedule': 6}
|
config['CELERY']['beat_schedule']['Process WebSub data'] = {'task': 'ayta.tasks.websub_process_data', 'schedule': 100}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_mapping(config)
|
app.config.from_mapping(config)
|
||||||
@@ -41,7 +41,8 @@ def create_app(test_config=None):
|
|||||||
app.jinja_env.filters['pretty_time'] = filters.pretty_time
|
app.jinja_env.filters['pretty_time'] = filters.pretty_time
|
||||||
app.jinja_env.filters['current_time'] = filters.current_time
|
app.jinja_env.filters['current_time'] = filters.current_time
|
||||||
app.jinja_env.filters['epoch_time'] = filters.epoch_time
|
app.jinja_env.filters['epoch_time'] = filters.epoch_time
|
||||||
|
app.jinja_env.filters['epoch_date'] = filters.epoch_date
|
||||||
|
|
||||||
from .blueprints import watch
|
from .blueprints import watch
|
||||||
from .blueprints import index
|
from .blueprints import index
|
||||||
from .blueprints import admin
|
from .blueprints import admin
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||||
from ..nosql import get_nosql
|
from ..nosql import get_nosql
|
||||||
from ..dlp import checkChannelId, getChannelInfo
|
from ..dlp import checkChannelId, getChannelInfo
|
||||||
from ..decorators import login_required
|
from ..decorators import login_required
|
||||||
from ..tasks import subscribe_websub_callback, unsubscribe_websub_callback
|
from ..tasks import test_sleep, websub_subscribe_callback, websub_unsubscribe_callback, video_download
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
@@ -71,15 +71,15 @@ def channel(channelId):
|
|||||||
value = request.form.get('value', None)
|
value = request.form.get('value', None)
|
||||||
|
|
||||||
if task == 'subscribe-websub':
|
if task == 'subscribe-websub':
|
||||||
task = subscribe_websub_callback.delay(channelId)
|
task = websub_subscribe_callback.delay(channelId)
|
||||||
flash(f"Started task {task.id}")
|
flash(f"Started task {task.id}")
|
||||||
return redirect(url_for('admin.channel', channelId=channelId))
|
return redirect(url_for('admin.channel', channelId=channelId))
|
||||||
|
|
||||||
if task == 'update-value':
|
if task == 'update-value':
|
||||||
if key == 'active':
|
if key in ['active', 'websub']:
|
||||||
value = True if value else False
|
value = True if value else False
|
||||||
|
|
||||||
if key == 'added_date':
|
if key in ['added_date']:
|
||||||
value = datetime.strptime(value, '%Y-%m-%d')
|
value = datetime.strptime(value, '%Y-%m-%d')
|
||||||
|
|
||||||
get_nosql().update_channel_key(channelId, key, value)
|
get_nosql().update_channel_key(channelId, key, value)
|
||||||
@@ -109,29 +109,41 @@ def run(runId):
|
|||||||
@bp.route('/websub', methods=['GET', 'POST'])
|
@bp.route('/websub', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def websub():
|
def websub():
|
||||||
|
render = {}
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
task = request.form.get('task', None)
|
task = request.form.get('task', None)
|
||||||
value = request.form.get('value', None)
|
value = request.form.get('value', None)
|
||||||
|
|
||||||
if task == 'unsubscribe':
|
if task == 'unsubscribe':
|
||||||
channelId = get_nosql().websub_getCallback(value).get('channel')
|
task = websub_unsubscribe_callback.delay(value)
|
||||||
|
|
||||||
task = unsubscribe_websub_callback.delay(value, channelId)
|
|
||||||
|
|
||||||
flash(f"Started task {task.id}")
|
flash(f"Started task {task.id}")
|
||||||
return redirect(url_for('admin.websub'))
|
return redirect(url_for('admin.websub'))
|
||||||
|
|
||||||
elif task == 'clean-retired':
|
elif task == 'clean-retired':
|
||||||
get_nosql().websub_cleanRetired()
|
get_nosql().websub_cleanRetired()
|
||||||
return redirect(url_for('admin.websub'))
|
return redirect(url_for('admin.websub'))
|
||||||
|
elif task == 'unsubscribe-callbacks':
|
||||||
|
for callbackId in get_nosql().websub_getCallbacks():
|
||||||
|
websub_unsubscribe_callback.delay(callbackId)
|
||||||
|
flash(f"Started unsubscribe tasks for all callbacks")
|
||||||
|
return redirect(url_for('admin.websub'))
|
||||||
|
elif task == 'subscribe-channels':
|
||||||
|
for channelId in get_nosql().list_all_channels(websub=True):
|
||||||
|
websub_subscribe_callback.delay(channelId)
|
||||||
|
flash(f'Started subscribe tasks for activated channels')
|
||||||
|
return redirect(url_for('admin.websub'))
|
||||||
|
|
||||||
|
|
||||||
callbackIds = get_nosql().websub_getCallbacks()
|
callbackIds = get_nosql().websub_getCallbacks()
|
||||||
callbacks = {}
|
callbacks = {}
|
||||||
|
|
||||||
|
render['stats'] = get_nosql().websub_statistics()
|
||||||
|
|
||||||
for callbackId in callbackIds:
|
for callbackId in callbackIds:
|
||||||
callbacks[callbackId] = get_nosql().websub_getCallback(callbackId)
|
callbacks[callbackId] = get_nosql().websub_getCallback(callbackId)
|
||||||
|
|
||||||
return render_template('admin/websub.html', callbacks=callbacks)
|
return render_template('admin/websub.html', callbacks=callbacks, render=render)
|
||||||
|
|
||||||
@bp.route('/reports', methods=['GET', 'POST'])
|
@bp.route('/reports', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -149,9 +161,9 @@ def reports():
|
|||||||
|
|
||||||
return render_template('admin/reports.html', reports=reports)
|
return render_template('admin/reports.html', reports=reports)
|
||||||
|
|
||||||
@bp.route('/posters', methods=['GET', 'POST'])
|
@bp.route('/queue', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def posters():
|
def queue():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
task = request.form.get('task', None)
|
task = request.form.get('task', None)
|
||||||
value = request.form.get('value', None)
|
value = request.form.get('value', None)
|
||||||
@@ -162,34 +174,45 @@ def posters():
|
|||||||
flash('Description must be at least 8 characters long')
|
flash('Description must be at least 8 characters long')
|
||||||
|
|
||||||
if value and len(value) >= 12:
|
if value and len(value) >= 12:
|
||||||
get_nosql().poster_newEndpoint(value, description)
|
get_nosql().queue_newEndpoint(value, description)
|
||||||
flash(f'Created endpoint ID: {value}')
|
flash(f'Created endpoint ID: {value}')
|
||||||
else:
|
else:
|
||||||
value = token_urlsafe(16)
|
value = token_urlsafe(16)
|
||||||
get_nosql().poster_newEndpoint(value, description)
|
get_nosql().queue_newEndpoint(value, description)
|
||||||
flash(f'Created endpoint ID: {value}')
|
flash(f'Created endpoint ID: {value}')
|
||||||
|
|
||||||
elif task == 'retire':
|
elif task == 'retire':
|
||||||
get_nosql().poster_retireEndpoint(value)
|
get_nosql().queue_retireEndpoint(value)
|
||||||
flash(f'Endpoint retired: {value}')
|
flash(f'Endpoint retired: {value}')
|
||||||
|
|
||||||
elif task == 'clean-retired':
|
elif task == 'clean-retired':
|
||||||
get_nosql().poster_cleanRetired()
|
get_nosql().queue_cleanRetired()
|
||||||
flash(f'Cleaned retired endpoints')
|
flash(f'Cleaned retired endpoints')
|
||||||
|
|
||||||
elif task == 'manual-queue':
|
elif task == 'manual-queue':
|
||||||
get_nosql().poster_insertQueue('manual', value)
|
direct = request.form.get('direct', None)
|
||||||
flash(f'Added to queue: {value}')
|
|
||||||
|
if direct:
|
||||||
|
task = video_download.delay(value)
|
||||||
|
flash(f"Started task {task.id}")
|
||||||
|
else:
|
||||||
|
get_nosql().queue_insertQueue(value, 'webui')
|
||||||
|
flash(f'Added to queue: {value}')
|
||||||
|
|
||||||
elif task == 'delete-queue':
|
elif task == 'delete-queue':
|
||||||
get_nosql().poster_deleteQueue(value)
|
get_nosql().queue_deleteQueue(value)
|
||||||
flash(f'Deleted from queue: {value}')
|
flash(f'Deleted from queue: {value}')
|
||||||
|
|
||||||
|
elif task == 'empty-queue':
|
||||||
|
get_nosql().queue_emptyQueue()
|
||||||
|
flash(f'Queue has been emptied')
|
||||||
|
|
||||||
return redirect(url_for('admin.posters'))
|
return redirect(url_for('admin.queue'))
|
||||||
|
|
||||||
endpoints = get_nosql().poster_getEndpoints()
|
endpoints = get_nosql().queue_getEndpoints()
|
||||||
queue = get_nosql().poster_getQueue()
|
queue = get_nosql().queue_getQueue()
|
||||||
|
|
||||||
return render_template('admin/posters.html', endpoints=endpoints, queue=queue)
|
return render_template('admin/queue.html', endpoints=endpoints, queue=queue)
|
||||||
|
|
||||||
@bp.route('/users', methods=['GET', 'POST'])
|
@bp.route('/users', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -217,4 +240,17 @@ def users():
|
|||||||
|
|
||||||
users = get_nosql().list_all_users()
|
users = get_nosql().list_all_users()
|
||||||
|
|
||||||
return render_template('admin/users.html', users=users)
|
return render_template('admin/users.html', users=users)
|
||||||
|
|
||||||
|
@bp.route('/workers', methods=['GET', 'POST'])
|
||||||
|
#@login_required
|
||||||
|
def workers():
|
||||||
|
if request.method == 'POST':
|
||||||
|
task = request.form.get('task', None)
|
||||||
|
if task == 'test-sleep':
|
||||||
|
test_sleep.delay()
|
||||||
|
|
||||||
|
celery = current_app.extensions.get('celery')
|
||||||
|
|
||||||
|
tasks = celery.control.inspect().active()
|
||||||
|
return render_template('admin/workers.html', tasks=tasks)
|
@@ -39,10 +39,10 @@ def websub(cap):
|
|||||||
|
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
@bp.route('/poster/<cap>', methods=['POST'])
|
@bp.route('/queue/<cap>', methods=['POST'])
|
||||||
def poster(cap):
|
def queue(cap):
|
||||||
# if endpoint does not exist
|
# if endpoint does not exist
|
||||||
if not get_nosql().poster_isActive(cap):
|
if not get_nosql().queue_isActive(cap):
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
videoId = request.form.get('v')
|
videoId = request.form.get('v')
|
||||||
@@ -60,7 +60,7 @@ def poster(cap):
|
|||||||
return abort(409)
|
return abort(409)
|
||||||
|
|
||||||
# try to insert
|
# try to insert
|
||||||
if get_nosql().poster_insertQueue(cap, videoId):
|
if get_nosql().queue_insertQueue(videoId, cap):
|
||||||
return '', 202
|
return '', 202
|
||||||
else:
|
else:
|
||||||
return abort(409)
|
return abort(409)
|
@@ -37,7 +37,7 @@ def base():
|
|||||||
render['info'] = get_nosql().get_video_info(vGet)
|
render['info'] = get_nosql().get_video_info(vGet)
|
||||||
render['params'] = request.args.get('v')
|
render['params'] = request.args.get('v')
|
||||||
|
|
||||||
if render['info']['_status'] != 'available':
|
if render['info'].get('_status') != 'available':
|
||||||
flash(render['info'].get('_status_description', 'Video unavailable because of technical errors. Come back later.'))
|
flash(render['info'].get('_status_description', 'Video unavailable because of technical errors. Come back later.'))
|
||||||
return redirect(url_for('index.base'))
|
return redirect(url_for('index.base'))
|
||||||
|
|
||||||
|
@@ -16,9 +16,15 @@ def pretty_time(time):
|
|||||||
except:
|
except:
|
||||||
return time # return given time
|
return time # return given time
|
||||||
|
|
||||||
def epoch_time(time):
|
def epoch_date(epoch):
|
||||||
try:
|
try:
|
||||||
return datetime.fromtimestamp(time).strftime('%d %b %Y')
|
return datetime.fromtimestamp(epoch).strftime('%d %b %Y')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def epoch_time(epoch):
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(epoch).strftime('%d %b %Y %H:%M:%S')
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
118
ayta/nosql.py
118
ayta/nosql.py
@@ -36,13 +36,13 @@ class Mango:
|
|||||||
self.db = self.client['ayta']
|
self.db = self.client['ayta']
|
||||||
self.channels = self.db['channels']
|
self.channels = self.db['channels']
|
||||||
self.info_json = self.db['info_json']
|
self.info_json = self.db['info_json']
|
||||||
self.posters_queue = self.db['posters_queue']
|
self.download_queue = self.db['download_queue']
|
||||||
self.run_log = self.db['run_log']
|
self.run_log = self.db['run_log']
|
||||||
self.channel_log = self.db['channel_log']
|
self.channel_log = self.db['channel_log']
|
||||||
self.websub_callbacks = self.db['websub_callbacks']
|
self.websub_callbacks = self.db['websub_callbacks']
|
||||||
self.websub_data = self.db['websub_data']
|
self.websub_data = self.db['websub_data']
|
||||||
self.reports = self.db['reports']
|
self.reports = self.db['reports']
|
||||||
self.posters_endpoints = self.db['posters_endpoints']
|
self.queue_endpoints = self.db['queue_endpoints']
|
||||||
self.users = self.db['users']
|
self.users = self.db['users']
|
||||||
|
|
||||||
self.ensure_indexes()
|
self.ensure_indexes()
|
||||||
@@ -98,7 +98,7 @@ class Mango:
|
|||||||
|
|
||||||
stats['videos'] = self.info_json.count_documents({})
|
stats['videos'] = self.info_json.count_documents({})
|
||||||
stats['channels'] = self.channels.count_documents({})
|
stats['channels'] = self.channels.count_documents({})
|
||||||
stats['queue'] = self.posters_queue.count_documents({})
|
stats['queue'] = self.download_queue.count_documents({})
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
@@ -142,12 +142,14 @@ class Mango:
|
|||||||
# channel operations #
|
# channel operations #
|
||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
def list_all_channels(self, active=False):
|
def list_all_channels(self, active=False, websub=False):
|
||||||
""" Returns a SET of YouTube channel ID's; Depending on given positional BOOL only active channels or everything"""
|
""" Returns a SET of YouTube channel ID's; Depending on given positional BOOL only active channels or everything"""
|
||||||
search_terms = {}
|
search_terms = {}
|
||||||
|
|
||||||
if active:
|
if active:
|
||||||
search_terms['active'] = True
|
search_terms['active'] = True
|
||||||
|
elif websub:
|
||||||
|
search_terms['websub'] = True
|
||||||
|
|
||||||
channels = []
|
channels = []
|
||||||
for channel in self.channels.find(search_terms, {'id': 1}):
|
for channel in self.channels.find(search_terms, {'id': 1}):
|
||||||
@@ -169,9 +171,6 @@ class Mango:
|
|||||||
def get_channel_info(self, channelId):
|
def get_channel_info(self, channelId):
|
||||||
return self.channels.find_one({'id': channelId})
|
return self.channels.find_one({'id': channelId})
|
||||||
|
|
||||||
def update_channel_state(self, channelId, state):
|
|
||||||
self.channels.update_one({'id': channelId}, {"$set": {"active": bool(state)}})
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_channel_key(self, channelId, key, value):
|
def update_channel_key(self, channelId, key, value):
|
||||||
self.channels.update_one({'id': channelId}, {"$set": {key: value}})
|
self.channels.update_one({'id': channelId}, {"$set": {key: value}})
|
||||||
@@ -196,11 +195,14 @@ class Mango:
|
|||||||
def get_orphaned_videos(self):
|
def get_orphaned_videos(self):
|
||||||
""" Returns a SET of YouTube video ID's which have info_jsons in the collection but no permanent channel is defined. SLOW OPERATION """
|
""" Returns a SET of YouTube video ID's which have info_jsons in the collection but no permanent channel is defined. SLOW OPERATION """
|
||||||
# Ok lemme explain. Perform inner join from channel collection on channel_id key. match only the fields which are empty. return video id
|
# Ok lemme explain. Perform inner join from channel collection on channel_id key. match only the fields which are empty. return video id
|
||||||
pipeline = [{'$lookup': {'from': 'channels', 'localField': 'channel_id', 'foreignField': 'id', 'as': 'channel'}}, {'$match': {'channel': {'$size': 0}}},{'$project': {'id': 1}}]
|
pipeline = [{'$match': {'_status': 'available'}},
|
||||||
|
{'$lookup': {'from': 'channels', 'localField': 'channel_id', 'foreignField': 'id', 'as': 'channel'}},
|
||||||
|
{'$match': {'channel': {'$size': 0}}},{'$project': {'id': 1}},
|
||||||
|
{'$project': {'id': 1}}]
|
||||||
|
|
||||||
results = self.info_json.aggregate(pipeline)
|
results = self.info_json.aggregate(pipeline)
|
||||||
ids = [result['id'] for result in results]
|
ids = [result['id'] for result in results]
|
||||||
|
|
||||||
return tuple(ids)
|
return tuple(ids)
|
||||||
|
|
||||||
def get_recent_videos(self, count=99):
|
def get_recent_videos(self, count=99):
|
||||||
@@ -301,18 +303,15 @@ class Mango:
|
|||||||
|
|
||||||
def websub_existsCallback(self, callbackId, channel=False):
|
def websub_existsCallback(self, callbackId, channel=False):
|
||||||
if channel:
|
if channel:
|
||||||
query = {'channel': callbackId}
|
query = {'channel': callbackId, 'status': {'$in': ['requesting', 'active', 'retiring']}}
|
||||||
else:
|
else:
|
||||||
query = {'id': callbackId}
|
query = {'id': callbackId, 'status': {'$in': ['requesting', 'active', 'retiring']}}
|
||||||
|
|
||||||
status = self.websub_callbacks.find_one(query, {'id': 1, 'status': 1})
|
status = self.websub_callbacks.find_one(query, {'id': 1, 'status': 1})
|
||||||
|
|
||||||
if not status:
|
if status:
|
||||||
return False
|
|
||||||
|
|
||||||
if status.get('status') in ['requesting', 'active', 'retiring']:
|
|
||||||
return status.get('id')
|
return status.get('id')
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def websub_retiringCallback(self, callbackId):
|
def websub_retiringCallback(self, callbackId):
|
||||||
@@ -333,7 +332,7 @@ class Mango:
|
|||||||
def websub_getCallback(self, callbackId):
|
def websub_getCallback(self, callbackId):
|
||||||
return self.websub_callbacks.find_one({'id': callbackId})
|
return self.websub_callbacks.find_one({'id': callbackId})
|
||||||
|
|
||||||
def websub_getCallbacks(self, channelId=''):
|
def websub_getCallbacks(self, channelId=None):
|
||||||
callbacks = []
|
callbacks = []
|
||||||
|
|
||||||
if channelId:
|
if channelId:
|
||||||
@@ -341,7 +340,6 @@ class Mango:
|
|||||||
else:
|
else:
|
||||||
filter = {}
|
filter = {}
|
||||||
|
|
||||||
|
|
||||||
for callback in self.websub_callbacks.find(filter, {'id': 1}):
|
for callback in self.websub_callbacks.find(filter, {'id': 1}):
|
||||||
callbacks.append(callback['id'])
|
callbacks.append(callback['id'])
|
||||||
|
|
||||||
@@ -363,64 +361,74 @@ class Mango:
|
|||||||
def websub_deletePostProcessing(self, _id):
|
def websub_deletePostProcessing(self, _id):
|
||||||
self.websub_data.delete_one({'_id': _id})
|
self.websub_data.delete_one({'_id': _id})
|
||||||
|
|
||||||
def websub_cleanRetired(self, days=3):
|
def websub_cleanRetired(self, days=1):
|
||||||
days = self.datetime.utcnow() - self.timedelta(days=days)
|
days = self.datetime.utcnow() - self.timedelta(days=days)
|
||||||
|
|
||||||
self.websub_callbacks.delete_many({'status': 'retired', 'retired_time': {'$lt': days}})
|
self.websub_callbacks.delete_many({'status': 'retired', 'retired_time': {'$lt': days}})
|
||||||
self.websub_callbacks.delete_many({'status': 'requesting', 'requesting_time': {'$lt': days}})
|
self.websub_callbacks.delete_many({'status': 'requesting', 'requesting_time': {'$lt': days}})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def websub_statistics(self):
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
stats['unprocessed_data'] = self.websub_data.count_documents({'state': 'unprocessed'})
|
||||||
|
stats['active_callbacks'] = self.websub_callbacks.count_documents({'status': 'active'})
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
# POSTER FUNCTIONS #
|
# QUEUE FUNCTIONS #
|
||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
def poster_newEndpoint(self, endpointId, description=''):
|
def queue_newEndpoint(self, endpointId, description=''):
|
||||||
self.posters_endpoints.insert_one({'id': endpointId, 'description': description, 'status': 'active', 'created_time': current_time(object=True)})
|
self.queue_endpoints.insert_one({'id': endpointId, 'description': description, 'status': 'active', 'created_time': current_time(object=True)})
|
||||||
return endpointId
|
return endpointId
|
||||||
|
|
||||||
|
def queue_retireEndpoint(self, endpointId):
|
||||||
|
return self.queue_endpoints.update_one({'id': endpointId}, {'$set': {'status': 'retired', 'retired_time': current_time(object=True)}})
|
||||||
|
|
||||||
|
def queue_isActive(self, endpointId):
|
||||||
|
status = self.queue_endpoints.find_one({'id': endpointId}, {'status': 1})
|
||||||
|
|
||||||
|
if not status:
|
||||||
|
return False
|
||||||
|
elif status.get('status') == 'active':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def queue_getEndpoints(self):
|
||||||
|
return self.queue_endpoints.find({})
|
||||||
|
|
||||||
def poster_insertQueue(self, endpointId, videoId):
|
def queue_cleanRetired(self, days=3):
|
||||||
|
days = self.datetime.utcnow() - self.timedelta(days=days)
|
||||||
|
|
||||||
|
self.queue_endpoints.delete_many({'status': 'retired', 'retired_time': {'$lt': days}})
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
##########################################
|
||||||
|
|
||||||
|
def queue_insertQueue(self, videoId, endpointId=None):
|
||||||
# if no document exists
|
# if no document exists
|
||||||
if not self.posters_queue.count_documents({'id': videoId}) >= 1:
|
if not self.download_queue.count_documents({'id': videoId}) >= 1:
|
||||||
self.posters_queue.insert_one({'id': videoId, 'endpoint': endpointId, 'created_time': current_time(object=True), 'status': 'queued'}).inserted_id
|
self.download_queue.insert_one({'id': videoId, 'endpoint': endpointId, 'created_time': current_time(object=True), 'status': 'queued'}).inserted_id
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# key already in queue
|
# key already in queue
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def poster_deleteQueue(self, videoId):
|
def queue_deleteQueue(self, videoId):
|
||||||
if self.posters_queue.delete_one({'id': videoId}):
|
if self.download_queue.delete_one({'id': videoId}):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def poster_retireEndpoint(self, endpointId):
|
def queue_getQueue(self):
|
||||||
return self.posters_endpoints.update_one({'id': endpointId}, {'$set': {'status': 'retired', 'retired_time': current_time(object=True)}})
|
return self.download_queue.find({})
|
||||||
|
|
||||||
def poster_isActive(self, endpointId):
|
def queue_emptyQueue(self):
|
||||||
status = self.posters_endpoints.find_one({'id': endpointId}, {'status': 1})
|
return self.download_queue.delete_many({})
|
||||||
|
|
||||||
if not status:
|
|
||||||
return False
|
|
||||||
|
|
||||||
status = status.get('status')
|
|
||||||
|
|
||||||
if status == 'active':
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def poster_getEndpoints(self):
|
|
||||||
return self.posters_endpoints.find({})
|
|
||||||
|
|
||||||
def poster_getQueue(self):
|
|
||||||
return self.posters_queue.find({})
|
|
||||||
|
|
||||||
def poster_cleanRetired(self, days=3):
|
|
||||||
days = self.datetime.utcnow() - self.timedelta(days=days)
|
|
||||||
|
|
||||||
self.posters_endpoints.delete_many({'status': 'retired', 'retired_time': {'$lt': days}})
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
# HELPER FUNCTIONS #
|
# HELPER FUNCTIONS #
|
||||||
|
59
ayta/oidc.py
59
ayta/oidc.py
@@ -1,4 +1,10 @@
|
|||||||
class OIDC():
|
class OIDC():
|
||||||
|
"""
|
||||||
|
This function class is nothing more than a nonce and state store for security in the authentication mechanism.
|
||||||
|
Additionally this class provides the function to generate redirect url's and check bearer tokens on their validity as well as caching jwt signing keys.
|
||||||
|
Fairly barebones and should be 100% secure. (famous last words)
|
||||||
|
This is made for form posted JWT's. While not the most secure it is the most easy way to implement. Moving on to a code based solution might be preferred in the future.
|
||||||
|
"""
|
||||||
def __init__(self, app=None):
|
def __init__(self, app=None):
|
||||||
self.states = {}
|
self.states = {}
|
||||||
self.nonces = {}
|
self.nonces = {}
|
||||||
@@ -15,27 +21,34 @@ class OIDC():
|
|||||||
self.client_id = config['OIDC_ID']
|
self.client_id = config['OIDC_ID']
|
||||||
self.provider = config['OIDC_PROVIDER']
|
self.provider = config['OIDC_PROVIDER']
|
||||||
self.domain = config['DOMAIN']
|
self.domain = config['DOMAIN']
|
||||||
|
self.window = 120 # the time window to allow states and nonces in seconds
|
||||||
|
|
||||||
|
# Authentication provider url must be HTTPS and end on a TLD
|
||||||
if self.provider[:8] != 'https://' or self.provider[-1] == '/':
|
if self.provider[:8] != 'https://' or self.provider[-1] == '/':
|
||||||
print('Incorrect OIDC provider URI', flush=True)
|
print('Incorrect OIDC provider URI', flush=True)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
|
# Get the provider configuration endpoints
|
||||||
configuration = requests.get(f'{self.provider}/.well-known/openid-configuration').json()
|
configuration = requests.get(f'{self.provider}/.well-known/openid-configuration').json()
|
||||||
|
|
||||||
jwks_uri = configuration.get('jwks_uri')
|
jwks_uri = configuration.get('jwks_uri')
|
||||||
self.authorize_uri = configuration.get('authorization_endpoint')
|
self.authorize_uri = configuration.get('authorization_endpoint')
|
||||||
|
|
||||||
|
# Start the JWKS management client, it will load the keys and maintain them
|
||||||
self.jwks_manager = jwt.PyJWKClient(jwks_uri)
|
self.jwks_manager = jwt.PyJWKClient(jwks_uri)
|
||||||
|
|
||||||
#################################
|
#######################################################
|
||||||
|
|
||||||
def state_maintenance(self):
|
def state_maintenance(self):
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
pivot = datetime.now().timestamp() - 120
|
# Current time minus the acceptable window
|
||||||
|
pivot = datetime.now().timestamp() - self.window
|
||||||
|
|
||||||
|
# List with expired states
|
||||||
expired_states = [state for state, timestamp in self.states.items() if timestamp <= pivot]
|
expired_states = [state for state, timestamp in self.states.items() if timestamp <= pivot]
|
||||||
|
|
||||||
|
# Remove expired states from store
|
||||||
for state in expired_states:
|
for state in expired_states:
|
||||||
del self.states[state]
|
del self.states[state]
|
||||||
|
|
||||||
@@ -43,30 +56,40 @@ class OIDC():
|
|||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Clean state store first
|
||||||
self.state_maintenance()
|
self.state_maintenance()
|
||||||
|
|
||||||
|
# Generate token and paired timestamp
|
||||||
state = secrets.token_urlsafe(8)
|
state = secrets.token_urlsafe(8)
|
||||||
timestamp = datetime.now().timestamp()
|
timestamp = datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Add token to the state store
|
||||||
self.states[state] = timestamp
|
self.states[state] = timestamp
|
||||||
|
|
||||||
|
# Return the state
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def state_check(self, state):
|
def state_check(self, state):
|
||||||
|
# Clean state store first
|
||||||
self.state_maintenance()
|
self.state_maintenance()
|
||||||
|
|
||||||
|
# If given state is actively stored
|
||||||
if state in self.states:
|
if state in self.states:
|
||||||
|
# Delete state and return True
|
||||||
del self.states[state]
|
del self.states[state]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Given state is not stored
|
||||||
return False
|
return False
|
||||||
|
|
||||||
#################################
|
#######################################################
|
||||||
|
# Same code as above but a different store for nonces #
|
||||||
|
#######################################################
|
||||||
|
|
||||||
def nonce_maintenance(self):
|
def nonce_maintenance(self):
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
pivot = datetime.now().timestamp() - 120
|
pivot = datetime.now().timestamp() - self.window
|
||||||
|
|
||||||
expired_nonces = [nonce for nonce, timestamp in self.nonces.items() if timestamp <= pivot]
|
expired_nonces = [nonce for nonce, timestamp in self.nonces.items() if timestamp <= pivot]
|
||||||
|
|
||||||
@@ -95,7 +118,7 @@ class OIDC():
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
#################################
|
#######################################################
|
||||||
|
|
||||||
def generate_redirect(self):
|
def generate_redirect(self):
|
||||||
return str(f'{self.authorize_uri}'
|
return str(f'{self.authorize_uri}'
|
||||||
@@ -107,21 +130,33 @@ class OIDC():
|
|||||||
|
|
||||||
def check_bearer(self, token):
|
def check_bearer(self, token):
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
|
# Test given JWT
|
||||||
try:
|
try:
|
||||||
|
# Get the signed public key from the token
|
||||||
signing_key = self.jwks_manager.get_signing_key_from_jwt(token).key
|
signing_key = self.jwks_manager.get_signing_key_from_jwt(token).key
|
||||||
|
|
||||||
|
# Try to decode the token, this will also check the validity in these points:
|
||||||
|
# 1. Token is signed by expected keys
|
||||||
|
# 2. Token is issued by the expected provider
|
||||||
|
# 3. Expected parameters are really in the token
|
||||||
|
# 4. Token is really intended for us
|
||||||
|
# 5. Token is still valid (with 5 sec margin)
|
||||||
decoded = jwt.decode(token, signing_key,
|
decoded = jwt.decode(token, signing_key,
|
||||||
algorithms=jwt.algorithms.get_default_algorithms(),
|
algorithms=jwt.algorithms.get_default_algorithms(),
|
||||||
issuer=self.provider,
|
issuer=self.provider,
|
||||||
require=['aud', 'client_id', 'exp', 'iat', 'iss', 'rat', 'sub'],
|
require=['aud', 'client_id', 'exp', 'iat', 'iss', 'rat', 'sub'],
|
||||||
audience=self.client_id,
|
audience=self.client_id,
|
||||||
leeway=5)
|
leeway=5)
|
||||||
|
|
||||||
|
# Any exception (invalid JWT, invalid formatting etc...) must return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e, flush=True)
|
print(e, flush=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# double check if given token is really requested by us
|
# Double check if given token is really requested by us by matching the nonce in the signed key
|
||||||
if not self.nonce_check(decoded.get('nonce', None)):
|
if not self.nonce_check(decoded.get('nonce', None)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Return the unique user identifier
|
||||||
return decoded.get('sub', False)
|
return decoded.get('sub', False)
|
||||||
|
@@ -5,6 +5,26 @@ from flask import current_app
|
|||||||
# CELERY TASKS #
|
# CELERY TASKS #
|
||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def test_sleep(time=60):
|
||||||
|
from time import sleep
|
||||||
|
sleep(time)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def video_download(videoId):
|
||||||
|
"""
|
||||||
|
I do not want to deal with the quirks of native yt-dlp in python, hence the subprocess.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
process = subprocess.run(['/usr/local/bin/yt-dlp', '--config-location', '/var/www/archive.ventilaar.net/goodstuff/config_video.conf', '--', f'https://www.youtube.com/watch?v={videoId}'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@shared_task()
|
@shared_task()
|
||||||
def websub_subscribe_callback(channelId):
|
def websub_subscribe_callback(channelId):
|
||||||
import requests
|
import requests
|
||||||
@@ -33,7 +53,9 @@ def websub_subscribe_callback(channelId):
|
|||||||
response = requests.post(url, data=data)
|
response = requests.post(url, data=data)
|
||||||
if response.status_code == 202:
|
if response.status_code == 202:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# maybe handle errors?
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@shared_task()
|
@shared_task()
|
||||||
@@ -61,6 +83,8 @@ def websub_unsubscribe_callback(callbackId):
|
|||||||
if response.status_code == 202:
|
if response.status_code == 202:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# maybe handle errors?
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@shared_task()
|
@shared_task()
|
||||||
@@ -68,37 +92,57 @@ def websub_process_data():
|
|||||||
from .nosql import get_nosql
|
from .nosql import get_nosql
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = get_nosql().websub_getFirstPostData()
|
blob = get_nosql().websub_getFirstPostData()
|
||||||
if not data:
|
if not blob:
|
||||||
break
|
break
|
||||||
|
|
||||||
_id, data = data
|
_id, data = blob
|
||||||
|
|
||||||
parsed = do_parse_data(data)
|
parsed = do_parse_data(data)
|
||||||
if not parsed:
|
if parsed:
|
||||||
get_nosql().websub_deletePostProcessing(_id)
|
state, channelId, videoId = parsed
|
||||||
|
|
||||||
state, channelId, videoId = parsed
|
if state == 'added':
|
||||||
|
if not get_nosql().check_exists(videoId): # if video not exists
|
||||||
|
get_nosql().queue_insertQueue(videoId, 'WebSub')
|
||||||
|
# note for future me
|
||||||
|
# the websub notifications report ALL videos, including shorts and livestreams
|
||||||
|
# so if you are going to work on individual video downloading make sure you filter them!
|
||||||
|
|
||||||
|
elif state == 'removed':
|
||||||
|
# we currently do not do anything with removed videos
|
||||||
|
# but the idea is to trigger a full channel mirror in case a creator started to mass delete videos
|
||||||
|
pass
|
||||||
|
|
||||||
get_nosql().websub_deletePostProcessing(_id)
|
get_nosql().websub_deletePostProcessing(_id)
|
||||||
|
|
||||||
@shared_task()
|
@shared_task()
|
||||||
def websub_renew_expiring(hours=6):
|
def websub_renew_expiring(hours=6):
|
||||||
from .nosql import get_nosql
|
from .nosql import get_nosql
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
for callbackId in get_nosql().websub_getCallbacks():
|
for callbackId in get_nosql().websub_getCallbacks():
|
||||||
data = get_nosql().websub_getCallback(callbackId)
|
data = get_nosql().websub_getCallback(callbackId)
|
||||||
|
|
||||||
pivot = datetime.utcnow() - timedelta(hours=hours)
|
if data.get('status') not in ['active']: # callback not active
|
||||||
expires = data.get('activation_time') + timedelta(seconds=data.get('lease'))
|
|
||||||
|
|
||||||
if pivot <= expires: # if expiration happens after the calculation time pass the loop
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f'{callbackId} should be renewed')
|
pivot = datetime.utcnow() + timedelta(hours=hours) # hours past now
|
||||||
|
expires = data.get('activation_time') + timedelta(seconds=data.get('lease')) # callback expires at
|
||||||
|
|
||||||
|
if pivot <= expires: # expiration happens after n hours fron now
|
||||||
|
continue # skip callback
|
||||||
|
|
||||||
|
# expiration happens within n hours
|
||||||
websub_subscribe_callback.delay(data.get('channel'))
|
websub_subscribe_callback.delay(data.get('channel'))
|
||||||
|
|
||||||
|
# limit amount of subscribe requests to spread out the requests over time
|
||||||
|
# with an expiration pivot of 6h and a maximum validity of 5 days we can currently handle 3072 channels
|
||||||
|
count = count + 1
|
||||||
|
if count >= 256:
|
||||||
|
break
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
# TASK MODULES #
|
# TASK MODULES #
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
{% for item in channelInfo %}
|
{% for item in channelInfo %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<span class="supporting-text">{{ item }}</span>
|
<span class="supporting-text mb-2">{{ item }}</span>
|
||||||
<input class="validate" type="text" value="{{ item }}" name="key" hidden>
|
<input class="validate" type="text" value="{{ item }}" name="key" hidden>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -11,79 +11,89 @@
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<h5>Global channel options</h5>
|
<h5>Global channel options</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.system') }}">
|
<a href="{{ url_for('admin.system') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">System</span>
|
<span class="card-title">System</span>
|
||||||
<p class="grey-text">Internal system settings</p>
|
<p class="grey-text">Internal system settings</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.channels') }}">
|
<a href="{{ url_for('admin.channels') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Channels</span>
|
<span class="card-title">Channels</span>
|
||||||
<p class="grey-text">Manage channels in the system</p>
|
<p class="grey-text">Manage channels in the system</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.runs') }}">
|
<a href="{{ url_for('admin.runs') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Archive runs</span>
|
<span class="card-title">Archive runs</span>
|
||||||
<p class="grey-text">Look at the cron run logs</p>
|
<p class="grey-text">Look at the cron run logs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.websub') }}">
|
<a href="{{ url_for('admin.websub') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">WebSub</span>
|
<span class="card-title">WebSub</span>
|
||||||
<p class="grey-text">Edit WebSub YouTube links</p>
|
<p class="grey-text">Edit WebSub YouTube links</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.reports') }}">
|
<a href="{{ url_for('admin.reports') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Reports</span>
|
<span class="card-title">Reports</span>
|
||||||
<p class="grey-text">View user reports</p>
|
<p class="grey-text">View user reports</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.posters') }}">
|
<a href="{{ url_for('admin.queue') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Posters</span>
|
<span class="card-title">Queue</span>
|
||||||
<p class="grey-text">Extension posters</p>
|
<p class="grey-text">Video download queue and API access</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 l4 m-4">
|
<div class="col s6 l4 m-4">
|
||||||
<a href="{{ url_for('admin.users') }}">
|
<a href="{{ url_for('admin.users') }}">
|
||||||
<div class="card black-text">
|
<div class="card black-text">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Users</span>
|
<span class="card-title">Users</span>
|
||||||
<p class="grey-text">Authenticated users</p>
|
<p class="grey-text">Authenticated users</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col s6 l4 m-4">
|
||||||
|
<a href="{{ url_for('admin.workers') }}">
|
||||||
|
<div class="card black-text">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Workers</span>
|
||||||
|
<p class="grey-text">Worker and task management</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@@ -1,25 +1,38 @@
|
|||||||
{% extends 'material_base.html' %}
|
{% extends 'material_base.html' %}
|
||||||
{% block title %}Posters administration page{% endblock %}
|
{% block title %}Queue administration page{% endblock %}
|
||||||
{% block description %}Posters administration page of the AYTA system{% endblock %}
|
{% block description %}Queue administration page of the AYTA system{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 l11">
|
<div class="col s12">
|
||||||
<h4>Posters administration page</h4>
|
<h4>Queue administration page</h4>
|
||||||
</div>
|
|
||||||
<div class="col s12 l1 m-5">
|
|
||||||
<form method="POST">
|
|
||||||
<input title="Prunes all deleted endpoints, but keeps last 3 days" type="submit" value="clean-retired" name="task">
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<h5>Poster options</h5>
|
<h5>Options</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col s12 l4 m-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Direct actions</span>
|
||||||
|
<form class="mt-4" method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<button class="btn mb-2 red" type="submit" name="task" value="empty-queue">Empty Queue</button>
|
||||||
|
<br>
|
||||||
|
<span class="supporting-text">Removes all queued ids</span>
|
||||||
|
</form>
|
||||||
|
<form class="mt-4" method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<button class="btn mb-2" type="submit" name="task" value="clean-retired">Clean retired</button>
|
||||||
|
<br>
|
||||||
|
<span class="supporting-text">Prunes all deactivated endpoints, but keeps last 3 days</span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col s12 l4 m-4">
|
<div class="col s12 l4 m-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@@ -52,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col s12 mt-5 input-field">
|
<div class="col s12 mt-5 input-field">
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
<label>Queue<input type="checkbox" value="direct" name="value" disabled><span class="lever"></span>Direct</label>
|
<label>Queue<input type="checkbox" value="direct" name="direct"><span class="lever"></span>Direct</label>
|
||||||
<span class="supporting-text">Queue up or start directly</span>
|
<span class="supporting-text">Queue up or start directly</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -4,14 +4,9 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 l11">
|
<div class="col s12">
|
||||||
<h4>WebSub administration page</h4>
|
<h4>WebSub administration page</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l1 m-5">
|
|
||||||
<form method="POST">
|
|
||||||
<input title="Prunes all retired callbacks, but keeps last 3 days" type="submit" value="clean-retired" name="task">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -19,6 +14,43 @@
|
|||||||
<h5>WebSub options</h5>
|
<h5>WebSub options</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12 l4 m-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Direct actions</span>
|
||||||
|
<form method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<button class="btn mb-2 green" type="submit" name="task" value="subscribe-channels">Subscribe channels</button>
|
||||||
|
<br>
|
||||||
|
<span class="supporting-text">Send WebSub subscription request for all activated channels. (This will renew existing ones as well)</span>
|
||||||
|
</form>
|
||||||
|
<form class="mt-4" method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<button class="btn mb-2 red" type="submit" name="task" value="unsubscribe-callbacks">Unsubscribe channels</button>
|
||||||
|
<br>
|
||||||
|
<span class="supporting-text">Send WebSub unsubscription request for all activated endpoints. (This will only unsubscribe, not disable)</span>
|
||||||
|
</form>
|
||||||
|
<form class="mt-4" method="post" onsubmit="return confirm('Are you sure?');">
|
||||||
|
<button class="btn mb-2" type="submit" name="task" value="clean-retired">Clean retired</button>
|
||||||
|
<br>
|
||||||
|
<span class="supporting-text">Prunes all retired callbacks, but keeps until last day</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col s12 l4 m-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Statistics</span>
|
||||||
|
<h6>Unprocessed callback datapoints</h6>
|
||||||
|
<p>{{ render['stats']['unprocessed_data'] }}</p>
|
||||||
|
<h6>Active callbacks</h6>
|
||||||
|
<p>{{ render['stats']['active_callbacks'] }}</p>
|
||||||
|
<h6>Something</h6>
|
||||||
|
<p>Blah</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s6 l9">
|
<div class="col s6 l9">
|
||||||
|
47
ayta/templates/admin/workers.html
Normal file
47
ayta/templates/admin/workers.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends 'material_base.html' %}
|
||||||
|
{% block title %}Workers administration page{% endblock %}
|
||||||
|
{% block description %}Workers administration page of the AYTA system{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<h4>Workers administration page</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<h5>Options</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST">
|
||||||
|
<input title="test-sleep" type="submit" value="test-sleep" name="task">
|
||||||
|
</form>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<h6>Current workers</h6>
|
||||||
|
{% for worker in tasks %}
|
||||||
|
<span>{{ worker }}</span>
|
||||||
|
<table class="striped highlight responsive-table" style=" border: 1px solid black;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Task</th>
|
||||||
|
<th>Time started</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for task in tasks[worker] %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ task.get('id') }}</td>
|
||||||
|
<td>{{ task.get('type') }}</td>
|
||||||
|
<td>{{ task.get('time_start')|epoch_time }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@@ -43,6 +43,10 @@
|
|||||||
<a href="{{ url_for('channel.channel', channelId='UCzGdxkzULCa9RlD-Q2EZPXQ') }}"><span class="title">Kalashnikov Group</span></a>
|
<a href="{{ url_for('channel.channel', channelId='UCzGdxkzULCa9RlD-Q2EZPXQ') }}"><span class="title">Kalashnikov Group</span></a>
|
||||||
<p>Reason: This account has been terminated for a violation of YouTube's Terms of Service.</p>
|
<p>Reason: This account has been terminated for a violation of YouTube's Terms of Service.</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="collection-item">
|
||||||
|
<a href="{{ url_for('channel.channel', channelId='UCtfg1tENiu3SgGMZVduFmTg') }}"><span class="title">FiberNinja</span></a>
|
||||||
|
<p>Reason: This channel was removed because it violated our Community Guidelines.</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<p><b>Video by:</b> <a href="{{ url_for('channel.channel', channelId=render.get('info').get('channel_id')) }}">{{ render.get('info').get('uploader') }}</a></p>
|
<p><b>Video by:</b> <a href="{{ url_for('channel.channel', channelId=render.get('info').get('channel_id')) }}">{{ render.get('info').get('uploader') }}</a></p>
|
||||||
<p><b>Upload date:</b> {{ render.get('info').get('upload_date')|pretty_time }}</p>
|
<p><b>Upload date:</b> {{ render.get('info').get('upload_date')|pretty_time }}</p>
|
||||||
<p><b>Archive date:</b> {{ render.get('info').get('epoch')|epoch_time }}</p>
|
<p><b>Archive date:</b> {{ render.get('info').get('epoch')|epoch_date }}</p>
|
||||||
<p><b>Video length:</b> {{ render.get('info').get('duration')|pretty_duration }}</p>
|
<p><b>Video length:</b> {{ render.get('info').get('duration')|pretty_duration }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s4 l3 center-align">
|
<div class="col s4 l3 center-align">
|
||||||
|
Reference in New Issue
Block a user