Browse Source

first commit

HOME 3 years ago
parent
commit
48030ee2a8

+ 29 - 0
FNZCM/FNZCM.ConHost/ConfigFile.cs

@@ -0,0 +1,29 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FNZCM.ConHost
+{
+    public interface IConfigFile
+    {
+        IReadOnlyDictionary<string, string> Libraries { get; }
+        string[] MediaFilePattern { get; }
+        string[] BkFilePattern { get; }
+        string ListenPrefix { get; }
+        string Title { get; }
+    }
+
+    public class ConfigFile : IConfigFile
+    {
+        public string ListenPrefix { get; set; }
+
+        public string Title { get; set; }
+
+        public IReadOnlyDictionary<string, string> Libraries { get; set; }
+        public string[] MediaFilePattern { get; set; }
+        public string[] BkFilePattern { get; set; }
+
+        public static IConfigFile Instance { get; } = JsonConvert.DeserializeObject<ConfigFile>(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json")));
+    }
+}

+ 25 - 0
FNZCM/FNZCM.ConHost/FNZCM.ConHost.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net5.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="config.json" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="config.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+    <PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
+  </ItemGroup>
+
+  <ProjectExtensions><VisualStudio><UserProperties config_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
+
+</Project>

+ 53 - 0
FNZCM/FNZCM.ConHost/LibraryModels.cs

@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace FNZCM.ConHost
+{
+    internal class Library
+    {
+        public Library(string name, string pathName, IReadOnlyCollection<Album> albums)
+        {
+            Name = name;
+            PathName = pathName;
+            Albums = albums;
+        }
+
+        public string Name { get; }
+        public string PathName { get; }
+        public IReadOnlyCollection<Album> Albums { get; }
+    }
+
+    internal class Album
+    {
+        public Album(string name, string pathName,
+            IReadOnlyList<string> trackers,
+            IReadOnlyList<string> trackerPaths,
+            IReadOnlyCollection<string> bks
+        )
+        {
+            Trackers = trackers;
+            Bks = bks;
+            TrackerPaths = trackerPaths;
+            Name = name;
+            PathName = pathName;
+        }
+
+        public IReadOnlyList<string> Trackers { get; }
+        public IReadOnlyList<string> TrackerPaths { get; }
+        public IReadOnlyCollection<string> Bks { get; }
+
+        public string Name { get; }
+        public string PathName { get; }
+    }
+
+    internal class MediaTag
+    {
+        public MediaTag(string title, int duration)
+        {
+            Title = title;
+            Duration = duration;
+        }
+
+        public string Title { get; }
+        public int Duration { get; }
+    }
+}

+ 486 - 0
FNZCM/FNZCM.ConHost/Program.cs

@@ -0,0 +1,486 @@
+using Microsoft.VisualBasic.FileIO;
+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 SearchOption = Microsoft.VisualBasic.FileIO.SearchOption;
+
+namespace FNZCM.ConHost
+{
+    
+
+    internal static class Program
+    {
+        private static bool _isRunning;
+
+        private static IReadOnlyDictionary<string, Library> _library;
+        private static IReadOnlyDictionary<string, string> _pathMapping;
+
+        private const int SmallFileSize = 1024 * 1024 * 5; //5MB
+        private static IReadOnlyDictionary<string, byte[]> _smallFileCaches;
+
+        private static readonly ConcurrentDictionary<string, MediaTag> MediaTags = new();
+
+        private static DateTime _lastRequestAccepted;
+
+        private static void Main()
+        {
+            Console.WriteLine("Scanning libraries...");
+
+            var libs = new Dictionary<string, Library>();
+            var mappings = new Dictionary<string, string>();
+
+            foreach (var library in ConfigFile.Instance.Libraries)
+            {
+                var libraryName = library.Key;
+                var libraryPathName = libraryName.ToLower();
+
+                var albums = Directory.GetDirectories(library.Value);
+
+                var albumObjects = new List<Album>();
+
+                foreach (var albumPath in albums)
+                {
+                    var albumName = Path.GetFileName(albumPath);
+                    var albumPathName = albumName.ToLower();
+
+                    //media
+                    var mediaFiles = FileSystem.GetFiles(albumPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.MediaFilePattern);
+                    foreach (var mediaFile in mediaFiles) mappings[$"/media/{library.Key}/{albumName}/{Path.GetFileName(mediaFile)}".ToLower()] = mediaFile;
+                    var trackers = mediaFiles.Select(Path.GetFileName).ToArray();
+                    var trackerPaths = trackers.Select(p => p.ToLower()).ToArray();
+
+                    //bk
+                    var bkPath = Path.Combine(albumPath, "bk");
+                    IReadOnlyCollection<string> bks = null;
+                    if (Directory.Exists(bkPath))
+                    {
+                        var bkFiles = FileSystem.GetFiles(bkPath, SearchOption.SearchTopLevelOnly, ConfigFile.Instance.BkFilePattern);
+                        foreach (var bkFile in bkFiles) mappings[$"/bk/{library.Key}/{albumName}/{Path.GetFileName(bkFile)}".ToLower()] = bkFile;
+                        bks = bkFiles.Select(s => Path.GetFileName(s).ToLower()).ToArray();
+                    }
+
+                    //cover
+                    var coverPath = Path.Combine(albumPath, "cover.jpg");
+                    if (File.Exists(coverPath)) mappings[$"/cover/{library.Key}/{albumName}/{Path.GetFileName(coverPath)}".ToLower()] = coverPath;
+
+                    albumObjects.Add(new Album(albumName, albumPathName, trackers, trackerPaths, bks));
+                }
+
+                libs[library.Key.ToLower()] = new Library(libraryName, libraryPathName, albumObjects);
+            }
+
+            _library = libs;
+            _pathMapping = mappings;
+
+            Console.WriteLine($" Scanned all {libs.Count} library");
+            Console.WriteLine($" Total {libs.Sum(p => p.Value.Albums.Count)} Albums, {libs.Sum(p => p.Value.Albums.Sum(q => q.Trackers.Count))} Trackers");
+
+            Console.Write("Reading small files to cache");
+            Console.WriteLine();
+
+            var caches = new Dictionary<string, byte[]>();
+            foreach (var map in _pathMapping)
+            {
+                var fileSize = new FileInfo(map.Value).Length;
+                if (fileSize < SmallFileSize)
+                {
+                    Console.Write($" Reading: {fileSize / (1024 * 1024.0):N6} MB {map.Value} ");
+                    caches[map.Key] = File.ReadAllBytes(map.Value);
+
+                    var left = Console.CursorLeft;
+                    Console.CursorLeft = 0;
+                    Console.Write("".PadLeft(left));
+                    Console.CursorLeft = 0;
+                }
+            }
+
+            _smallFileCaches = caches;
+
+            Console.WriteLine($" Cached {_smallFileCaches.Count} small files");
+
+            Console.WriteLine("Starting...");
+
+            var tWorker = new Thread(Working);
+            _isRunning = true;
+            tWorker.Start();
+
+            Console.WriteLine("Press ENTER to Stop.");
+            Console.ReadLine();
+
+            Console.Write("Shutting down...");
+            _isRunning = false;
+            tWorker.Join();
+
+            Console.Write("Stopped.");
+
+            Console.WriteLine();
+            Console.Write("Press ENTER to Exit.");
+            Console.ReadLine();
+        }
+
+        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 LA" + $" {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}" +
+                              $" / UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.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/trackers/ list all trackers as text list
+            // GET /list/foo/bar/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
+
+            try
+            {
+                // ReSharper disable once PossibleNullReferenceException
+                var requestPath = request.Url.LocalPath.ToLower();
+                var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+                if (requestPath == "/")
+                {
+                    var sb = new StringBuilder();
+                    sb.Append($"<title> Libraries - {ConfigFile.Instance.Title} </title>");
+                    sb.Append("<body bgColor=skyBlue style=font-size:5vh><h1>Libraries</h1>");
+
+                    sb.Append("<ul>");
+                    foreach (var library in ConfigFile.Instance.Libraries.Keys)
+                    {
+                        sb.Append("<li>");
+                        sb.Append($"<a href=/list/{library}/>{library}</a>");
+                        sb.Append("</li>");
+                    }
+
+                    sb.Append("</ul>");
+
+                    context.Response.WriteText(sb.ToString());
+                }
+                else if (pathParts.Count == 2 && pathParts[0] == "list")
+                {
+                    var libName = pathParts[1];
+                    if (_library.TryGetValue(libName, out var lib))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<title> Albums of {lib.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><h1>Albums of {lib.Name}</h1>");
+                        sb.Append("<div><a href=/>Back to home</a></div>");
+
+                        //Cover list
+                        foreach (var a in lib.Albums)
+                        {
+                            sb.Append("<div class=item>");
+                            sb.Append($"<img class=cover src=\"/cover/{lib.PathName}/{a.PathName}/cover.jpg\" />");
+                            sb.Append("<div class=buttons>");
+                            sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName}/trackers/\">[TRACKERS]</a>");
+                            sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName.FuckVlc()}/playlist.m3u8\">[M3U8]</a>");
+                            if (a.Bks != null) sb.Append($"<a class=button href=\"/list/{lib.PathName}/{a.PathName}/bk/\">[BK]</a>");
+                            sb.Append("</div>");
+                            sb.Append($"<span>{a.Name}<span>");
+                            sb.Append("</div>");
+                        }
+
+                        context.Response.ContentType = "text/html";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4
+                         && pathParts[0] == "list"
+                         //&& pathParts[1] == "lib"
+                         //&& pathParts[2] == "alb"
+                         && pathParts[3] == "trackers"
+                )
+                {
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+                    Album alb;
+                    if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<h1>Trackers of {alb.Name}</h1>");
+                        sb.Append($"<div><a href=/list/{lib.PathName}/>Back to library</a></div>");
+
+                        for (var i = 0; i < alb.TrackerPaths.Count; i++)
+                        {
+                            sb.Append($"<li><a href=\"/media/{lib.PathName.FuckVlc()}/{alb.PathName.FuckVlc()}/{alb.TrackerPaths[i].FuckVlc()}\" >{alb.Trackers[i]}</a></li>");
+                        }
+
+                        context.Response.ContentType = "text/html";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4
+                         && pathParts[0] == "list"
+                         //&& pathParts[1] == "lib"
+                         //&& pathParts[2] == "alb"
+                         && pathParts[3] == "bk"
+                        )
+                {
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+                    Album alb;
+                    if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
+                    {
+                        var sb = new StringBuilder();
+                        sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
+                        sb.Append($"<body bgColor=skyBlue style=font-size:4vh><h1>BK of {alb.Name}</h1>");
+                        sb.Append($"<div><a href=/list/{lib.PathName}/>Back to library</a></div>");
+
+                        foreach (var albBk in alb.Bks)
+                        {
+                            //TODO: auto gen thumbnail
+                            sb.Append($"<img src='/bk/{lib.PathName}/{alb.PathName}/{albBk}' 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.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (pathParts.Count == 4
+                        && pathParts[0] == "list"
+                        //&& pathParts[1] == "lib"
+                        //&& pathParts[2] == "alb"
+                        && pathParts[3] == "playlist.m3u8"
+               )
+                {
+                    var libName = pathParts[1];
+                    var albPath = pathParts[2];
+
+                    Album alb;
+                    if (_library.TryGetValue(libName, out var lib) && null != (alb = lib.Albums.FirstOrDefault(p => p.PathName == albPath)))
+                    {
+                        // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags
+                        var prefix = $"{request.Url.GetLeftPart(UriPartial.Scheme | UriPartial.Authority)}";
+
+                        var sb = new StringBuilder();
+
+                        sb.AppendLine("#EXTM3U");
+                        foreach (var tracker in alb.TrackerPaths)
+                        {
+                            var mediaKey = $"/media/{lib.PathName}/{alb.PathName}/{tracker}";
+                            var coverKey = $"/cover/{lib.PathName}/{alb.PathName}/cover.jpg";
+
+                            if (false == MediaTags.TryGetValue(mediaKey, out var mediaTag))
+                            {
+                                using var tagLib = TagLib.File.Create(_pathMapping[mediaKey]);
+                                mediaTag = MediaTags[mediaKey] = new MediaTag($"{string.Join(";", tagLib.Tag.Performers)} - {tagLib.Tag.Title}", (int)tagLib.Properties.Duration.TotalSeconds);
+                            }
+
+                            sb.AppendLine($"#EXTINF:{mediaTag.Duration} logo=\"{prefix + coverKey}\",{mediaTag.Title}");
+                            sb.AppendLine(prefix + mediaKey.FuckVlc());
+                        }
+
+                        context.Response.ContentType = "audio/x-mpegurl";
+                        context.Response.ContentEncoding = Encoding.UTF8;
+                        context.Response.WriteText(sb.ToString());
+                    }
+                    else
+                    {
+                        context.Response.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+                    }
+                }
+                else if (_pathMapping.TryGetValue(requestPath, out var realPath))
+                {
+                    if (_smallFileCaches.TryGetValue(requestPath, out var data))
+                    {
+                        context.Response.OutputStream.Write(data);
+                    }
+                    else
+                    {
+                        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);
+                                }
+                                // ReSharper disable once UnusedVariable
+                                else if (rngParts.Length == 2 && long.TryParse(rngParts[0], out start) && long.TryParse(rngParts[1], out var end))
+                                {
+                                    //TODO
+                                }
+                            }
+                            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 FuckVlc(this string input)
+        {
+            if (input == null) return null;
+            return input
+                .Replace("[", "%5B")
+                .Replace("]", "%5D")
+                ;
+        }
+    }
+}

+ 20 - 0
FNZCM/FNZCM.ConHost/config.json

@@ -0,0 +1,20 @@
+{
+  "ListenPrefix": "http://+:38964/",
+  "Title": "FNZCM [Fuck Neteasy eaZy Cloud Music]",
+  "Libraries": {
+    "FLAC": "Y:/FLAC",
+    "LOSSY": "Y:/LOSSY"
+  },
+  "MediaFilePattern": [
+    "*.flac",
+    "*.m4a",
+    "*.mp3",
+    "*.mkv"
+  ],
+  "BkFilePattern": [
+    "*.jpg",
+    "*.jpeg",
+    "*.png",
+    "*.webp"
+  ]
+}

+ 30 - 0
FNZCM/FNZCM.sln

@@ -0,0 +1,30 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.32106.194
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FNZCM.ConHost", "FNZCM.ConHost\FNZCM.ConHost.csproj", "{788B1339-FEA3-446D-9584-A311BD0F5114}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "@", "@", "{C7D1253A-E6BA-4C4A-8BB1-6CFAC434A035}"
+	ProjectSection(SolutionItems) = preProject
+		NuGet.config = NuGet.config
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{788B1339-FEA3-446D-9584-A311BD0F5114}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{788B1339-FEA3-446D-9584-A311BD0F5114}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{788B1339-FEA3-446D-9584-A311BD0F5114}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{788B1339-FEA3-446D-9584-A311BD0F5114}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {3650D491-7030-4242-9FE8-F1737EF69376}
+	EndGlobalSection
+EndGlobal

+ 8 - 0
FNZCM/FNZCM.sln.DotSettings

@@ -0,0 +1,8 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=DOCTYPE/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=EXTINF/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=EXTM/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=FLAC/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=FNZCM/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Neteasy/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=webp/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 6 - 0
FNZCM/NuGet.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+	<config>
+		<add key="repositorypath" value="C:\NuGetLocalRepo" />
+	</config>
+</configuration>