HostProgram.cs 59 KB

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