Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC working with socket.io flask #129

Merged
merged 3 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from os import urandom

from flask import Flask, render_template, request
from flask_socketio import SocketIO
from waitress import create_server
from werkzeug.middleware.dispatcher import DispatcherMiddleware

Expand All @@ -18,6 +19,19 @@

__API_PREFIX__ = '/api'

socketio = SocketIO()


def send_event(event: str, data: dict, namespace: str = '/') -> None:
"""Send a socketio event to all connected clients.

Args:
event (str): The event name to send.
data (dict): The data to send with the event.
namespace (str, optional): The namespace to send the event to. Defaults to '/'.
"""
socketio.emit(event, data, namespace=namespace)

def create_app() -> Flask:
"""Creates an flask app instance that can be used to start a web server

Expand All @@ -34,6 +48,11 @@ def create_app() -> Flask:
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
app.config['JSON_SORT_KEYS'] = False

socketio.init_app(
app, path=__API_PREFIX__ + '/socket.io', cors_allowed_origins='*',
async_mode='threading', allow_upgrades=False, transports='polling',
)

# Add error handlers
@app.errorhandler(404)
def not_found(e):
Expand Down
46 changes: 25 additions & 21 deletions backend/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Task(ABC):
@abstractmethod
def stop(self) -> bool:
return

@property
@abstractmethod
def message(self) -> str:
Expand All @@ -48,7 +48,7 @@ def category(self) -> str:
@abstractmethod
def volume_id(self) -> int:
return

@property
@abstractmethod
def issue_id(self) -> int:
Expand All @@ -59,7 +59,7 @@ def run(self) -> Union[None, List[tuple]]:
"""Run the task

Returns:
Union[None, List[tuple]]: Either `None` if the task has no result or
Union[None, List[tuple]]: Either `None` if the task has no result or
`List[tuple]` if the task returns search results.
"""
return
Expand All @@ -68,16 +68,16 @@ def run(self) -> Union[None, List[tuple]]:
# Issue tasks
#=====================
class AutoSearchIssue(Task):
"Do an automatic search for an issue"

"Do an automatic search for an issue"
stop = False
message = ''
action = 'auto_search_issue'
display_title = 'Auto Search'
category = 'download'
volume_id = None
issue_id = None

def __init__(self, volume_id: int, issue_id: int):
"""Create the task

Expand All @@ -87,7 +87,7 @@ def __init__(self, volume_id: int, issue_id: int):
"""
self.volume_id = volume_id
self.issue_id = issue_id

def run(self) -> List[tuple]:
issue = Issue(self.issue_id)
volume = Volume(issue['volume_id'])
Expand Down Expand Up @@ -115,15 +115,15 @@ class AutoSearchVolume(Task):
category = 'download'
volume_id = None
issue_id = None

def __init__(self, volume_id: int):
"""Create the task

Args:
volume_id (int): The id of the volume to search for
"""
self.volume_id = volume_id

def run(self) -> List[tuple]:
self.message = f'Searching for {Volume(self.volume_id)["title"]}'

Expand All @@ -143,13 +143,13 @@ class RefreshAndScanVolume(Task):
category = ''
volume_id = None
issue_id = None

def __init__(self, volume_id: int):
"""Create the task

Args:
volume_id (int): The id of the volume for which to perform the task
"""
"""
self.volume_id = volume_id

def run(self) -> None:
Expand Down Expand Up @@ -186,7 +186,7 @@ def run(self) -> None:
return

class SearchAll(Task):
"Trigger an automatic search for each volume in the library"
"Trigger an automatic search for each volume in the library"

stop = False
message = ''
Expand Down Expand Up @@ -241,6 +241,8 @@ def __run_task(self, task: Task) -> None:
Args:
task (Task): The task to run
"""
from backend.server import send_event

logging.debug(f'Running task {task.display_title}')
with self.context():
try:
Expand Down Expand Up @@ -270,11 +272,12 @@ def __run_task(self, task: Task) -> None:

finally:
if not task.stop:
send_event('task_success', self.__format_entry(self.queue[0]))
self.queue.pop(0)
self._process_queue()

return

def _process_queue(self) -> None:
"""
Handle the queue. In the case that there is something in the queue and
Expand Down Expand Up @@ -332,7 +335,7 @@ def __check_intervals(self) -> None:
# Add task to queue
task_class = task_library[task['task_name']]
self.add(task_class())

# Update next_run
next_run = round(current_time + task['interval'])
cursor.execute(
Expand All @@ -351,21 +354,22 @@ def handle_intervals(self) -> None:
).fetchone()[0]
timedelta = next_run - round(time()) + 1
logging.debug(f'Next interval task is in {timedelta} seconds')

# Create sleep thread for that time and that will run self.__check_intervals.
self.task_interval_waiter = Timer(timedelta, self.__check_intervals)
self.task_interval_waiter.start()
return

def stop_handle(self) -> None:
"Stop the task handler"
"Stop the task handler"

logging.debug('Stopping task thread')
self.task_interval_waiter.cancel()
if self.queue:
self.queue[0]['task'].stop = True
self.queue[0]['thread'].join()
return

def __format_entry(self, task: dict) -> dict:
"""Format a queue entry for API response

Expand All @@ -391,7 +395,7 @@ def get_all(self) -> List[dict]:
Returns:
List[dict]: A list with all tasks in the queue.
Formatted using `self.__format_entry()`.
"""
"""
return [self.__format_entry(t) for t in self.queue]

def get_one(self, task_id: int) -> dict:
Expand Down Expand Up @@ -425,7 +429,7 @@ def remove(self, task_id: int) -> None:
# Get task and check if id exists
# Raises TaskNotFound if the id isn't found
task = self.get_one(task_id)

# Check if task is allowed to be deleted
if self.queue[0] == task:
raise TaskNotDeletable
Expand All @@ -447,7 +451,7 @@ def get_task_history(offset: int=0) -> List[dict]:

Returns:
List[dict]: The history entries.
"""
"""
result = list(map(
dict,
get_db(dict).execute(
Expand Down
20 changes: 16 additions & 4 deletions frontend/static/js/volumes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function populatePosters(volumes, api_key) {
cover.alt = "";
cover.loading = "lazy";
entry.appendChild(cover);

const progress = document.createElement('div');
const progress_bar = document.createElement('div');
const calc = volume.issues_downloaded_monitored / volume.issue_count_monitored * 100;
Expand Down Expand Up @@ -87,7 +87,7 @@ function populateTable(volumes) {
const year = document.createElement('td');
year.innerText = volume.year;
entry.appendChild(year);

const progress_container = document.createElement('td');
const progress = document.createElement('div');
const progress_bar = document.createElement('div');
Expand All @@ -105,14 +105,14 @@ function populateTable(volumes) {
progress.appendChild(progress_text);
progress_container.appendChild(progress);
entry.appendChild(progress_container);

const monitored_container = document.createElement('td');
const monitored = document.createElement('img');
monitored.src = volume.monitored ? `${url_base}/static/img/monitored.svg` : `${url_base}/static/img/unmonitored.svg`;
monitored.title = volume.monitored ? 'Monitored' : 'Unmonitored';
monitored_container.appendChild(monitored);
entry.appendChild(monitored_container);

list.appendChild(entry);
});
};
Expand Down Expand Up @@ -189,6 +189,18 @@ usingApiKey()
fetchLibrary(api_key);
fetchStats(api_key);

var socket = io({
path: `/api/socket.io`,
transports: ["polling", "websocket"],
upgrade: true,
rememberUpgrade: true,
autoConnect: false,
});
socket.on('connect', function() { console.log('connected'); });
socket.on('doisconnect', function() { console.log('disconnected'); });
socket.on("task_success", function(data) { console.log(data); });
socket.connect();

addEventListener('#clear-search', 'click', e => clearSearch(api_key));
addEventListener('#updateall-button', 'click', e => updateAll(api_key));
addEventListener('#searchall-button', 'click', e => searchAll(api_key));
Expand Down
1 change: 1 addition & 0 deletions frontend/templates/volumes.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script src="{{ url_for('static', filename='js/general.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/volumes.js') }}" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>

<title>Volumes - Kapowarr</title>
</head>
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tenacity >= 5.1.5
bencoding >= 0.2.6
simplejson >= 3.16.0
aiohttp >= 3.8.1
flask-socketio