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 Microsoft.VisualBasic.FileIO; using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption; namespace FNZCM.ConHost.Ver0 { internal static class Program0 { private static bool _isRunning; private static IReadOnlyDictionary _library; private static IReadOnlyDictionary _pathMapping; private const int SmallFileSize = 1024 * 1024 * 5; //5MB private static IReadOnlyDictionary _smallFileCaches; private static readonly ConcurrentDictionary MediaTags = new(); private static DateTime _lastRequestAccepted; private static void Main0() { Console.WriteLine("Scanning libraries..."); var libs = new Dictionary(); var mappings = new Dictionary(); foreach (var library in ConfigFile.Instance.Libraries) { var libraryName = library.Key; var libraryPathName = libraryName.ToLower(); var albums = Directory.GetDirectories(library.Value); var albumObjects = new List(); foreach (var albumPath in albums) { var albumName = Path.GetFileName(albumPath); var albumPathName = albumName.ToLower(); //media var mediaFiles = FileSystem.GetFiles(albumPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern); foreach (var mediaFile in mediaFiles) mappings[$"/media/{library.Key}/{albumName}/{Path.GetFileName(mediaFile)}".ToLower()] = mediaFile; var tracks = mediaFiles.Select(Path.GetFileName).ToArray(); var trackPaths = tracks.Select(p => p.ToLower()).ToArray(); //bk var bkPath = Path.Combine(albumPath, "bk"); IReadOnlyCollection bks = null; if (Directory.Exists(bkPath)) { var bkFiles = FileSystem.GetFiles(bkPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern); foreach (var bkFile in bkFiles) mappings[$"/bk/{library.Key}/{albumName}/{Path.GetFileName(bkFile)}".ToLower()] = bkFile; bks = bkFiles.Select(s => Path.GetFileName(s).ToLower()).ToArray(); } //cover var coverPath = Path.Combine(albumPath, "cover.jpg"); if (File.Exists(coverPath)) mappings[$"/cover/{library.Key}/{albumName}/{Path.GetFileName(coverPath)}".ToLower()] = coverPath; albumObjects.Add(new Album(albumName, albumPathName, tracks, trackPaths, bks)); } libs[library.Key.ToLower()] = new Library(libraryName, libraryPathName, albumObjects); } _library = libs; _pathMapping = mappings; Console.WriteLine($" Scanned all {libs.Count} library"); Console.WriteLine($" Total {libs.Sum(p => p.Value.Albums.Count)} Albums, {libs.Sum(p => p.Value.Albums.Sum(q => q.Tracks.Count))} Tracks"); Console.Write("Reading small files to cache"); Console.WriteLine(); var caches = new Dictionary(); foreach (var map in _pathMapping) { var fileSize = new FileInfo(map.Value).Length; if (fileSize < SmallFileSize) { Console.Write($" Reading: {fileSize / (1024 * 1024.0):N6} MB {map.Value} "); caches[map.Key] = File.ReadAllBytes(map.Value); var left = Console.CursorLeft; Console.CursorLeft = 0; Console.Write("".PadLeft(left)); Console.CursorLeft = 0; } } _smallFileCaches = caches; Console.WriteLine($" Cached {_smallFileCaches.Count} small files, total {_smallFileCaches.Values.Sum(p => p.Length) / (1024.0 * 1024):N6} MB"); Console.WriteLine("Starting..."); var tWorker = new Thread(Working); _isRunning = true; tWorker.Start(); 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 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 // 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 try { // ReSharper disable once PossibleNullReferenceException var requestPath = request.Url.LocalPath.ToLower(); var pathParts = (IReadOnlyList)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (requestPath == "/") { var sb = new StringBuilder(); sb.Append($" Libraries - {ConfigFile.Instance.Title} "); sb.Append("

Libraries

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

Albums of {lib.Name}

"); sb.Append(""); //Cover list foreach (var a in lib.Albums) { sb.Append("
"); sb.Append($""); sb.Append("
"); sb.Append($"[TRACKERS]"); if (a.Bks != null) sb.Append($"[BK]"); sb.Append($"[M3U8]"); sb.Append("
"); sb.Append($"{a.Name}"); sb.Append("
"); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { 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]; Album alb; if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath))) { var sb = new StringBuilder(); sb.Append(""); sb.Append($"

Tracks of

{alb.Name}

"); sb.Append($""); for (var i = 0; i < alb.TrackPaths.Count; i++) { sb.Append($"
  • {alb.Tracks[i]}
  • "); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { 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]; Album alb; if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath))) { var sb = new StringBuilder(); sb.Append(""); sb.Append($"

    BK of

    {alb.Name}

    "); sb.Append($""); foreach (var albBk in alb.Bks) { //TODO: auto gen thumbnail sb.Append($""); } context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { 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]; Album alb; if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath))) { // 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.TrackPaths) { var mediaKey = $"/media/{lib.PathName}/{alb.PathName}/{track}"; var coverKey = $"/cover/{lib.PathName}/{alb.PathName}/cover.jpg"; if (false == MediaTags.TryGetValue(mediaKey, out var mediaTag)) { using var tagLib = TagLib.File.Create(_pathMapping[mediaKey]); mediaTag = MediaTags[mediaKey] = new MediaTag($"{string.Join(";", tagLib.Tag.Performers)} - {tagLib.Tag.Title}", (int)tagLib.Properties.Duration.TotalSeconds); } sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{prefix + coverKey.FuckVlc()}\",{mediaTag.Title}"); sb.AppendLine(prefix + mediaKey.FuckVlc()); } context.Response.ContentType = "audio/mpegurl"; context.Response.ContentEncoding = Encoding.UTF8; context.Response.WriteText(sb.ToString()); } else { context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (_pathMapping.TryGetValue(requestPath, out var realPath)) { var range = request.Headers.GetValues("Range"); if (_smallFileCaches.TryGetValue(requestPath, out var data)) { 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)) { context.Response.StatusCode = 206; context.Response.Headers.Add("Accept-Ranges", "bytes"); context.Response.Headers.Add("Content-Range", $"bytes {start}-{data.Length - 1}/{data.Length}"); context.Response.ContentLength64 = data.Length - start; context.Response.ContentType = "video/mp4"; context.Response.OutputStream.Write(new ReadOnlySpan(data, (int)start, (int)(data.Length - start))); } } else { context.Response.ContentType = "video/mp4"; context.Response.OutputStream.Write(data); } } else { 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 void WriteText(this HttpListenerResponse response, string content) { var bytes = Encoding.UTF8.GetBytes(content); response.OutputStream.Write(bytes); } private static string FuckVlc(this string input) { if (input == null) return null; return input .Replace("[", "%5B") .Replace("]", "%5D") ; } } }