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"/>
-
-
-
+
+
+
+
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index e51e1a5..9536223 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -148,13 +148,33 @@ public partial class MainWindow : Window
TxtStatus.Text = "Lade Senderliste…";
try
{
- var client = new FritzBoxClient(_settings.FritzBoxIp);
- var channels = await client.LoadAllAsync();
+ // FritzBox + Online-Sender parallel laden
+ var fritzClient = new FritzBoxClient(_settings.FritzBoxIp);
+ var onlineClient = new OnlineSourceClient();
+
+ var fritzTask = fritzClient.LoadAllAsync();
+ var onlineTask = _settings.ShowOnlineSources
+ ? onlineClient.LoadAllAsync()
+ : Task.FromResult(new List());
+
+ List fritzChannels;
+ try
+ {
+ fritzChannels = await fritzTask;
+ }
+ catch (Exception ex)
+ {
+ // FritzBox nicht erreichbar (z.B. unterwegs)? Trotzdem mit Online-Sendern weitermachen
+ fritzChannels = new List();
+ TxtStatus.Text = $"FritzBox-Fehler: {ex.Message}";
+ }
+ var onlineChannels = await onlineTask;
+
+ var channels = fritzChannels.Concat(onlineChannels).ToList();
foreach (var ch in channels)
{
ch.IsFavorite = _settings.Favorites.Contains(ch.Name);
- // Cached Logo gleich setzen, fehlende werden async geladen
ch.LogoPath = _logoService.GetCachedLogoPath(ch.Name);
}
@@ -162,17 +182,18 @@ public partial class MainWindow : Window
foreach (var ch in channels) _allChannels.Add(ch);
ApplyFilter();
- TxtStatus.Text = $"{channels.Count} Sender geladen";
+ TxtStatus.Text = onlineChannels.Count > 0
+ ? $"{fritzChannels.Count} FritzBox + {onlineChannels.Count} Online = {channels.Count} Sender"
+ : $"{fritzChannels.Count} Sender geladen";
- // Logos im Hintergrund nachladen — UI updated automatisch via INotifyPropertyChanged
_ = LoadLogosInBackgroundAsync();
}
catch (Exception ex)
{
TxtStatus.Text = $"Fehler: {ex.Message}";
MessageBox.Show(
- $"Fehler beim Laden der Senderliste von {_settings.FritzBoxIp}:\n\n{ex.Message}",
- "FRITZ!TV", MessageBoxButton.OK, MessageBoxImage.Warning);
+ $"Fehler beim Laden der Senderliste:\n\n{ex.Message}",
+ "HomeStream", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
@@ -235,10 +256,11 @@ public partial class MainWindow : Window
q = _currentCategory switch
{
- "tv" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio)),
- "radio" => q.Where(c => c.Kind == ChannelKind.Radio),
- "fav" => MergeHdSd(q.Where(c => c.IsFavorite)),
- _ => MergeHdSd(q)
+ "tv" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio)),
+ "radio" => q.Where(c => c.Kind == ChannelKind.Radio),
+ "fav" => MergeHdSd(q.Where(c => c.IsFavorite)),
+ "online" => MergeHdSd(q.Where(c => c.Source == ChannelSource.Online)),
+ _ => MergeHdSd(q)
};
if (!string.IsNullOrEmpty(_searchTerm))
diff --git a/Models/Channel.cs b/Models/Channel.cs
index 603ee2b..3672462 100644
--- a/Models/Channel.cs
+++ b/Models/Channel.cs
@@ -5,6 +5,9 @@ namespace FritzTV.Models;
public enum ChannelKind { TvSd, TvHd, Radio }
+/// Woher kommt der Sender? FritzBox (DVB-C/lokal) oder Online (HLS-Stream)
+public enum ChannelSource { FritzBox, Online }
+
public class Channel : INotifyPropertyChanged
{
public required string Name { get; init; }
@@ -12,6 +15,9 @@ public class Channel : INotifyPropertyChanged
public required ChannelKind Kind { get; init; }
public int Number { get; set; }
+ /// Quelle: lokale FritzBox oder Online-Stream. Default ist FritzBox (Abwärtskompatibilität).
+ public ChannelSource Source { get; init; } = ChannelSource.FritzBox;
+
private bool _isFavorite;
public bool IsFavorite
{
diff --git a/Services/AppSettings.cs b/Services/AppSettings.cs
index 336ad7b..c9ae0b8 100644
--- a/Services/AppSettings.cs
+++ b/Services/AppSettings.cs
@@ -10,6 +10,9 @@ public class AppSettings
public string LastChannel { get; set; } = "";
public double Volume { get; set; } = 80;
+ /// Online-Sender (ÖR-TV + Webradio aus Assets\online-sources.json) zusätzlich zur FritzBox-Liste anzeigen
+ public bool ShowOnlineSources { get; set; } = true;
+
private static readonly string ConfigPath = AppPaths.Settings;
public static AppSettings Load()
diff --git a/Services/OnlineSourceClient.cs b/Services/OnlineSourceClient.cs
new file mode 100644
index 0000000..7b265fb
--- /dev/null
+++ b/Services/OnlineSourceClient.cs
@@ -0,0 +1,79 @@
+using System.IO;
+using System.Text.Json;
+using FritzTV.Models;
+
+namespace FritzTV.Services;
+
+///
+/// Lädt eine kuratierte Liste von Online-Streams (öffentlich-rechtliche TV + Webradio)
+/// aus Assets\online-sources.json. Diese Datei wird beim Build neben die .exe kopiert.
+///
+public class OnlineSourceClient
+{
+ private static readonly string JsonPath = Path.Combine(
+ Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!,
+ "Assets", "online-sources.json");
+
+ public Task> LoadAllAsync()
+ {
+ // Synchron lesen ist OK, die Datei liegt lokal und ist klein (<10 KB)
+ try
+ {
+ if (!File.Exists(JsonPath))
+ return Task.FromResult(new List());
+
+ var json = File.ReadAllText(JsonPath);
+ var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+
+ var channels = new List();
+ int number = 1000; // Online-Sender bekommen Nummern ab 1000 damit sie nicht mit FritzBox-Nummern kollidieren
+
+ if (root.TryGetProperty("tv", out var tvArr))
+ {
+ foreach (var item in tvArr.EnumerateArray())
+ {
+ var name = item.GetProperty("name").GetString();
+ var url = item.GetProperty("url").GetString();
+ var kindStr = item.TryGetProperty("kind", out var k) ? k.GetString() : "TvHd";
+ if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url)) continue;
+
+ channels.Add(new Channel
+ {
+ Name = name,
+ Url = url,
+ Kind = Enum.TryParse(kindStr, out var kind) ? kind : ChannelKind.TvHd,
+ Number = number++,
+ Source = ChannelSource.Online
+ });
+ }
+ }
+
+ if (root.TryGetProperty("radio", out var radioArr))
+ {
+ foreach (var item in radioArr.EnumerateArray())
+ {
+ var name = item.GetProperty("name").GetString();
+ var url = item.GetProperty("url").GetString();
+ if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url)) continue;
+
+ channels.Add(new Channel
+ {
+ Name = name,
+ Url = url,
+ Kind = ChannelKind.Radio,
+ Number = number++,
+ Source = ChannelSource.Online
+ });
+ }
+ }
+
+ return Task.FromResult(channels);
+ }
+ catch
+ {
+ // Bei Parse-Fehler einfach leere Liste — App soll trotzdem laufen
+ return Task.FromResult(new List());
+ }
+ }
+}
diff --git a/SettingsWindow.xaml b/SettingsWindow.xaml
index a98fb53..c2d938d 100644
--- a/SettingsWindow.xaml
+++ b/SettingsWindow.xaml
@@ -1,11 +1,14 @@
+
+
+
@@ -19,7 +22,16 @@
-
+
+
+
+
+
+