Program0.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Text;
  8. using System.Threading;
  9. using FNZCM.Core;
  10. using Microsoft.VisualBasic.FileIO;
  11. using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption;
  12. namespace FNZCM.ConHost.Ver0
  13. {
  14. internal static class Program0
  15. {
  16. private static bool _isRunning;
  17. private static IReadOnlyDictionary<string, Library> _library;
  18. private static IReadOnlyDictionary<string, string> _pathMapping;
  19. private const int SmallFileSize = 1024 * 1024 * 5; //5MB
  20. private static IReadOnlyDictionary<string, byte[]> _smallFileCaches;
  21. private static readonly ConcurrentDictionary<string, MediaTag> MediaTags = new();
  22. private static DateTime _lastRequestAccepted;
  23. private static void Main0()
  24. {
  25. Console.WriteLine("Scanning libraries...");
  26. var libs = new Dictionary<string, Library>();
  27. var mappings = new Dictionary<string, string>();
  28. foreach (var library in ConfigFile.Instance.Libraries)
  29. {
  30. var libraryName = library.Key;
  31. var libraryPathName = libraryName.ToLower();
  32. var albums = Directory.GetDirectories(library.Value);
  33. var albumObjects = new List<Album>();
  34. foreach (var albumPath in albums)
  35. {
  36. var albumName = Path.GetFileName(albumPath);
  37. var albumPathName = albumName.ToLower();
  38. //media
  39. var mediaFiles = FileSystem.GetFiles(albumPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern);
  40. foreach (var mediaFile in mediaFiles) mappings[$"/media/{library.Key}/{albumName}/{Path.GetFileName(mediaFile)}".ToLower()] = mediaFile;
  41. var tracks = mediaFiles.Select(Path.GetFileName).ToArray();
  42. var trackPaths = tracks.Select(p => p.ToLower()).ToArray();
  43. //bk
  44. var bkPath = Path.Combine(albumPath, "bk");
  45. IReadOnlyCollection<string> bks = null;
  46. if (Directory.Exists(bkPath))
  47. {
  48. var bkFiles = FileSystem.GetFiles(bkPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern);
  49. foreach (var bkFile in bkFiles) mappings[$"/bk/{library.Key}/{albumName}/{Path.GetFileName(bkFile)}".ToLower()] = bkFile;
  50. bks = bkFiles.Select(s => Path.GetFileName(s).ToLower()).ToArray();
  51. }
  52. //cover
  53. var coverPath = Path.Combine(albumPath, "cover.jpg");
  54. if (File.Exists(coverPath)) mappings[$"/cover/{library.Key}/{albumName}/{Path.GetFileName(coverPath)}".ToLower()] = coverPath;
  55. albumObjects.Add(new Album(albumName, albumPathName, tracks, trackPaths, bks));
  56. }
  57. libs[library.Key.ToLower()] = new Library(libraryName, libraryPathName, albumObjects);
  58. }
  59. _library = libs;
  60. _pathMapping = mappings;
  61. Console.WriteLine($" Scanned all {libs.Count} library");
  62. Console.WriteLine($" Total {libs.Sum(p => p.Value.Albums.Count)} Albums, {libs.Sum(p => p.Value.Albums.Sum(q => q.Tracks.Count))} Tracks");
  63. Console.Write("Reading small files to cache");
  64. Console.WriteLine();
  65. var caches = new Dictionary<string, byte[]>();
  66. foreach (var map in _pathMapping)
  67. {
  68. var fileSize = new FileInfo(map.Value).Length;
  69. if (fileSize < SmallFileSize)
  70. {
  71. Console.Write($" Reading: {fileSize / (1024 * 1024.0):N6} MB {map.Value} ");
  72. caches[map.Key] = File.ReadAllBytes(map.Value);
  73. var left = Console.CursorLeft;
  74. Console.CursorLeft = 0;
  75. Console.Write("".PadLeft(left));
  76. Console.CursorLeft = 0;
  77. }
  78. }
  79. _smallFileCaches = caches;
  80. Console.WriteLine($" Cached {_smallFileCaches.Count} small files, total {_smallFileCaches.Values.Sum(p => p.Length) / (1024.0 * 1024):N6} MB");
  81. Console.WriteLine("Starting...");
  82. var tWorker = new Thread(Working);
  83. _isRunning = true;
  84. tWorker.Start();
  85. Console.WriteLine("Press ENTER to Stop.");
  86. Console.ReadLine();
  87. Console.WriteLine("Shutting down...");
  88. _isRunning = false;
  89. tWorker.Join();
  90. Console.WriteLine("Stopped.");
  91. Console.WriteLine();
  92. Console.Write("Press ENTER to Exit.");
  93. Console.ReadLine();
  94. }
  95. private static void Working()
  96. {
  97. var listener = new HttpListener();
  98. listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix);
  99. listener.Start();
  100. var upTime = DateTime.Now;
  101. Console.WriteLine($"HTTP Server started, listening on {ConfigFile.Instance.ListenPrefix}");
  102. listener.BeginGetContext(ContextGet, listener);
  103. _lastRequestAccepted = DateTime.Now;
  104. while (_isRunning)
  105. {
  106. var timeSpan = DateTime.Now - _lastRequestAccepted;
  107. var up = DateTime.Now - upTime;
  108. Console.Title =
  109. "FNZCM"
  110. + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}"
  111. + $" / "
  112. + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}"
  113. ;
  114. Thread.Sleep(1000);
  115. }
  116. listener.Close();
  117. Thread.Sleep(1000);
  118. }
  119. private static void ContextGet(IAsyncResult ar)
  120. {
  121. var listener = (HttpListener)ar.AsyncState;
  122. HttpListenerContext context;
  123. try
  124. {
  125. // ReSharper disable once PossibleNullReferenceException
  126. context = listener.EndGetContext(ar);
  127. }
  128. catch (Exception e)
  129. {
  130. Console.WriteLine(e);
  131. return;
  132. }
  133. if (_isRunning) listener.BeginGetContext(ContextGet, listener);
  134. ProcessRequest(context);
  135. }
  136. private static void ProcessRequest(HttpListenerContext context)
  137. {
  138. _lastRequestAccepted = DateTime.Now;
  139. var request = context.Request;
  140. Console.WriteLine($"Request from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}");
  141. // GET / show all libraries
  142. // foo=library bar=album
  143. // GET /list/foo/ show all album and cover with name, provide m3u path
  144. // GET /list/foo/bar/bk/ list all picture as grid
  145. // GET /list/foo/bar/tracks/ list all tracks as text list
  146. // GET /list/foo/bar/playlist.m3u8 auto gen
  147. // media streaming HTTP Partial RANGE SUPPORT
  148. // GET /cover/foo/bar/cover.jpg
  149. // GET /media/foo/bar/01.%20foobar.flac
  150. // GET /bk/foo/bar/foobar.jpg
  151. try
  152. {
  153. // ReSharper disable once PossibleNullReferenceException
  154. var requestPath = request.Url.LocalPath.ToLower();
  155. var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  156. if (requestPath == "/")
  157. {
  158. var sb = new StringBuilder();
  159. sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
  160. sb.Append("<body bgColor=skyBlue style=font-size:3vh><h1>Libraries</h1>");
  161. sb.Append("<ul>");
  162. foreach (var library in ConfigFile.Instance.Libraries.Keys)
  163. {
  164. sb.Append("<li>");
  165. sb.Append($"<a href=/list/{library}/>{library}</a>");
  166. sb.Append("</li>");
  167. }
  168. sb.Append("</ul>");
  169. context.Response.WriteText(sb.ToString());
  170. }
  171. else if (pathParts.Count == 2 && pathParts[0] == "list")
  172. {
  173. var libName = pathParts[1];
  174. if (_library.TryGetValue(libName, out var lib))
  175. {
  176. var sb = new StringBuilder();
  177. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  178. sb.Append($"<title> Albums of {lib.Name} - {ConfigFile.Instance.Title} </title>");
  179. sb.Append(
  180. "<style>" +
  181. "a:link{ text-decoration: none; }" +
  182. "div.item{" +
  183. " vertical-align:top;" +
  184. " height:20vh;" +
  185. " margin-bottom:1vh;" +
  186. " padding:0.5vh;" +
  187. " border:solid 1px;" +
  188. " border-radius:0.5vh;" +
  189. " font-size:2.5vh;" +
  190. " overflow:scroll;" +
  191. "}" +
  192. "div.item::-webkit-scrollbar{" +
  193. " display: none;" +
  194. "}" +
  195. "img.cover{" +
  196. " float:left;" +
  197. " background-size:cover;" +
  198. " max-width:25vw;" +
  199. " max-height:20vh" +
  200. "}" +
  201. "div.buttons{" +
  202. "}" +
  203. "a.button{" +
  204. " margin-left:4vw" +
  205. "}" +
  206. "</style>");
  207. sb.Append($"<body bgColor=skyBlue><h1>Albums of {lib.Name}</h1>");
  208. sb.Append("<div><a href=/>Back to home</a></div>");
  209. //Cover list
  210. foreach (var a in lib.Albums)
  211. {
  212. sb.Append("<div class=item>");
  213. sb.Append($"<img class=cover src=\"/cover/{lib.PathName}/{a.PathName}/cover.jpg\" />");
  214. sb.Append("<div class=buttons>");
  215. sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName}/tracks/\">[TRACKERS]</a>");
  216. if (a.Bks != null) sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName}/bk/\">[BK]</a>");
  217. sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName.FuckVlc()}/playlist.m3u8\">[M3U8]</a>");
  218. sb.Append("</div>");
  219. sb.Append($"<span>{a.Name}<span>");
  220. sb.Append("</div>");
  221. }
  222. context.Response.ContentType = "text/html";
  223. context.Response.ContentEncoding = Encoding.UTF8;
  224. context.Response.WriteText(sb.ToString());
  225. }
  226. else
  227. {
  228. context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  229. }
  230. }
  231. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "tracks")
  232. {
  233. var libName = pathParts[1];
  234. var albPath = pathParts[2];
  235. Album alb;
  236. if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
  237. {
  238. var sb = new StringBuilder();
  239. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  240. sb.Append($"<body bgColor=skyBlue style=font-size:2vh><h2>Tracks of</h2><h1>{alb.Name}</h1>");
  241. sb.Append($"<div><a href=/list/{lib.PathName}/>Back to library</a></div>");
  242. for (var i = 0; i < alb.TrackPaths.Count; i++)
  243. {
  244. sb.Append($"<li><a href=\"/media/{lib.PathName.FuckVlc()}/{alb.PathName.FuckVlc()}/{alb.TrackPaths[i].FuckVlc()}\" >{alb.Tracks[i]}</a></li>");
  245. }
  246. context.Response.ContentType = "text/html";
  247. context.Response.ContentEncoding = Encoding.UTF8;
  248. context.Response.WriteText(sb.ToString());
  249. }
  250. else
  251. {
  252. context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  253. }
  254. }
  255. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "bk")
  256. {
  257. var libName = pathParts[1];
  258. var albPath = pathParts[2];
  259. Album alb;
  260. if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
  261. {
  262. var sb = new StringBuilder();
  263. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  264. sb.Append($"<body bgColor=skyBlue style=font-size:2vh><h2>BK of </h2><h1>{alb.Name}</h1>");
  265. sb.Append($"<div><a href=/list/{lib.PathName}/>Back to library</a></div>");
  266. foreach (var albBk in alb.Bks)
  267. {
  268. //TODO: auto gen thumbnail
  269. sb.Append($"<img src='/bk/{lib.PathName}/{alb.PathName}/{albBk}' style=max-width:24vw;max-height:24vw;margin-right:1vw;margin-bottom:1vh; />");
  270. }
  271. context.Response.ContentType = "text/html";
  272. context.Response.ContentEncoding = Encoding.UTF8;
  273. context.Response.WriteText(sb.ToString());
  274. }
  275. else
  276. {
  277. context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  278. }
  279. }
  280. else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "playlist.m3u8")
  281. {
  282. var libName = pathParts[1];
  283. var albPath = pathParts[2];
  284. Album alb;
  285. if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
  286. {
  287. // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags
  288. var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}";
  289. var sb = new StringBuilder();
  290. sb.AppendLine("#EXTM3U");
  291. foreach (var track in alb.TrackPaths)
  292. {
  293. var mediaKey = $"/media/{lib.PathName}/{alb.PathName}/{track}";
  294. var coverKey = $"/cover/{lib.PathName}/{alb.PathName}/cover.jpg";
  295. if (false == MediaTags.TryGetValue(mediaKey, out var mediaTag))
  296. {
  297. using var tagLib = TagLib.File.Create(_pathMapping[mediaKey]);
  298. mediaTag = MediaTags[mediaKey] = new MediaTag($"{string.Join(";", tagLib.Tag.Performers)} - {tagLib.Tag.Title}", (int)tagLib.Properties.Duration.TotalSeconds);
  299. }
  300. sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{prefix + coverKey.FuckVlc()}\",{mediaTag.Title}");
  301. sb.AppendLine(prefix + mediaKey.FuckVlc());
  302. }
  303. context.Response.ContentType = "audio/mpegurl";
  304. context.Response.ContentEncoding = Encoding.UTF8;
  305. context.Response.WriteText(sb.ToString());
  306. }
  307. else
  308. {
  309. context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
  310. }
  311. }
  312. else if (_pathMapping.TryGetValue(requestPath, out var realPath))
  313. {
  314. var range = request.Headers.GetValues("Range");
  315. if (_smallFileCaches.TryGetValue(requestPath, out var data))
  316. {
  317. if (range is { Length: > 0 })
  318. {
  319. var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
  320. if (rngParts.Length >= 1 && long.TryParse(rngParts[0], out var start))
  321. {
  322. context.Response.StatusCode = 206;
  323. context.Response.Headers.Add("Accept-Ranges", "bytes");
  324. context.Response.Headers.Add("Content-Range", $"bytes {start}-{data.Length - 1}/{data.Length}");
  325. context.Response.ContentLength64 = data.Length - start;
  326. context.Response.ContentType = "video/mp4";
  327. context.Response.OutputStream.Write(new ReadOnlySpan<byte>(data, (int)start, (int)(data.Length - start)));
  328. }
  329. }
  330. else
  331. {
  332. context.Response.ContentType = "video/mp4";
  333. context.Response.OutputStream.Write(data);
  334. }
  335. }
  336. else
  337. {
  338. FileStream fs = null;
  339. try
  340. {
  341. fs = File.OpenRead(realPath);
  342. if (range is { Length: > 0 })
  343. {
  344. var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
  345. if (rngParts.Length >= 1 && long.TryParse(rngParts[0], out var start))
  346. {
  347. fs.Position = start;
  348. context.Response.StatusCode = 206;
  349. context.Response.Headers.Add("Accept-Ranges", "bytes");
  350. context.Response.Headers.Add("Content-Range", $"bytes {start}-{fs.Length - 1}/{fs.Length}");
  351. context.Response.ContentLength64 = fs.Length - start;
  352. context.Response.ContentType = "video/mp4";
  353. fs.CopyTo(context.Response.OutputStream);
  354. }
  355. }
  356. else
  357. {
  358. context.Response.ContentType = "video/mp4";
  359. context.Response.ContentLength64 = fs.Length;
  360. fs.CopyTo(context.Response.OutputStream);
  361. }
  362. }
  363. catch (Exception e)
  364. {
  365. Console.WriteLine(e);
  366. }
  367. finally
  368. {
  369. fs?.Close();
  370. }
  371. }
  372. }
  373. else
  374. {
  375. context.Response.StatusCode = 404;
  376. }
  377. }
  378. catch (Exception e)
  379. {
  380. Console.WriteLine(e);
  381. try
  382. {
  383. context.Response.StatusCode = 500;
  384. }
  385. catch (Exception exception)
  386. {
  387. Console.WriteLine(exception);
  388. }
  389. }
  390. finally
  391. {
  392. try
  393. {
  394. context.Response.Close();
  395. }
  396. catch (Exception e)
  397. {
  398. Console.WriteLine(e);
  399. }
  400. }
  401. }
  402. private static void WriteText(this HttpListenerResponse response, string content)
  403. {
  404. var bytes = Encoding.UTF8.GetBytes(content);
  405. response.OutputStream.Write(bytes);
  406. }
  407. private static string FuckVlc(this string input)
  408. {
  409. if (input == null) return null;
  410. return input
  411. .Replace("[", "%5B")
  412. .Replace("]", "%5D")
  413. ;
  414. }
  415. }
  416. }