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<string, Library> Libraries = new();
        private static readonly ConcurrentDictionary<string, string> PathMapping = new();
        private static readonly ConcurrentDictionary<string, LoadedModule> Modules = new();
        private static ConcurrentDictionary<string, MediaTag> 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();

                    var libAbsPath = ConfigFile.Instance.LibraryPathFollowLink
                        ? Path.Combine(Path.GetDirectoryName(ConfigFile.Instance.RealConfigFilePath), kvpLib.Value)
                        : kvpLib.Value;

                    Console.WriteLine($"Library {kvpLib.Key} - {libAbsPath}");

                    var libPath = kvpLib.Key.ToLower();
                    var lib = Libraries[libPath] = new Library(kvpLib.Key);
                    var discDirArray = Directory.GetDirectories(libAbsPath);

                    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<string, byte[]>()
                    };

                    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/<ext>
                // GET /media/foo/bar/01.%20foobar.flac audio/<ext>
                // GET /media/foo/bar/aac_q1.00/01.%20foobar.m4a audio/<ext>
                // GET /bk/foo/bar/foobar.jpg image/<ext>

                // metadata catalog
                // GET /metadata/progress.json
                // GET /metadata/file-set.json
                // GET /metadata/tag-dict.json

                // loadable module
                // GET /module/<module-key>/<in-module-path>/

                // ReSharper disable once PossibleNullReferenceException
                var requestPath = request.Url.LocalPath.ToLower();
                var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

                if (requestPath == "/admin/" && false == request.QueryString.AllKeys.Contains("action"))
                {
                    var sb = new StringBuilder();
                    sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
                    sb.Append($"<title> Admin - {ConfigFile.Instance.Title} </title>");
                    sb.Append("<body bgColor=skyBlue>");
                    sb.Append($"<h3>Admin</h3>");
                    sb.Append("<div><a href=/>Back to home</a></div>");
                    sb.Append($"<form method=GET>");
                    sb.Append($"Password: <input type=password name=pass />");
                    sb.Append($"<br/>");
                    sb.Append($"Operation: ");
                    sb.Append($"<input type=submit name=action value=Reload /> ");
                    sb.Append($"<input type=submit name=action value=ReloadFully /> ");
                    sb.Append($"<input type=submit name=action value=ReloadModules /> ");
                    sb.Append($"</form>");

                    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, $"<base href=\"/modules/{moduleKey}/\" />");
                                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("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
                    sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
                    sb.Append("<body bgColor=skyBlue style=font-size:3vh>");

                    if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");

                    sb.Append($"<h2>{ConfigFile.Instance.Title}</h2>");

                    if (Modules?.Any() == true)
                    {
                        sb.Append($"<h3>Modules</h3>");
                        sb.Append($"<h4>(Total number of modules: {Modules.Count})</h4>");

                        sb.Append("<ul>");
                        foreach (var module in Modules)
                        {
                            sb.Append("<li>");
                            sb.Append($"<a href='/modules/{module.Key.UrlEscape()}/{module.Value.DefaultDocument}'>{module.Value.DisplayText}</a>");
                            sb.Append("</li>");
                        }
                        sb.Append("</ul>");
                    }

                    sb.Append($"<h3>Libraries</h3>");
                    sb.Append($"<h4>(Total number of disc: {Libraries.Sum(p => p.Value.Discs.Count)})</h4>");

                    sb.Append("<ul>");
                    foreach (var library in Libraries.OrderBy(p => p.Key))
                    {
                        sb.Append("<li>");
                        sb.Append($"<a href='/list/{library.Key.UrlEscape()}/'>{library.Value.Name}</a>");
                        sb.Append($"<br/>&nbsp;&nbsp;&nbsp; Number of disc: {library.Value.Discs.Count}");
                        sb.Append("</li>");
                    }
                    sb.Append("</ul>");

                    sb.Append($"<hr/>");
                    sb.Append($"<div>Your IP: {context?.Request?.RemoteEndPoint?.Address.ToString() ?? "Unknown"}</div>");
                    sb.Append($"<div><a href=/admin/>Admin</a></div>");
                    sb.Append($"<div>-</div>");
                    sb.Append($"<div>Author: Coder (V)</div>");
                    sb.Append($"<div>Blog: <a target=_blank href=https://topcl.net/myapps/private-colud-music.html>https://topcl.net/myapps/private-colud-music.html</a></div>");
                    sb.Append($"<div>Source Repo: <a target=_blank href=https://topcl.net/gogs/coder/CloudMusic/>https://topcl.net/gogs/coder/CloudMusic/</a></div>");

                    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("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
                        sb.Append($"<title> Discs of {lib.Name} - {ConfigFile.Instance.Title} </title>");
                        sb.Append(
                            "<style>" +
                            "a:link{ text-decoration: none; }" +
                            "div.item{" +
                            " vertical-align:top;" +
                            " margin-bottom:1vh;" +
                            " padding:0.5vh;" +
                            " border:solid 1px;" +
                            " border-radius:0.5vh;" +
                            " font-size:2.2vh;" +
                            "}" +
                            "div.item::-webkit-scrollbar{" +
                            " display: none;" +
                            "}" +
                            "img.cover{" +
                            " float:left;" +
                            " width:45vw;" +
                            "}" +
                            "div.disc_name{" +
                            "}" +
                            "div.links{" +
                            " clear:both;" +
                            "}" +
                            "a.button{" +
                            " margin-left:4vw;" +
                            "}" +
                            "</style>");

                        sb.Append("<script>" +
                                  "function listPage(a){" +
                                  "window.open(a.href+'.html','CNZCM_LIST_PAGE',1);" +
                                  "return false;" +
                                  "}" +
                                  "</script>");

                        sb.Append($"<body bgColor=skyBlue>");

                        if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");

                        sb.Append($"<h1>Discs of {lib.Name}</h1>");
                        sb.Append("<div><a href=/>Back to home</a></div>");

                        if (!_isLoading)
                        {
                            sb.Append("<div style=margin-top:1vh;margin-bottom:1vh;>");

                            //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($"<br/>{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/lib_list/{libName}/playlist.m3u8\">ALL_M3U8_MAIN</a>");

                            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($"<br/>{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/lib_list/{libName}/{setName}/playlist.m3u8\">ALL_M3U8_{setName.ToUpper()}</a>");
                            }

                            sb.Append("</div>");
                        }

                        //Cover list
                        foreach (var disc in lib.Discs.OrderByDescending(p => p.Key))
                        {
                            sb.Append("<div class=item>");

                            sb.Append($"<div>");
                            sb.Append($"<img class=cover src=\"/cover/{libName}/{disc.Key.UrlEscape()}/cover.jpg\" />");
                            sb.Append($"<div class=disc_name>{disc.Value.Name}</div>");
                            sb.Append($"</div>");

                            sb.Append("<div class=links>");

                            sb.Append("<div>");
                            sb.Append($"Number of track: {disc.Value.MainTracks.Count} <a href=\"/list/{libName}/{disc.Key.UrlEscape()}/tracks/\">[TRACKERS]</a>");
                            if (disc.Value.Bks?.Count > 0) sb.Append($"<a class=button href=\"/list/{libName}/{disc.Key.UrlEscape()}/bk/\">[BK]</a>");
                            sb.Append("</div>");

                            var totalDur = disc.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{p.Key}", true)?.Duration);
                            var totalLen = disc.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{p.Key}", true)?.Length);
                            sb.Append($"{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/list/{libName}/{disc.Key.UrlEscape()}/playlist.m3u8\">M3U8_MAIN</a>");
                            if (disc.Value.SubTracks.Count > 0)
                            {
                                foreach (var subTrack in disc.Value.SubTracks)
                                {
                                    totalDur = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{subTrack.Key}/{p.Key}", true)?.Duration);
                                    totalLen = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{subTrack.Key}/{p.Key}", true)?.Length);
                                    sb.Append($"<br/>{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/list/{libName}/{disc.Key.UrlEscape()}/{subTrack.Key.UrlEscape()}/playlist.m3u8\">{subTrack.Value.Name}</a>");
                                }
                            }
                            sb.Append("</div>");

                            sb.Append("</div>");
                        }

                        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("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
                        sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");

                        if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");

                        sb.Append($"<h2>Tracks of</h2><h1>{disc.Name}</h1>");
                        sb.Append($"<div><a href='/list/{libName.UrlEscape()}/'>Back to library</a></div>");
                        sb.Append($"<img style=float:left;max-width:50vw src=\"/cover/{libName}/{discPath}/cover.jpg\" />");

                        var durTotal = 0;
                        var sizeTotal = 0L;

                        var sbm = new StringBuilder();
                        foreach (var kvpTrack in disc.MainTracks.OrderBy(p => p.Key))
                        {
                            sbm.Append($"<li>");
                            sbm.Append($"<a href=\"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{kvpTrack.Key.UrlEscape()}\" >{kvpTrack.Value}</a>");

                            var tag = GetTag($"/media/{libName}/{discPath}/{kvpTrack.Key}");
                            durTotal += tag.Duration;
                            sizeTotal += tag.Length;
                            sbm.Append($"<br> &nbsp; &nbsp; &nbsp; {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}");

                            sbm.Append($"</li>");
                        }

                        sb.Append($"<h2>Main ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}</h2>");
                        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($"<li>");
                                sbm.Append($"<a href=\"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{kvpSubSet.Key.UrlEscape()}/{kvpTrack.Key.UrlEscape()}\" >{kvpTrack.Value}</a>");

                                var tag = GetTag($"/media/{libName}/{discPath}/{kvpSubSet.Key}/{kvpTrack.Key}");
                                durTotal += tag.Duration;
                                sizeTotal += tag.Length;
                                sbm.Append($"<br/> &nbsp; &nbsp; &nbsp; {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}");

                                sbm.Append($"</li>");
                            }

                            sb.Append($"<h2>{kvpSubSet.Value.Name} ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}</h2>");
                            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("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
                        sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");

                        if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");

                        sb.Append($"<h2>BK of </h2><h1>{disc.Name}</h1>");
                        sb.Append($"<div><a href='/list/{libName.UrlEscape()}/'>Back to library</a></div>");

                        foreach (var discBk in disc.Bks.OrderBy(p => p.Key))
                        {
                            sb.Append($"<img src='/bk/{libName.UrlEscape()}/{discPath.UrlEscape()}/{discBk.Key.UrlEscape()}' style=max-width:24vw;max-height:24vw;margin-right:1vw;margin-bottom:1vh; />");
                        }

                        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()}";

                                //<li><span class="simp-source" data-src="http://">Title</span><span class="simp-desc">Artist</span></li>

                                if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
                                {
                                    sb.AppendLine($"<li>" +
                                                  $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
                                                  $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
                                                  $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
                                                  $"</li>");
                                }
                                else
                                {
                                    sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
                                }
                            }
                        }

                        var sbPage = new StringBuilder(Resource.listPage);
                        sbPage.Replace("<!-- list place here -->", 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($"<li>" +
                                                      $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
                                                      $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
                                                      $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
                                                      $"</li>");
                                    }
                                    else
                                    {
                                        sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
                                    }
                                }
                            }
                        }

                        var sbPage = new StringBuilder(Resource.listPage);
                        sbPage.Replace("<!-- list place here -->", 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($"<li>" +
                                              $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
                                              $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
                                              $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
                                              $"</li>");
                            }
                            else
                            {
                                sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
                            }
                        }

                        var sbPage = new StringBuilder(Resource.listPage);
                        sbPage.Replace("<!-- list place here -->", 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($"<li>" +
                                                  $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
                                                  $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
                                                  $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
                                                  $"</li>");
                                }
                                else
                                {
                                    sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
                                }
                            }

                            var sbPage = new StringBuilder(Resource.listPage);
                            sbPage.Replace("<!-- list place here -->", 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<string, object>()
                    {
                        {"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;
                    }


                    FileStream fs = null;
                    try
                    {
                        fs = File.OpenRead(realPath);

                        context.Response.Headers.Add("Accept-Ranges", "bytes");

                        var range = request.Headers.GetValues("Range");
                        if (range == null || range.Length == 0)
                        {
                            context.Response.ContentLength64 = fs.Length;
                            fs.CopyTo(context.Response.OutputStream);
                        }
                        else
                        {
                            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 rangeStart) && long.TryParse(rngParts[1], out var rangeEnd))
                            {
                                var bodyLen = rangeEnd - rangeStart + 1;
                                context.Response.StatusCode = 206;
                                context.Response.Headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{fs.Length}");
                                context.Response.ContentLength64 = bodyLen;

                                const int megaBytes = 1024 * 1024;
                                const int bufferChunk = megaBytes * 16;

                                var remainBytes = bodyLen;

                                fs.Position = rangeStart;
                                while (remainBytes > 0)
                                {
                                    var buf = new byte[remainBytes > bufferChunk ? bufferChunk : remainBytes];

                                    var r = fs.Read(buf, 0, buf.Length);
                                    context.Response.OutputStream.Write(buf, 0, r);

                                    remainBytes -= r;
                                }
                            }
                        }
                    }
                    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;
        }
    }
}
