using FNZCM.Core; using FNZCM.Shared.Helpers; using FNZCM.Shared.MediaModels; using FNZCM.Shared.MetadataModels; using Microsoft.VisualBasic.FileIO; using Newtonsoft.Json; 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 SearchOption = Microsoft.VisualBasic.FileIO.SearchOption; namespace FNZCM.ConHost { internal static class HostProgram { //0. start http server //1. scan libraries and fill data struct // libs // disc // 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 Modules = new(); private static ConcurrentDictionary MediaTags = new(); private static string _fileSetJson; private static string _fileSetJsonChecksum; private static string _tagSetJson; private static string _tagSetJsonChecksum; private static DateTime _cacheTime; private static LoadedModule _defaultModule; private static bool _isRunning; private static bool _isLoading; private static int TotalTrackCount => PathMapping.Count(p => p.Key.StartsWith("/media/")); private static string LoadingText => $"Loading... {MediaTags.Count} / {TotalTrackCount}"; private static DateTime _lastRequestAccepted; private static int _requestId = 0; private static void Main() { Console.WriteLine("Starting..."); var tWorker = new Thread(Working); _isRunning = true; tWorker.Start(); Task.Run(ReloadConfig); 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 ReloadConfig() { if (_isLoading) { Console.WriteLine("Still loading, SKIP"); return; } _isLoading = true; try { ConfigFile.Reload(); ReloadModulesInternal(); 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 Library(kvpLib.Key); var discDirArray = Directory.GetDirectories(kvpLib.Value); foreach (var discDir in discDirArray) { if (_isRunning == false) throw new OperationCanceledException(); Console.WriteLine($" Disc {discDir}"); var discName = Path.GetFileName(discDir); var discPath = discName.ToLower(); try { var disc = new Disc(discName); var bkDir = Path.Combine(discDir, "bk"); var mainTrackFiles = FileSystem.GetFiles(discDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern); foreach (var mainTrackFile in mainTrackFiles) { var trackName = Path.GetFileName(mainTrackFile); var trackPath = trackName.ToLower(); disc.MainTracks[trackPath] = trackName; PathMapping[$"/media/{libPath}/{discPath}/{trackPath}"] = mainTrackFile; } 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(); disc.Bks[bkPath] = bkName; PathMapping[$"/bk/{libPath}/{discPath}/{bkPath}"] = file; } } var aacTrackDirArray = Directory.GetDirectories(discDir, "AAC_Q*"); foreach (var aacTrackDir in aacTrackDirArray) { var aacTrackSetName = Path.GetFileName(aacTrackDir); var aacTrackSetPath = aacTrackSetName.ToLower(); var aacTrackSet = disc.SubTracks[aacTrackSetPath] = new TrackSet(aacTrackSetName); foreach (var file in FileSystem.GetFiles(aacTrackDir, SearchOption.SearchTopLevelOnly, "*.m4a", "*.mp4")) { var aacTrackName = Path.GetFileName(file); var aacTrackPath = aacTrackName.ToLower(); aacTrackSet.Tracks[aacTrackPath] = aacTrackName; PathMapping[$"/media/{libPath}/{discPath}/{aacTrackSetPath}/{aacTrackPath}"] = file; } } var coverFilePath = Path.Combine(discDir, "cover.jpg"); if (File.Exists(coverFilePath)) PathMapping[$"/cover/{libPath}/{discPath}/cover.jpg"] = coverFilePath; lib.Discs[discPath] = disc; } catch (Exception ex) { Console.WriteLine(ex); } } } Console.WriteLine("Loading meta caches..."); var cache = MetaCache.Load(); if (cache != null) MediaTags = cache; Console.WriteLine("Looking tags..."); foreach (var k in PathMapping.Keys.Where(p => p.StartsWith("/media/"))) { GetTag(k); } Console.WriteLine("Saving meta caches..."); MetaCache.Save(MediaTags); _fileSetJson = JsonConvert.SerializeObject(Libraries); _tagSetJson = JsonConvert.SerializeObject(MediaTags); _fileSetJsonChecksum = _fileSetJson.ToMd5(); _tagSetJsonChecksum = _tagSetJson.ToMd5(); _cacheTime = DateTime.Now; Console.WriteLine("Looking tags...Done"); } catch (Exception e) { Console.WriteLine($"Load error: {e}"); } _isLoading = false; } private static void ReloadModules() { ConfigFile.Reload(); ReloadModulesInternal(); } private static void ReloadModulesInternal() { Modules.Clear(); _defaultModule = null; if (ConfigFile.Instance.Modules?.Any() == true) { foreach (var modEnt in ConfigFile.Instance.Modules) { Console.WriteLine($"Loading module `{modEnt.Value.DisplayText}'..."); var module = new LoadedModule { VirtualPath = modEnt.Key, DisplayText = modEnt.Value.DisplayText, DefaultDocument = modEnt.Value.DefaultDocument, EnableFallbackRoute = modEnt.Value.EnableFallbackRoute, HtmlBaseReplace = modEnt.Value.HtmlBaseReplace, Files = new Dictionary() }; if (Directory.Exists(modEnt.Value.Path)) { //load by fs var files = Directory.GetFiles(modEnt.Value.Path, "*", System.IO.SearchOption.AllDirectories); foreach (var item in files) { var k = item.Substring(modEnt.Value.Path.Length + 1).Replace("\\", "/").ToLower(); module.Files[k] = File.ReadAllBytes(item); } } else if (File.Exists(modEnt.Value.Path)) { //load by package using var arc = SharpCompress.Archives.ArchiveFactory.Open(modEnt.Value.Path); foreach (var ent in arc.Entries.Where(p => p.IsDirectory == false)) { var buf = new byte[ent.Size]; using var s = ent.OpenEntryStream(); var r = s.Read(buf, 0, buf.Length); module.Files[ent.Key.ToLower()] = buf; } } else { Console.WriteLine("WARN: resource not found"); continue; } if (modEnt.Value.IsDefault && _defaultModule == null) _defaultModule = module; Modules[modEnt.Key] = module; Console.WriteLine($"Module `{modEnt.Value.DisplayText}' loaded."); } } } private static void Working() { var listener = new HttpListener(); listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix); if (ConfigFile.Instance.AliasPrefix?.Any() == true) foreach (var prefix in ConfigFile.Instance.AliasPrefix) listener.Prefixes.Add(prefix); 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 num = Interlocked.Increment(ref _requestId); var request = context.Request; Console.WriteLine($"Request #{num:00000} from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}"); try { var requestAbsPrefix = ConfigFile.Instance.M3uPrefix ?? $"{request.Url.Scheme + Uri.SchemeDelimiter + request.Url.Host + ":" + request.Url.Port}"; if (request.Headers.AllKeys.Contains("Origin") && false == string.IsNullOrEmpty(request.Headers["Origin"])) { if (ConfigFile.Instance.AccessControlAllowOrigin?.Any(p => request.Headers["Origin"].StartsWith(p)) == true) context.Response.Headers["Access-Control-Allow-Origin"] = "*"; } // GET / show all libraries // foo=library bar=disc // GET /list/foo/ show all disc 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 /lib_list/foo/playlist.m3u8 auto gen // GET /lib_list/foo/aac_q1.00/playlist.m3u8 auto gen // 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 image/ // GET /media/foo/bar/01.%20foobar.flac audio/ // GET /media/foo/bar/aac_q1.00/01.%20foobar.m4a audio/ // GET /bk/foo/bar/foobar.jpg image/ // metadata catalog // GET /metadata/progress.json // GET /metadata/file-set.json // GET /metadata/tag-dict.json // loadable module // GET /module/// // ReSharper disable once PossibleNullReferenceException var requestPath = request.Url.LocalPath.ToLower(); var pathParts = (IReadOnlyList)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (requestPath == "/admin/" && false == request.QueryString.AllKeys.Contains("action")) { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Admin - {ConfigFile.Instance.Title} "); sb.Append(""); sb.Append($"

Admin

"); sb.Append(""); sb.Append($"
"); sb.Append($"Password: "); sb.Append($"
"); sb.Append($"Operation: "); sb.Append($" "); sb.Append($" "); sb.Append($" "); sb.Append($"
"); context.WriteTextUtf8(sb.ToString()); } else if (requestPath == "/admin/" && request.QueryString["action"] == "Reload" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword) { Task.Run(ReloadConfig); context.Response.Redirect("/"); } else if (requestPath == "/admin/" && request.QueryString["action"] == "ReloadModules" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword) { Task.Run(ReloadModules); context.Response.Redirect("/"); } else if (requestPath == "/admin/" && request.QueryString["action"] == "ReloadFully" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword) { MetaCache.Clear(); Task.Run(ReloadConfig); context.Response.Redirect("/"); } else if (requestPath == "/admin/") { context.Response.Redirect("/"); } else if (requestPath.StartsWith("/modules/") && pathParts.Count > 1) { var moduleKey = pathParts[1]; if (Modules.TryGetValue(moduleKey, out var module)) { var entPath = string.Join("/", pathParts.Skip(2)); void Output(byte[] bin) { if (entPath.ToLower().EndsWith(".js")) context.Response.ContentType = "application/javascript"; else if (entPath.ToLower().EndsWith(".wasm")) context.Response.ContentType = "application/wasm"; else if (entPath.ToLower().EndsWith(".css")) context.Response.ContentType = "text/css"; else if (module.HtmlBaseReplace != null && entPath.ToLower().EndsWith(".html")) { //base replace var html = Encoding.UTF8.GetString(bin); var r = html.Replace(module.HtmlBaseReplace, $""); bin = Encoding.UTF8.GetBytes(r); } else context.Response.ContentType = "application/octet-stream"; context.Response.ContentLength64 = bin.Length; context.Response.OutputStream.Write(bin, 0, bin.Length); } if (module.Files.TryGetValue(entPath, out var bin)) { Output(bin); } else if (module.EnableFallbackRoute && module.Files.TryGetValue(module.DefaultDocument, out var defBin)) { entPath = module.DefaultDocument; Output(defBin); } else context.Response.StatusCode = 404; } else context.Response.StatusCode = 404; } else if (requestPath == "/" && _defaultModule != null) { if (_defaultModule.EnableFallbackRoute) context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/"); else context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/{_defaultModule.DefaultDocument}"); } else if (requestPath == "/" || requestPath == "/classic-index") { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Libraries - {ConfigFile.Instance.Title} "); sb.Append(""); if (_isLoading) sb.Append($"

{LoadingText}

"); sb.Append($"

{ConfigFile.Instance.Title}

"); if (Modules?.Any() == true) { sb.Append($"

Modules

"); sb.Append($"

(Total number of modules: {Modules.Count})

"); sb.Append(""); } sb.Append($"

Libraries

"); sb.Append($"

(Total number of disc: {Libraries.Sum(p => p.Value.Discs.Count)})

"); sb.Append("
    "); foreach (var library in Libraries.OrderBy(p => p.Key)) { sb.Append("
  • "); sb.Append($"{library.Value.Name}"); sb.Append($"
        Number of disc: {library.Value.Discs.Count}"); sb.Append("
  • "); } sb.Append("
"); sb.Append($"
"); sb.Append($"
Your IP: {context?.Request?.RemoteEndPoint?.Address.ToString() ?? "Unknown"}
"); sb.Append($""); sb.Append($"
-
"); sb.Append($"
Author: Coder (V)
"); sb.Append($""); sb.Append($""); context.WriteTextUtf8(sb.ToString()); } else if (pathParts.Count == 2 && pathParts[0] == "list") { var libName = pathParts[1]; if (Libraries.TryGetValue(libName, out var lib)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Discs of {lib.Name} - {ConfigFile.Instance.Title} "); sb.Append( ""); sb.Append(""); sb.Append($""); if (_isLoading) sb.Append($"

{LoadingText}

"); sb.Append($"

Discs of {lib.Name}

"); sb.Append(""); if (!_isLoading) { sb.Append("
"); //big m3u8 var trackKeys = lib.Discs.SelectMany(p => p.Value.MainTracks.Keys.Select(q => new { DiscName = p.Key, TrackName = q })).ToArray(); var totalDur = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{p.TrackName}", true)?.Duration); var totalLen = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{p.TrackName}", true)?.Length); sb.Append($"Number of track: {trackKeys.Length}"); sb.Append($"
{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} ALL_M3U8_MAIN"); var subTrackSetNames = lib.Discs.SelectMany(p => p.Value.SubTracks.Keys).Distinct().ToArray(); foreach (var setName in subTrackSetNames) { trackKeys = lib.Discs.SelectMany(p => { if (p.Value.SubTracks.TryGetValue(setName, out var tSet)) { return tSet.Tracks.Select(q => new { DiscName = p.Key, TrackName = q.Key }); } return new string[0].Select(q => new { DiscName = p.Key, TrackName = q }); }).ToArray(); totalDur = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{setName}/{p.TrackName}", true)?.Duration); totalLen = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{setName}/{p.TrackName}", true)?.Length); sb.Append($"
{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} ALL_M3U8_{setName.ToUpper()}"); } sb.Append("
"); } //Cover list foreach (var disc in lib.Discs.OrderByDescending(p => p.Key)) { sb.Append("
"); sb.Append($"
"); sb.Append($""); sb.Append($"
{disc.Value.Name}
"); sb.Append($"
"); sb.Append(""); sb.Append("
"); } context.WriteTextUtf8(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 discPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var l) && l.Discs.TryGetValue(discPath, out var disc)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($""); if (_isLoading) sb.Append($"

{LoadingText}

"); sb.Append($"

Tracks of

{disc.Name}

"); sb.Append($""); sb.Append($""); var durTotal = 0; var sizeTotal = 0L; var sbm = new StringBuilder(); foreach (var kvpTrack in disc.MainTracks.OrderBy(p => p.Key)) { sbm.Append($"
  • "); sbm.Append($"{kvpTrack.Value}"); var tag = GetTag($"/media/{libName}/{discPath}/{kvpTrack.Key}"); durTotal += tag.Duration; sizeTotal += tag.Length; sbm.Append($"
          {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}"); sbm.Append($"
  • "); } sb.Append($"

    Main ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}

    "); sb.Append(sbm); foreach (var kvpSubSet in disc.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}/{discPath}/{kvpSubSet.Key}/{kvpTrack.Key}"); durTotal += tag.Duration; sizeTotal += tag.Length; sbm.Append($"
          {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}"); sbm.Append($"
  • "); } sb.Append($"

    {kvpSubSet.Value.Name} ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}

    "); sb.Append(sbm); } context.WriteTextUtf8(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 discPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc)) { var sb = new StringBuilder(); sb.Append(""); sb.Append($""); if (_isLoading) sb.Append($"

    {LoadingText}

    "); sb.Append($"

    BK of

    {disc.Name}

    "); sb.Append($""); foreach (var discBk in disc.Bks.OrderBy(p => p.Key)) { sb.Append($""); } context.WriteTextUtf8(sb.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 3 && pathParts[0] == "lib_list" && pathParts[2] == "playlist.m3u8.html") { var libName = pathParts[1]; if (Libraries.TryGetValue(libName, out var lib)) { var sb = new StringBuilder(); //var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}"; foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key)) { var disc = discKvp.Value; var discPath = discKvp.Key; var tracks = disc.MainTracks; foreach (var track in tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}"); var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}"; //
  • TitleArtist
  • if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"
  • " + $"{mediaTag.Title}" + $"{mediaTag.Artist}" + $"{mediaTag.Duration.SecondToDur()}" + $"
  • "); } else { sb.AppendLine($"
  • {track.Value}
  • "); } } } var sbPage = new StringBuilder(Resource.listPage); sbPage.Replace("", sb.ToString()); context.WriteTextUtf8(sbPage.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 3 && pathParts[0] == "lib_list" && pathParts[2] == "playlist.m3u8") { var libName = pathParts[1]; if (Libraries.TryGetValue(libName, out var lib)) { var sb = new StringBuilder(); sb.WriteM3U8Header(); foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key)) { var disc = discKvp.Value; var discPath = discKvp.Key; var tracks = disc.MainTracks; foreach (var track in tracks.OrderBy(p => p.Key)) { var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg"; var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}"); if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}"); } else { sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}"); } var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}"; sb.AppendLine(requestAbsPrefix + mediaPath); } } context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 4 && pathParts[0] == "lib_list" && pathParts[3] == "playlist.m3u8.html") { var libName = pathParts[1]; var trackSetName = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib)) { var sb = new StringBuilder(); foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key)) { var disc = discKvp.Value; var discPath = discKvp.Key; if (disc.SubTracks.TryGetValue(trackSetName, out var tracksSet)) { var tracks = tracksSet.Tracks; foreach (var track in tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{trackSetName}/{track.Key}"); var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{trackSetName.UrlEscape()}/{track.Key.UrlEscape()}"; if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"
  • " + $"{mediaTag.Title}" + $"{mediaTag.Artist}" + $"{mediaTag.Duration.SecondToDur()}" + $"
  • "); } else { sb.AppendLine($"
  • {track.Value}
  • "); } } } } var sbPage = new StringBuilder(Resource.listPage); sbPage.Replace("", sb.ToString()); context.WriteTextUtf8(sbPage.ToString()); } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (pathParts.Count == 4 && pathParts[0] == "lib_list" && pathParts[3] == "playlist.m3u8") { var libName = pathParts[1]; var trackSetName = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib)) { var sb = new StringBuilder(); sb.WriteM3U8Header(); foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key)) { var disc = discKvp.Value; var discPath = discKvp.Key; if (disc.SubTracks.TryGetValue(trackSetName, out var tracksSet)) { var tracks = tracksSet.Tracks; foreach (var track in tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{trackSetName}/{track.Key}"); var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg"; var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{trackSetName.UrlEscape()}/{track.Key.UrlEscape()}"; if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}"); } else { sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}"); } sb.AppendLine(requestAbsPrefix + mediaPath); } } } context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U); } 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.html") { var libName = pathParts[1]; var discPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc)) { var sb = new StringBuilder(); foreach (var track in disc.MainTracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}"); var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}"; if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"
  • " + $"{mediaTag.Title}" + $"{mediaTag.Artist}" + $"{mediaTag.Duration.SecondToDur()}" + $"
  • "); } else { sb.AppendLine($"
  • {track.Value}
  • "); } } var sbPage = new StringBuilder(Resource.listPage); sbPage.Replace("", sb.ToString()); context.WriteTextUtf8(sbPage.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 discPath = pathParts[2]; if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc)) { var sb = new StringBuilder(); sb.WriteM3U8Header(); sb.AppendLine($"#EXTALB: {discPath}"); var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg"; foreach (var track in disc.MainTracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}"); if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}"); } else { sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}"); } var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}"; sb.AppendLine(requestAbsPrefix + mediaPath); } context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U); } 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.html") { var libName = pathParts[1]; var discPath = pathParts[2]; var subSetPath = pathParts[3]; if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc)) { if (false == disc.SubTracks.TryGetValue(subSetPath, out var trackSet)) { context.Response.StatusCode = 404; } else { var sb = new StringBuilder(); foreach (var track in trackSet.Tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{subSetPath}/{track.Key}"); var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{subSetPath.UrlEscape()}/{track.Key.UrlEscape()}"; if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"
  • " + $"{mediaTag.Title}" + $"{mediaTag.Artist}" + $"{mediaTag.Duration.SecondToDur()}" + $"
  • "); } else { sb.AppendLine($"
  • {track.Value}
  • "); } } var sbPage = new StringBuilder(Resource.listPage); sbPage.Replace("", sb.ToString()); context.WriteTextUtf8(sbPage.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 discPath = pathParts[2]; var subSetPath = pathParts[3]; if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc)) { if (false == disc.SubTracks.TryGetValue(subSetPath, out var trackSet)) { context.Response.StatusCode = 404; } else { var sb = new StringBuilder(); sb.WriteM3U8Header(); foreach (var track in trackSet.Tracks.OrderBy(p => p.Key)) { var mediaTag = GetTag($"/media/{libName}/{discPath}/{subSetPath}/{track.Key}"); var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg"; if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title)) { sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}"); } else { sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}"); } var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{subSetPath.UrlEscape()}/{track.Key.UrlEscape()}"; sb.AppendLine(requestAbsPrefix + mediaPath); } context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U); } } else { context.Response.StatusCode = 404; //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } } else if (requestPath == "/metadata/progress.json") { var dic = new Dictionary() { {"IsLoading",_isLoading}, {nameof(TotalTrackCount),TotalTrackCount}, {nameof(MediaTags),MediaTags.Count}, }; context.WriteTextUtf8(JsonConvert.SerializeObject(new LoadingProgress { IsLoading = _isLoading, TotalTrackCount = TotalTrackCount, LoadedTags = MediaTags.Count }), Const.ApplicationJson); } else if (requestPath == "/metadata/file-set.json/checksum") context.WriteTextUtf8(_fileSetJsonChecksum); else if (requestPath == "/metadata/file-set.json") { if (_fileSetJsonChecksum != null && request.Headers["If-None-Match"] == _fileSetJsonChecksum) { context.Response.StatusCode = 304; context.Response.AddHeader("x-custom", "cache applied 304"); } else { context.Response.AddHeader("Last-Modified", _cacheTime.ToString("R")); context.Response.AddHeader("Cache-Control", "public"); context.Response.AddHeader("ETAG", _fileSetJsonChecksum); context.WriteTextUtf8(_fileSetJson ?? "null", Const.ApplicationJson); } } else if (requestPath == "/metadata/tag-dict.json/checksum") context.WriteTextUtf8(_tagSetJsonChecksum); else if (requestPath == "/metadata/tag-dict.json") { if (_tagSetJsonChecksum != null && request.Headers["If-None-Match"] == _tagSetJsonChecksum) { context.Response.StatusCode = 304; context.Response.AddHeader("x-custom", "cache applied 304"); } else { context.Response.AddHeader("Last-Modified", _cacheTime.ToString("R")); context.Response.AddHeader("Cache-Control", "public"); context.Response.AddHeader("ETAG", _tagSetJsonChecksum); context.WriteTextUtf8(_tagSetJson ?? "null", Const.ApplicationJson); } } else if (PathMapping.TryGetValue(requestPath, out var realPath)) { var ext = requestPath.Split('.').LastOrDefault()?.ToLower(); switch (ext) { case "flac": context.Response.ContentType = "audio/flac"; break; case "m4a": context.Response.ContentType = "audio/mp4"; break; case "mp3": context.Response.ContentType = "audio/mpeg"; break; case "aac": context.Response.ContentType = "audio/aac"; break; case "mp4": context.Response.ContentType = "video/mp4"; break; case "mkv": context.Response.ContentType = "video/webm"; break; case "jpg": case "jpeg": context.Response.ContentType = "image/jpeg"; break; case "png": context.Response.ContentType = "image/png"; break; case "bmp": context.Response.ContentType = "image/bmp"; break; default: var firstParts = requestPath.Split('/').FirstOrDefault(); switch (firstParts) { case "media": context.Response.ContentType = "audio/" + ext; break; case "bk": case "cover": context.Response.ContentType = "image/" + ext; break; } break; } var range = request.Headers.GetValues("Range"); FileStream fs = null; try { fs = File.OpenRead(realPath); context.Response.Headers.Add("Accept-Ranges", "bytes"); 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("Content-Range", $"bytes {start}-{fs.Length - 1}/{fs.Length}"); context.Response.ContentLength64 = fs.Length - start; fs.CopyTo(context.Response.OutputStream); } else if (rngParts.Length == 2 && long.TryParse(rngParts[0], out var start2) && long.TryParse(rngParts[1], out var end)) { fs.Position = start2; var realLen = end - start2 + 1; context.Response.StatusCode = 206; context.Response.Headers.Add("Content-Range", $"bytes {start2}-{end}/{fs.Length}"); context.Response.ContentLength64 = realLen; if (realLen < 4096) { var buf = new byte[realLen]; fs.Read(buf, 0, buf.Length); context.Response.OutputStream.Write(buf, 0, buf.Length); } else { fs.CopyTo(context.Response.OutputStream); } } } else { 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); } } Console.WriteLine($"Request #{num:00000} ends with status code: {context.Response.StatusCode}"); } private static MediaTag GetTag(string internalPath, bool peekMem = false) { if (peekMem) { return MediaTags.TryGetValue(internalPath, out var peek) ? peek : null; } if (MediaTags.TryGetValue(internalPath, out var mediaTag) || !PathMapping.TryGetValue(internalPath, out var mediaFilePath)) return mediaTag; try { var fi = new FileInfo(mediaFilePath); using var tagLib = TagLib.File.Create(mediaFilePath, TagLib.ReadStyle.Average); mediaTag = MediaTags[internalPath] = new MediaTag( tagLib.Tag.Album, (int)tagLib.Tag.Disc, (int)tagLib.Tag.Track, tagLib.Tag.Title, string.Join(";", tagLib.Tag.Performers ?? new string[0]), tagLib.Properties?.BitsPerSample ?? 0, tagLib.Properties?.AudioSampleRate ?? 0, (int)(tagLib.Properties?.Duration.TotalSeconds ?? 0), fi.Length, tagLib.Properties?.AudioBitrate ?? 0 ); } catch (Exception e) { Console.WriteLine($"ERROR on get tags: {mediaFilePath}{Environment.NewLine} {e.Message}"); return null; } return mediaTag; } } }