Skip to content
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
5 changes: 4 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ on:
push:
branches:
- main
- testing
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build-and-push:
Expand Down Expand Up @@ -43,7 +45,8 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=testing,enable=${{ github.ref == 'refs/heads/testing' }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
Expand Down
5 changes: 4 additions & 1 deletion app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.scheduler import Scheduler
from app.routes import init_browser_routes, init_download_routes
from app.routes.scheduler_routes import init_scheduler_routes
from app.routes.events_routes import init_events_routes


def create_app():
Expand All @@ -18,7 +19,7 @@ def create_app():
config.log_startup_info(logger)

# Initialize services
download_service = DownloadService(config.DOWNLOAD_DIR)
download_service = DownloadService(config.DOWNLOAD_DIR, history_file=config.HISTORY_FILE)
browser_service = BrowserService(config, download_service)

# Initialize Scheduler
Expand All @@ -32,11 +33,13 @@ def create_app():
browser_bp = init_browser_routes(browser_service, download_service, config)
download_bp = init_download_routes(download_service, config.DOWNLOAD_DIR, scheduler)
scheduler_bp = init_scheduler_routes(scheduler)
events_bp = init_events_routes(browser_service, download_service)

# Register blueprints
flask_app.register_blueprint(browser_bp)
flask_app.register_blueprint(download_bp)
flask_app.register_blueprint(scheduler_bp)
flask_app.register_blueprint(events_bp)

# Main route
@flask_app.route('/')
Expand Down
5 changes: 5 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def __init__(self):
# Schedules
self.SCHEDULES_FILE = os.path.join(self.CHROME_USER_DATA_DIR, 'schedules.json')

# Logs / history
self.LOGS_DIR = '/app/logs'
self.HISTORY_FILE = os.path.join(self.LOGS_DIR, 'download_history.json')

# Chrome paths
self.CHROMEDRIVER_PATH = '/usr/local/bin/chromedriver'
self.CHROMEDRIVER_LOG_PATH = '/app/logs/chromedriver.log'
Expand Down Expand Up @@ -48,6 +52,7 @@ def check_directories(self):
"""Ensure required directories exist"""
os.makedirs(self.DOWNLOAD_DIR, exist_ok=True)
os.makedirs(self.CHROME_USER_DATA_DIR, exist_ok=True)
os.makedirs(self.LOGS_DIR, exist_ok=True)

def log_startup_info(self, logger):
"""Log startup information"""
Expand Down
10 changes: 10 additions & 0 deletions app/routes/download_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,14 @@ def delete_download(filename):
logger.error(f"Delete download error: {e}")
return jsonify({'error': str(e)}), 500

@download_bp.route('/history', methods=['GET'])
def download_history():
"""Return the persisted download history log."""
try:
history = download_service.get_history()
return jsonify({'history': history})
except Exception as e:
logger.error(f"History error: {e}")
return jsonify({'error': str(e)}), 500

return download_bp
97 changes: 97 additions & 0 deletions app/routes/events_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import json
import time
import logging
from flask import Blueprint, Response, stream_with_context

logger = logging.getLogger(__name__)

events_bp = Blueprint('events', __name__, url_prefix='/api/events')


def init_events_routes(browser_service, download_service):
"""Initialize SSE event-stream routes."""

@events_bp.route('/browser/<browser_id>')
def browser_events(browser_id):
"""
SSE stream for a specific browser/download session.

Replaces polling on /api/browser/status/<browser_id>.
Closes automatically when the browser/download is no longer running.
"""
def generate():
while True:
try:
status = browser_service.get_browser_status(browser_id)

if status is None:
# Check direct download status as fallback
with download_service._queue_lock:
direct = download_service.direct_download_status.get(browser_id)
if direct:
status = dict(direct)
else:
# Browser gone — send a final closed event and stop
yield f"data: {json.dumps({'is_running': False, 'closed': True})}\n\n"
return

# Attach download info if available
download_info = download_service.get_download_status(browser_id)
if download_info:
status['download'] = download_info

yield f"data: {json.dumps(status)}\n\n"

# Stop streaming once the browser/download has finished
if not status.get('is_running', True):
return

except Exception as e:
logger.error(f"SSE error for browser {browser_id}: {e}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"

time.sleep(2)

return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
'Connection': 'keep-alive',
},
)

@events_bp.route('/active')
def active_events():
"""
SSE stream for the active downloads list.

Streams a compact (no thumbnail data) snapshot every 3 seconds.
Thumbnails are still fetched on demand via /api/browser/status/<id>.
"""
def generate():
while True:
try:
active = download_service.get_active_downloads()
# Strip bulky thumbnail data to keep the SSE stream lightweight
compact = [
{k: v for k, v in d.items() if k != 'thumbnail'}
for d in active
]
yield f"data: {json.dumps({'active_downloads': compact})}\n\n"
except Exception as e:
logger.error(f"SSE active events error: {e}")
time.sleep(3)

return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
'Connection': 'keep-alive',
},
)

return events_bp
17 changes: 17 additions & 0 deletions app/routes/scheduler_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ def update_schedule(schedule_id):
logger.error(f"Error updating schedule: {e}")
return jsonify({'error': str(e)}), 500

@scheduler_bp.route('/<schedule_id>/pause', methods=['POST'])
def pause_schedule(schedule_id):
"""Toggle the paused state of a schedule"""
try:
updated = scheduler.pause_schedule(schedule_id)
if updated:
return jsonify({
'success': True,
'schedule': updated,
'paused': updated.get('paused', False)
})
else:
return jsonify({'error': 'Schedule not found'}), 404
except Exception as e:
logger.error(f"Error toggling pause for schedule {schedule_id}: {e}")
return jsonify({'error': str(e)}), 500

@scheduler_bp.route('/refresh', methods=['POST'])
def refresh_schedules():
"""Force refresh all schedule next_check times"""
Expand Down
Loading
Loading