HostProgram.cs 58 KB

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