using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualBasic.FileIO; using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption; namespace FNZCM.ConHost.Ver2 { internal static class Program2 { //0. start http server //1. scan libraries and fill data struct // libs // albums // Tracks(FLAC / AAC_*) // Meta(title(artist) / duration) // FSI ( size ) // TODO: Generate thumbnail of BKS private static readonly ConcurrentDictionary Libraries = new(); private static readonly ConcurrentDictionary PathMapping = new(); private static readonly ConcurrentDictionary MediaTags = new(); private static bool _isRunning; private static bool _isLoading; private static DateTime _lastRequestAccepted; private static void Main() { Console.WriteLine("Starting..."); var tWorker = new Thread(Working); _isRunning = true; tWorker.Start(); Task.Run(ScanLibrary); Console.WriteLine("Press ENTER to Stop."); Console.ReadLine(); Console.WriteLine("Shutting down..."); _isRunning = false; tWorker.Join(); Console.WriteLine("Stopped."); Console.WriteLine(); Console.Write("Press ENTER to Exit."); Console.ReadLine(); } private static void ScanLibrary() { if (_isLoading) return; _isLoading = true; try { Console.WriteLine("Scanning libraries..."); MediaTags.Clear(); PathMapping.Clear(); Libraries.Clear(); foreach (var kvpLib in ConfigFile.Instance.Libraries) { if (_isRunning == false) throw new OperationCanceledException(); Console.WriteLine($"Library {kvpLib.Key} - {kvpLib.Value}"); var libPath = kvpLib.Key.ToLower(); var lib = Libraries[libPath] = new Library2(kvpLib.Key); var albDirArray = Directory.GetDirectories(kvpLib.Value); foreach (var albDir in albDirArray) { if (_isRunning == false) throw new OperationCanceledException(); Console.WriteLine($" Album {albDir}"); var albName = Path.GetFileName(albDir); var albPath = albName.ToLower(); var alb = lib.Albums[albPath] = new Album2(albName); var coverFilePath = Path.Combine(albDir, "cover.jpg"); if (File.Exists(coverFilePath)) PathMapping[$"/cover/{libPath}/{albPath}/cover.jpg"] = coverFilePath; var bkDir = Path.Combine(albDir, "bk"); if (Directory.Exists(bkDir)) { var bkFiles = FileSystem.GetFiles(bkDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern); foreach (var file in bkFiles) { var bkName = Path.GetFileName(file); var bkPath = bkName.ToLower(); alb.Bks[bkPath] = bkName; PathMapping[$"/bk/{libPath}/{albPath}/{bkPath}"] = file; } } var mainTrackFiles = FileSystem.GetFiles(albDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern); foreach (var mainTrackFile in mainTrackFiles) { var trackName = Path.GetFileName(mainTrackFile); var trackPath = trackName.ToLower(); alb.MainTracks[trackPath] = trackName; PathMapping[$"/media/{libPath}/{albPath}/{trackPath}"] = mainTrackFile; } var aacTrackDirArray = Directory.GetDirectories(albDir, "AAC_Q*"); foreach (var aacTrackDir in aacTrackDirArray) { var aacTrackSetName = Path.GetFileName(aacTrackDir); var aacTrackSetPath = aacTrackSetName.ToLower(); var aacTrackSet = alb.SubTracks[aacTrackSetPath] = new TrackSet(aacTrackSetName); foreach (var file in Directory.GetFiles(aacTrackDir)) { var aacTrackName = Path.GetFileName(file); var aacTrackPath = aacTrackName.ToLower(); aacTrackSet.Tracks[aacTrackPath] = aacTrackName; PathMapping[$"/media/{libPath}/{albPath}/{aacTrackSetPath}/{aacTrackPath}"] = file; } } } } Console.WriteLine("Looking tags..."); Parallel.ForEach(PathMapping.Keys.Where(p => p.StartsWith("/media/")), k => GetTag(k)); Console.WriteLine("Looking tags...Done"); } catch (Exception e) { Console.WriteLine($"Load error: {e}"); } _isLoading = false; } private static void Working() { var listener = new HttpListener(); listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix); listener.Start(); var upTime = DateTime.Now; Console.WriteLine($"HTTP Server started, listening on {ConfigFile.Instance.ListenPrefix}"); listener.BeginGetContext(ContextGet, listener); _lastRequestAccepted = DateTime.Now; while (_isRunning) { var timeSpan = DateTime.Now - _lastRequestAccepted; var up = DateTime.Now - upTime; Console.Title = "FNZCM" + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}" + $" / " + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}" ; Thread.Sleep(1000); } listener.Close(); Thread.Sleep(1000); } private static void ContextGet(IAsyncResult ar) { var listener = (HttpListener)ar.AsyncState; HttpListenerContext context; try { // ReSharper disable once PossibleNullReferenceException context = listener.EndGetContext(ar); } catch (Exception e) { Console.WriteLine(e); return; } if (_isRunning) listener.BeginGetContext(ContextGet, listener); ProcessRequest(context); } private static void ProcessRequest(HttpListenerContext context) { _lastRequestAccepted = DateTime.Now; var request = context.Request; Console.WriteLine($"Request from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}"); // GET / show all libraries // foo=library bar=album // GET /list/foo/ show all album and cover with name, provide m3u path // GET /list/foo/bar/bk/ list all picture as grid // GET /list/foo/bar/tracks/ list all tracks as text list // GET /list/foo/bar/playlist.m3u8 auto gen // GET /list/foo/bar/aac_q1.00/playlist.m3u8 auto gen // media streaming HTTP Partial RANGE SUPPORT // GET /cover/foo/bar/cover.jpg // GET /media/foo/bar/01.%20foobar.flac // GET /bk/foo/bar/foobar.jpg // GET /media/foo/aac_q1.00/01.%20foobar.m4a try { // ReSharper disable once PossibleNullReferenceException var requestPath = request.Url.LocalPath.ToLower(); var pathParts = (IReadOnlyList)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (requestPath == "/scan/") { Task.Run(ScanLibrary); context.Response.Redirect("/"); } else if (requestPath == "/") { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Libraries - {ConfigFile.Instance.Title} "); sb.Append(""); if (_isLoading) sb.Append("

Still Loading...

"); sb.Append($"

{ConfigFile.Instance.Title}

"); sb.Append("

Libraries

"); sb.Append("
    "); foreach (var library in Libraries.OrderBy(p => p.Key)) { sb.Append("
  • "); sb.Append($"{library.Value.Name}"); sb.Append("
  • "); } sb.Append("
"); sb.Append(" Scan Libraries"); context.Response.WriteText(sb.ToString()); } else if (pathParts.Count == 2 && pathParts[0] == "list") { var libName = pathParts[1]; if (Libraries.TryGetValue(libName, out var l)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Albums of {l.Name} - {ConfigFile.Instance.Title} "); sb.Append( ""); sb.Append($""); if (_isLoading) sb.Append("

Still Loading...

"); sb.Append($"

Albums of {l.Name}

"); sb.Append("
Back to home
"); //Cover list foreach (var a in l.Albums.OrderBy(p => p.Key)) { sb.Append("
"); sb.Append($""); sb.Append("
"); sb.Append($"[TRACKERS]"); if (a.Value.Bks?.Count > 0) sb.Append($"[BK]"); sb.Append("
"); sb.Append("
"); var totalDur = a.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{a.Key}/{p.Key}", true)?.Duration ?? 0); var totalLen = a.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{a.Key}/{p.Key}", true)?.Length ?? 0); sb.Append($"[M3U8({totalDur.FormatDuration()}){totalLen.FormatFileSize()}]"); if (a.Value.SubTracks.Count > 0) { foreach (var subTrack in a.Value.SubTracks) { totalDur = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{a.Key}/{subTrack.Key}/{p.Key}", true)?.Duration ?? 0); totalLen = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{a.Key}/{subTrack.Key}/{p.Key}", true)?.Length ?? 0); sb.Append($"
[{subTrack.Value.Name}({totalDur.FormatDuration()}){totalLen.FormatFileSize()}]"); } } sb.Append("
"); sb.Append($"
{a.Value.Name}
"); sb.Append("
"); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "tracks") { var libName = pathParts[1]; var albPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var l) && l.Albums.TryGetValue(albPath, out var alb)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($""); if (_isLoading) sb.Append("

Still Loading...

"); sb.Append($"

Tracks of

{alb.Name}

"); sb.Append($"
Back to library
"); var durTotal = 0; var sizeTotal = 0L; var sbm = new StringBuilder(); foreach (var kvpTrack in alb.MainTracks.OrderBy(p => p.Key)) { sbm.Append($"
  • "); sbm.Append($"{kvpTrack.Value}"); var tag = GetTag($"/media/{libName}/{albPath}/{kvpTrack.Key}"); durTotal += tag.Duration; sizeTotal += tag.Length; sbm.Append($" ({tag.Duration.FormatDuration()}) {tag.Length.FormatFileSize()}"); sbm.Append($"
  • "); } sb.Append($"

    Main ({durTotal.FormatDuration()}) {sizeTotal.FormatFileSize()}

    "); sb.Append(sbm); foreach (var kvpSubSet in alb.SubTracks.OrderBy(p => p.Key)) { durTotal = 0; sizeTotal = 0L; sbm.Clear(); foreach (var kvpTrack in kvpSubSet.Value.Tracks.OrderBy(p => p.Key)) { sbm.Append($"
  • "); sbm.Append($"{kvpTrack.Value}"); var tag = GetTag($"/media/{libName}/{albPath}/{kvpSubSet.Key}/{kvpTrack.Key}"); durTotal += tag.Duration; sizeTotal += tag.Length; sbm.Append($" ({tag.Duration.FormatDuration()}) {tag.Length.FormatFileSize()}"); sbm.Append($"
  • "); } sb.Append($"

    {kvpSubSet.Value.Name} ({durTotal.FormatDuration()}) {sizeTotal.FormatFileSize()}

    "); sb.Append(sbm); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "bk") { var libName = pathParts[1]; var albPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib) && lib.Albums.TryGetValue(albPath, out var alb)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($""); if (_isLoading) sb.Append("

    Still Loading...

    "); sb.Append($"

    BK of

    {alb.Name}

    "); sb.Append($"
    Back to library
    "); foreach (var albBk in alb.Bks.OrderBy(p => p.Key)) { //TODO: auto gen thumbnail 512x512 jpg 80 sb.Append($""); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "playlist.m3u8") { var libName = pathParts[1]; var albPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib) && lib.Albums.TryGetValue(albPath, out var alb)) { // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}"; var sb = new StringBuilder(); sb.AppendLine("#EXTM3U"); foreach (var track in alb.MainTracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{albPath}/{track.Key}"); if (mediaTag != null) { var coverPath = $"/cover/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/cover.jpg"; sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{prefix + coverPath}\",{mediaTag.Title}"); } var mediaPath = $"/media/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{track.Key.FuckVlcAndEscape()}"; sb.AppendLine(prefix + mediaPath); } context.Response.ContentType = "audio/mpegurl"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 5 && pathParts[0] == "list" && pathParts[4] == "playlist.m3u8") { var libName = pathParts[1]; var albPath = pathParts[2]; var subSetPath = pathParts[3]; if (Libraries.TryGetValue(libName, out var lib) && lib.Albums.TryGetValue(albPath, out var alb)) { // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}"; if (false == alb.SubTracks.TryGetValue(subSetPath, out var trackSet)) { context.Response.StatusCode = 404; } else { var sb = new StringBuilder(); sb.AppendLine("#EXTM3U"); foreach (var track in trackSet.Tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{albPath}/{subSetPath}/{track.Key}"); if (mediaTag != null) { var coverPath = $"/cover/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/cover.jpg"; sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{prefix + coverPath}\",{mediaTag.Title}"); } var mediaPath = $"/media/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{subSetPath.FuckVlcAndEscape()}/{track.Key.FuckVlcAndEscape()}"; sb.AppendLine(prefix + mediaPath); } context.Response.ContentType = "audio/mpegurl"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (PathMapping.TryGetValue(requestPath, out var realPath)) { var range = request.Headers.GetValues("Range"); FileStream fs = null; try { fs = File.OpenRead(realPath); if (range is { Length: > 0 }) { var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries); if (rngParts.Length >= 1 && long.TryParse(rngParts[0], out var start)) { fs.Position = start; context.Response.StatusCode = 206; context.Response.Headers.Add("Accept-Ranges", "bytes"); context.Response.Headers.Add("Content-Range", $"bytes {start}-{fs.Length - 1}/{fs.Length}"); context.Response.ContentLength64 = fs.Length - start; context.Response.ContentType = "video/mp4"; fs.CopyTo(context.Response.OutputStream); } } else { context.Response.ContentType = "video/mp4"; context.Response.ContentLength64 = fs.Length; fs.CopyTo(context.Response.OutputStream); } } catch (Exception e) { Console.WriteLine(e); } finally { fs?.Close(); } } else { context.Response.StatusCode = 404; } } catch (Exception e) { Console.WriteLine(e); try { context.Response.StatusCode = 500; } catch (Exception exception) { Console.WriteLine(exception); } } finally { try { context.Response.Close(); } catch (Exception e) { Console.WriteLine(e); } } } private static string FormatDuration(this int second) { var sbd = new StringBuilder(); var ts = TimeSpan.FromSeconds(second); if (ts.TotalHours > 1) sbd.Append($"{ts.TotalHours:00}:"); sbd.Append($"{ts.Minutes:00}:{ts.Seconds:00}"); return sbd.ToString(); } private static string FormatFileSize(this long length) { string[] sizes = { "B", "KB", "MB", "GB", "TB" }; double len = length; int order = 0; while (len >= 1024 && order < sizes.Length - 1) { order++; len = len / 1024; } // Adjust the format string to your preferences. For example "{0:0.#}{1}" would // show a single decimal place, and no space. string result = String.Format("{0:0.##} {1}", len, sizes[order]); return result; } private static void WriteText(this HttpListenerResponse response, string content) { var bytes = Encoding.UTF8.GetBytes(content); response.OutputStream.Write(bytes); } private static string FuckVlcAndEscape(this string input) { if (input == null) return null; return input .Replace("[", "%5B") .Replace("]", "%5D") .Replace("'", "%27") ; } private static MediaTag2 GetTag(string internalPath, bool peek = false) { if (peek) { if (MediaTags.TryGetValue(internalPath, out var mediaTag)) { return mediaTag; } return null; } else { if (false == MediaTags.TryGetValue(internalPath, out var mediaTag) && PathMapping.TryGetValue(internalPath, out var mediaFilePath)) { var fi = new FileInfo(mediaFilePath); using var tagLib = TagLib.File.Create(mediaFilePath); mediaTag = MediaTags[internalPath] = new MediaTag2( $"{string.Join(";", tagLib.Tag.Performers)} - {tagLib.Tag.Title}", (int)tagLib.Properties.Duration.TotalSeconds, fi.Length ); } return mediaTag; } } } }