From 7fe19b90ee78e49151e04ddb6406db97703d3b0d Mon Sep 17 00:00:00 2001 From: administrator Date: Sun, 10 May 2026 23:46:12 +0200 Subject: [PATCH 01/52] publish.ps1 + gitignore Hilfsskripte --- .gitignore | 2 + publish.ps1 | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 publish.ps1 diff --git a/.gitignore b/.gitignore index 0625409..e4e5532 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ logs/ # Temp *.tmp *.temp +response.json +_*.ps1 # OS Thumbs.db diff --git a/publish.ps1 b/publish.ps1 new file mode 100644 index 0000000..8da35d0 --- /dev/null +++ b/publish.ps1 @@ -0,0 +1,105 @@ +# HomeStream Release-Build und Upload zu Forgejo +# Verwendung: +# .\publish.ps1 # baut + zippt +# .\publish.ps1 -Tag v1.0.0 # taggt + baut + zippt + erstellt Release auf Forgejo +# +# Token wird aus Umgebungsvariable HOMESTREAM_TOKEN gelesen, ansonsten aus dem +# Default-Token unten (gleicher Token wie MailPrint). + +param( + [string]$Tag = '', + [string]$Token = $env:HOMESTREAM_TOKEN +) + +if (-not $Token) { + $Token = '63f650934f69d5684cb556a9a9e7d8e65495e257' +} + +$ErrorActionPreference = 'Stop' +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoUser = 'dimedtec' +$RepoName = 'HomeStream' +$ApiBase = "https://www.dimedtec.net/api/v1/repos/$RepoUser/$RepoName" + +# ── 1. Tag setzen falls angegeben ───────────────────────────────────────── +if ($Tag) { + if ($Tag -notmatch '^v\d+\.\d+\.\d+') { + throw "Tag muss Format v1.2.3 haben" + } + Write-Host "→ Tag $Tag setzen..." + git -C $ScriptDir tag $Tag 2>&1 | Out-Null + git -C $ScriptDir push origin $Tag 2>&1 | Out-Null + Write-Host " OK" +} + +# Aktuelle Version aus Git-Tag bestimmen +$currentTag = $null +try { $currentTag = (git -C $ScriptDir describe --tags --abbrev=0 2>$null) } catch { } +if (-not $currentTag) { $currentTag = 'v0.1.0' } +$version = $currentTag -replace '^v','' + +# ── 2. Publish ──────────────────────────────────────────────────────────── +$PublishDir = Join-Path $ScriptDir 'publish' +if (Test-Path $PublishDir) { Remove-Item -Recurse -Force $PublishDir } + +Write-Host "→ Publish (self-contained, single-file, win-x64)..." +dotnet publish $ScriptDir\HomeStream.csproj ` + -c Release ` + -r win-x64 ` + --self-contained true ` + -p:PublishSingleFile=true ` + -p:IncludeNativeLibrariesForSelfExtract=true ` + -p:DebugType=None ` + -p:DebugSymbols=false ` + -o $PublishDir + +if ($LASTEXITCODE -ne 0) { throw "Publish fehlgeschlagen" } + +# Aufräumen: PDBs raus +Get-ChildItem $PublishDir -Filter *.pdb -Recurse | Remove-Item -Force + +$exeSize = [math]::Round((Get-Item "$PublishDir\HomeStream.exe").Length / 1MB, 1) +Write-Host " HomeStream.exe: $exeSize MB" + +# ── 3. ZIP packen ───────────────────────────────────────────────────────── +$ZipPath = Join-Path $ScriptDir "HomeStream-$version-win-x64.zip" +if (Test-Path $ZipPath) { Remove-Item $ZipPath } +Compress-Archive -Path "$PublishDir\*" -DestinationPath $ZipPath -CompressionLevel Optimal +$zipSize = [math]::Round((Get-Item $ZipPath).Length / 1MB, 1) +Write-Host "→ ZIP: $ZipPath ($zipSize MB)" + +# ── 4. Release auf Forgejo erstellen (nur bei explizitem Tag) ───────────── +if (-not $Tag) { + Write-Host "`nFertig. Zum Veröffentlichen: .\publish.ps1 -Tag v$version" + exit 0 +} + +Write-Host "→ Forgejo-Release $Tag erstellen..." +$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json' } +$body = @{ + tag_name = $Tag + name = "HomeStream $version" + body = "Self-contained Release für Windows 10/11 (x64). Keine .NET-Installation nötig." + draft = $false + prerelease = $false +} | ConvertTo-Json + +$release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -Body $body +Write-Host " Release-ID: $($release.id)" + +# ── 5. ZIP als Asset hochladen ──────────────────────────────────────────── +Write-Host "→ ZIP hochladen..." +$uploadUrl = "$ApiBase/releases/$($release.id)/assets?name=" + [uri]::EscapeDataString((Split-Path $ZipPath -Leaf)) +$bytes = [System.IO.File]::ReadAllBytes($ZipPath) + +# Multipart form upload via curl (PowerShell Invoke-WebRequest hat Issues mit grossen Files) +& curl.exe -X POST ` + -H "Authorization: token $Token" ` + -F "attachment=@$ZipPath" ` + "$ApiBase/releases/$($release.id)/assets?name=$(Split-Path $ZipPath -Leaf)" ` + --silent --show-error --output - | Out-Null + +if ($LASTEXITCODE -ne 0) { throw "Upload fehlgeschlagen" } + +Write-Host "`n✅ Release veröffentlicht:" +Write-Host " $($release.html_url)" From 22d09621eee0b561aae4ecf0bc1638bd4b1bd552 Mon Sep 17 00:00:00 2001 From: administrator Date: Sun, 10 May 2026 23:50:31 +0200 Subject: [PATCH 02/52] _delete/ in gitignore aufnehmen --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e4e5532..6b96325 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ logs/ *.temp response.json _*.ps1 +_delete/ # OS Thumbs.db From 1ad2d35e58f07b1d9dd15263dad3d18a78dda67d Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 01:20:28 +0200 Subject: [PATCH 03/52] EPG Zeitzone-Fix + --rtsp-tcp --- MainWindow.xaml.cs | 4 +++- Services/EpgService.cs | 17 +++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index f7ccce1..01f000f 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -115,7 +115,9 @@ public partial class MainWindow : Window "--network-caching=1000", "--no-video-title-show", "--no-osd", - "--no-snapshot-preview" + "--no-snapshot-preview", + "--rtsp-tcp" // RTP over TCP: stabilere Sessions, vermeidet UDP-Routing-Kollisionen + // wenn mehrere Instanzen parallel auf die gleiche FritzBox zugreifen ); _player = new MediaPlayer(_libVLC); VideoView.MediaPlayer = _player; diff --git a/Services/EpgService.cs b/Services/EpgService.cs index 8c93a85..945a99b 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -162,22 +162,27 @@ public class EpgService if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; try { - // Format: "yyyyMMddHHmmss +0200" oder ohne Offset var parts = s.Trim().Split(' ', 2); var dt = DateTime.ParseExact(parts[0], "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture); + // dt.Kind = Unspecified hier if (parts.Length == 2 && parts[1].Length == 5) { + // Offset parsen: "+0200" oder "+0000" var sign = parts[1][0] == '-' ? -1 : 1; var hh = int.Parse(parts[1].Substring(1, 2)); var mm = int.Parse(parts[1].Substring(3, 2)); - var offsetMin = sign * (hh * 60 + mm); - // dt war als UTC+offset interpretiert, nach lokaler Zeit konvertieren - var asUtc = new DateTimeOffset(dt, TimeSpan.FromMinutes(offsetMin)); - return asUtc.LocalDateTime; + var offset = TimeSpan.FromMinutes(sign * (hh * 60 + mm)); + + // dt ist die Uhrzeit im angegebenen Offset (nicht UTC) + // DateTimeOffset(Unspecified, offset) interpretiert dt als "diese Zeit in dieser Zone" + // .ToLocalTime() konvertiert korrekt in die Systemzeitzone + return new DateTimeOffset(dt, offset).ToLocalTime().DateTime; } - return dt; + + // Kein Offset → UTC annehmen (epg.pw liefert immer +0000, aber sicherheitshalber) + return new DateTimeOffset(dt, TimeSpan.Zero).ToLocalTime().DateTime; } catch { return DateTime.MinValue; } } From 4bf42b96b5d52a51d0f57e2a46cd303d2f30d0de Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 07:26:49 +0200 Subject: [PATCH 04/52] EPG: async Grid-Build (kein UI-Hang), dunkle Scrollbars im Overlay --- MainWindow.xaml | 45 +++++++++++++++++++++++++++++++++++++++++++++ MainWindow.xaml.cs | 21 ++++++++++++--------- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index 7cae4fc..d40dc3a 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -287,10 +287,55 @@ Background="#0A0A0A"/> + + + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 01f000f..e51e1a5 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -682,9 +682,9 @@ public partial class MainWindow : Window private async Task ShowEpgOverlayAsync() { EpgOverlay.Visibility = Visibility.Visible; - TxtEpgOverlayStatus.Text = "Lade EPG-Daten…"; + EpgCanvas.Children.Clear(); + TxtEpgOverlayStatus.Text = "Lade EPG-Daten\u2026"; - // Datum/Zeit-Anzeige im Header aktualisieren TxtEpgDate.Text = DateTime.Now.ToString("d. MMM", System.Globalization.CultureInfo.GetCultureInfo("de-DE")); TxtEpgTime.Text = DateTime.Now.ToString("HH:mm"); @@ -698,7 +698,8 @@ public partial class MainWindow : Window } } - BuildEpgGrid(); + TxtEpgOverlayStatus.Text = "Baue Programm\u2026"; + await BuildEpgGridAsync(); } private void HideEpgOverlay() @@ -707,11 +708,10 @@ public partial class MainWindow : Window EpgCanvas.Children.Clear(); } - private void BuildEpgGrid() + private async Task BuildEpgGridAsync() { EpgCanvas.Children.Clear(); - // Sender: TV-only, HD vor SD, alphabetisch wie in der Senderliste var channels = _allChannels .Where(c => c.Kind != ChannelKind.Radio) .GroupBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase) @@ -719,7 +719,6 @@ public partial class MainWindow : Window .OrderBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase) .ToList(); - // Aktuelles Sendefenster: ab letzter ganzer Stunde _epgStartTime = DateTime.Now.Date.AddHours(DateTime.Now.Hour); var totalMinutes = EpgTotalHours * 60; var contentWidth = EpgChannelColWidth + totalMinutes * EpgPxPerMin; @@ -731,12 +730,16 @@ public partial class MainWindow : Window BuildEpgHeader(totalMinutes, contentWidth, contentHeight); BuildEpgNowLine(totalMinutes, contentHeight); + // Zeilen in Batches einf\u00fcgen damit UI-Thread nicht h\u00e4ngt (82 Sender w\u00e4ren >5s) + const int batchSize = 10; for (int i = 0; i < channels.Count; i++) + { BuildEpgRow(channels[i], i, contentWidth); + if (i % batchSize == batchSize - 1) + await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Background); + } - TxtEpgOverlayStatus.Text = $"{channels.Count} Sender · {EpgTotalHours} Stunden"; - - // Immer ganz nach links scrollen wenn Overlay geöffnet wird + TxtEpgOverlayStatus.Text = $"{channels.Count} Sender \u00b7 {EpgTotalHours} Stunden"; EpgScrollViewer.ScrollToHorizontalOffset(0); EpgScrollViewer.ScrollToVerticalOffset(0); } From e2aeeb43328df8cad82b2eefa79477591e4db456 Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 07:38:54 +0200 Subject: [PATCH 05/52] EPG: IsCurrent prueft auch Cache-Datei-Timestamp --- Services/EpgService.cs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Services/EpgService.cs b/Services/EpgService.cs index 945a99b..c109841 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -27,10 +27,25 @@ public class EpgService /// In-Memory-Index: Sendername (normalisiert) → Liste der Events, sortiert nach Startzeit private Dictionary> _eventsByChannel = new(); private DateTime _loadedAt = DateTime.MinValue; + private string _loadedFile = ""; - /// True wenn Daten geladen sind und nicht älter als 12h - public bool IsCurrent => _eventsByChannel.Count > 0 - && (DateTime.Now - _loadedAt).TotalHours < 12; + /// True wenn Daten geladen sind, nicht älter als 12h, und die Cache-Datei nicht geändert wurde + public bool IsCurrent + { + get + { + if (_eventsByChannel.Count == 0) return false; + if ((DateTime.Now - _loadedAt).TotalHours >= 12) return false; + // Cache-Datei neuer als letzter Parse? → neu laden + var todayFile = Path.Combine(CacheDir, $"epg_{DateTime.Today:yyyyMMdd}.xml"); + if (File.Exists(todayFile)) + { + var fileTime = new FileInfo(todayFile).LastWriteTime; + if (fileTime > _loadedAt) return false; + } + return true; + } + } /// Lädt EPG-Daten (Cache-Hit oder Web), parst und indiziert sie public async Task LoadAsync(IProgress? progress = null) @@ -53,6 +68,7 @@ public class EpgService progress?.Report("Parse EPG…"); ParseXmlTv(todayFile); _loadedAt = DateTime.Now; + _loadedFile = todayFile; progress?.Report($"EPG: {_eventsByChannel.Count} Sender, {_eventsByChannel.Values.Sum(l => l.Count)} Events"); } From 45928dd97b480cfbcbe9a0f76deb7307400da0e6 Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 08:41:44 +0200 Subject: [PATCH 06/52] EPG: epg.pw liefert lokale Zeit mit falschem +0000 - speziell behandeln --- Services/EpgService.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Services/EpgService.cs b/Services/EpgService.cs index c109841..0c844cc 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -172,7 +172,14 @@ public class EpgService _eventsByChannel = events; } - /// XMLTV-Zeit "20260510120000 +0200" → DateTime (lokale Zeit) + /// + /// XMLTV-Zeit "20260510120000 +0200" → lokale DateTime. + /// + /// WICHTIG: epg.pw markiert alle Zeiten mit "+0000", liefert sie aber faktisch + /// schon in lokaler Zeit (MEZ/MESZ). Das ist ein Bug im Feed. Wir behandeln + /// daher "+0000" speziell: Zeit direkt als lokal interpretieren, nicht konvertieren. + /// Echte Offsets (z.B. "+0200") werden korrekt umgerechnet. + /// private static DateTime ParseXmltvTime(string s) { if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; @@ -181,24 +188,24 @@ public class EpgService var parts = s.Trim().Split(' ', 2); var dt = DateTime.ParseExact(parts[0], "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture); - // dt.Kind = Unspecified hier if (parts.Length == 2 && parts[1].Length == 5) { - // Offset parsen: "+0200" oder "+0000" + // Sonderfall: epg.pw liefert IMMER "+0000", obwohl die Zeiten in Wahrheit + // schon lokale Sendezeiten sind (Tagesschau 20:00 steht als 200000 +0000, + // nicht 180000 +0000). Daher "+0000" als "bereits lokal" behandeln. + if (parts[1] == "+0000" || parts[1] == "-0000") + return dt; + + // Echter Offset → konvertieren var sign = parts[1][0] == '-' ? -1 : 1; var hh = int.Parse(parts[1].Substring(1, 2)); var mm = int.Parse(parts[1].Substring(3, 2)); var offset = TimeSpan.FromMinutes(sign * (hh * 60 + mm)); - - // dt ist die Uhrzeit im angegebenen Offset (nicht UTC) - // DateTimeOffset(Unspecified, offset) interpretiert dt als "diese Zeit in dieser Zone" - // .ToLocalTime() konvertiert korrekt in die Systemzeitzone return new DateTimeOffset(dt, offset).ToLocalTime().DateTime; } - // Kein Offset → UTC annehmen (epg.pw liefert immer +0000, aber sicherheitshalber) - return new DateTimeOffset(dt, TimeSpan.Zero).ToLocalTime().DateTime; + return dt; } catch { return DateTime.MinValue; } } From 3804bb989d961bfb72393400f77fda895df39770 Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 09:10:17 +0200 Subject: [PATCH 07/52] Revert "EPG: epg.pw liefert lokale Zeit mit falschem +0000 - speziell behandeln" This reverts commit 45928dd97b480cfbcbe9a0f76deb7307400da0e6. --- Services/EpgService.cs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/Services/EpgService.cs b/Services/EpgService.cs index 0c844cc..c109841 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -172,14 +172,7 @@ public class EpgService _eventsByChannel = events; } - /// - /// XMLTV-Zeit "20260510120000 +0200" → lokale DateTime. - /// - /// WICHTIG: epg.pw markiert alle Zeiten mit "+0000", liefert sie aber faktisch - /// schon in lokaler Zeit (MEZ/MESZ). Das ist ein Bug im Feed. Wir behandeln - /// daher "+0000" speziell: Zeit direkt als lokal interpretieren, nicht konvertieren. - /// Echte Offsets (z.B. "+0200") werden korrekt umgerechnet. - /// + /// XMLTV-Zeit "20260510120000 +0200" → DateTime (lokale Zeit) private static DateTime ParseXmltvTime(string s) { if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; @@ -188,24 +181,24 @@ public class EpgService var parts = s.Trim().Split(' ', 2); var dt = DateTime.ParseExact(parts[0], "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture); + // dt.Kind = Unspecified hier if (parts.Length == 2 && parts[1].Length == 5) { - // Sonderfall: epg.pw liefert IMMER "+0000", obwohl die Zeiten in Wahrheit - // schon lokale Sendezeiten sind (Tagesschau 20:00 steht als 200000 +0000, - // nicht 180000 +0000). Daher "+0000" als "bereits lokal" behandeln. - if (parts[1] == "+0000" || parts[1] == "-0000") - return dt; - - // Echter Offset → konvertieren + // Offset parsen: "+0200" oder "+0000" var sign = parts[1][0] == '-' ? -1 : 1; var hh = int.Parse(parts[1].Substring(1, 2)); var mm = int.Parse(parts[1].Substring(3, 2)); var offset = TimeSpan.FromMinutes(sign * (hh * 60 + mm)); + + // dt ist die Uhrzeit im angegebenen Offset (nicht UTC) + // DateTimeOffset(Unspecified, offset) interpretiert dt als "diese Zeit in dieser Zone" + // .ToLocalTime() konvertiert korrekt in die Systemzeitzone return new DateTimeOffset(dt, offset).ToLocalTime().DateTime; } - return dt; + // Kein Offset → UTC annehmen (epg.pw liefert immer +0000, aber sicherheitshalber) + return new DateTimeOffset(dt, TimeSpan.Zero).ToLocalTime().DateTime; } catch { return DateTime.MinValue; } } From ed7a237e338d591c81057c91e87431f2efebc325 Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 09:15:10 +0200 Subject: [PATCH 08/52] Reapply "EPG: epg.pw liefert lokale Zeit mit falschem +0000 - speziell behandeln" This reverts commit 3804bb989d961bfb72393400f77fda895df39770. --- Services/EpgService.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Services/EpgService.cs b/Services/EpgService.cs index c109841..0c844cc 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -172,7 +172,14 @@ public class EpgService _eventsByChannel = events; } - /// XMLTV-Zeit "20260510120000 +0200" → DateTime (lokale Zeit) + /// + /// XMLTV-Zeit "20260510120000 +0200" → lokale DateTime. + /// + /// WICHTIG: epg.pw markiert alle Zeiten mit "+0000", liefert sie aber faktisch + /// schon in lokaler Zeit (MEZ/MESZ). Das ist ein Bug im Feed. Wir behandeln + /// daher "+0000" speziell: Zeit direkt als lokal interpretieren, nicht konvertieren. + /// Echte Offsets (z.B. "+0200") werden korrekt umgerechnet. + /// private static DateTime ParseXmltvTime(string s) { if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; @@ -181,24 +188,24 @@ public class EpgService var parts = s.Trim().Split(' ', 2); var dt = DateTime.ParseExact(parts[0], "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture); - // dt.Kind = Unspecified hier if (parts.Length == 2 && parts[1].Length == 5) { - // Offset parsen: "+0200" oder "+0000" + // Sonderfall: epg.pw liefert IMMER "+0000", obwohl die Zeiten in Wahrheit + // schon lokale Sendezeiten sind (Tagesschau 20:00 steht als 200000 +0000, + // nicht 180000 +0000). Daher "+0000" als "bereits lokal" behandeln. + if (parts[1] == "+0000" || parts[1] == "-0000") + return dt; + + // Echter Offset → konvertieren var sign = parts[1][0] == '-' ? -1 : 1; var hh = int.Parse(parts[1].Substring(1, 2)); var mm = int.Parse(parts[1].Substring(3, 2)); var offset = TimeSpan.FromMinutes(sign * (hh * 60 + mm)); - - // dt ist die Uhrzeit im angegebenen Offset (nicht UTC) - // DateTimeOffset(Unspecified, offset) interpretiert dt als "diese Zeit in dieser Zone" - // .ToLocalTime() konvertiert korrekt in die Systemzeitzone return new DateTimeOffset(dt, offset).ToLocalTime().DateTime; } - // Kein Offset → UTC annehmen (epg.pw liefert immer +0000, aber sicherheitshalber) - return new DateTimeOffset(dt, TimeSpan.Zero).ToLocalTime().DateTime; + return dt; } catch { return DateTime.MinValue; } } From 4f8d924df5703146c92824ef8f6dae47148e28bd Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 09:51:34 +0200 Subject: [PATCH 09/52] EPG-Quelle gewechselt: epg.pw -> iptv-epg.org (sauberes UTC, korrekte Daten) --- Services/EpgService.cs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Services/EpgService.cs b/Services/EpgService.cs index 0c844cc..47e8a54 100644 --- a/Services/EpgService.cs +++ b/Services/EpgService.cs @@ -21,8 +21,10 @@ public class EpgService /// Cache-Verzeichnis für heruntergeladene XMLTV-Dateien private static readonly string CacheDir = AppPaths.Epg; - /// EPG-Quelle: kostenloser XMLTV-Feed für deutschsprachige Sender (DE/AT/CH) - private const string EpgUrl = "https://epg.pw/xmltv/epg_DE.xml.gz"; + /// EPG-Quelle: kostenloser XMLTV-Feed fuer Deutschland + /// iptv-epg.org liefert sauberes UTC mit korrektem +0000 (Tagesschau 20:00 lokal = 1800 +0000). + /// Zuvor war epg.pw im Einsatz, das aber unsystematisch falsche Zeiten lieferte. + private const string EpgUrl = "https://iptv-epg.org/files/epg-de.xml.gz"; /// In-Memory-Index: Sendername (normalisiert) → Liste der Events, sortiert nach Startzeit private Dictionary> _eventsByChannel = new(); @@ -172,14 +174,7 @@ public class EpgService _eventsByChannel = events; } - /// - /// XMLTV-Zeit "20260510120000 +0200" → lokale DateTime. - /// - /// WICHTIG: epg.pw markiert alle Zeiten mit "+0000", liefert sie aber faktisch - /// schon in lokaler Zeit (MEZ/MESZ). Das ist ein Bug im Feed. Wir behandeln - /// daher "+0000" speziell: Zeit direkt als lokal interpretieren, nicht konvertieren. - /// Echte Offsets (z.B. "+0200") werden korrekt umgerechnet. - /// + /// XMLTV-Zeit "20260510120000 +0200" → lokale DateTime. private static DateTime ParseXmltvTime(string s) { if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; @@ -191,13 +186,6 @@ public class EpgService if (parts.Length == 2 && parts[1].Length == 5) { - // Sonderfall: epg.pw liefert IMMER "+0000", obwohl die Zeiten in Wahrheit - // schon lokale Sendezeiten sind (Tagesschau 20:00 steht als 200000 +0000, - // nicht 180000 +0000). Daher "+0000" als "bereits lokal" behandeln. - if (parts[1] == "+0000" || parts[1] == "-0000") - return dt; - - // Echter Offset → konvertieren var sign = parts[1][0] == '-' ? -1 : 1; var hh = int.Parse(parts[1].Substring(1, 2)); var mm = int.Parse(parts[1].Substring(3, 2)); @@ -214,7 +202,15 @@ public class EpgService public static string NormalizeName(string name) { if (string.IsNullOrWhiteSpace(name)) return ""; - var s = name.Trim().ToLowerInvariant(); + var s = name.Trim(); + + // Länder-Präfixe entfernen (iptv-epg.org Feed verwendet "DE - Sendername") + string[] prefixes = { "DE - ", "AT - ", "CH - " }; + foreach (var pre in prefixes) + if (s.StartsWith(pre, StringComparison.OrdinalIgnoreCase)) + s = s[pre.Length..].TrimStart(); + + s = s.ToLowerInvariant(); // HD/SD-Suffixe entfernen string[] suffixes = { " hd", " uhd", " 4k", " sd", " austria", " österreich", " schweiz" }; foreach (var suf in suffixes) From 7702330c9a0c7c1ac6b0e8e7463c1b005eb60484 Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 10:05:52 +0200 Subject: [PATCH 10/52] publish.ps1: stderr von git tag/push abfangen, Tag-Existenz toleriert --- publish.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/publish.ps1 b/publish.ps1 index 8da35d0..4b3daec 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -27,8 +27,10 @@ if ($Tag) { throw "Tag muss Format v1.2.3 haben" } Write-Host "→ Tag $Tag setzen..." - git -C $ScriptDir tag $Tag 2>&1 | Out-Null - git -C $ScriptDir push origin $Tag 2>&1 | Out-Null + # Git schreibt Status auf stderr; mit try-catch abfangen damit PS nicht abbricht. + # Tag existiert evtl. schon — das ist OK, wir ignorieren den Fehler. + try { git -C $ScriptDir tag $Tag 2>$null | Out-Null } catch { } + try { git -C $ScriptDir push origin $Tag 2>$null | Out-Null } catch { } Write-Host " OK" } From a129f9b98e0ea996466d174766b9e44ffc9671ae Mon Sep 17 00:00:00 2001 From: administrator Date: Mon, 11 May 2026 10:15:39 +0200 Subject: [PATCH 11/52] Online-Streams: OeR-TV + Webradio aus Assets-JSON, Sidebar-Kategorie, Settings-Toggle --- Assets/online-sources.json | 191 +++++++++++++++++++++++++++++++++ HomeStream.csproj | 7 ++ MainWindow.xaml | 10 +- MainWindow.xaml.cs | 44 ++++++-- Models/Channel.cs | 6 ++ Services/AppSettings.cs | 3 + Services/OnlineSourceClient.cs | 79 ++++++++++++++ SettingsWindow.xaml | 16 ++- SettingsWindow.xaml.cs | 2 + 9 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 Assets/online-sources.json create mode 100644 Services/OnlineSourceClient.cs diff --git a/Assets/online-sources.json b/Assets/online-sources.json new file mode 100644 index 0000000..952ec04 --- /dev/null +++ b/Assets/online-sources.json @@ -0,0 +1,191 @@ +{ + "_comment": "Online-Streams für HomeStream. URLs Stand Mai 2026, Quelle: harryshomepage.de + rundfunkforum. Privatsender (RTL, ProSieben, Sat.1) NICHT enthalten wegen Lizenz/Paywall (Joyn, RTL+).", + "tv": [ + { + "name": "Das Erste HD", + "url": "https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ZDF HD", + "url": "https://zdf-hls-15.akamaized.net/hls/live/2016498/de/veryhigh/master.m3u8", + "kind": "TvHd" + }, + { + "name": "3sat HD", + "url": "https://zdf-hls-18.akamaized.net/hls/live/2016501/dach/veryhigh/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ARTE HD", + "url": "https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ARD-alpha HD", + "url": "https://mcdn.br.de/br/fs/ard_alpha/hls/de/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ONE HD", + "url": "https://mcdn-one.ard.de/ardone/hls/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ZDFneo HD", + "url": "https://zdf-hls-16.akamaized.net/hls/live/2016499/de/veryhigh/master.m3u8", + "kind": "TvHd" + }, + { + "name": "ZDFinfo HD", + "url": "https://zdf-hls-17.akamaized.net/hls/live/2016500/de/veryhigh/master.m3u8", + "kind": "TvHd" + }, + { + "name": "Tagesschau24 HD", + "url": "https://tagesschau.akamaized.net/hls/live/2020117/tagesschau/tagesschau_3/master.m3u8", + "kind": "TvHd" + }, + { + "name": "Phoenix HD", + "url": "https://zdf-hls-19.akamaized.net/hls/live/2016502/de/veryhigh/master.m3u8", + "kind": "TvHd" + }, + { + "name": "BR Süd HD", + "url": "https://livestreams.br.de/i/bfssued_germany@119890/master.m3u8", + "kind": "TvHd" + }, + { + "name": "WDR HD", + "url": "https://wdr-wdrfernsehen-koeln.icecast.wdr.de/wdr/wdrfernsehen/koeln/hls/master.m3u8", + "kind": "TvHd" + }, + { + "name": "NDR HD", + "url": "https://ndrfs-lh.akamaihd.net/i/ndrfs_nds@430233/master.m3u8", + "kind": "TvHd" + }, + { + "name": "HR HD", + "url": "https://hr-hrfernsehen-live.akamaized.net/hls/live/2016105/hrfernsehen/master.m3u8", + "kind": "TvHd" + }, + { + "name": "MDR Sachsen HD", + "url": "https://mdrsnhls-lh.akamaihd.net/i/livetvmdr_sachsen@439432/master.m3u8", + "kind": "TvHd" + }, + { + "name": "RBB Berlin HD", + "url": "https://rbb_berlin-lh.akamaihd.net/i/rbbBerlin_Live@380294/master.m3u8", + "kind": "TvHd" + }, + { + "name": "SWR BW HD", + "url": "https://swrbw-lh.akamaihd.net/i/swrbw_live@196738/master.m3u8", + "kind": "TvHd" + }, + { + "name": "SR Fernsehen HD", + "url": "https://live2_sr-lh.akamaihd.net/i/sr_universal02@107595/master.m3u8", + "kind": "TvHd" + }, + { + "name": "KiKA HD", + "url": "https://kikageohls.akamaized.net/hls/live/2022693/livetvkika_de/master.m3u8", + "kind": "TvHd" + }, + { + "name": "Deutsche Welle", + "url": "https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8", + "kind": "TvHd" + } + ], + "radio": [ + { + "name": "Bayern 1", + "url": "https://dispatcher.rndfnk.com/br/br1/obb/mp3/high" + }, + { + "name": "Bayern 2", + "url": "https://dispatcher.rndfnk.com/br/br2/sued/mp3/high" + }, + { + "name": "Bayern 3", + "url": "https://dispatcher.rndfnk.com/br/br3/live/mp3/high" + }, + { + "name": "BR Klassik", + "url": "https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high" + }, + { + "name": "B5 aktuell", + "url": "https://dispatcher.rndfnk.com/br/br5/live/mp3/high" + }, + { + "name": "Deutschlandfunk", + "url": "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3" + }, + { + "name": "Deutschlandfunk Kultur", + "url": "https://st02.sslstream.dlf.de/dlf/02/128/mp3/stream.mp3" + }, + { + "name": "Deutschlandfunk Nova", + "url": "https://st03.sslstream.dlf.de/dlf/03/128/mp3/stream.mp3" + }, + { + "name": "WDR 2", + "url": "https://wdr-wdr2-rheinland.icecastssl.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3" + }, + { + "name": "WDR 4", + "url": "https://wdr-wdr4-live.icecastssl.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3" + }, + { + "name": "1LIVE", + "url": "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3" + }, + { + "name": "NDR 2", + "url": "https://icecast.ndr.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3" + }, + { + "name": "N-JOY", + "url": "https://icecast.ndr.de/ndr/njoy/live/mp3/128/stream.mp3" + }, + { + "name": "SWR1 BW", + "url": "https://liveradio.swr.de/sw282p3/swr1bw/play.mp3" + }, + { + "name": "SWR3", + "url": "https://liveradio.swr.de/sw282p3/swr3/play.mp3" + }, + { + "name": "HR1", + "url": "https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3" + }, + { + "name": "HR3", + "url": "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3" + }, + { + "name": "MDR Sachsen", + "url": "https://mdr-284350-0.cast.mdr.de/mdr/284350/0/mp3/high/stream.mp3" + }, + { + "name": "RBB Radio Eins", + "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3" + }, + { + "name": "Antenne Bayern", + "url": "https://stream.antenne.de/antenne/stream/mp3" + }, + { + "name": "Radio Eins", + "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3" + } + ] +} diff --git a/HomeStream.csproj b/HomeStream.csproj index 219c2d9..de50237 100644 --- a/HomeStream.csproj +++ b/HomeStream.csproj @@ -27,4 +27,11 @@ + + + + PreserveNewest + + + diff --git a/MainWindow.xaml b/MainWindow.xaml index d40dc3a..301c323 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -137,11 +137,13 @@ ToolTip="Kategorien einklappen"/> -