From d730a7fe8bedcb4b47bf6931afb3e6c817fe3469 Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:38:10 -0600 Subject: [PATCH] Update 4.2.1 - Added horizontal scroll/refresh as time moves forward - Added API dynamic guide timing refresh --- CHANGELOG.md | 26 ++++++++----- README.md | 4 +- app.py | 60 +++++++++++++++--------------- static/js/guide-now-sync.js | 74 +++++++++++++++++++++++++++++++++++++ static/js/guide-refresh.js | 25 +++++++++++++ templates/guide.html | 5 +++ 6 files changed, 152 insertions(+), 42 deletions(-) create mode 100644 static/js/guide-now-sync.js create mode 100644 static/js/guide-refresh.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d8bfa6..c570d08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## v4.2.1 - 2025-11-10 + +### Added +- Added horizontal scroll/refresh as time moves forward +- Added API dynamic guide timing refresh + +--- + +## [Unreleased] + +- Planned: add `.m3u8` tuner support. +- Planned: move logs to SQLite DB. +- Planned: log filtering and pagination. + +--- + ## v4.2.0 - 2025-11-06 This version introduces mobile responsiveness, a new theme, refinements to auto-scroll, and backend API structures. @@ -24,15 +40,6 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Resolved layout inconsistencies across themes and display sizes. - General code cleanup and alignment for CI/CD consistency. ---- - -## [Unreleased] - -- Planned: add `.m3u8` tuner support. -- Planned: move logs to SQLite DB. -- Planned: log filtering and pagination. - ---- ## v4.1.0 - 2025-10-25 ### New Features @@ -64,7 +71,6 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Improved guide performance and browser compatibility with the new auto-scroll implementation. - Minor visual and layout corrections across settings and guide pages. ---- ## v4.0.0 — 2025-10-19 **Status:** Public Release (Feature Complete) diff --git a/README.md b/README.md index 3a60f71..63191cd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# 📺 RetroIPTVGuide v4.2.0 +# 📺 RetroIPTVGuide v4.2.1

- Version + Version GHCR diff --git a/app.py b/app.py index 4bc2b84..ae27266 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ # app.py — merged version (features from both sources) -APP_VERSION = "v4.2.0" -APP_RELEASE_DATE = "2025-11-06" +APP_VERSION = "v4.2.1" +APP_RELEASE_DATE = "2025-11-10" from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user @@ -1284,29 +1284,28 @@ def ensure_default_tuner(): def api_guide_snapshot(): """ Public unified guide data for framebuffer/RetroIPTV OS clients. - Combines channels + EPG info + active tuner in a single JSON response. - Emits precise start/stop ISO and duration (minutes) per program. + Supports ?hours=N (0.5–8) to control window size. """ try: now = datetime.now(timezone.utc) - start = now.replace(minute=(0 if now.minute < 30 else 30), second=0, microsecond=0) - # Allow variable window length via ?hours= parameter - try: - hours = float(request.args.get("hours", 2)) - except (TypeError, ValueError): - hours = 2 - - # Clamp to a sane maximum - if hours < 0.5: - hours = 0.5 - elif hours > 8: - hours = 8 + # Read optional hours parameter + hours_param = request.args.get("hours", type=float) + if hours_param is None: + hours = 2.0 + else: + hours = max(0.5, min(hours_param, 8.0)) # clamp 0.5–8h + start = now.replace( + minute=(0 if now.minute < 30 else 30), + second=0, + microsecond=0 + ) end = start + timedelta(hours=hours) - # Timeline labels (30-minute increments) - slots = [start + timedelta(minutes=i*30) for i in range(0, 5)] + # 30-minute timeline labels across the window + slot_count = int((hours * 60) / 30) + 1 + slots = [start + timedelta(minutes=30 * i) for i in range(slot_count)] timeline = [s.strftime("%I:%M %p").lstrip("0") for s in slots] tuner_name = get_current_tuner() @@ -1314,7 +1313,7 @@ def api_guide_snapshot(): tuner_info = tuners.get(tuner_name, {}) channels_out = [] - for ch in cached_channels[:50]: # limit for perf + for ch in cached_channels[:50]: # perf cap tvg_id = ch.get('tvg_id') progs = cached_epg.get(tvg_id, []) visible_programs = [] @@ -1324,32 +1323,33 @@ def api_guide_snapshot(): if not st or not sp: continue - # include any program overlapping our 2h window + # include any program overlapping [start, end) if st < end and sp > start: - # clamp to window for a stable box inside the 2h view clipped_start = max(st, start) - clipped_stop = min(sp, end) - dur_min = max(1, int((clipped_stop - clipped_start).total_seconds() // 60)) + clipped_stop = min(sp, end) + dur_min = max( + 1, + int((clipped_stop - clipped_start).total_seconds() // 60) + ) visible_programs.append({ "title": p.get('title') or "No Data", "desc": p.get('desc') or "", - "start": st.isoformat(), # full original ISO (tz-aware) + "start": st.isoformat(), "stop": sp.isoformat(), "clipped_start": clipped_start.isoformat(), "clipped_stop": clipped_stop.isoformat(), - "duration": dur_min # minutes within the 2h window + "duration": dur_min }) if not visible_programs: - # 30-min placeholder, pinned to window start for display sanity visible_programs = [{ "title": "No Data", "desc": "", "start": None, "stop": None, "clipped_start": start.isoformat(), - "clipped_stop": (start + timedelta(minutes=30)).isoformat(), + "clipped_stop": (start + timedelta(minutes=30)).isoformat(), "duration": 30 }] @@ -1367,13 +1367,13 @@ def api_guide_snapshot(): "tuner_xml": tuner_info.get('xml'), "tuner_m3u": tuner_info.get('m3u'), "theme": "default_crt_blue", - "version": APP_VERSION + "version": APP_VERSION, }, "timeline": timeline, "window": { "start_iso": start.isoformat(), - "end_iso": end.isoformat(), - "minutes": 120 + "end_iso": end.isoformat(), + "minutes": int(hours * 60), }, "channels": channels_out } diff --git a/static/js/guide-now-sync.js b/static/js/guide-now-sync.js new file mode 100644 index 0000000..108ce71 --- /dev/null +++ b/static/js/guide-now-sync.js @@ -0,0 +1,74 @@ +(() => { + const ENDPOINT = '/api/guide_snapshot?hours=6'; + const REFRESH_INTERVAL_MIN = 30; // re-sync with server window (Cairo-style) + const TICK_MS = 60000; // move line every 1 min + const SCALE = 5; // must match SCALE in app.py + + const timeRow = document.getElementById('gridTimeRow'); + if (!timeRow) return; + + const nowLine = document.getElementById('nowLineOriginal'); + if (!nowLine) return; + + // Element that owns the CSS variable; your layout already wraps now-line in .grid-content + const gridContent = timeRow.querySelector('.time-header-wrap .grid-content') || timeRow; + if (!gridContent) return; + + let windowStart = null; // Date from API + let windowMinutes = null; // total minutes from API + + function setNowOffset(px) { + gridContent.style.setProperty('--now-offset', px + 'px'); + } + + async function syncWindowFromAPI() { + try { + const res = await fetch(ENDPOINT, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + if (!data.window || !data.window.start_iso || !data.window.minutes) return; + + windowStart = new Date(data.window.start_iso); + windowMinutes = data.window.minutes; + + // Make sure the line is visible once we have a valid window + nowLine.style.display = 'block'; + + updateNowLine(); // snap immediately after sync + console.log('[guide-now-sync] window synced'); + } catch (e) { + console.error('[guide-now-sync] failed to sync window', e); + } + } + + function minutesSinceStart() { + if (!windowStart) return 0; + return (Date.now() - windowStart.getTime()) / 60000; + } + + function updateNowLine() { + if (!windowStart || !windowMinutes) return; + + const elapsed = minutesSinceStart(); + + if (elapsed < 0 || elapsed > windowMinutes) { + // Outside current window: hide line so it doesn't lie + nowLine.style.display = 'none'; + return; + } + + nowLine.style.display = 'block'; + const offsetPx = elapsed * SCALE; + setNowOffset(offsetPx); + } + + // Kick things off + syncWindowFromAPI(); + + // Re-sync to server window (so page follows the same rolling window as Cairo) + setInterval(syncWindowFromAPI, REFRESH_INTERVAL_MIN * 60 * 1000); + + // Smoothly advance between syncs + setInterval(updateNowLine, TICK_MS); +})(); + diff --git a/static/js/guide-refresh.js b/static/js/guide-refresh.js new file mode 100644 index 0000000..afb9d0b --- /dev/null +++ b/static/js/guide-refresh.js @@ -0,0 +1,25 @@ +(() => { + // Adjust the hours parameter if you prefer 8-hour browser view + const ENDPOINT = '/api/guide_snapshot?hours=6'; + const REFRESH_INTERVAL_MIN = 30; // same cadence as Cairo + + async function refreshGuide() { + try { + const res = await fetch(ENDPOINT, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + // You can either rebuild just the EPG grid, or simply reload the page + // depending on how your current guide is generated. + // For now, safest approach is a full reload: + window.location.reload(); + + console.log(`[guide-refresh] Guide refreshed at ${new Date().toLocaleTimeString()}`); + } catch (err) { + console.error('[guide-refresh] Failed to refresh guide:', err); + } + } + + // Run once every X minutes so the grid rolls forward with real time + setInterval(refreshGuide, REFRESH_INTERVAL_MIN * 60 * 1000); +})(); + diff --git a/templates/guide.html b/templates/guide.html index c44914e..7126ff3 100644 --- a/templates/guide.html +++ b/templates/guide.html @@ -556,6 +556,11 @@

Program Info

+ + + + +