HostProgram.cs 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272
  1. using FNZCM.Core;
  2. using FNZCM.Shared.Helpers;
  3. using FNZCM.Shared.MediaModels;
  4. using FNZCM.Shared.MetadataModels;
  5. using Microsoft.VisualBasic.FileIO;
  6. using Newtonsoft.Json;
  7. using System;
  8. using System.Collections.Concurrent;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Net;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption;
  17. namespace FNZCM.ConHost
  18. {
  19. internal static class HostProgram
  20. {
  21. //0. start http server
  22. //1. scan libraries and fill data struct
  23. // libs
  24. // disc
  25. // Tracks(FLAC / AAC_*)
  26. // Meta(title(artist) / duration)
  27. // FSI ( size )
  28. // TODO: Generate thumbnail of BKS
  29. private static readonly ConcurrentDictionary<string, Library> Libraries = new();
  30. private static readonly ConcurrentDictionary<string, string> PathMapping = new();
  31. private static readonly ConcurrentDictionary<string, LoadedModule> Modules = new();
  32. private static ConcurrentDictionary<string, MediaTag> MediaTags = new();
  33. private static string _fileSetJson;
  34. private static string _fileSetJsonChecksum;
  35. private static string _tagSetJson;
  36. private static string _tagSetJsonChecksum;
  37. private static DateTime _cacheTime;
  38. private static LoadedModule _defaultModule;
  39. private static bool _isRunning;
  40. private static bool _isLoading;
  41. private static int TotalTrackCount => PathMapping.Count(p => p.Key.StartsWith("/media/"));
  42. private static string LoadingText => $"Loading... {MediaTags.Count} / {TotalTrackCount}";
  43. private static DateTime _lastRequestAccepted;
  44. private static int _requestId = 0;
  45. private static void Main()
  46. {
  47. Console.WriteLine("Starting...");
  48. var tWorker = new Thread(Working);
  49. _isRunning = true;
  50. tWorker.Start();
  51. Task.Run(ReloadConfig);
  52. Console.WriteLine("Press ENTER to Stop.");
  53. Console.ReadLine();
  54. Console.WriteLine("Shutting down...");
  55. _isRunning = false;
  56. tWorker.Join();
  57. Console.WriteLine("Stopped.");
  58. Console.WriteLine();
  59. Console.Write("Press ENTER to Exit.");
  60. Console.ReadLine();
  61. }
  62. private static void ReloadConfig()
  63. {
  64. if (_isLoading)
  65. {
  66. Console.WriteLine("Still loading, SKIP");
  67. return;
  68. }
  69. _isLoading = true;
  70. try
  71. {
  72. ConfigFile.Reload();
  73. ReloadModulesInternal();
  74. Console.WriteLine("Scanning libraries...");
  75. MediaTags.Clear();
  76. PathMapping.Clear();
  77. Libraries.Clear();
  78. foreach (var kvpLib in ConfigFile.Instance.Libraries)
  79. {
  80. if (_isRunning == false) throw new OperationCanceledException();
  81. Console.WriteLine($"Library {kvpLib.Key} - {kvpLib.Value}");
  82. var libPath = kvpLib.Key.ToLower();
  83. var lib = Libraries[libPath] = new Library(kvpLib.Key);
  84. var discDirArray = Directory.GetDirectories(kvpLib.Value);
  85. foreach (var discDir in discDirArray)
  86. {
  87. if (_isRunning == false) throw new OperationCanceledException();
  88. Console.WriteLine($" Disc {discDir}");
  89. var discName = Path.GetFileName(discDir);
  90. var discPath = discName.ToLower();
  91. try
  92. {
  93. var disc = new Disc(discName);
  94. var bkDir = Path.Combine(discDir, "bk");
  95. var mainTrackFiles = FileSystem.GetFiles(discDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern);
  96. foreach (var mainTrackFile in mainTrackFiles)
  97. {
  98. var trackName = Path.GetFileName(mainTrackFile);
  99. var trackPath = trackName.ToLower();
  100. disc.MainTracks[trackPath] = trackName;
  101. PathMapping[$"/media/{libPath}/{discPath}/{trackPath}"] = mainTrackFile;
  102. }
  103. if (Directory.Exists(bkDir))
  104. {
  105. var bkFiles = FileSystem.GetFiles(bkDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern);
  106. foreach (var file in bkFiles)
  107. {
  108. var bkName = Path.GetFileName(file);
  109. var bkPath = bkName.ToLower();
  110. disc.Bks[bkPath] = bkName;
  111. PathMapping[$"/bk/{libPath}/{discPath}/{bkPath}"] = file;
  112. }
  113. }
  114. var aacTrackDirArray = Directory.GetDirectories(discDir, "AAC_Q*");
  115. foreach (var aacTrackDir in aacTrackDirArray)
  116. {
  117. var aacTrackSetName = Path.GetFileName(aacTrackDir);
  118. var aacTrackSetPath = aacTrackSetName.ToLower();
  119. var aacTrackSet = disc.SubTracks[aacTrackSetPath] = new TrackSet(aacTrackSetName);
  120. foreach (var file in FileSystem.GetFiles(aacTrackDir, SearchOption.SearchTopLevelOnly, "*.m4a", "*.mp4"))
  121. {
  122. var aacTrackName = Path.GetFileName(file);
  123. var aacTrackPath = aacTrackName.ToLower();
  124. aacTrackSet.Tracks[aacTrackPath] = aacTrackName;
  125. PathMapping[$"/media/{libPath}/{discPath}/{aacTrackSetPath}/{aacTrackPath}"] = file;
  126. }
  127. }
  128. var coverFilePath = Path.Combine(discDir, "cover.jpg");
  129. if (File.Exists(coverFilePath)) PathMapping[$"/cover/{libPath}/{discPath}/cover.jpg"] = coverFilePath;
  130. lib.Discs[discPath] = disc;
  131. }
  132. catch (Exception ex)
  133. {
  134. Console.WriteLine(ex);
  135. }
  136. }
  137. }
  138. Console.WriteLine("Loading meta caches...");
  139. var cache = MetaCache.Load();
  140. if (cache != null) MediaTags = cache;
  141. Console.WriteLine("Looking tags...");
  142. foreach (var k in PathMapping.Keys.Where(p => p.StartsWith("/media/")))
  143. {
  144. GetTag(k);
  145. }
  146. Console.WriteLine("Saving meta caches...");
  147. MetaCache.Save(MediaTags);
  148. _fileSetJson = JsonConvert.SerializeObject(Libraries);
  149. _tagSetJson = JsonConvert.SerializeObject(MediaTags);
  150. _fileSetJsonChecksum = _fileSetJson.ToMd5();
  151. _tagSetJsonChecksum = _tagSetJson.ToMd5();
  152. _cacheTime = DateTime.Now;
  153. Console.WriteLine("Looking tags...Done");
  154. }
  155. catch (Exception e)
  156. {
  157. Console.WriteLine($"Load error: {e}");
  158. }
  159. _isLoading = false;
  160. }
  161. private static void ReloadModules()
  162. {
  163. ConfigFile.Reload();
  164. ReloadModulesInternal();
  165. }
  166. private static void ReloadModulesInternal()
  167. {
  168. Modules.Clear();
  169. _defaultModule = null;
  170. if (ConfigFile.Instance.Modules?.Any() == true)
  171. {
  172. foreach (var modEnt in ConfigFile.Instance.Modules)
  173. {
  174. Console.WriteLine($"Loading module `{modEnt.Value.DisplayText}'...");
  175. var module = new LoadedModule
  176. {
  177. VirtualPath = modEnt.Key,
  178. DisplayText = modEnt.Value.DisplayText,
  179. DefaultDocument = modEnt.Value.DefaultDocument,
  180. EnableFallbackRoute = modEnt.Value.EnableFallbackRoute,
  181. HtmlBaseReplace = modEnt.Value.HtmlBaseReplace,
  182. Files = new Dictionary<string, byte[]>()
  183. };
  184. if (Directory.Exists(modEnt.Value.Path))
  185. {
  186. //load by fs
  187. var files = Directory.GetFiles(modEnt.Value.Path, "*", System.IO.SearchOption.AllDirectories);
  188. foreach (var item in files)
  189. {
  190. var k = item.Substring(modEnt.Value.Path.Length + 1).Replace("\\", "/").ToLower();
  191. module.Files[k] = File.ReadAllBytes(item);
  192. }
  193. }
  194. else if (File.Exists(modEnt.Value.Path))
  195. {
  196. //load by package
  197. using var arc = SharpCompress.Archives.ArchiveFactory.Open(modEnt.Value.Path);
  198. foreach (var ent in arc.Entries.Where(p => p.IsDirectory == false))
  199. {
  200. var buf = new byte[ent.Size];
  201. using var s = ent.OpenEntryStream();
  202. var r = s.Read(buf, 0, buf.Length);
  203. module.Files[ent.Key.ToLower()] = buf;
  204. }
  205. }
  206. else
  207. {
  208. Console.WriteLine("WARN: resource not found");
  209. continue;
  210. }
  211. if (modEnt.Value.IsDefault && _defaultModule == null) _defaultModule = module;
  212. Modules[modEnt.Key] = module;
  213. }
  214. }
  215. }
  216. private static void Working()
  217. {
  218. var listener = new HttpListener();
  219. listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix);
  220. if (ConfigFile.Instance.AliasPrefix?.Any() == true)
  221. foreach (var prefix in ConfigFile.Instance.AliasPrefix)
  222. listener.Prefixes.Add(prefix);
  223. listener.Start();
  224. var upTime = DateTime.Now;
  225. Console.WriteLine($"HTTP Server started, listening on {ConfigFile.Instance.ListenPrefix}");
  226. listener.BeginGetContext(ContextGet, listener);
  227. _lastRequestAccepted = DateTime.Now;
  228. while (_isRunning)
  229. {
  230. var timeSpan = DateTime.Now - _lastRequestAccepted;
  231. var up = DateTime.Now - upTime;
  232. Console.Title =
  233. "FNZCM"
  234. + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}"
  235. + $" / "
  236. + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}"
  237. ;
  238. Thread.Sleep(1000);
  239. }
  240. listener.Close();
  241. Thread.Sleep(1000);
  242. }
  243. private static void ContextGet(IAsyncResult ar)
  244. {
  245. var listener = (HttpListener)ar.AsyncState;
  246. HttpListenerContext context;
  247. try
  248. {
  249. // ReSharper disable once PossibleNullReferenceException
  250. context = listener.EndGetContext(ar);
  251. }
  252. catch (Exception e)
  253. {
  254. Console.WriteLine(e);
  255. return;
  256. }
  257. if (_isRunning) listener.BeginGetContext(ContextGet, listener);
  258. ProcessRequest(context);
  259. }
  260. private static void ProcessRequest(HttpListenerContext context)
  261. {
  262. _lastRequestAccepted = DateTime.Now;
  263. var request = context.Request;
  264. var requestAbsPrefix = ConfigFile.Instance.M3uPrefix ?? $"{request.Url.Scheme + Uri.SchemeDelimiter + request.Url.Host + ":" + request.Url.Port}";
  265. var num = Interlocked.Increment(ref _requestId);
  266. Console.WriteLine($"Request #{num:00000} from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}");
  267. if (false == string.IsNullOrEmpty(request.Headers["Origin"]))
  268. {
  269. if (ConfigFile.Instance.AccessControlAllowOrigin?.Any(p => request.Headers["Origin"].StartsWith(p)) == true)
  270. context.Response.Headers["Access-Control-Allow-Origin"] = "*";
  271. }
  272. // GET / show all libraries
  273. // foo=library bar=disc
  274. // GET /list/foo/ show all disc and cover with name, provide m3u path
  275. // GET /list/foo/bar/bk/ list all picture as grid
  276. // GET /list/foo/bar/tracks/ list all tracks as text list
  277. // GET /lib_list/foo/playlist.m3u8 auto gen
  278. // GET /lib_list/foo/aac_q1.00/playlist.m3u8 auto gen
  279. // GET /list/foo/bar/playlist.m3u8 auto gen
  280. // GET /list/foo/bar/aac_q1.00/playlist.m3u8 auto gen
  281. // media streaming HTTP Partial RANGE SUPPORT
  282. // GET /cover/foo/bar/cover.jpg image/<ext>
  283. // GET /media/foo/bar/01.%20foobar.flac audio/<ext>
  284. // GET /media/foo/bar/aac_q1.00/01.%20foobar.m4a audio/<ext>
  285. // GET /bk/foo/bar/foobar.jpg image/<ext>
  286. // metadata catalog
  287. // GET /metadata/progress.json
  288. // GET /metadata/file-set.json
  289. // GET /metadata/tag-dict.json
  290. // loadable module
  291. // GET /module/<module-key>/<in-module-path>/
  292. try
  293. {
  294. // ReSharper disable once PossibleNullReferenceException
  295. var requestPath = request.Url.LocalPath.ToLower();
  296. var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  297. if (requestPath == "/admin/" && false == request.QueryString.AllKeys.Contains("action"))
  298. {
  299. var sb = new StringBuilder();
  300. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  301. sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
  302. sb.Append("<body bgColor=skyBlue>");
  303. sb.Append($"<h3>Admin</h3>");
  304. sb.Append("<div><a href=/>Back to home</a></div>");
  305. sb.Append($"<form method=GET>");
  306. sb.Append($"Password: <input type=password name=pass />");
  307. sb.Append($"<br/>");
  308. sb.Append($"Operation: ");
  309. sb.Append($"<input type=submit name=action value=Reload /> ");
  310. sb.Append($"<input type=submit name=action value=ReloadFully /> ");
  311. sb.Append($"<input type=submit name=action value=ReloadModules /> ");
  312. sb.Append($"</form>");
  313. context.WriteTextUtf8(sb.ToString());
  314. }
  315. else if (requestPath == "/admin/" && request.QueryString["action"] == "Reload" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword)
  316. {
  317. Task.Run(ReloadConfig);
  318. context.Response.Redirect("/");
  319. }
  320. else if (requestPath == "/admin/" && request.QueryString["action"] == "ReloadModules" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword)
  321. {
  322. Task.Run(ReloadModules);
  323. context.Response.Redirect("/");
  324. }
  325. else if (requestPath == "/admin/" && request.QueryString["action"] == "ReloadFully" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword)
  326. {
  327. MetaCache.Clear();
  328. Task.Run(ReloadConfig);
  329. context.Response.Redirect("/");
  330. }
  331. else if (requestPath == "/admin/")
  332. {
  333. context.Response.Redirect("/");
  334. }
  335. else if (requestPath.StartsWith("/modules/") && pathParts.Count > 1)
  336. {
  337. var moduleKey = pathParts[1];
  338. if (Modules.TryGetValue(moduleKey, out var module))
  339. {
  340. var entPath = string.Join("/", pathParts.Skip(2));
  341. void Output(byte[] bin)
  342. {
  343. if (entPath.ToLower().EndsWith(".js")) context.Response.ContentType = "application/javascript";
  344. else if (module.HtmlBaseReplace != null && entPath.ToLower().EndsWith(".html"))
  345. {
  346. //base replace
  347. var html = Encoding.UTF8.GetString(bin);
  348. var r = html.Replace(module.HtmlBaseReplace, $"<base href=\"/modules/{moduleKey}/\" />");
  349. bin = Encoding.UTF8.GetBytes(r);
  350. }
  351. context.Response.OutputStream.Write(bin, 0, bin.Length);
  352. }
  353. if (module.Files.TryGetValue(entPath, out var bin))
  354. {
  355. Output(bin);
  356. }
  357. else if (module.EnableFallbackRoute && module.Files.TryGetValue(module.DefaultDocument, out var defBin))
  358. {
  359. entPath = module.DefaultDocument;
  360. Output(defBin);
  361. }
  362. else context.Response.StatusCode = 404;
  363. }
  364. else context.Response.StatusCode = 404;
  365. }
  366. else if (requestPath == "/" && _defaultModule != null)
  367. {
  368. if (_defaultModule.EnableFallbackRoute) context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/");
  369. else context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/{_defaultModule.DefaultDocument}");
  370. }
  371. else if (requestPath == "/" || requestPath == "/classic-index")
  372. {
  373. var sb = new StringBuilder();
  374. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  375. sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
  376. sb.Append("<body bgColor=skyBlue style=font-size:3vh>");
  377. if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");
  378. sb.Append($"<h2>{ConfigFile.Instance.Title}</h2>");
  379. if (Modules?.Any() == true)
  380. {
  381. sb.Append($"<h3>Modules</h3>");
  382. sb.Append($"<h4>(Total number of modules: {Modules.Count})</h4>");
  383. sb.Append("<ul>");
  384. foreach (var module in Modules)
  385. {
  386. sb.Append("<li>");
  387. sb.Append($"<a href='/modules/{module.Key.UrlEscape()}/{module.Value.DefaultDocument}'>{module.Value.DisplayText}</a>");
  388. sb.Append("</li>");
  389. }
  390. sb.Append("</ul>");
  391. }
  392. sb.Append($"<h3>Libraries</h3>");
  393. sb.Append($"<h4>(Total number of disc: {Libraries.Sum(p => p.Value.Discs.Count)})</h4>");
  394. sb.Append("<ul>");
  395. foreach (var library in Libraries.OrderBy(p => p.Key))
  396. {
  397. sb.Append("<li>");
  398. sb.Append($"<a href='/list/{library.Key.UrlEscape()}/'>{library.Value.Name}</a>");
  399. sb.Append($"<br/>&nbsp;&nbsp;&nbsp; Number of disc: {library.Value.Discs.Count}");
  400. sb.Append("</li>");
  401. }
  402. sb.Append("</ul>");
  403. sb.Append($"<hr/>");
  404. sb.Append($"<div>Your IP: {context?.Request?.RemoteEndPoint?.Address.ToString() ?? "Unknown"}</div>");
  405. sb.Append($"<div><a href=/admin/>Admin</a></div>");
  406. sb.Append($"<div>-</div>");
  407. sb.Append($"<div>Author: Coder (V)</div>");
  408. 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>");
  409. sb.Append($"<div>Source Repo: <a target=_blank href=https://topcl.net/gogs/coder/CloudMusic/>https://topcl.net/gogs/coder/CloudMusic/</a></div>");
  410. context.WriteTextUtf8(sb.ToString());
  411. }
  412. else if (pathParts.Count == 2 && pathParts[0] == "list")
  413. {
  414. var libName = pathParts[1];
  415. if (Libraries.TryGetValue(libName, out var lib))
  416. {
  417. var sb = new StringBuilder();
  418. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  419. sb.Append($"<title> Discs of {lib.Name} - {ConfigFile.Instance.Title} </title>");
  420. sb.Append(
  421. "<style>" +
  422. "a:link{ text-decoration: none; }" +
  423. "div.item{" +
  424. " vertical-align:top;" +
  425. " margin-bottom:1vh;" +
  426. " padding:0.5vh;" +
  427. " border:solid 1px;" +
  428. " border-radius:0.5vh;" +
  429. " font-size:2.2vh;" +
  430. "}" +
  431. "div.item::-webkit-scrollbar{" +
  432. " display: none;" +
  433. "}" +
  434. "img.cover{" +
  435. " float:left;" +
  436. " width:45vw;" +
  437. "}" +
  438. "div.disc_name{" +
  439. "}" +
  440. "div.links{" +
  441. " clear:both;" +
  442. "}" +
  443. "a.button{" +
  444. " margin-left:4vw;" +
  445. "}" +
  446. "</style>");
  447. sb.Append("<script>" +
  448. "function listPage(a){" +
  449. "window.open(a.href+'.html','CNZCM_LIST_PAGE',1);" +
  450. "return false;" +
  451. "}" +
  452. "</script>");
  453. sb.Append($"<body bgColor=skyBlue>");
  454. if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");
  455. sb.Append($"<h1>Discs of {lib.Name}</h1>");
  456. sb.Append("<div><a href=/>Back to home</a></div>");
  457. if (!_isLoading)
  458. {
  459. sb.Append("<div style=margin-top:1vh;margin-bottom:1vh;>");
  460. //big m3u8
  461. var trackKeys = lib.Discs.SelectMany(p => p.Value.MainTracks.Keys.Select(q => new { DiscName = p.Key, TrackName = q })).ToArray();
  462. var totalDur = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{p.TrackName}", true)?.Duration);
  463. var totalLen = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{p.TrackName}", true)?.Length);
  464. sb.Append($"Number of track: {trackKeys.Length}");
  465. sb.Append($"<br/>{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/lib_list/{libName}/playlist.m3u8\">ALL_M3U8_MAIN</a>");
  466. var subTrackSetNames = lib.Discs.SelectMany(p => p.Value.SubTracks.Keys).Distinct().ToArray();
  467. foreach (var setName in subTrackSetNames)
  468. {
  469. trackKeys = lib.Discs.SelectMany(p =>
  470. {
  471. if (p.Value.SubTracks.TryGetValue(setName, out var tSet))
  472. {
  473. return tSet.Tracks.Select(q => new { DiscName = p.Key, TrackName = q.Key });
  474. }
  475. return new string[0].Select(q => new { DiscName = p.Key, TrackName = q });
  476. }).ToArray();
  477. totalDur = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{setName}/{p.TrackName}", true)?.Duration);
  478. totalLen = trackKeys.Sum(p => GetTag($"/media/{libName}/{p.DiscName}/{setName}/{p.TrackName}", true)?.Length);
  479. sb.Append($"<br/>{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/lib_list/{libName}/{setName}/playlist.m3u8\">ALL_M3U8_{setName.ToUpper()}</a>");
  480. }
  481. sb.Append("</div>");
  482. }
  483. //Cover list
  484. foreach (var disc in lib.Discs.OrderByDescending(p => p.Key))
  485. {
  486. sb.Append("<div class=item>");
  487. sb.Append($"<div>");
  488. sb.Append($"<img class=cover src=\"/cover/{libName}/{disc.Key.UrlEscape()}/cover.jpg\" />");
  489. sb.Append($"<div class=disc_name>{disc.Value.Name}</div>");
  490. sb.Append($"</div>");
  491. sb.Append("<div class=links>");
  492. sb.Append("<div>");
  493. sb.Append($"Number of track: {disc.Value.MainTracks.Count} <a href=\"/list/{libName}/{disc.Key.UrlEscape()}/tracks/\">[TRACKERS]</a>");
  494. if (disc.Value.Bks?.Count > 0) sb.Append($"<a class=button href=\"/list/{libName}/{disc.Key.UrlEscape()}/bk/\">[BK]</a>");
  495. sb.Append("</div>");
  496. var totalDur = disc.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{p.Key}", true)?.Duration);
  497. var totalLen = disc.Value.MainTracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{p.Key}", true)?.Length);
  498. sb.Append($"{totalDur.SecondToDur()} {totalLen.BytesToFileSize()} <a onclick=\"return listPage(this)\" href=\"/list/{libName}/{disc.Key.UrlEscape()}/playlist.m3u8\">M3U8_MAIN</a>");
  499. if (disc.Value.SubTracks.Count > 0)
  500. {
  501. foreach (var subTrack in disc.Value.SubTracks)
  502. {
  503. totalDur = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{subTrack.Key}/{p.Key}", true)?.Duration);
  504. totalLen = subTrack.Value.Tracks.Sum(p => GetTag($"/media/{libName}/{disc.Key}/{subTrack.Key}/{p.Key}", true)?.Length);
  505. 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>");
  506. }
  507. }
  508. sb.Append("</div>");
  509. sb.Append("</div>");
  510. }
  511. context.WriteTextUtf8(sb.ToString());
  512. }
  513. else
  514. {
  515. context.Response.StatusCode = 404;
  516. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  517. }
  518. }
  519. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "tracks")
  520. {
  521. var libName = pathParts[1];
  522. var discPath = pathParts[2];
  523. if (Libraries.TryGetValue(libName, out var l) && l.Discs.TryGetValue(discPath, out var disc))
  524. {
  525. var sb = new StringBuilder();
  526. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  527. sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");
  528. if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");
  529. sb.Append($"<h2>Tracks of</h2><h1>{disc.Name}</h1>");
  530. sb.Append($"<div><a href='/list/{libName.UrlEscape()}/'>Back to library</a></div>");
  531. sb.Append($"<img style=float:left;max-width:50vw src=\"/cover/{libName}/{discPath}/cover.jpg\" />");
  532. var durTotal = 0;
  533. var sizeTotal = 0L;
  534. var sbm = new StringBuilder();
  535. foreach (var kvpTrack in disc.MainTracks.OrderBy(p => p.Key))
  536. {
  537. sbm.Append($"<li>");
  538. sbm.Append($"<a href=\"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{kvpTrack.Key.UrlEscape()}\" >{kvpTrack.Value}</a>");
  539. var tag = GetTag($"/media/{libName}/{discPath}/{kvpTrack.Key}");
  540. durTotal += tag.Duration;
  541. sizeTotal += tag.Length;
  542. sbm.Append($"<br> &nbsp; &nbsp; &nbsp; {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}");
  543. sbm.Append($"</li>");
  544. }
  545. sb.Append($"<h2>Main ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}</h2>");
  546. sb.Append(sbm);
  547. foreach (var kvpSubSet in disc.SubTracks.OrderBy(p => p.Key))
  548. {
  549. durTotal = 0;
  550. sizeTotal = 0L;
  551. sbm.Clear();
  552. foreach (var kvpTrack in kvpSubSet.Value.Tracks.OrderBy(p => p.Key))
  553. {
  554. sbm.Append($"<li>");
  555. sbm.Append($"<a href=\"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{kvpSubSet.Key.UrlEscape()}/{kvpTrack.Key.UrlEscape()}\" >{kvpTrack.Value}</a>");
  556. var tag = GetTag($"/media/{libName}/{discPath}/{kvpSubSet.Key}/{kvpTrack.Key}");
  557. durTotal += tag.Duration;
  558. sizeTotal += tag.Length;
  559. sbm.Append($"<br/> &nbsp; &nbsp; &nbsp; {tag.Duration.SecondToDur()} {tag.Length.BytesToFileSize()}");
  560. sbm.Append($"</li>");
  561. }
  562. sb.Append($"<h2>{kvpSubSet.Value.Name} ({durTotal.SecondToDur()}) {sizeTotal.BytesToFileSize()}</h2>");
  563. sb.Append(sbm);
  564. }
  565. context.WriteTextUtf8(sb.ToString());
  566. }
  567. else
  568. {
  569. context.Response.StatusCode = 404;
  570. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  571. }
  572. }
  573. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "bk")
  574. {
  575. var libName = pathParts[1];
  576. var discPath = pathParts[2];
  577. if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc))
  578. {
  579. var sb = new StringBuilder();
  580. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  581. sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");
  582. if (_isLoading) sb.Append($"<h4 style=position:fixed;right:0px;top:0px;margin:0>{LoadingText}</h4>");
  583. sb.Append($"<h2>BK of </h2><h1>{disc.Name}</h1>");
  584. sb.Append($"<div><a href='/list/{libName.UrlEscape()}/'>Back to library</a></div>");
  585. foreach (var discBk in disc.Bks.OrderBy(p => p.Key))
  586. {
  587. 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; />");
  588. }
  589. context.WriteTextUtf8(sb.ToString());
  590. }
  591. else
  592. {
  593. context.Response.StatusCode = 404;
  594. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  595. }
  596. }
  597. else if (pathParts.Count == 3 && pathParts[0] == "lib_list" && pathParts[2] == "playlist.m3u8.html")
  598. {
  599. var libName = pathParts[1];
  600. if (Libraries.TryGetValue(libName, out var lib))
  601. {
  602. var sb = new StringBuilder();
  603. //var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}";
  604. foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key))
  605. {
  606. var disc = discKvp.Value;
  607. var discPath = discKvp.Key;
  608. var tracks = disc.MainTracks;
  609. foreach (var track in tracks.OrderBy(p => p.Key))
  610. {
  611. var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}");
  612. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}";
  613. //<li><span class="simp-source" data-src="http://">Title</span><span class="simp-desc">Artist</span></li>
  614. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  615. {
  616. sb.AppendLine($"<li>" +
  617. $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
  618. $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
  619. $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
  620. $"</li>");
  621. }
  622. else
  623. {
  624. sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
  625. }
  626. }
  627. }
  628. var sbPage = new StringBuilder(Resource.listPage);
  629. sbPage.Replace("<!-- list place here -->", sb.ToString());
  630. context.WriteTextUtf8(sbPage.ToString());
  631. }
  632. else
  633. {
  634. context.Response.StatusCode = 404;
  635. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  636. }
  637. }
  638. else if (pathParts.Count == 3 && pathParts[0] == "lib_list" && pathParts[2] == "playlist.m3u8")
  639. {
  640. var libName = pathParts[1];
  641. if (Libraries.TryGetValue(libName, out var lib))
  642. {
  643. var sb = new StringBuilder();
  644. sb.WriteM3U8Header();
  645. foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key))
  646. {
  647. var disc = discKvp.Value;
  648. var discPath = discKvp.Key;
  649. var tracks = disc.MainTracks;
  650. foreach (var track in tracks.OrderBy(p => p.Key))
  651. {
  652. var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg";
  653. var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}");
  654. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  655. {
  656. sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}");
  657. }
  658. else
  659. {
  660. sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}");
  661. }
  662. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}";
  663. sb.AppendLine(requestAbsPrefix + mediaPath);
  664. }
  665. }
  666. context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U);
  667. }
  668. else
  669. {
  670. context.Response.StatusCode = 404;
  671. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  672. }
  673. }
  674. else if (pathParts.Count == 4 && pathParts[0] == "lib_list" && pathParts[3] == "playlist.m3u8.html")
  675. {
  676. var libName = pathParts[1];
  677. var trackSetName = pathParts[2];
  678. if (Libraries.TryGetValue(libName, out var lib))
  679. {
  680. var sb = new StringBuilder();
  681. foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key))
  682. {
  683. var disc = discKvp.Value;
  684. var discPath = discKvp.Key;
  685. if (disc.SubTracks.TryGetValue(trackSetName, out var tracksSet))
  686. {
  687. var tracks = tracksSet.Tracks;
  688. foreach (var track in tracks.OrderBy(p => p.Key))
  689. {
  690. var mediaTag = GetTag($"/media/{libName}/{discPath}/{trackSetName}/{track.Key}");
  691. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{trackSetName.UrlEscape()}/{track.Key.UrlEscape()}";
  692. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  693. {
  694. sb.AppendLine($"<li>" +
  695. $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
  696. $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
  697. $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
  698. $"</li>");
  699. }
  700. else
  701. {
  702. sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
  703. }
  704. }
  705. }
  706. }
  707. var sbPage = new StringBuilder(Resource.listPage);
  708. sbPage.Replace("<!-- list place here -->", sb.ToString());
  709. context.WriteTextUtf8(sbPage.ToString());
  710. }
  711. else
  712. {
  713. context.Response.StatusCode = 404;
  714. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  715. }
  716. }
  717. else if (pathParts.Count == 4 && pathParts[0] == "lib_list" && pathParts[3] == "playlist.m3u8")
  718. {
  719. var libName = pathParts[1];
  720. var trackSetName = pathParts[2];
  721. if (Libraries.TryGetValue(libName, out var lib))
  722. {
  723. var sb = new StringBuilder();
  724. sb.WriteM3U8Header();
  725. foreach (var discKvp in lib.Discs.OrderByDescending(p => p.Key))
  726. {
  727. var disc = discKvp.Value;
  728. var discPath = discKvp.Key;
  729. if (disc.SubTracks.TryGetValue(trackSetName, out var tracksSet))
  730. {
  731. var tracks = tracksSet.Tracks;
  732. foreach (var track in tracks.OrderBy(p => p.Key))
  733. {
  734. var mediaTag = GetTag($"/media/{libName}/{discPath}/{trackSetName}/{track.Key}");
  735. var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg";
  736. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{trackSetName.UrlEscape()}/{track.Key.UrlEscape()}";
  737. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  738. {
  739. sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}");
  740. }
  741. else
  742. {
  743. sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}");
  744. }
  745. sb.AppendLine(requestAbsPrefix + mediaPath);
  746. }
  747. }
  748. }
  749. context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U);
  750. }
  751. else
  752. {
  753. context.Response.StatusCode = 404;
  754. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  755. }
  756. }
  757. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "playlist.m3u8.html")
  758. {
  759. var libName = pathParts[1];
  760. var discPath = pathParts[2];
  761. if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc))
  762. {
  763. var sb = new StringBuilder();
  764. foreach (var track in disc.MainTracks.OrderBy(p => p.Key))
  765. {
  766. var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}");
  767. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}";
  768. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  769. {
  770. sb.AppendLine($"<li>" +
  771. $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
  772. $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
  773. $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
  774. $"</li>");
  775. }
  776. else
  777. {
  778. sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
  779. }
  780. }
  781. var sbPage = new StringBuilder(Resource.listPage);
  782. sbPage.Replace("<!-- list place here -->", sb.ToString());
  783. context.WriteTextUtf8(sbPage.ToString());
  784. }
  785. else
  786. {
  787. context.Response.StatusCode = 404;
  788. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  789. }
  790. }
  791. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "playlist.m3u8")
  792. {
  793. var libName = pathParts[1];
  794. var discPath = pathParts[2];
  795. if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc))
  796. {
  797. var sb = new StringBuilder();
  798. sb.WriteM3U8Header();
  799. sb.AppendLine($"#EXTALB: {discPath}");
  800. var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg";
  801. foreach (var track in disc.MainTracks.OrderBy(p => p.Key))
  802. {
  803. var mediaTag = GetTag($"/media/{libName}/{discPath}/{track.Key}");
  804. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  805. {
  806. sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}");
  807. }
  808. else
  809. {
  810. sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}");
  811. }
  812. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{track.Key.UrlEscape()}";
  813. sb.AppendLine(requestAbsPrefix + mediaPath);
  814. }
  815. context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U);
  816. }
  817. else
  818. {
  819. context.Response.StatusCode = 404;
  820. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  821. }
  822. }
  823. else if (pathParts.Count == 5 && pathParts[0] == "list" && pathParts[4] == "playlist.m3u8.html")
  824. {
  825. var libName = pathParts[1];
  826. var discPath = pathParts[2];
  827. var subSetPath = pathParts[3];
  828. if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc))
  829. {
  830. if (false == disc.SubTracks.TryGetValue(subSetPath, out var trackSet))
  831. {
  832. context.Response.StatusCode = 404;
  833. }
  834. else
  835. {
  836. var sb = new StringBuilder();
  837. foreach (var track in trackSet.Tracks.OrderBy(p => p.Key))
  838. {
  839. var mediaTag = GetTag($"/media/{libName}/{discPath}/{subSetPath}/{track.Key}");
  840. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{subSetPath.UrlEscape()}/{track.Key.UrlEscape()}";
  841. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  842. {
  843. sb.AppendLine($"<li>" +
  844. $"<span class=\"simp-source\" data-src=\"{mediaPath}\">{mediaTag.Title}</span>" +
  845. $"<span class=\"simp-desc\">{mediaTag.Artist}</span>" +
  846. $"<span style=\"float:right\">{mediaTag.Duration.SecondToDur()}</span>" +
  847. $"</li>");
  848. }
  849. else
  850. {
  851. sb.AppendLine($"<li><span class=\"simp-source\" data-src=\"{mediaPath}\">{track.Value}</span></li>");
  852. }
  853. }
  854. var sbPage = new StringBuilder(Resource.listPage);
  855. sbPage.Replace("<!-- list place here -->", sb.ToString());
  856. context.WriteTextUtf8(sbPage.ToString());
  857. }
  858. }
  859. else
  860. {
  861. context.Response.StatusCode = 404;
  862. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  863. }
  864. }
  865. else if (pathParts.Count == 5 && pathParts[0] == "list" && pathParts[4] == "playlist.m3u8")
  866. {
  867. var libName = pathParts[1];
  868. var discPath = pathParts[2];
  869. var subSetPath = pathParts[3];
  870. if (Libraries.TryGetValue(libName, out var lib) && lib.Discs.TryGetValue(discPath, out var disc))
  871. {
  872. if (false == disc.SubTracks.TryGetValue(subSetPath, out var trackSet))
  873. {
  874. context.Response.StatusCode = 404;
  875. }
  876. else
  877. {
  878. var sb = new StringBuilder();
  879. sb.WriteM3U8Header();
  880. foreach (var track in trackSet.Tracks.OrderBy(p => p.Key))
  881. {
  882. var mediaTag = GetTag($"/media/{libName}/{discPath}/{subSetPath}/{track.Key}");
  883. var coverPath = $"/cover/{libName.UrlEscape()}/{discPath.UrlEscape()}/cover.jpg";
  884. if (mediaTag != null && false == string.IsNullOrWhiteSpace(mediaTag.Title))
  885. {
  886. sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{requestAbsPrefix + coverPath}\",{mediaTag.Artist} - {mediaTag.Title}");
  887. }
  888. else
  889. {
  890. sb.AppendLine($"#EXTINF:0 logo=\"{requestAbsPrefix + coverPath}\",{Path.GetFileNameWithoutExtension(track.Key)}");
  891. }
  892. var mediaPath = $"/media/{libName.UrlEscape()}/{discPath.UrlEscape()}/{subSetPath.UrlEscape()}/{track.Key.UrlEscape()}";
  893. sb.AppendLine(requestAbsPrefix + mediaPath);
  894. }
  895. context.WriteTextUtf8(sb.ToString(), Const.ContentTypeM3U);
  896. }
  897. }
  898. else
  899. {
  900. context.Response.StatusCode = 404;
  901. //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  902. }
  903. }
  904. else if (requestPath == "/metadata/progress.json")
  905. {
  906. var dic = new Dictionary<string, object>()
  907. {
  908. {"IsLoading",_isLoading},
  909. {nameof(TotalTrackCount),TotalTrackCount},
  910. {nameof(MediaTags),MediaTags.Count},
  911. };
  912. context.WriteTextUtf8(JsonConvert.SerializeObject(new LoadingProgress
  913. {
  914. IsLoading = _isLoading,
  915. TotalTrackCount = TotalTrackCount,
  916. LoadedTags = MediaTags.Count
  917. }), Const.ApplicationJson);
  918. }
  919. else if (requestPath == "/metadata/file-set.json/checksum") context.WriteTextUtf8(_fileSetJsonChecksum);
  920. else if (requestPath == "/metadata/file-set.json")
  921. {
  922. if (_fileSetJsonChecksum != null && request.Headers["If-None-Match"] == _fileSetJsonChecksum)
  923. {
  924. context.Response.StatusCode = 304;
  925. context.Response.AddHeader("x-custom", "cache applied 304");
  926. }
  927. else
  928. {
  929. context.Response.AddHeader("Last-Modified", _cacheTime.ToString("R"));
  930. context.Response.AddHeader("Cache-Control", "public");
  931. context.Response.AddHeader("ETAG", _fileSetJsonChecksum);
  932. context.WriteTextUtf8(_fileSetJson ?? "null", Const.ApplicationJson);
  933. }
  934. }
  935. else if (requestPath == "/metadata/tag-dict.json/checksum") context.WriteTextUtf8(_tagSetJsonChecksum);
  936. else if (requestPath == "/metadata/tag-dict.json")
  937. {
  938. if (_tagSetJsonChecksum != null && request.Headers["If-None-Match"] == _tagSetJsonChecksum)
  939. {
  940. context.Response.StatusCode = 304;
  941. context.Response.AddHeader("x-custom", "cache applied 304");
  942. }
  943. else
  944. {
  945. context.Response.AddHeader("Last-Modified", _cacheTime.ToString("R"));
  946. context.Response.AddHeader("Cache-Control", "public");
  947. context.Response.AddHeader("ETAG", _tagSetJsonChecksum);
  948. context.WriteTextUtf8(_tagSetJson ?? "null", Const.ApplicationJson);
  949. }
  950. }
  951. else if (PathMapping.TryGetValue(requestPath, out var realPath))
  952. {
  953. var ext = requestPath.Split('.').LastOrDefault()?.ToLower();
  954. switch (ext)
  955. {
  956. case "flac": context.Response.ContentType = "audio/flac"; break;
  957. case "m4a": context.Response.ContentType = "audio/mp4"; break;
  958. case "mp3": context.Response.ContentType = "audio/mpeg"; break;
  959. case "aac": context.Response.ContentType = "audio/aac"; break;
  960. case "mp4": context.Response.ContentType = "video/mp4"; break;
  961. case "mkv": context.Response.ContentType = "video/webm"; break;
  962. case "jpg":
  963. case "jpeg": context.Response.ContentType = "image/jpeg"; break;
  964. case "png": context.Response.ContentType = "image/png"; break;
  965. case "bmp": context.Response.ContentType = "image/bmp"; break;
  966. default:
  967. var firstParts = requestPath.Split('/').FirstOrDefault();
  968. switch (firstParts)
  969. {
  970. case "media": context.Response.ContentType = "audio/" + ext; break;
  971. case "bk":
  972. case "cover": context.Response.ContentType = "image/" + ext; break;
  973. }
  974. break;
  975. }
  976. var range = request.Headers.GetValues("Range");
  977. FileStream fs = null;
  978. try
  979. {
  980. fs = File.OpenRead(realPath);
  981. context.Response.Headers.Add("Accept-Ranges", "bytes");
  982. if (range is { Length: > 0 })
  983. {
  984. var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
  985. if (rngParts.Length == 1 && long.TryParse(rngParts[0], out var start))
  986. {
  987. fs.Position = start;
  988. context.Response.StatusCode = 206;
  989. context.Response.Headers.Add("Content-Range", $"bytes {start}-{fs.Length - 1}/{fs.Length}");
  990. context.Response.ContentLength64 = fs.Length - start;
  991. fs.CopyTo(context.Response.OutputStream);
  992. }
  993. else if (rngParts.Length == 2 && long.TryParse(rngParts[0], out var start2) && long.TryParse(rngParts[1], out var end))
  994. {
  995. fs.Position = start2;
  996. var realLen = end - start2 + 1;
  997. context.Response.StatusCode = 206;
  998. context.Response.Headers.Add("Content-Range", $"bytes {start2}-{end}/{fs.Length}");
  999. context.Response.ContentLength64 = realLen;
  1000. if (realLen < 4096)
  1001. {
  1002. var buf = new byte[realLen];
  1003. fs.Read(buf, 0, buf.Length);
  1004. context.Response.OutputStream.Write(buf, 0, buf.Length);
  1005. }
  1006. else
  1007. {
  1008. fs.CopyTo(context.Response.OutputStream);
  1009. }
  1010. }
  1011. }
  1012. else
  1013. {
  1014. context.Response.ContentLength64 = fs.Length;
  1015. fs.CopyTo(context.Response.OutputStream);
  1016. }
  1017. }
  1018. catch (Exception e)
  1019. {
  1020. Console.WriteLine(e);
  1021. }
  1022. finally
  1023. {
  1024. fs?.Close();
  1025. }
  1026. }
  1027. else
  1028. {
  1029. context.Response.StatusCode = 404;
  1030. }
  1031. }
  1032. catch (Exception e)
  1033. {
  1034. Console.WriteLine(e);
  1035. try
  1036. {
  1037. context.Response.StatusCode = 500;
  1038. }
  1039. catch (Exception exception)
  1040. {
  1041. Console.WriteLine(exception);
  1042. }
  1043. }
  1044. finally
  1045. {
  1046. try
  1047. {
  1048. context.Response.Close();
  1049. }
  1050. catch (Exception e)
  1051. {
  1052. Console.WriteLine(e);
  1053. }
  1054. }
  1055. Console.WriteLine($"Request #{num:00000} ends with status code: {context.Response.StatusCode}");
  1056. }
  1057. private static MediaTag GetTag(string internalPath, bool peekMem = false)
  1058. {
  1059. if (peekMem)
  1060. {
  1061. return MediaTags.TryGetValue(internalPath, out var peek) ? peek : null;
  1062. }
  1063. if (MediaTags.TryGetValue(internalPath, out var mediaTag) || !PathMapping.TryGetValue(internalPath, out var mediaFilePath)) return mediaTag;
  1064. try
  1065. {
  1066. var fi = new FileInfo(mediaFilePath);
  1067. using var tagLib = TagLib.File.Create(mediaFilePath);
  1068. mediaTag = MediaTags[internalPath] = new MediaTag(
  1069. $"{tagLib.Tag.Title}",
  1070. (int)tagLib.Properties.Duration.TotalSeconds,
  1071. fi.Length,
  1072. string.Join(";", tagLib.Tag.Performers)
  1073. );
  1074. }
  1075. catch (Exception e)
  1076. {
  1077. Console.WriteLine($"ERROR on get tags: {mediaFilePath}{Environment.NewLine} {e.Message}");
  1078. return null;
  1079. }
  1080. return mediaTag;
  1081. }
  1082. }
  1083. }