From 88b9d96af56aea2c246802981b275da59a8b0a5d Mon Sep 17 00:00:00 2001 From: "nate.emmert" Date: Thu, 26 Mar 2026 08:46:30 -0400 Subject: [PATCH] Integrate desktop features into web UI tabs with API endpoints --- journeyfm/web_service.py | 190 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/journeyfm/web_service.py b/journeyfm/web_service.py index 81a4e04..9ee482e 100644 --- a/journeyfm/web_service.py +++ b/journeyfm/web_service.py @@ -6,7 +6,9 @@ import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path +from journeyfm.config_store import load_runtime_config from journeyfm.paths import data_path +from journeyfm.update_service import run_update_job from journeyfm.config_store import load_runtime_config from journeyfm.update_service import run_update_job @@ -81,6 +83,56 @@ def load_recent_stats(db_path=None): return stats +def load_history_entries(db_path=None, limit=100): + db_path = db_path or data_path("playlist_history.db") + rows = [] + if not Path(db_path).exists(): + return rows + conn = sqlite3.connect(db_path) + try: + c = conn.cursor() + c.execute( + "SELECT id, date, status, scraped_count, matched_count, added_count, missing_count, duplicate_count, skipped_count, station_breakdown FROM history ORDER BY date DESC LIMIT ?", + (limit,) + ) + result = c.fetchall() + for r in result: + rows.append({ + "id": r[0], + "date": r[1], + "status": r[2], + "scraped_count": r[3], + "matched_count": r[4], + "added_count": r[5], + "missing_count": r[6], + "duplicate_count": r[7], + "skipped_count": r[8], + "station_breakdown": json.loads(r[9] or '[]'), + }) + except Exception: + return [] + finally: + conn.close() + return rows + + +def load_buy_list(buy_list_path=None): + buy_list_path = buy_list_path or data_path("amazon_buy_list.txt") + if not Path(buy_list_path).exists(): + return [] + entries = [] + try: + with open(buy_list_path, 'r', encoding='utf-8') as f: + lines = [ln.strip() for ln in f if ln.strip()] + for ln in lines: + if ln.startswith('http'): + continue + entries.append(ln) + except Exception: + return [] + return entries + + def render_dashboard_html(stats): title = "Journey FM Song Count Command Center" station_rows = "".join( @@ -137,6 +189,9 @@ def render_dashboard_html(stats): + + + @@ -171,6 +226,30 @@ def render_dashboard_html(stats): +
+

App Controls (desktop features)

+ + + + +
+
+ +
+

Buy List

+ +
+ +
+

Run History

+
+ + + +
DateStatusScrapedMatchedAddedMissingDuplicatesSkipped
+
+
+

Raw API snapshot

Loading…
@@ -236,6 +315,7 @@ def render_dashboard_html(stats): }) .then(function(data) { status.textContent = data.status === 'ok' ? 'Refresh complete' : 'Refresh failed'; + document.getElementById('app-result').textContent = JSON.stringify(data.result || data, null, 2); setTimeout(function() { status.textContent = ''; }, 4500); setTimeout(function() { location.reload(); }, 1000); }) @@ -244,6 +324,65 @@ def render_dashboard_html(stats): setTimeout(function() { status.textContent = ''; }, 6500); }); } + + function previewStats() { + var status = document.getElementById('refresh-status'); + status.textContent = 'Previewing ...'; + fetch('/api/preview') + .then(function(response) { return response.json(); }) + .then(function(data) { + status.textContent = 'Preview complete'; + document.getElementById('app-result').textContent = JSON.stringify(data, null, 2); + setTimeout(function() { status.textContent = ''; }, 4500); + }) + .catch(function(err) { + status.textContent = 'Error: ' + err; + setTimeout(function() { status.textContent = ''; }, 6500); + }); + } + + function fetchBuyList() { + fetch('/api/buy-list') + .then(function(response) { return response.json(); }) + .then(function(data) { + var container = document.getElementById('buy-list-items'); + container.innerHTML = ''; + data.forEach(function(item) { + var li = document.createElement('li'); + li.textContent = item; + container.appendChild(li); + }); + document.getElementById('app-result').textContent = 'Loaded buy list (' + data.length + ' items).'; + }) + .catch(function(err) { + document.getElementById('app-result').textContent = 'Error loading buy list: ' + err; + }); + } + + function fetchHistory() { + fetch('/api/history') + .then(function(response) { return response.json(); }) + .then(function(data) { + var rows = document.getElementById('history-rows'); + rows.innerHTML = ''; + data.forEach(function(item) { + var tr = document.createElement('tr'); + tr.innerHTML = '' + item.date + '' + + '' + item.status + '' + + '' + item.scraped_count + '' + + '' + item.matched_count + '' + + '' + item.added_count + '' + + '' + item.missing_count + '' + + '' + item.duplicate_count + '' + + '' + item.skipped_count + ''; + rows.appendChild(tr); + }); + document.getElementById('app-result').textContent = 'Loaded history (' + data.length + ' rows).'; + }) + .catch(function(err) { + document.getElementById('app-result').textContent = 'Error loading history: ' + err; + }); + } @@ -302,6 +441,57 @@ def _build_handler(stats_supplier): self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) + elif self.path == "/api/preview": + try: + result = run_update_job(load_runtime_config(), dry_run=True, persist_history=False, write_buy_list=False) + content = json.dumps(result, default=str).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + except Exception as exc: + response = {"status": "error", "message": str(exc)} + content = json.dumps(response, default=str).encode("utf-8") + self.send_response(500) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + elif self.path == "/api/buy-list": + try: + entries = load_buy_list() + content = json.dumps(entries, default=str).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + except Exception as exc: + response = {"status": "error", "message": str(exc)} + content = json.dumps(response, default=str).encode("utf-8") + self.send_response(500) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + elif self.path == "/api/history": + try: + history = load_history_entries() + content = json.dumps(history, default=str).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + except Exception as exc: + response = {"status": "error", "message": str(exc)} + content = json.dumps(response, default=str).encode("utf-8") + self.send_response(500) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) else: self.send_response(404) self.send_header("Content-Type", "text/plain; charset=utf-8")