You've already forked amazing-ytdlp-archive
							
							Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 548a4860fc | ||
|   | da333ab4f6 | ||
|   | f2b01033ea | ||
|   | 49f0ea7481 | ||
|   | f1287a4212 | ||
|   | 30ea647ca9 | ||
|   | a7c640a8cf | ||
|   | f6da232164 | ||
|   | 1d5934275c | ||
|   | 72af6b6126 | 
| @@ -1,4 +1,4 @@ | |||||||
| name: Generate release | name: Generate docker image | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   release: |   release: | ||||||
| @@ -23,12 +23,3 @@ jobs: | |||||||
|       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 | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM python:3-alpine | FROM python:3.12-alpine | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| COPY requirements.txt /app | COPY requirements.txt /app | ||||||
| RUN pip install --no-cache-dir -r requirements.txt | RUN pip install --no-cache-dir -r requirements.txt | ||||||
|   | |||||||
| @@ -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,6 +41,7 @@ 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 | ||||||
|   | |||||||
| @@ -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 websub_subscribe_callback, websub_unsubscribe_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 | ||||||
|  |  | ||||||
| @@ -76,10 +76,10 @@ def channel(channelId): | |||||||
|             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,6 +109,8 @@ 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) | ||||||
| @@ -118,18 +120,30 @@ def websub(): | |||||||
|              |              | ||||||
|             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 | ||||||
| @@ -147,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) | ||||||
| @@ -160,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) | ||||||
|  |              | ||||||
|  |             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}') |                 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}') | ||||||
|              |              | ||||||
|         return redirect(url_for('admin.posters')) |         elif task == 'empty-queue': | ||||||
|  |             get_nosql().queue_emptyQueue() | ||||||
|  |             flash(f'Queue has  been emptied') | ||||||
|          |          | ||||||
|     endpoints = get_nosql().poster_getEndpoints() |         return redirect(url_for('admin.queue')) | ||||||
|     queue = get_nosql().poster_getQueue() |  | ||||||
|          |          | ||||||
|     return render_template('admin/posters.html', endpoints=endpoints, queue=queue) |     endpoints = get_nosql().queue_getEndpoints() | ||||||
|  |     queue = get_nosql().queue_getQueue() | ||||||
|  |     | ||||||
|  |     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 | ||||||
| @@ -216,3 +241,16 @@ 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 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										112
									
								
								ayta/nosql.py
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								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,7 +195,10 @@ 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] | ||||||
| @@ -301,16 +303,13 @@ 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 | ||||||
| @@ -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,7 +361,7 @@ 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}}) | ||||||
| @@ -371,56 +369,66 @@ class Mango: | |||||||
|          |          | ||||||
|         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 poster_insertQueue(self, endpointId, videoId): |     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 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             # | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								ayta/oidc.py
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								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}' | ||||||
| @@ -108,20 +131,32 @@ 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 | ||||||
| @@ -34,6 +54,8 @@ def websub_subscribe_callback(channelId): | |||||||
|     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,18 +92,28 @@ 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() | ||||||
| @@ -87,19 +121,29 @@ 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> | ||||||
| 		   | 		   | ||||||
|   | |||||||
| @@ -66,11 +66,11 @@ | |||||||
|     </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> | ||||||
| @@ -85,5 +85,15 @@ | |||||||
|       </div> |       </div> | ||||||
|     </a> |     </a> | ||||||
|   </div> |   </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> | ||||||
| {% 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