|
@@ -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")
|
|
|
+ ;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|