HostProgram.cs 62 KB

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