Program0.cs 21 KB

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