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
38 changes: 33 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 📑 Changelog
# đź“‘ Changelog

All notable changes to this project will be documented here.
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Expand All @@ -14,6 +14,34 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [v3.0.1] - 2025-10-07
### Added
- **EPG Fallback System**
- Channels without XMLTV data now display “No Guide Data Available”.
- Added `apply_epg_fallback()` helper to ensure all channels have at least one program entry.
- Fallback automatically applied after login and tuner switch.
- **Invalid XML Detection**
- If a user enters the same `.m3u` URL for both M3U and XML, the system treats it as invalid XML and loads fallback placeholders instead.
- **Visual Placeholders**
- `guide.html` updated to render gray, italicized “No Guide Data Available” banners in program grid.
- Works across all existing Light/Dark/Retro themes.

### Changed
- **Tuner Switching Behavior**
- Active tuner now refreshes immediately without requiring logout/relogin.
- Cached channel/EPG data reloaded dynamically when tuner changes.
- **Login Page UI**
- Redesigned with floating centered box, shadow, and right-aligned RetroIPTVGuide logo.

### Fixed
- **EPG Cache Sync**
- Prevented guide from displaying outdated EPG after tuner change.
- Corrected case where missing XML data produced empty grid.


---


## [3.0.0] - 2025-10-03
### Added
- **Windows Support**:
Expand Down Expand Up @@ -72,7 +100,7 @@ This project follows [Semantic Versioning](https://semver.org/).
- Shows dynamic system info: version, release date, Python version, OS, install path, database path, logs path, uptime.
- **Admin Log Management**:
- Log file size shown on Logs page (human-readable + color-coded).
- Admin-only “Clear Logs” button added to truncate activity log.
- Admin-only “Clear Logs” button added to truncate activity log.
- **Automated Version Bump Tool**:
- Added `bump_version.py` to sync `APP_VERSION` in app.py with CHANGELOG.md.
- Inserts new version section under `[Unreleased]`.
Expand All @@ -99,7 +127,7 @@ This project follows [Semantic Versioning](https://semver.org/).
- Data is pulled from `app.py` constants and runtime environment, no manual edits required.

#### Notes
- **Windows validation pending**: Installer and uninstaller are implemented but require verification on Windows; tracked in ROADMAP “Priority Suggestions”.
- **Windows validation pending**: Installer and uninstaller are implemented but require verification on Windows; tracked in ROADMAP “Priority Suggestions”.

---

Expand All @@ -119,7 +147,7 @@ This project follows [Semantic Versioning](https://semver.org/).



## [v2.0.0] 2025-09-24
## [v2.0.0] – 2025-09-24
### Added
- Tuner URL validation: new validate_tuner_url() function checks XML/M3U inputs before saving.
- Detects invalid/empty URLs, unresolvable hostnames, and distinguishes between public vs. private IPs.
Expand Down Expand Up @@ -157,7 +185,7 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [v1.x.x] 2025-09-01 2025-09-23
## [v1.x.x] – 2025-09-01 → 2025-09-23
### Added
- Initial IPTV Flask application with:
- User authentication (login/logout, password change).
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
# RetroIPTVGuide

![Version](https://img.shields.io/badge/version-v3.0.0-blue)
![Version](https://img.shields.io/badge/version-v3.0.1-blue)

RetroIPTVGuide is an IPTV Web Interface inspired by 90s/2000s cable TV guides.
It is designed to work with [ErsatzTV](https://ersatztv.org/) [(GitRepo)](https://github.com/ErsatzTV/ErsatzTV/tree/main) but should support any `.m3u` and `.xml` IPTV source.

⚠️ **Note:** This is still a BETA release. It is not recommended for direct Internet/public-facing deployments.
โ��๏ธ� **Note:** This is still a BETA release. It is not recommended for direct Internet/public-facing deployments.

- [Installation / Uninstall Guide](INSTALL.md)
- [Changelog](CHANGELOG.md)
- [Roadmap](ROADMAP.md)
- [License](LICENSE)

## Features (v3.0.0)
- 🔑 **User Authentication**
## โ�จ Features (v3.0.0)
- ๐�”‘ **User Authentication**
- Login/logout system with hashed passwords.
- Admin and regular user accounts.
- Password change functionality.
- Admin-only user management (add/delete users).
- 📡 **Tuner Management**
- ๐�“ก **Tuner Management**
- Multiple tuner support stored in `tuners.db`.
- Switch between active tuners via the web UI.
- Update `.m3u` and `.xml` tuner URLs (persisted in DB).
- 📺 **Guide & Playback**
- Program guide rendered from XMLTV.
- Channel list parsed from M3U playlist.
- ๐�“บ **Guide & Playback**
- Program guide rendered from XMLTV with automatic fallback for missing data.
- Channels without guide entries display “No Guide Data Available”.
- Video playback using HTML5 + HLS.js.
- Playback events logged with user + channel + timestamp.
- 📑 **Logging**
- ๐�“‘ **Logging**
- Activity log (`activity.log`) records authentication events, tuner changes, playback, and admin actions.
- Admin-only **Logs page** with real-time log viewing.
- Log file size display with color coding.
- Admin-only Clear Logs” button to truncate logs.
- 🎨 **UI Enhancements**
- Unified header across all pages (Guide, Logs, Add User, Delete User, Change Password, Change Tuner).
- Admin-only โ€�Clear Logsโ€� button to truncate logs.
- ๐��จ **UI Enhancements**
- Unified header across all pages (Guide, Logs, Add User, Delete User, Change Password, Change Tuner, Login).
- Active tuner display + live clock in header.
- **Themes submenu** with multiple options:
- Light
- Dark
- AOL/CompuServe
- TV Guide Magazine
- Theme persistence stored in browser localStorage, applied instantly across all pages.
- **About Page under Settings menu** shows version, Python, OS, uptime, paths.
- ⚙️ **System**
- **About Page under Settings menu** โ€” shows version, Python, OS, uptime, paths.
- โ��๏ธ� **System**
- Automatic initialization of `users.db` and `tuners.db` on first run.
- SQLite databases use WAL mode for better concurrency.
- Preloads tuner/channel/guide data from DB on startup.
Expand All @@ -52,15 +52,15 @@ It is designed to work with [ErsatzTV](https://ersatztv.org/) [(GitRepo)](https:

---

## 🌐 Browser Compatibility
## ๐��� Browser Compatibility
This project is designed to work with **all major browsers**.
It has been tested on:
- Firefox
- Chrome
- Safari
- Edge

## 💻 Tested Devices & OS
## ๐�’ป Tested Devices & OS
The web interface has been tested on:
- **Ubuntu (desktop/server)**
- **iOS (mobile/tablet)**
Expand All @@ -70,31 +70,31 @@ The web interface has been tested on:
- **MacOS**
- **Windows**

## 🛠️ Installation Platform
## ๐���๏ธ� Installation Platform
- **Debian Based Linux (desktop/server)**
- **Windows 10/11**

## 📺 Screenshots
## ๐�“บ Screenshots

## 📺 Guide Page
## ๐�“บ Guide Page
![Guide Screenshot](docs/screenshots/guide.png)

## 📺 Video Pop Out
## ๐�“บ Video Pop Out
![Video Pop Out](docs/screenshots/guide_with_video_breakout.png)

## 📺 Video Pop Out on Desktop
## ๐�“บ Video Pop Out on Desktop
![Desktop Pop Out](docs/screenshots/video_breakout_desktop.png)

## 📺 TV Guide Magazine Theme
## ๐�“บ TV Guide Magazine Theme
![TV Guide Theme](docs/screenshots/TV_Guide_Theme.png)

## 📺 AOL / CompuServe Theme
## ๐�“บ AOL / CompuServe Theme
![AOL / CompuServe Theme](docs/screenshots/AOL_Compuserve_Theme.png)

---

## 🤝 Contributing
Contributions are welcome! Here’s how you can help:
## ๐�ค� Contributing
Contributions are welcome! Hereโ€�s how you can help:
1. **Report Issues**: Found a bug or want to suggest a feature? Open an [issue](../../issues).
2. **Submit Pull Requests**: Fork the repo, make changes, and submit a PR. Please ensure code is tested before submitting.
3. **Improve Documentation**: Help refine the installation guide, add screenshots, or improve explanations in the README.
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ These are **not yet implemented**, but provide a development path for future rel
### 1. Tuner Management
- [x] Add ability to **add/remove tuners** from the UI (v2.3.0).
- [x] Add ability to rename tuners via the UI (v2.3.0).
- [ ] Support for **.m3u8 single-channel playlists** as tuner sources.
- [ ] Support for **.m3u8 single-channel playlists** as tuner sources (planned v3.1.0).
- Option A: Special-case `.m3u8` handling in parser.
- Option B: Add explicit `hls` column to `tuners.db`.
- [x] Validate tuner URLs (ping/check format before saving) (v2.0.0).
Expand All @@ -26,8 +26,8 @@ These are **not yet implemented**, but provide a development path for future rel
### 3. Guide & Playback
- [ ] Add **search/filter box** to guide for channels/programs.
- [ ] Add ability to set **favorites** for quick channel access.
- [x] Add fallback message (“No Guide Data Available”) for channels missing EPG info (v3.0.1).
- [ ] Add **reminders/notifications** for upcoming programs.
- [ ] (Future) Support recording/saving streams for offline playback.

### 4. User Management
- [ ] Add role-based access control (admin, regular user, read-only).
Expand Down
48 changes: 47 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
APP_VERSION = "v3.0.0"
APP_VERSION = "v3.0.1"
APP_RELEASE_DATE = "2025-09-26"

from flask import Flask, render_template, request, redirect, url_for, flash
Expand Down Expand Up @@ -225,6 +225,11 @@ def parse_m3u(m3u_url):
# ------------------- XMLTV EPG Parsing -------------------
def parse_epg(xml_url):
programs = {}

# ✅ Handle when user pastes same .m3u for XML
if xml_url.lower().endswith(('.m3u', '.m3u8')):
return programs # empty, fallback will fill it later

try:
r = requests.get(xml_url, timeout=15)
r.raise_for_status()
Expand Down Expand Up @@ -261,6 +266,23 @@ def parse_epg(xml_url):
programs[cid].append({'title': title, 'desc': desc, 'start': start, 'stop': stop})
return programs

# ------------------- EPG Fallback Helper -------------------
def apply_epg_fallback(channels, epg):
"""Ensure each channel has at least one program entry, even if missing in XML."""
for ch in channels:
tvg_id = ch.get('tvg_id')
if not tvg_id:
continue
if tvg_id not in epg or not epg[tvg_id]:
epg[tvg_id] = [{
'title': 'No Guide Data Available',
'desc': '',
'start': None,
'stop': None
}]
return epg


# ------------------- Routes -------------------
@app.route('/')
def home():
Expand All @@ -282,6 +304,8 @@ def login():
global cached_channels, cached_epg
cached_channels = parse_m3u(m3u_url)
cached_epg = parse_epg(xml_url)
# ✅ Apply “No Guide Data Available” fallback
cached_epg = apply_epg_fallback(cached_channels, cached_epg)
return redirect(url_for('guide'))
else:
log_event(username if username else "unknown", "Failed login attempt")
Expand Down Expand Up @@ -414,6 +438,16 @@ def change_tuner():
log_event(current_user.username, f"Switched active tuner to {new_tuner}")
flash(f"Active tuner switched to {new_tuner}")

# ✅ Refresh cached guide data immediately
global cached_channels, cached_epg
tuners = get_tuners()
m3u_url = tuners[new_tuner]["m3u"]
xml_url = tuners[new_tuner]["xml"]
cached_channels = parse_m3u(m3u_url)
cached_epg = parse_epg(xml_url)
# ✅ Apply “No Guide Data Available” fallback
cached_epg = apply_epg_fallback(cached_channels, cached_epg)

elif action == "update_urls":
tuner = request.form["tuner"]
xml_url = request.form["xml_url"]
Expand Down Expand Up @@ -485,6 +519,18 @@ def guide():
total_width = slots * SLOT_MINUTES * SCALE
minutes_from_start = (now - grid_start).total_seconds() / 60.0
now_offset = int(minutes_from_start * SCALE)

# --- DEBUG: Show alignment between M3U and EPG ---
#print("\n=== DEBUG: Cached Channels and EPG Keys ===")
#print("First 5 channel IDs from M3U:")
#for ch in cached_channels[:5]:
# print(" ", ch.get('tvg_id'))

#print("\nFirst 5 EPG keys:")
#for key in list(cached_epg.keys())[:5]:
# print(" ", key)
#print("==========================================\n")


return render_template(
'guide.html',
Expand Down
10 changes: 9 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
set -e

VERSION="2.1.0"
VERSION="3.0.1"
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
LOGFILE="install_${TIMESTAMP}.log"

Expand Down Expand Up @@ -220,6 +220,14 @@ else
echo "Unsupported environment: $OSTYPE"
exit 1
fi

echo ""
echo "Installation complete!"
echo "End time: $(date)"
echo "Access the server in your browser at: http://<your-server-ip>:5000"
echo "Default login: admin / strongpassword123"
echo "NOTE: This is a **BETA build**. Do not expose it directly to the public internet."
echo ""
}

if [ "$ENVIRONMENT" = "GITBASH" ]; then
Expand Down
54 changes: 38 additions & 16 deletions templates/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@
body.retro-magazine .now-line { background:#000; height:3px; }
body.retro-magazine #current-tuner { color:#000000 !important; font-weight:bold; }

/* Placeholder when no guide data exists */
.program.no-guide {
font-style: italic;
color: #999;
background: #2a2a2a;
border: 1px dashed #555;
}



</style>
</head>
Expand Down Expand Up @@ -299,22 +308,35 @@ <h3>Program Info</h3>
</div>
<div class="grid-col">
<div class="grid-content">
{% set cid = ch.tvg_id %}
{% for prog in epg.get(cid, []) %}
{% if prog.start and prog.stop %}
{% set left = ((prog.start - grid_start).total_seconds()/60) * SCALE %}
{% set calc_width = (prog.stop - prog.start).total_seconds()/60 * SCALE %}
{% set width = 24 if calc_width < 24 else calc_width %}
<div class="program {% if prog.start <= now <= prog.stop %}now{% endif %}"
style="left:{{ left }}px; width:{{ width }}px;"
data-start="{{ prog.start.isoformat() }}"
data-stop="{{ prog.stop.isoformat() }}"
data-title="{{ prog.title }}"
data-desc="{{ prog.desc }}">
{{ prog.title }}
</div>
{% endif %}
{% endfor %}
{% set cid = ch.tvg_id %}
{% set channel_epg = epg[cid] if cid in epg else [] %}
{% if channel_epg|length == 0 %}
<div class="program no-guide"
style="left:0px; width:{{ 60 * SCALE }}px;">
No Guide Data Available
</div>
{% else %}
{% for prog in channel_epg %}
{% if prog.title == 'No Guide Data Available' %}
<div class="program no-guide"
style="left:0px; width:{{ 60 * SCALE }}px;">
{{ prog.title }}
</div>
{% elif prog.start and prog.stop %}
{% set left = ((prog.start - grid_start).total_seconds()/60) * SCALE %}
{% set calc_width = (prog.stop - prog.start).total_seconds()/60 * SCALE %}
{% set width = 24 if calc_width < 24 else calc_width %}
<div class="program {% if prog.start <= now <= prog.stop %}now{% endif %}"
style="left:{{ left }}px; width:{{ width }}px;"
data-start="{{ prog.start.isoformat() }}"
data-stop="{{ prog.stop.isoformat() }}"
data-title="{{ prog.title }}"
data-desc="{{ prog.desc }}">
{{ prog.title }}
</div>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
Expand Down
Loading