This commit is contained in:
Michal Vrba
2026-09-11 12:00:26 +02:00
parent 55ea0752b6
commit d2fc5b8f43
5 changed files with 291 additions and 8 deletions
@@ -17,7 +17,7 @@ public class RadioLibraryService
public async Task<List<RadioStation>> LoadStationsAsync() public async Task<List<RadioStation>> LoadStationsAsync()
{ {
var folders = AudioCatalog.Folders; var folders = await DiscoverFoldersAsync();
var loaded = new List<RadioStation>(); var loaded = new List<RadioStation>();
int index = 0; int index = 0;
@@ -31,6 +31,129 @@ public class RadioLibraryService
return _stations; return _stations;
} }
private async Task<List<string>> DiscoverFoldersAsync()
{
var folders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// 1. Try querying IIS / server directory listing for audio/
try
{
var response = await _http.GetAsync("audio/");
if (response.IsSuccessStatusCode)
{
var html = await response.Content.ReadAsStringAsync();
if (html.Contains("<html", StringComparison.OrdinalIgnoreCase) || html.Contains("<a ", StringComparison.OrdinalIgnoreCase) || html.Contains("<A ", StringComparison.OrdinalIgnoreCase))
{
var discovered = ParseDirectoryListing(html);
foreach (var f in discovered)
{
folders.Add(f);
}
}
}
}
catch
{
// Directory browsing might be disabled or unavailable
}
// 2. Merge with compiled AudioCatalog
foreach (var f in AudioCatalog.Folders)
{
folders.Add(f);
}
return folders.ToList();
}
private async Task<List<string>> DiscoverTracksAsync(string folder)
{
var tracks = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// 1. Try querying IIS / server directory listing for audio/{folder}/
try
{
var response = await _http.GetAsync($"audio/{folder}/");
if (response.IsSuccessStatusCode)
{
var html = await response.Content.ReadAsStringAsync();
if (html.Contains("<html", StringComparison.OrdinalIgnoreCase) || html.Contains("<a ", StringComparison.OrdinalIgnoreCase) || html.Contains("<A ", StringComparison.OrdinalIgnoreCase))
{
var discovered = ParseAudioFilesFromHtml(html);
foreach (var t in discovered)
{
tracks.Add(t);
}
}
}
}
catch
{
// Directory browsing might be disabled or unavailable
}
// 2. Merge with compiled AudioCatalog
if (AudioCatalog.FolderTracks.TryGetValue(folder, out var compiledTracks) && compiledTracks != null)
{
foreach (var t in compiledTracks)
{
tracks.Add(t);
}
}
// Deduplicate tracks by base name, preferring .mp3 over .wav
var audioExtOrder = new[] { ".mp3", ".ogg", ".flac", ".m4a", ".wav", ".aac" };
return tracks
.GroupBy(f => Path.GetFileNameWithoutExtension(f), StringComparer.OrdinalIgnoreCase)
.Select(g => g.OrderBy(f =>
{
var ext = Path.GetExtension(f).ToLowerInvariant();
var idx = Array.IndexOf(audioExtOrder, ext);
return idx >= 0 ? idx : 99;
}).First())
.OrderBy(t => t)
.ToList();
}
private List<string> ParseDirectoryListing(string html)
{
var list = new List<string>();
var matches = System.Text.RegularExpressions.Regex.Matches(html, @"href\s*=\s*[""']([^""'#?]+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
foreach (System.Text.RegularExpressions.Match m in matches)
{
var href = m.Groups[1].Value.Trim();
href = href.TrimEnd('/');
var name = Path.GetFileName(href);
if (string.IsNullOrWhiteSpace(name) || name.StartsWith(".") || name.Equals("audio", StringComparison.OrdinalIgnoreCase))
continue;
var ext = Path.GetExtension(name);
if (!string.IsNullOrEmpty(ext))
continue;
list.Add(name);
}
return list;
}
private List<string> ParseAudioFilesFromHtml(string html)
{
var list = new List<string>();
var validExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac" };
var matches = System.Text.RegularExpressions.Regex.Matches(html, @"href\s*=\s*[""']([^""'#?]+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
foreach (System.Text.RegularExpressions.Match m in matches)
{
var href = m.Groups[1].Value.Trim();
var name = Path.GetFileName(href);
var ext = Path.GetExtension(name);
if (validExts.Contains(ext))
{
list.Add(name);
}
}
return list;
}
private async Task<RadioStation> LoadStationFromFolderAsync(string folder, int index) private async Task<RadioStation> LoadStationFromFolderAsync(string folder, int index)
{ {
var formattedName = FormatStationName(folder); var formattedName = FormatStationName(folder);
@@ -61,13 +184,11 @@ public class RadioLibraryService
} }
// Dynamically discovered tracks from audio folder // Dynamically discovered tracks from audio folder
if (AudioCatalog.FolderTracks.TryGetValue(folder, out var fileNames) && fileNames != null && fileNames.Length > 0) var tracks = await DiscoverTracksAsync(folder);
foreach (var fileName in tracks)
{ {
foreach (var fileName in fileNames) var track = BuildTrack(folder, fileName, station.DefaultCoverUrl);
{ station.Tracks.Add(track);
var track = BuildTrack(folder, fileName, station.DefaultCoverUrl);
station.Tracks.Add(track);
}
} }
// If no tracks found, add a fallback track entry // If no tracks found, add a fallback track entry
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#181a24" />
<stop offset="100%" stop-color="#08090d" />
</linearGradient>
<linearGradient id="neon" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ff007f" />
<stop offset="50%" stop-color="#00f0ff" />
<stop offset="100%" stop-color="#39ff14" />
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<!-- Radio Body -->
<rect x="2" y="2" width="60" height="60" rx="12" fill="url(#bg)" stroke="#00f0ff" stroke-width="2.5" />
<!-- Tuning Scale Window -->
<rect x="8" y="10" width="48" height="18" rx="4" fill="#040a12" stroke="#ff007f" stroke-width="1.5" />
<!-- Scale markings -->
<line x1="14" y1="14" x2="14" y2="20" stroke="#00f0ff" stroke-width="1.2" opacity="0.9" />
<line x1="20" y1="16" x2="20" y2="20" stroke="#00f0ff" stroke-width="1" opacity="0.6" />
<line x1="26" y1="14" x2="26" y2="20" stroke="#00f0ff" stroke-width="1.2" opacity="0.9" />
<line x1="32" y1="12" x2="32" y2="24" stroke="#ff007f" stroke-width="2.2" filter="url(#glow)" />
<line x1="38" y1="14" x2="38" y2="20" stroke="#00f0ff" stroke-width="1.2" opacity="0.9" />
<line x1="44" y1="16" x2="44" y2="20" stroke="#00f0ff" stroke-width="1" opacity="0.6" />
<line x1="50" y1="14" x2="50" y2="20" stroke="#00f0ff" stroke-width="1.2" opacity="0.9" />
<!-- Equalizer / Speaker Bars -->
<rect x="9" y="34" width="5" height="18" rx="2" fill="#00f0ff" />
<rect x="17" y="40" width="5" height="12" rx="2" fill="#39ff14" />
<rect x="25" y="32" width="5" height="20" rx="2" fill="#ff007f" filter="url(#glow)" />
<!-- Big Tuning Knob -->
<circle cx="45" cy="44" r="10" fill="#12151f" stroke="#00f0ff" stroke-width="2" />
<circle cx="45" cy="44" r="3.5" fill="#ff007f" filter="url(#glow)" />
<line x1="45" y1="36" x2="45" y2="40" stroke="#00f0ff" stroke-width="2" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3 -1
View File
@@ -12,7 +12,9 @@
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/radio.css" /> <link rel="stylesheet" href="css/radio.css" />
<link rel="icon" type="image/png" href="icon-192.png" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="icon-192.png" />
<script type="importmap"></script> <script type="importmap"></script>
</head> </head>
+117
View File
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<!-- Allow static content file extensions and define explicit MIME types -->
<staticContent>
<!-- WebAssembly & Blazor Runtime files -->
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<remove fileExtension=".dll" />
<mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
<remove fileExtension=".pdb" />
<mimeMap fileExtension=".pdb" mimeType="application/octet-stream" />
<remove fileExtension=".dat" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<remove fileExtension=".blat" />
<mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<remove fileExtension=".jsonl" />
<mimeMap fileExtension=".jsonl" mimeType="application/x-jsonlines" />
<remove fileExtension=".webmanifest" />
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
<!-- Audio & Media files -->
<remove fileExtension=".mp3" />
<mimeMap fileExtension=".mp3" mimeType="audio/mpeg" />
<remove fileExtension=".wav" />
<mimeMap fileExtension=".wav" mimeType="audio/wav" />
<remove fileExtension=".ogg" />
<mimeMap fileExtension=".ogg" mimeType="audio/ogg" />
<remove fileExtension=".m4a" />
<mimeMap fileExtension=".m4a" mimeType="audio/mp4" />
<remove fileExtension=".flac" />
<mimeMap fileExtension=".flac" mimeType="audio/flac" />
<remove fileExtension=".aac" />
<mimeMap fileExtension=".aac" mimeType="audio/aac" />
<!-- Text, Lyrics & Station Metadata -->
<remove fileExtension=".txt" />
<mimeMap fileExtension=".txt" mimeType="text/plain; charset=utf-8" />
<remove fileExtension=".md" />
<mimeMap fileExtension=".md" mimeType="text/markdown; charset=utf-8" />
<!-- Fonts & Images -->
<remove fileExtension=".svg" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<remove fileExtension=".woff" />
<mimeMap fileExtension=".woff" mimeType="font/woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
<remove fileExtension=".ttf" />
<mimeMap fileExtension=".ttf" mimeType="font/ttf" />
<remove fileExtension=".otf" />
<mimeMap fileExtension=".otf" mimeType="font/otf" />
<remove fileExtension=".webp" />
<mimeMap fileExtension=".webp" mimeType="image/webp" />
</staticContent>
<!-- Request Filtering: Ensure IIS does not block downloading .dll or .wasm files -->
<security>
<requestFiltering>
<fileExtensions allowUnlisted="true">
<remove fileExtension=".dll" />
<add fileExtension=".dll" allowed="true" />
<remove fileExtension=".wasm" />
<add fileExtension=".wasm" allowed="true" />
<remove fileExtension=".dat" />
<add fileExtension=".dat" allowed="true" />
<remove fileExtension=".pdb" />
<add fileExtension=".pdb" allowed="true" />
</fileExtensions>
</requestFiltering>
</security>
<!-- SPA URL Rewrite: Route all non-file/non-directory requests to index.html -->
<rewrite>
<rules>
<rule name="SPA fallback routing" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.html" />
</rule>
</rules>
</rewrite>
<!-- Pass-through for custom errors so Blazor client gets proper HTTP response codes -->
<httpErrors errorMode="Custom" existingResponse="PassThrough" />
</system.webServer>
<!-- Enable directory browsing for audio folder so newly added MP3s / folders are dynamically listed -->
<location path="audio">
<system.webServer>
<directoryBrowse enabled="true" />
</system.webServer>
</location>
</configuration>