HOME 2 роки тому
батько
коміт
5f38d8e7d3

+ 1 - 1
FNZCM/FNZCM.ConHost/LibraryModels.cs

@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 
-namespace FNZCM.ConHost
+namespace FNZCM.ConHost.Ver0
 {
     internal class Library
     {

+ 5 - 5
FNZCM/FNZCM.ConHost/Program.cs

@@ -1,5 +1,4 @@
-using Microsoft.VisualBasic.FileIO;
-using System;
+using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
@@ -7,11 +6,12 @@ using System.Linq;
 using System.Net;
 using System.Text;
 using System.Threading;
+using Microsoft.VisualBasic.FileIO;
 using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption;
 
-namespace FNZCM.ConHost
+namespace FNZCM.ConHost.Ver0
 {
-    internal static class Program
+    internal static class Program0
     {
         private static bool _isRunning;
 
@@ -25,7 +25,7 @@ namespace FNZCM.ConHost
 
         private static DateTime _lastRequestAccepted;
 
-        private static void Main()
+        private static void Main0()
         {
             Console.WriteLine("Scanning libraries...");
 

+ 47 - 0
FNZCM/FNZCM.ConHost/Ver2/LibraryModels2.cs

@@ -0,0 +1,47 @@
+using System.Collections.Concurrent;
+
+namespace FNZCM.ConHost.Ver2
+{
+    internal class Library2
+    {
+        public Library2(string name) => Name = name;
+
+        public string Name { get; }
+
+        public ConcurrentDictionary<string, Album2> Albums { get; } = new();
+    }
+
+    internal class Album2
+    {
+        public string Name { get; }
+
+        public Album2(string name) => Name = name;
+
+        public ConcurrentDictionary<string, string> Bks { get; } = new();
+
+        public ConcurrentDictionary<string, string> MainTracks { get; } = new();
+        public ConcurrentDictionary<string, TrackSet> SubTracks { get; } = new();
+    }
+
+    internal class TrackSet
+    {
+        public string Name { get; }
+
+        public TrackSet(string name) => Name = name;
+
+        public ConcurrentDictionary<string, string> Tracks { get; } = new();
+
+    }
+
+    internal class MediaTag2
+    {
+        public MediaTag2(string title, int duration)
+        {
+            Title = title;
+            Duration = duration;
+        }
+
+        public string Title { get; }
+        public int Duration { get; }
+    }
+}

+ 526 - 0
FNZCM/FNZCM.ConHost/Ver2/Program2.cs

@@ -0,0 +1,526 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualBasic.FileIO;
+using SearchOption = Microsoft.VisualBasic.FileIO.SearchOption;
+
+namespace FNZCM.ConHost.Ver2
+{
+    internal static class Program2
+    {
+        //0. start http server
+        //1. scan libraries and fill data struct
+        //    libs
+        //     albums
+        //      Tracks(FLAC / AAC_*)
+        //       Meta(title / artist / duration)
+
+        // TODO: Generate thumbnail of BKS
+
+        private static readonly ConcurrentDictionary<string, Library2> _library = new();
+        private static readonly ConcurrentDictionary<string, string> _pathMapping = new();
+        private static readonly ConcurrentDictionary<string, MediaTag2> _mediaTags = new();
+
+        private static bool _isRunning;
+        private static bool _isLoading;
+        private static DateTime _lastRequestAccepted;
+
+
+        private static void Main()
+        {
+            Console.WriteLine("Starting...");
+
+            var tWorker = new Thread(Working);
+            _isRunning = true;
+            tWorker.Start();
+
+            Task.Run(ScanLibrary);
+
+            Console.WriteLine("Press ENTER to Stop.");
+            Console.ReadLine();
+
+            Console.WriteLine("Shutting down...");
+            _isRunning = false;
+            tWorker.Join();
+
+            Console.WriteLine("Stopped.");
+
+            Console.WriteLine();
+            Console.Write("Press ENTER to Exit.");
+            Console.ReadLine();
+        }
+
+        private static void ScanLibrary()
+        {
+            if (_isLoading) return;
+            _isLoading = true;
+            try
+            {
+                Console.WriteLine("Scanning libraries...");
+
+                foreach (var kvpLib in ConfigFile.Instance.Libraries)
+                {
+                    Console.WriteLine($"Library {kvpLib.Key} - {kvpLib.Value}");
+
+                    var libPath = kvpLib.Key.ToLower();
+                    var lib = _library[libPath] = new Library2(kvpLib.Key);
+                    var albDirArray = Directory.GetDirectories(kvpLib.Value);
+
+                    foreach (var albDir in albDirArray)
+                    {
+                        Console.WriteLine($" Album {albDir}");
+
+                        var albName = Path.GetFileName(albDir);
+                        var albPath = albName.ToLower();
+                        var alb = lib.Albums[albPath] = new Album2(albName);
+
+                        var coverFilePath = Path.Combine(albDir, "cover.jpg");
+                        if (File.Exists(coverFilePath)) _pathMapping[$"/cover/{libPath}/{albPath}/cover.jpg"] = coverFilePath;
+
+                        var bkDir = Path.Combine(albDir, "bk");
+                        if (Directory.Exists(bkDir))
+                        {
+                            var bkFiles = FileSystem.GetFiles(bkDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern);
+                            foreach (var file in bkFiles)
+                            {
+                                var bkName = Path.GetFileName(file);
+                                var bkPath = bkName.ToLower();
+                                alb.Bks[bkPath] = bkName;
+
+                                _pathMapping[$"/bk/{libPath}/{albPath}/{bkPath}"] = file;
+                            }
+                        }
+
+                        var mainTrackFiles = FileSystem.GetFiles(albDir, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern);
+
+                        foreach (var mainTrackFile in mainTrackFiles)
+                        {
+                            var trackName = Path.GetFileName(mainTrackFile);
+                            var trackPath = trackName.ToLower();
+                            alb.MainTracks[trackPath] = trackName;
+
+                            _pathMapping[$"/media/{libPath}/{albPath}/{trackPath}"] = mainTrackFile;
+                        }
+
+                        var aacTrackDirArray = Directory.GetDirectories(albDir, "AAC_Q*");
+                        foreach (var aacTrackDir in aacTrackDirArray)
+                        {
+                            var aacTrackSetName = Path.GetFileName(aacTrackDir);
+                            var aacTrackSetPath = aacTrackSetName.ToLower();
+                            var aacTrackSet = alb.SubTracks[aacTrackSetPath] = new TrackSet(aacTrackSetName);
+
+                            foreach (var file in Directory.GetFiles(aacTrackDir))
+                            {
+                                var aacTrackName = Path.GetFileName(file);
+                                var aacTrackPath = aacTrackName.ToLower();
+                                aacTrackSet.Tracks[aacTrackPath] = aacTrackName;
+
+                                _pathMapping[$"/media/{libPath}/{albPath}/{aacTrackSetPath}/{aacTrackPath}"] = file;
+                            }
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine($"Load error: {e}");
+            }
+
+            _isLoading = false;
+        }
+
+        private static void Working()
+        {
+            var listener = new HttpListener();
+            listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix);
+            listener.Start();
+
+            var upTime = DateTime.Now;
+
+            Console.WriteLine($"HTTP Server started, listening on {ConfigFile.Instance.ListenPrefix}");
+
+            listener.BeginGetContext(ContextGet, listener);
+
+            _lastRequestAccepted = DateTime.Now;
+            while (_isRunning)
+            {
+                var timeSpan = DateTime.Now - _lastRequestAccepted;
+                var up = DateTime.Now - upTime;
+                Console.Title =
+                    "FNZCM"
+                    + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}"
+                    + $" / "
+                    + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}"
+                ;
+
+                Thread.Sleep(1000);
+            }
+
+            listener.Close();
+
+            Thread.Sleep(1000);
+        }
+
+        private static void ContextGet(IAsyncResult ar)
+        {
+            var listener = (HttpListener)ar.AsyncState;
+            HttpListenerContext context;
+
+            try
+            {
+                // ReSharper disable once PossibleNullReferenceException
+                context = listener.EndGetContext(ar);
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+                return;
+            }
+
+            if (_isRunning) listener.BeginGetContext(ContextGet, listener);
+            ProcessRequest(context);
+        }
+
+        private static void ProcessRequest(HttpListenerContext context)
+        {
+            _lastRequestAccepted = DateTime.Now;
+
+            var request = context.Request;
+            Console.WriteLine($"Request from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}");
+
+            // GET / show all libraries
+
+            // foo=library bar=album
+            // GET /list/foo/ show all album and cover with name, provide m3u path
+            // GET /list/foo/bar/bk/ list all picture as grid
+            // GET /list/foo/bar/tracks/ list all tracks as text list
+            // GET /list/foo/bar/playlist.m3u8 auto gen
+
+            // GET /list/foo/bar/aac_q1.00/playlist.m3u8 auto gen
+
+            // media streaming HTTP Partial RANGE SUPPORT
+            // GET /cover/foo/bar/cover.jpg
+            // GET /media/foo/bar/01.%20foobar.flac
+            // GET /bk/foo/bar/foobar.jpg
+
+            // GET /media/foo/aac_q1.00/01.%20foobar.m4a
+
+            try
+            {
+                // ReSharper disable once PossibleNullReferenceException
+                var requestPath = request.Url.LocalPath.ToLower();
+                var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+                if (requestPath == "/scan/")
+                {
+                    Task.Run(ScanLibrary);
+                    context.Response.Redirect("/");
+                }
+                else if (requestPath == "/")
+                {
+                    var sb = new StringBuilder();
+                    sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                    sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
+                    sb.Append("<body bgColor=skyBlue style=font-size:3vh>");
+
+                    if (_isLoading) sb.Append("<h2 style=position:fixed;right:0px;top:0px;margin:0>Still Loading...</h2>");
+
+                    sb.Append($"<h1>{ConfigFile.Instance.Title}</h1>");
+                    sb.Append("<h1>Libraries</h1>");
+
+                    sb.Append("<ul>");
+                    foreach (var library in _library.OrderBy(p => p.Key))
+                    {
+                        sb.Append("<li>");
+                        sb.Append($"<a href='/list/{library.Key.FuckVlcAndEscape()}/'>{library.Value.Name}</a>");
+                        sb.Append("</li>");
+                    }
+
+                    sb.Append("</ul>");
+
+                    sb.Append("<a href=/scan/> Scan Libraries</a>");
+
+                    context.Response.WriteText(sb.ToString());
+                }
+                else if (pathParts.Count == 2 && pathParts[0] == "list")
+                {
+                    var libName = pathParts[1];
+                    if (_library.TryGetValue(libName, out var l))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<title> Albums of {l.Name} - {ConfigFile.Instance.Title} </title>");
+                        sb.Append(
+                            "<style>" +
+                            "a:link{ text-decoration: none; }" +
+                            "div.item{" +
+                            " vertical-align:top;" +
+                            " height:20vh;" +
+                            " margin-bottom:1vh;" +
+                            " padding:0.5vh;" +
+                            " border:solid 1px;" +
+                            " border-radius:0.5vh;" +
+                            " font-size:2.5vh;" +
+                            " overflow:scroll;" +
+                            "}" +
+                            "div.item::-webkit-scrollbar{" +
+                            " display: none;" +
+                            "}" +
+                            "img.cover{" +
+                            " float:left;" +
+                            " background-size:cover;" +
+                            " max-width:25vw;" +
+                            " max-height:20vh" +
+                            "}" +
+                            "div.buttons{" +
+                            "}" +
+                            "a.button{" +
+                            " margin-left:4vw" +
+                            "}" +
+                            "</style>");
+
+                        sb.Append($"<body bgColor=skyBlue>");
+
+                        if (_isLoading) sb.Append("<h2 style=position:fixed;right:0px;top:0px;margin:0>Still Loading...</h2>");
+
+                        sb.Append($"<h1>Albums of {l.Name}</h1>");
+                        sb.Append("<div><a href=/>Back to home</a></div>");
+
+                        //Cover list
+                        foreach (var a in l.Albums.OrderBy(p => p.Key))
+                        {
+                            sb.Append("<div class=item>");
+                            sb.Append($"<img class=cover src=\"/cover/{libName}/{a.Key}/cover.jpg\" />");
+                            sb.Append("<div class=buttons>");
+                            sb.Append($"<a class=button href=\"/list/{libName}/{a.Key}/tracks/\">[TRACKERS]</a>");
+                            if (a.Value.Bks?.Count > 0) sb.Append($"<a class=button href=\"/list/{libName}/{a.Key}/bk/\">[BK]</a>");
+                            sb.Append($"<a class=button href=\"/list/{libName}/{a.Key.FuckVlcAndEscape()}/playlist.m3u8\">[M3U8]</a>");
+                            //TODO: AAC M3U8
+                            sb.Append("</div>");
+                            sb.Append($"<span>{a.Value.Name}<span>");
+                            sb.Append("</div>");
+                        }
+
+                        context.Response.ContentType = "text/html";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.StatusCode = 404;
+                        //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "tracks")
+                {
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+
+                    if (_library.TryGetValue(libName, out var l) && l.Albums.TryGetValue(albPath, out var alb))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");
+
+                        if (_isLoading) sb.Append("<h2 style=position:fixed;right:0px;top:0px;margin:0>Still Loading...</h2>");
+
+                        sb.Append($"<h2>Tracks of</h2><h1>{alb.Name}</h1>");
+                        sb.Append($"<div><a href=/list/{libName}/>Back to library</a></div>");
+
+                        if (alb.SubTracks.Count > 0) sb.Append($"<div>Main</div>");
+
+                        foreach (var kvpTrack in alb.MainTracks.OrderBy(p => p.Key))
+                        {
+                            sb.Append($"<li><a href=\"/media/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{kvpTrack.Key.FuckVlcAndEscape()}\" >{kvpTrack.Value}</a></li>");
+                        }
+
+                        foreach (var kvpAacSet in alb.SubTracks.OrderBy(p => p.Key))
+                        {
+                            sb.Append($"<div>{kvpAacSet.Value.Name}</div>");
+
+                            foreach (var kvpTrack in kvpAacSet.Value.Tracks.OrderBy(p => p.Key))
+                            {
+                                sb.Append($"<li><a href=\"/media/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{kvpAacSet.Key.FuckVlcAndEscape()}/{kvpTrack.Key.FuckVlcAndEscape()}\" >{kvpTrack.Value}</a></li>");
+                            }
+                        }
+
+                        context.Response.ContentType = "text/html";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.StatusCode = 404;
+                        //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "bk")
+                {
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+                    if (_library.TryGetValue(libName, out var lib) && lib.Albums.TryGetValue(albPath, out var alb))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<body bgColor=skyBlue style=font-size:2vh>");
+
+                        if (_isLoading) sb.Append("<h2 style=position:fixed;right:0px;top:0px;margin:0>Still Loading...</h2>");
+
+                        sb.Append($"<h2>BK of </h2><h1>{alb.Name}</h1>");
+                        sb.Append($"<div><a href=/list/{libName}/>Back to library</a></div>");
+
+                        foreach (var albBk in alb.Bks.OrderBy(p => p.Key))
+                        {
+                            //TODO: auto gen thumbnail 512x512 jpg 80 
+                            sb.Append($"<img src='/bk/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{albBk.Key.FuckVlcAndEscape()}' style=max-width:24vw;max-height:24vw;margin-right:1vw;margin-bottom:1vh; />");
+                        }
+
+                        context.Response.ContentType = "text/html";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.StatusCode = 404;
+                        //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4 && pathParts[0] == "list" && pathParts[3] == "playlist.m3u8")
+                {
+                    //TODO: AAC M3U8
+
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+                    if (_library.TryGetValue(libName, out var lib) && lib.Albums.TryGetValue(albPath, out var alb))
+                    {
+                        // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags
+                        var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}";
+
+                        var sb = new StringBuilder();
+
+                        sb.AppendLine("#EXTM3U");
+                        foreach (var track in alb.MainTracks.OrderBy(p => p.Key))
+                        {
+                            var mediaInternalPath = $"/media/{libName}/{albPath}/{track.Key}";
+                            if (false == _mediaTags.TryGetValue(mediaInternalPath, out var mediaTag) && _pathMapping.TryGetValue(mediaInternalPath, out var mediaFilePath))
+                            {
+                                using var tagLib = TagLib.File.Create(mediaFilePath);
+                                mediaTag = _mediaTags[mediaInternalPath] = new MediaTag2($"{string.Join(";", tagLib.Tag.Performers)} - {tagLib.Tag.Title}", (int)tagLib.Properties.Duration.TotalSeconds);
+                            }
+
+                            if (mediaTag != null)
+                            {
+                                var coverPath = $"/cover/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/cover.jpg";
+                                sb.AppendLine($"#EXTINF:{mediaTag.Duration} tvg-logo=\"{prefix + coverPath}\",{mediaTag.Title}");
+                            }
+
+                            var mediaPath = $"/media/{libName.FuckVlcAndEscape()}/{albPath.FuckVlcAndEscape()}/{track.Key.FuckVlcAndEscape()}";
+                            sb.AppendLine(prefix + mediaPath);
+                        }
+
+                        context.Response.ContentType = "audio/mpegurl";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.StatusCode = 404;
+                        //context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (_pathMapping.TryGetValue(requestPath, out var realPath))
+                {
+                    var range = request.Headers.GetValues("Range");
+
+                    FileStream fs = null;
+                    try
+                    {
+                        fs = File.OpenRead(realPath);
+
+                        if (range is { Length: > 0 })
+                        {
+                            var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
+                            if (rngParts.Length >= 1 && long.TryParse(rngParts[0], out var start))
+                            {
+                                fs.Position = start;
+                                context.Response.StatusCode = 206;
+                                context.Response.Headers.Add("Accept-Ranges", "bytes");
+                                context.Response.Headers.Add("Content-Range", $"bytes {start}-{fs.Length - 1}/{fs.Length}");
+                                context.Response.ContentLength64 = fs.Length - start;
+                                context.Response.ContentType = "video/mp4";
+                                fs.CopyTo(context.Response.OutputStream);
+                            }
+                        }
+                        else
+                        {
+                            context.Response.ContentType = "video/mp4";
+                            context.Response.ContentLength64 = fs.Length;
+                            fs.CopyTo(context.Response.OutputStream);
+                        }
+                    }
+                    catch (Exception e)
+                    {
+                        Console.WriteLine(e);
+                    }
+                    finally
+                    {
+                        fs?.Close();
+                    }
+                }
+                else
+                {
+                    context.Response.StatusCode = 404;
+                }
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+
+                try
+                {
+                    context.Response.StatusCode = 500;
+                }
+                catch (Exception exception)
+                {
+                    Console.WriteLine(exception);
+                }
+            }
+            finally
+            {
+                try
+                {
+                    context.Response.Close();
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine(e);
+                }
+            }
+        }
+
+        private static void WriteText(this HttpListenerResponse response, string content)
+        {
+            var bytes = Encoding.UTF8.GetBytes(content);
+            response.OutputStream.Write(bytes);
+        }
+
+        private static string FuckVlcAndEscape(this string input)
+        {
+            if (input == null) return null;
+            return input
+                .Replace("[", "%5B")
+                .Replace("]", "%5D")
+                .Replace("'", "%27")
+                ;
+        }
+    }
+}

+ 5 - 3
FNZCM/FNZCM.ConHost/config.json

@@ -2,8 +2,8 @@
   "ListenPrefix": "http://+:38964/",
   "Title": "FNZCM [Fuck Neteasy eaZy Cloud Music]",
   "Libraries": {
-    "FLAC": "Y:/FLAC",
-    "LOSSY": "Y:/LOSSY"
+    "1. Main": "X:\\音乐库\\Main",
+    "2. Other Lossy": "X:\\音乐库\\OtherLossy"
   },
   "MediaFilePattern": [
     "*.flac",
@@ -15,6 +15,8 @@
     "*.jpg",
     "*.jpeg",
     "*.png",
-    "*.webp"
+    "*.webp",
+    "*.tif",
+    "*.bmp"
   ]
 }