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
-
+
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
+
+
+
+
+