Online-Streams: OeR-TV + Webradio aus Assets-JSON, Sidebar-Kategorie, Settings-Toggle

This commit is contained in:
administrator 2026-05-11 10:15:39 +02:00
parent 7702330c9a
commit a129f9b98e
9 changed files with 341 additions and 17 deletions

191
Assets/online-sources.json Normal file
View file

@ -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"
}
]
}

View file

@ -27,4 +27,11 @@
<Resource Include="Assets\logo.svg" />
</ItemGroup>
<ItemGroup>
<!-- Online-Stream-Liste wird zur Laufzeit gelesen, daher Content (kein Resource) -->
<Content Include="Assets\online-sources.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -137,11 +137,13 @@
ToolTip="Kategorien einklappen"/>
</Grid>
<StackPanel DockPanel.Dock="Top">
<Button x:Name="BtnAll" Content="📺 Alle Sender" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="all"/>
<Button x:Name="BtnTv" Content="🎬 TV" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="tv"/>
<Button x:Name="BtnRadio" Content="📡 Radio" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="radio"/>
<Button x:Name="BtnAll" Content="📺 Alle Sender" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="all"/>
<Button x:Name="BtnTv" Content="🎬 TV" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="tv"/>
<Button x:Name="BtnRadio" Content="📡 Radio" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="radio"/>
<Button x:Name="BtnOnline" Content="🌐 Online" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="online"
ToolTip="ÖR-TV und Webradio (Internet-Streams)"/>
<Separator Margin="8" Background="#333"/>
<Button x:Name="BtnFav" Content="⭐ Favoriten" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="fav"/>
<Button x:Name="BtnFav" Content="⭐ Favoriten" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="fav"/>
<Separator Margin="8" Background="#333"/>
<Button x:Name="BtnEpgGrid" Content="📅 Programm" Style="{StaticResource SidebarButton}" Click="BtnEpgGrid_Click"
ToolTip="Programmübersicht aller Sender"/>

View file

@ -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<Channel>());
List<Channel> fritzChannels;
try
{
fritzChannels = await fritzTask;
}
catch (Exception ex)
{
// FritzBox nicht erreichbar (z.B. unterwegs)? Trotzdem mit Online-Sendern weitermachen
fritzChannels = new List<Channel>();
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))

View file

@ -5,6 +5,9 @@ namespace FritzTV.Models;
public enum ChannelKind { TvSd, TvHd, Radio }
/// <summary>Woher kommt der Sender? FritzBox (DVB-C/lokal) oder Online (HLS-Stream)</summary>
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; }
/// <summary>Quelle: lokale FritzBox oder Online-Stream. Default ist FritzBox (Abwärtskompatibilität).</summary>
public ChannelSource Source { get; init; } = ChannelSource.FritzBox;
private bool _isFavorite;
public bool IsFavorite
{

View file

@ -10,6 +10,9 @@ public class AppSettings
public string LastChannel { get; set; } = "";
public double Volume { get; set; } = 80;
/// <summary>Online-Sender (ÖR-TV + Webradio aus Assets\online-sources.json) zusätzlich zur FritzBox-Liste anzeigen</summary>
public bool ShowOnlineSources { get; set; } = true;
private static readonly string ConfigPath = AppPaths.Settings;
public static AppSettings Load()

View file

@ -0,0 +1,79 @@
using System.IO;
using System.Text.Json;
using FritzTV.Models;
namespace FritzTV.Services;
/// <summary>
/// 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.
/// </summary>
public class OnlineSourceClient
{
private static readonly string JsonPath = Path.Combine(
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!,
"Assets", "online-sources.json");
public Task<List<Channel>> LoadAllAsync()
{
// Synchron lesen ist OK, die Datei liegt lokal und ist klein (<10 KB)
try
{
if (!File.Exists(JsonPath))
return Task.FromResult(new List<Channel>());
var json = File.ReadAllText(JsonPath);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var channels = new List<Channel>();
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<ChannelKind>(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<Channel>());
}
}
}

View file

@ -1,11 +1,14 @@
<Window x:Class="FritzTV.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Einstellungen" Height="240" Width="420"
Title="Einstellungen" Height="320" Width="440"
Background="#1A1A1A" Foreground="White"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@ -19,7 +22,16 @@
<TextBlock Grid.Row="2" Margin="0,4,0,0" FontSize="11" Foreground="#666"
Text="z.B. 192.168.178.1 oder fritz.box · DVB-C muss aktiv sein"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Separator Grid.Row="3" Margin="0,16,0,16" Background="#333"/>
<CheckBox Grid.Row="4" x:Name="ChkOnlineSources"
Content="Online-Sender anzeigen (ÖR-TV + Webradio)"
Foreground="White" FontSize="13"/>
<TextBlock Grid.Row="5" Margin="22,4,0,0" FontSize="11" Foreground="#666"
TextWrapping="Wrap"
Text="Zusätzliche Sender aus dem Internet (ARD, ZDF, 3sat, arte, Webradios). Funktioniert auch ohne FritzBox."/>
<StackPanel Grid.Row="7" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Abbrechen" Width="100" Height="32" Margin="0,0,8,0"
Click="BtnCancel_Click" Background="#333" Foreground="White" BorderThickness="0"/>
<Button Content="Speichern" Width="100" Height="32"

View file

@ -13,6 +13,7 @@ public partial class SettingsWindow : Window
DarkTitleBar.EnableFor(this);
_settings = settings;
TxtIp.Text = settings.FritzBoxIp;
ChkOnlineSources.IsChecked = settings.ShowOnlineSources;
}
private void BtnSave_Click(object sender, RoutedEventArgs e)
@ -25,6 +26,7 @@ public partial class SettingsWindow : Window
return;
}
_settings.FritzBoxIp = ip;
_settings.ShowOnlineSources = ChkOnlineSources.IsChecked == true;
DialogResult = true;
Close();
}