();
+
+ private readonly HttpServer server;
+
+ public HttpAuthorizer()
+ {
+ }
+
+ public HttpAuthorizer(HttpServer server)
+ {
+ if (server == null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+ this.server = server;
+ server.OnAuthorizeClient += OnAuthorize;
+ }
+
+ public void AddMethod(IHttpAuthorizationMethod method)
+ {
+ if (method == null)
+ {
+ throw new ArgumentNullException(nameof(method));
+ }
+ methods.Add(method);
+ }
+
+ public bool Authorize(IHeaders headers, IPEndPoint endPoint)
+ {
+ if (methods.Count == 0)
+ {
+ return true;
+ }
+ try
+ {
+ return methods.Any(m => m.Authorize(headers, endPoint));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to authorize [{ ex }]");
+ return false;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (server != null)
+ {
+ server.OnAuthorizeClient -= OnAuthorize;
+ }
+ }
+
+ private void OnAuthorize(object sender, HttpAuthorizationEventArgs e)
+ {
+ e.Cancel = !Authorize(
+ e.Headers,
+ e.RemoteEndpoint
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/HttpClient.cs b/Roadie.Dlna/Server/Http/HttpClient.cs
new file mode 100644
index 0000000..2483b18
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/HttpClient.cs
@@ -0,0 +1,514 @@
+using Microsoft.Extensions.Logging;
+using Roadie.Dlna.Utility;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class HttpClient : IRequest, IDisposable
+ {
+ private const uint BEGIN_TIMEOUT = 30;
+
+ private const int BUFFER_SIZE = 1 << 16;
+
+ private const string CRLF = "\r\n";
+
+ private static readonly Regex bytes =
+ new Regex(@"^bytes=(\d+)(?:-(\d+)?)?$", RegexOptions.Compiled);
+
+ private static readonly IHandler error403 =
+ new StaticHandler(new StringResponse(
+ HttpCode.Denied,
+ "Access denied!Access denied!
You're not allowed to access the requested resource.
"
+ )
+ );
+
+ private static readonly IHandler error404 =
+ new StaticHandler(new StringResponse(
+ HttpCode.NotFound,
+ "Not found!Not found!
The requested resource was not found!
"
+ )
+ );
+
+ private static readonly IHandler error416 =
+ new StaticHandler(new StringResponse(
+ HttpCode.RangeNotSatisfiable,
+ "Requested Range not satisfiable!Requested Range not satisfiable!
Nice try, but do not try again :p
"
+ )
+ );
+
+ private static readonly IHandler error500 =
+ new StaticHandler(new StringResponse(
+ HttpCode.InternalError,
+ "Internal Server ErrorInternal Server Error
Something is very rotten in the State of Denmark!
"
+ )
+ );
+
+ private readonly byte[] buffer = new byte[2048];
+
+ private readonly TcpClient client;
+
+ private readonly HttpServer owner;
+
+ private readonly uint readTimeout =
+ (uint)TimeSpan.FromMinutes(1).TotalSeconds;
+
+ private readonly NetworkStream stream;
+
+ private readonly uint writeTimeout =
+ (uint)TimeSpan.FromMinutes(180).TotalSeconds;
+
+ private uint bodyBytes;
+
+ private bool hasHeaders;
+
+ private DateTime lastActivity;
+
+ private MemoryStream readStream;
+
+ private uint requestCount;
+
+ private IResponse response;
+
+ private HttpStates state;
+
+ public string Body { get; private set; }
+
+ public IHeaders Headers { get; } = new Headers();
+
+ public bool IsATimeout
+ {
+ get
+ {
+ var diff = (DateTime.Now - lastActivity).TotalSeconds;
+ switch (state)
+ {
+ case HttpStates.Accepted:
+ case HttpStates.ReadBegin:
+ case HttpStates.WriteBegin:
+ return diff > BEGIN_TIMEOUT;
+
+ case HttpStates.Reading:
+ return diff > readTimeout;
+
+ case HttpStates.Writing:
+ return diff > writeTimeout;
+
+ case HttpStates.Closed:
+ return true;
+
+ default:
+ throw new InvalidOperationException("Invalid state");
+ }
+ }
+ }
+
+ public IPEndPoint LocalEndPoint { get; }
+
+ public string Method { get; private set; }
+
+ public string Path { get; private set; }
+
+ public IPEndPoint RemoteEndpoint { get; }
+
+ private HttpStates State
+ {
+ set
+ {
+ lastActivity = DateTime.Now;
+ state = value;
+ }
+ }
+
+ public HttpClient(HttpServer aOwner, TcpClient aClient)
+ {
+ State = HttpStates.Accepted;
+ lastActivity = DateTime.Now;
+
+ owner = aOwner;
+ client = aClient;
+ stream = client.GetStream();
+ client.Client.UseOnlyOverlappedIO = true;
+
+ RemoteEndpoint = client.Client.RemoteEndPoint as IPEndPoint;
+ LocalEndPoint = client.Client.LocalEndPoint as IPEndPoint;
+ }
+
+ internal enum HttpStates
+ {
+ Accepted,
+ Closed,
+ ReadBegin,
+ Reading,
+ WriteBegin,
+ Writing
+ }
+
+ public void Dispose()
+ {
+ Close();
+ readStream?.Dispose();
+ }
+
+ public void Start()
+ {
+ ReadNext();
+ }
+
+ public override string ToString()
+ {
+ return RemoteEndpoint.ToString();
+ }
+
+ internal void Close()
+ {
+ State = HttpStates.Closed;
+
+ owner.Logger.LogTrace($"{this} - Closing connection after { requestCount} requests");
+ try
+ {
+ client.Close();
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ owner.RemoveClient(this);
+ if (stream != null)
+ {
+ try
+ {
+ stream.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+ }
+
+ private long GetContentLengthFromStream(Stream responseBody)
+ {
+ long contentLength = -1;
+ try
+ {
+ string clf;
+ if (!response.Headers.TryGetValue("Content-Length", out clf) ||
+ !long.TryParse(clf, out contentLength))
+ {
+ contentLength = responseBody.Length - responseBody.Position;
+ if (contentLength < 0)
+ {
+ throw new InvalidDataException();
+ }
+ response.Headers["Content-Length"] = contentLength.ToString();
+ }
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ return contentLength;
+ }
+
+ private Stream ProcessRanges(IResponse rangedResponse, ref HttpCode status)
+ {
+ var responseBody = rangedResponse.Body;
+ var contentLength = GetContentLengthFromStream(responseBody);
+ try
+ {
+ string ar;
+ if (status != HttpCode.Ok && contentLength > 0 ||
+ !Headers.TryGetValue("Range", out ar))
+ {
+ return responseBody;
+ }
+ var m = bytes.Match(ar);
+ if (!m.Success)
+ {
+ throw new InvalidDataException("Not parsed!");
+ }
+ var totalLength = contentLength;
+ long start;
+ long end;
+ if (!long.TryParse(m.Groups[1].Value, out start) || start < 0)
+ {
+ throw new InvalidDataException("Not parsed");
+ }
+ if (m.Groups.Count != 3 ||
+ !long.TryParse(m.Groups[2].Value, out end) ||
+ end <= start || end >= totalLength)
+ {
+ end = totalLength - 1;
+ }
+ if (start >= end)
+ {
+ responseBody.Close();
+ rangedResponse = error416.HandleRequest(this);
+ return rangedResponse.Body;
+ }
+
+ if (start > 0)
+ {
+ responseBody.Seek(start, SeekOrigin.Current);
+ }
+ contentLength = end - start + 1;
+ rangedResponse.Headers["Content-Length"] = contentLength.ToString();
+ rangedResponse.Headers.Add(
+ "Content-Range",
+ $"bytes {start}-{end}/{totalLength}"
+ );
+ status = HttpCode.Partial;
+ }
+ catch (Exception ex)
+ {
+ owner.Logger.LogTrace($"{this} - Failed to process range request! Ex [{ ex }]");
+ }
+ return responseBody;
+ }
+
+ private void Read()
+ {
+ try
+ {
+ stream.BeginRead(buffer, 0, buffer.Length, ReadCallback, 0);
+ }
+ catch (IOException ex)
+ {
+ owner.Logger.LogTrace($"{this} - Failed to BeginRead [{ ex }]");
+ Close();
+ }
+ }
+
+ private void ReadCallback(IAsyncResult result)
+ {
+ if (state == HttpStates.Closed)
+ {
+ return;
+ }
+
+ State = HttpStates.Reading;
+
+ try
+ {
+ var read = stream.EndRead(result);
+ if (read < 0)
+ {
+ throw new HttpException("Client did not send anything");
+ }
+ owner.Logger.LogTrace($"{this} - Read {read} bytes");
+ readStream.Write(buffer, 0, read);
+ lastActivity = DateTime.Now;
+ }
+ catch (Exception)
+ {
+ if (!IsATimeout)
+ {
+ owner.Logger.LogTrace($"{this} - Failed to read data");
+ Close();
+ }
+ return;
+ }
+
+ try
+ {
+ if (!hasHeaders)
+ {
+ readStream.Seek(0, SeekOrigin.Begin);
+ var reader = new StreamReader(readStream);
+ for (var line = reader.ReadLine();
+ line != null;
+ line = reader.ReadLine())
+ {
+ line = line.Trim();
+ if (string.IsNullOrEmpty(line))
+ {
+ hasHeaders = true;
+ readStream = StreamManager.GetStream();
+ if (Headers.ContainsKey("content-length") &&
+ uint.TryParse(Headers["content-length"], out bodyBytes))
+ {
+ if (bodyBytes > 1 << 20)
+ {
+ throw new IOException("Body too long");
+ }
+ var ascii = Encoding.ASCII.GetBytes(reader.ReadToEnd());
+ readStream.Write(ascii, 0, ascii.Length);
+ owner.Logger.LogTrace($"Must read body bytes {bodyBytes}");
+ }
+ break;
+ }
+ if (Method == null)
+ {
+ var parts = line.Split(new[] { ' ' }, 3);
+ Method = parts[0].Trim().ToUpperInvariant();
+ Path = parts[1].Trim();
+ owner.Logger.LogTrace($"{this} - {Method} request for {Path}");
+ }
+ else
+ {
+ var parts = line.Split(new[] { ':' }, 2);
+ Headers[parts[0]] = Uri.UnescapeDataString(parts[1]).Trim();
+ }
+ }
+ }
+ if (bodyBytes != 0 && bodyBytes > readStream.Length)
+ {
+ owner.Logger.LogTrace($"{this} - Bytes to go { (bodyBytes - readStream.Length) }");
+ Read();
+ return;
+ }
+ using (readStream)
+ {
+ Body = Encoding.UTF8.GetString(readStream.ToArray());
+ }
+ SetupResponse();
+ }
+ catch (Exception ex)
+ {
+ owner.Logger.LogTrace($"{this} - Failed to process request Ex [{ ex }]");
+ response = error500.HandleRequest(this);
+ SendResponse();
+ }
+ }
+
+ private void ReadNext()
+ {
+ Method = null;
+ Headers.Clear();
+ hasHeaders = false;
+ Body = null;
+ bodyBytes = 0;
+ readStream = StreamManager.GetStream();
+
+ ++requestCount;
+ State = HttpStates.ReadBegin;
+
+ Read();
+ }
+
+ private void SendResponse()
+ {
+ var statusCode = response.Status;
+ var responseBody = ProcessRanges(response, ref statusCode);
+ var responseStream = new ConcatenatedStream();
+ try
+ {
+ var headerBlock = new StringBuilder();
+ headerBlock.AppendFormat(
+ "HTTP/1.1 {0} {1}\r\n",
+ (uint)statusCode,
+ HttpPhrases.Phrases[statusCode]
+ );
+ headerBlock.Append(response.Headers.HeaderBlock);
+ headerBlock.Append(CRLF);
+
+ var headerStream = new MemoryStream(
+ Encoding.ASCII.GetBytes(headerBlock.ToString()));
+ responseStream.AddStream(headerStream);
+ if (Method != "HEAD" && responseBody != null)
+ {
+ responseStream.AddStream(responseBody);
+ responseBody = null;
+ }
+ owner.Logger.LogTrace($"{this} - {(uint)statusCode} response for {Path}");
+ state = HttpStates.Writing;
+ var sp = new StreamPump(responseStream, stream, BUFFER_SIZE);
+ sp.Pump((pump, result) =>
+ {
+ pump.Input.Close();
+ pump.Input.Dispose();
+ if (result == StreamPumpResult.Delivered)
+ {
+ owner.Logger.LogTrace($"{this} - Done writing response");
+
+ string conn;
+ if (Headers.TryGetValue("connection", out conn) &&
+ conn.ToUpperInvariant() == "KEEP-ALIVE")
+ {
+ ReadNext();
+ return;
+ }
+ }
+ else
+ {
+ owner.Logger.LogTrace($"{this} - Client aborted connection");
+ }
+ Close();
+ });
+ }
+ catch (Exception)
+ {
+ responseStream.Dispose();
+ throw;
+ }
+ finally
+ {
+ responseBody?.Dispose();
+ }
+ }
+
+ private void SetupResponse()
+ {
+ State = HttpStates.WriteBegin;
+ try
+ {
+ if (!owner.AuthorizeClient(this))
+ {
+ throw new HttpStatusException(HttpCode.Denied);
+ }
+ if (string.IsNullOrEmpty(Path))
+ {
+ throw new HttpStatusException(HttpCode.NotFound);
+ }
+ var handler = owner.FindHandler(Path);
+ if (handler == null)
+ {
+ throw new HttpStatusException(HttpCode.NotFound);
+ }
+ response = handler.HandleRequest(this);
+ if (response == null)
+ {
+ throw new ArgumentException("Handler did not return a response");
+ }
+ }
+ catch (HttpStatusException ex)
+ {
+ owner.Logger.LogTrace($"{this} - Got a {ex.Code}: {Path}");
+
+ switch (ex.Code)
+ {
+ case HttpCode.NotFound:
+ response = error404.HandleRequest(this);
+ break;
+
+ case HttpCode.Denied:
+ response = error403.HandleRequest(this);
+ break;
+
+ case HttpCode.InternalError:
+ response = error500.HandleRequest(this);
+ break;
+
+ default:
+ response = new StaticHandler(new StringResponse(
+ ex.Code,
+ "text/plain",
+ ex.Message
+ )).HandleRequest(this);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ owner.Logger.LogTrace($"{this} - Failed to process response Ex [{ ex }]");
+ response = error500.HandleRequest(this);
+ }
+ SendResponse();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/HttpCode.cs b/Roadie.Dlna/Server/Http/HttpCode.cs
new file mode 100644
index 0000000..8713f98
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/HttpCode.cs
@@ -0,0 +1,16 @@
+namespace Roadie.Dlna.Server
+{
+ public enum HttpCode
+ {
+ None = 0,
+ Ok = 200,
+ Partial = 206,
+ MovedPermanently = 301,
+ NotModified = 304,
+ TemporaryRedirect = 307,
+ Denied = 403,
+ NotFound = 404,
+ RangeNotSatisfiable = 416,
+ InternalError = 500
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/HttpPhrases.cs b/Roadie.Dlna/Server/Http/HttpPhrases.cs
new file mode 100644
index 0000000..e45d88a
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/HttpPhrases.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace Roadie.Dlna.Server
+{
+ internal static class HttpPhrases
+ {
+ public static readonly IDictionary Phrases =
+ new Dictionary
+ {
+ {HttpCode.Ok, "OK"},
+ {HttpCode.Partial, "Partial Content"},
+ {HttpCode.MovedPermanently, "Moved Permanently"},
+ {HttpCode.NotModified, "Not Modified"},
+ {HttpCode.TemporaryRedirect, "Temprary Redirect"},
+ {HttpCode.Denied, "Forbidden"},
+ {HttpCode.NotFound, "Not Found"},
+ {HttpCode.RangeNotSatisfiable, "Requested Range not satisfiable"},
+ {HttpCode.InternalError, "Internal Server Error"}
+ };
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/IHttpAuthorizationMethod.cs b/Roadie.Dlna/Server/Http/IHttpAuthorizationMethod.cs
new file mode 100644
index 0000000..22df797
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/IHttpAuthorizationMethod.cs
@@ -0,0 +1,15 @@
+using System.Net;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IHttpAuthorizationMethod
+ {
+ ///
+ /// Checks if a request is authorized.
+ ///
+ /// Client supplied HttpHeaders.
+ /// Client EndPoint
+ /// true if authorized
+ bool Authorize(IHeaders headers, IPEndPoint endPoint);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/IPAddressAuthorizer.cs b/Roadie.Dlna/Server/Http/IPAddressAuthorizer.cs
new file mode 100644
index 0000000..48ac34c
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/IPAddressAuthorizer.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+
+namespace Roadie.Dlna.Server
+{
+ public sealed class IPAddressAuthorizer : IHttpAuthorizationMethod
+ {
+ private readonly Dictionary ips =
+ new Dictionary();
+
+ public IPAddressAuthorizer(IEnumerable addresses)
+ {
+ if (addresses == null)
+ {
+ throw new ArgumentNullException(nameof(addresses));
+ }
+ foreach (var ip in addresses)
+ {
+ ips.Add(ip, null);
+ }
+ }
+
+ public IPAddressAuthorizer(IEnumerable addresses)
+ : this(from a in addresses select IPAddress.Parse(a))
+ {
+ }
+
+ public bool Authorize(IHeaders headers, IPEndPoint endPoint)
+ {
+ var addr = endPoint?.Address;
+ if (addr == null)
+ {
+ return false;
+ }
+ var rv = ips.ContainsKey(addr);
+ Trace.WriteLine(!rv ? $"Rejecting {addr}. Not in IP whitelist" : $"Accepted {addr} via IP whitelist");
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/ResponseHeaders.cs b/Roadie.Dlna/Server/Http/ResponseHeaders.cs
new file mode 100644
index 0000000..adf77d0
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/ResponseHeaders.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ public sealed class ResponseHeaders : RawHeaders
+ {
+ public ResponseHeaders()
+ : this(true)
+ {
+ }
+
+ public ResponseHeaders(bool noCache)
+ {
+ this["Server"] = HttpServer.Signature;
+ this["Date"] = DateTime.Now.ToString("R");
+ this["Connection"] = "keep-alive";
+ if (noCache)
+ {
+ this["Cache-Control"] = "no-cache";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Http/UserAgentAuthorizer.cs b/Roadie.Dlna/Server/Http/UserAgentAuthorizer.cs
new file mode 100644
index 0000000..9979a5a
--- /dev/null
+++ b/Roadie.Dlna/Server/Http/UserAgentAuthorizer.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+
+namespace Roadie.Dlna.Server
+{
+ public sealed class UserAgentAuthorizer : IHttpAuthorizationMethod
+ {
+ private readonly Dictionary userAgents = new Dictionary();
+
+ public UserAgentAuthorizer(IEnumerable userAgents)
+ {
+ if (userAgents == null)
+ {
+ throw new ArgumentNullException(nameof(userAgents));
+ }
+ foreach (var u in userAgents)
+ {
+ if (string.IsNullOrEmpty(u))
+ {
+ throw new FormatException("Invalid User-Agent supplied");
+ }
+ this.userAgents.Add(u, null);
+ }
+ }
+
+ public bool Authorize(IHeaders headers, IPEndPoint endPoint)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException(nameof(headers));
+ }
+ string ua;
+ if (!headers.TryGetValue("User-Agent", out ua))
+ {
+ return false;
+ }
+ if (string.IsNullOrEmpty(ua))
+ {
+ return false;
+ }
+ var rv = userAgents.ContainsKey(ua);
+ Trace.WriteLine(!rv ? $"Rejecting {ua}. Not in User-Agent whitelist" : $"Accepted {ua} via User-Agent whitelist");
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IBookmarkable.cs b/Roadie.Dlna/Server/Interfaces/IBookmarkable.cs
new file mode 100644
index 0000000..8bf77d0
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IBookmarkable.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server
+{
+ public interface IBookmarkable
+ {
+ long? Bookmark { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IHandler.cs b/Roadie.Dlna/Server/Interfaces/IHandler.cs
new file mode 100644
index 0000000..17df6dd
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IHandler.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server
+{
+ internal interface IHandler
+ {
+ IResponse HandleRequest(IRequest request);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IHeaders.cs b/Roadie.Dlna/Server/Interfaces/IHeaders.cs
new file mode 100644
index 0000000..2559556
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IHeaders.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IHeaders : IDictionary
+ {
+ string HeaderBlock { get; }
+
+ Stream HeaderStream { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaAudioResource.cs b/Roadie.Dlna/Server/Interfaces/IMediaAudioResource.cs
new file mode 100644
index 0000000..0c8eda1
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaAudioResource.cs
@@ -0,0 +1,8 @@
+using Roadie.Dlna.Server.Metadata;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaAudioResource : IMediaResource, IMetaAudioItem, IMetaInfo
+ {
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaCover.cs b/Roadie.Dlna/Server/Interfaces/IMediaCover.cs
new file mode 100644
index 0000000..72a80f1
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaCover.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaCover
+ {
+ IMediaCoverResource Cover { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaCoverResource.cs b/Roadie.Dlna/Server/Interfaces/IMediaCoverResource.cs
new file mode 100644
index 0000000..621c216
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaCoverResource.cs
@@ -0,0 +1,8 @@
+using Roadie.Dlna.Server.Metadata;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaCoverResource : IMediaResource, IMetaResolution
+ {
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaFolder.cs b/Roadie.Dlna/Server/Interfaces/IMediaFolder.cs
new file mode 100644
index 0000000..af51e19
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaFolder.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaFolder : IMediaItem
+ {
+ int ChildCount { get; }
+
+ IEnumerable ChildFolders { get; }
+ IEnumerable ChildItems { get; }
+ int FullChildCount { get; }
+ IMediaFolder Parent { get; set; }
+
+ void AddResource(IMediaResource res);
+
+ void Cleanup();
+
+ bool RemoveResource(IMediaResource res);
+
+ void Sort(IComparer sortComparer, bool descending);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaImageResource.cs b/Roadie.Dlna/Server/Interfaces/IMediaImageResource.cs
new file mode 100644
index 0000000..efda5c6
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaImageResource.cs
@@ -0,0 +1,8 @@
+using Roadie.Dlna.Server.Metadata;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaImageResource : IMediaResource, IMetaImageItem
+ {
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaItem.cs b/Roadie.Dlna/Server/Interfaces/IMediaItem.cs
new file mode 100644
index 0000000..03fa729
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaItem.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaItem : IComparable, IEquatable, ITitleComparable
+ {
+ string Id { get; set; }
+
+ string Path { get; }
+
+ IHeaders Properties { get; }
+
+ string Title { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaResource.cs b/Roadie.Dlna/Server/Interfaces/IMediaResource.cs
new file mode 100644
index 0000000..76f0fe7
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaResource.cs
@@ -0,0 +1,15 @@
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaResource : IMediaItem, IMediaCover
+ {
+ DlnaMediaTypes MediaType { get; }
+
+ string PN { get; }
+
+ DlnaMime Type { get; }
+
+ Stream CreateContentStream();
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaServer.cs b/Roadie.Dlna/Server/Interfaces/IMediaServer.cs
new file mode 100644
index 0000000..fceac17
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaServer.cs
@@ -0,0 +1,17 @@
+using Roadie.Library.Configuration;
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaServer
+ {
+ void Preload();
+
+ IHttpAuthorizationMethod Authorizer { get; }
+ string FriendlyName { get; }
+
+ Guid UUID { get; }
+
+ IMediaItem GetItem(string id, bool isFileRequest);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IMediaVideoResource.cs b/Roadie.Dlna/Server/Interfaces/IMediaVideoResource.cs
new file mode 100644
index 0000000..cef7a81
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IMediaVideoResource.cs
@@ -0,0 +1,8 @@
+using Roadie.Dlna.Server.Metadata;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IMediaVideoResource : IMediaResource, IMetaVideoItem
+ {
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IPrefixHandler.cs b/Roadie.Dlna/Server/Interfaces/IPrefixHandler.cs
new file mode 100644
index 0000000..6122250
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IPrefixHandler.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server
+{
+ internal interface IPrefixHandler : IHandler
+ {
+ string Prefix { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IRequest.cs b/Roadie.Dlna/Server/Interfaces/IRequest.cs
new file mode 100644
index 0000000..eededd9
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IRequest.cs
@@ -0,0 +1,19 @@
+using System.Net;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IRequest
+ {
+ string Body { get; }
+
+ IHeaders Headers { get; }
+
+ IPEndPoint LocalEndPoint { get; }
+
+ string Method { get; }
+
+ string Path { get; }
+
+ IPEndPoint RemoteEndpoint { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IResponse.cs b/Roadie.Dlna/Server/Interfaces/IResponse.cs
new file mode 100644
index 0000000..3fd62a3
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IResponse.cs
@@ -0,0 +1,13 @@
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ internal interface IResponse
+ {
+ Stream Body { get; }
+
+ IHeaders Headers { get; }
+
+ HttpCode Status { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/ITitleComparable.cs b/Roadie.Dlna/Server/Interfaces/ITitleComparable.cs
new file mode 100644
index 0000000..b80faaa
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/ITitleComparable.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server
+{
+ public interface ITitleComparable
+ {
+ string ToComparableTitle();
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/IVolatileMediaServer.cs b/Roadie.Dlna/Server/Interfaces/IVolatileMediaServer.cs
new file mode 100644
index 0000000..79f0eb4
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/IVolatileMediaServer.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ public interface IVolatileMediaServer
+ {
+ bool Rescanning { get; set; }
+
+ event EventHandler Changed;
+
+ void Rescan();
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaAudioItem.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaAudioItem.cs
new file mode 100644
index 0000000..92d4909
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaAudioItem.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaAudioItem : IMetaInfo, IMetaDescription, IMetaDuration, IMetaGenre
+ {
+ int? MetaReleaseYear { get; }
+
+ string MetaAlbum { get; }
+
+ string MetaArtist { get; }
+
+ string MetaPerformer { get; }
+
+ int? MetaTrack { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDescription.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDescription.cs
new file mode 100644
index 0000000..2ca5587
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDescription.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaDescription
+ {
+ string MetaDescription { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDuration.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDuration.cs
new file mode 100644
index 0000000..c33ef5c
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaDuration.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaDuration
+ {
+ TimeSpan? MetaDuration { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaGenre.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaGenre.cs
new file mode 100644
index 0000000..2e22402
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaGenre.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaGenre
+ {
+ string MetaGenre { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaImageItem.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaImageItem.cs
new file mode 100644
index 0000000..f62716c
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaImageItem.cs
@@ -0,0 +1,8 @@
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaImageItem
+ : IMetaInfo, IMetaResolution, IMetaDescription
+ {
+ string MetaCreator { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaInfo.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaInfo.cs
new file mode 100644
index 0000000..99e1fa7
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaInfo.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaInfo
+ {
+ DateTime InfoDate { get; }
+
+ long? InfoSize { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaResolution.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaResolution.cs
new file mode 100644
index 0000000..3b3f12d
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaResolution.cs
@@ -0,0 +1,9 @@
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaResolution
+ {
+ int? MetaHeight { get; }
+
+ int? MetaWidth { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Interfaces/Metadata/IMetaVideoItem.cs b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaVideoItem.cs
new file mode 100644
index 0000000..4bdd0f3
--- /dev/null
+++ b/Roadie.Dlna/Server/Interfaces/Metadata/IMetaVideoItem.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Roadie.Dlna.Server.Metadata
+{
+ public interface IMetaVideoItem
+ : IMetaInfo, IMetaDescription, IMetaGenre, IMetaDuration, IMetaResolution
+ {
+ IEnumerable MetaActors { get; }
+
+ string MetaDirector { get; }
+
+ Subtitle Subtitle { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/MSMediaReceiverRegistrar.xml b/Roadie.Dlna/Server/Resources/MSMediaReceiverRegistrar.xml
new file mode 100644
index 0000000..4f5745c
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/MSMediaReceiverRegistrar.xml
@@ -0,0 +1,88 @@
+
+
+
+ 1
+ 0
+
+
+
+ IsAuthorized
+
+
+ DeviceID
+ in
+ A_ARG_TYPE_DeviceID
+
+
+ Result
+ out
+ A_ARG_TYPE_Result
+
+
+
+
+ RegisterDevice
+
+
+ RegistrationReqMsg
+ in
+ A_ARG_TYPE_RegistrationReqMsg
+
+
+ RegistrationRespMsg
+ out
+ A_ARG_TYPE_RegistrationRespMsg
+
+
+
+
+ IsValidated
+
+
+ DeviceID
+ in
+ A_ARG_TYPE_DeviceID
+
+
+ Result
+ out
+ A_ARG_TYPE_Result
+
+
+
+
+
+
+ A_ARG_TYPE_DeviceID
+ string
+
+
+ A_ARG_TYPE_Result
+ int
+
+
+ A_ARG_TYPE_RegistrationReqMsg
+ bin.base64
+
+
+ A_ARG_TYPE_RegistrationRespMsg
+ bin.base64
+
+
+ AuthorizationGrantedUpdateID
+ ui4
+
+
+ AuthorizationDeniedUpdateID
+ ui4
+
+
+ ValidationSucceededUpdateID
+ ui4
+
+
+ ValidationRevokedUpdateID
+ ui4
+
+
+
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/browse.css b/Roadie.Dlna/Server/Resources/browse.css
new file mode 100644
index 0000000..364807a
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/browse.css
@@ -0,0 +1,164 @@
+* {
+ margin: 0;
+ padding: 0;
+ font-family: 'Segoe UI', Helvetica, sans-serif;
+}
+
+html {
+ color: white;
+ background: #404040;
+ padding: 0;
+ margin: 0;
+}
+
+article {
+ background: #404040;
+ padding: 5ex;
+ min-height: 30ex;
+}
+
+footer,
+article {
+ margin: auto;
+ padding-left: 40px;
+ padding-right: 40px;
+}
+
+ article:after {
+ content: '.';
+ visibility: hidden;
+ display: block;
+ clear: both;
+ }
+
+footer {
+ border-top: 2px solid #244050;
+ background-color: #404040;
+ padding-top: 2em;
+ font-size: small;
+ margin-bottom: 1em;
+ color: white;
+ text-shadow: 2px 2px darkslategray;
+}
+
+ footer > img {
+ float: left;
+ margin-right: 2ex;
+ }
+
+ footer > h3 {
+ margin-top: 0;
+ text-shadow: 2px 2px darkslategray;
+ }
+
+ footer > a {
+ color: #acddfa;
+ }
+
+a {
+ color: white;
+ text-decoration: none;
+}
+
+ a:hover {
+ color: lightgray;
+ }
+
+h1, h2, h3 {
+ margin-bottom: 1ex;
+ text-shadow: 1px 1px darkgray;
+}
+
+h2, h3 {
+ margin-top: 2em;
+}
+
+p {
+ margin-top: 0.4em;
+ margin-bottom: 0.6em;
+}
+
+ul {
+ clear: left;
+}
+
+ ul.folders {
+ border-radius: 6px;
+ background: #161e24;
+ }
+
+ ul.folders > li {
+ display: inline-block;
+ padding: 1ex;
+ padding-right: 2em;
+ font-weight: bold;
+ }
+
+ ul.items {
+ margin: 1ex;
+ block;
+ }
+
+ ul.items > li {
+ display: block;
+ float: left;
+ margin: 1ex 2ex;
+ padding: 1em 2em;
+ border: 1px solid gray;
+ border-radius: 6px;
+ width: 400px;
+ height: 475px;
+ overflow-y: auto;
+ background: #161e24;
+ }
+
+ ul.items > li h3 {
+ margin-top: 1ex;
+ overflow: hidden;
+ text-overflow: ellipsis ellipsis;
+ }
+
+ ul.items > li table {
+ font-size: small;
+ width: 100%;
+ }
+
+ ul.items > li th {
+ text-align: left;
+ font-weight: normal;
+ padding-right: 1em;
+ }
+
+ ul.items > li td {
+ text-align: left;
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis ellipsis;
+ }
+
+img,
+li > a,
+details {
+ display: block;
+ margin: auto;
+}
+
+img {
+ margin-top: 1em;
+ margin-bottom: 2ex;
+}
+
+li h3 {
+ display: block;
+ text-align: center;
+ font-weight: bold;
+ margin-bottom: 1ex;
+}
+
+.desc {
+ font-style: italic;
+}
+
+.clear {
+ clear: both;
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/connectionmanager.xml b/Roadie.Dlna/Server/Resources/connectionmanager.xml
new file mode 100644
index 0000000..09ad275
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/connectionmanager.xml
@@ -0,0 +1,136 @@
+
+
+
+ 1
+ 0
+
+
+
+ GetCurrentConnectionInfo
+
+
+ ConnectionID
+ in
+ A_ARG_TYPE_ConnectionID
+
+
+ RcsID
+ out
+ A_ARG_TYPE_RcsID
+
+
+ AVTransportID
+ out
+ A_ARG_TYPE_AVTransportID
+
+
+ ProtocolInfo
+ out
+ A_ARG_TYPE_ProtocolInfo
+
+
+ PeerConnectionManager
+ out
+ A_ARG_TYPE_ConnectionManager
+
+
+ PeerConnectionID
+ out
+ A_ARG_TYPE_ConnectionID
+
+
+ Direction
+ out
+ A_ARG_TYPE_Direction
+
+
+ Status
+ out
+ A_ARG_TYPE_ConnectionStatus
+
+
+
+
+ GetProtocolInfo
+
+
+ Source
+ out
+ SourceProtocolInfo
+
+
+ Sink
+ out
+ SinkProtocolInfo
+
+
+
+
+ GetCurrentConnectionIDs
+
+
+ ConnectionIDs
+ out
+ CurrentConnectionIDs
+
+
+
+
+
+
+ A_ARG_TYPE_ProtocolInfo
+ string
+
+
+ A_ARG_TYPE_ConnectionStatus
+ string
+
+ OK
+ ContentFormatMismatch
+ InsufficientBandwidth
+ UnreliableChannel
+ Unknown
+
+
+
+ A_ARG_TYPE_AVTransportID
+ i4
+ 0
+
+
+ A_ARG_TYPE_RcsID
+ i4
+ 0
+
+
+ A_ARG_TYPE_ConnectionID
+ i4
+ 0
+
+
+ A_ARG_TYPE_ConnectionManager
+ string
+
+
+ SourceProtocolInfo
+ string
+
+
+ SinkProtocolInfo
+ string
+
+
+ A_ARG_TYPE_Direction
+ string
+
+ Input
+ Output
+
+
+
+ CurrentConnectionIDs
+ string
+ 0
+
+
+
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/contentdirectory.xml b/Roadie.Dlna/Server/Resources/contentdirectory.xml
new file mode 100644
index 0000000..88e657d
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/contentdirectory.xml
@@ -0,0 +1,207 @@
+
+
+
+ 1
+ 0
+
+
+
+ GetSystemUpdateID
+
+
+ Id
+ out
+ SystemUpdateID
+
+
+
+
+ GetSearchCapabilities
+
+
+ SearchCaps
+ out
+ SearchCapabilities
+
+
+
+
+ GetSortCapabilities
+
+
+ SortCaps
+ out
+ SortCapabilities
+
+
+
+
+ Browse
+
+
+ ObjectID
+ in
+ A_ARG_TYPE_ObjectID
+
+
+ BrowseFlag
+ in
+ A_ARG_TYPE_BrowseFlag
+
+
+ Filter
+ in
+ A_ARG_TYPE_Filter
+
+
+ StartingIndex
+ in
+ A_ARG_TYPE_Index
+
+
+ RequestedCount
+ in
+ A_ARG_TYPE_Count
+
+
+ SortCriteria
+ in
+ A_ARG_TYPE_SortCriteria
+
+
+ Result
+ out
+ A_ARG_TYPE_Result
+
+
+ NumberReturned
+ out
+ A_ARG_TYPE_Count
+
+
+ TotalMatches
+ out
+ A_ARG_TYPE_Count
+
+
+ UpdateID
+ out
+ A_ARG_TYPE_UpdateID
+
+
+
+
+ X_GetFeatureList
+
+
+ FeatureList
+ out
+ A_ARG_TYPE_Featurelist
+
+
+
+
+ X_SetBookmark
+
+
+ CategoryType
+ in
+ A_ARG_TYPE_CategoryType
+
+
+ RID
+ in
+ A_ARG_TYPE_RID
+
+
+ ObjectID
+ in
+ A_ARG_TYPE_ObjectID
+
+
+ PosSecond
+ in
+ A_ARG_TYPE_PosSec
+
+
+
+
+
+
+ A_ARG_TYPE_SortCriteria
+ string
+
+
+ A_ARG_TYPE_UpdateID
+ ui4
+
+
+ A_ARG_TYPE_SearchCriteria
+ string
+
+
+ A_ARG_TYPE_Filter
+ string
+
+
+ A_ARG_TYPE_Result
+ string
+
+
+ A_ARG_TYPE_Index
+ ui4
+
+
+ A_ARG_TYPE_ObjectID
+ string
+
+
+ SortCapabilities
+ string
+
+
+ SearchCapabilities
+ string
+
+
+ A_ARG_TYPE_Count
+ ui4
+
+
+ A_ARG_TYPE_BrowseFlag
+ string
+
+ BrowseMetadata
+ BrowseDirectChildren
+
+
+
+ SystemUpdateID
+ ui4
+
+
+ A_ARG_TYPE_BrowseLetter
+ string
+
+
+ A_ARG_TYPE_CategoryType
+ ui4
+
+
+
+ A_ARG_TYPE_RID
+ ui4
+
+
+
+ A_ARG_TYPE_PosSec
+ ui4
+
+
+
+ A_ARG_TYPE_Featurelist
+ string
+
+
+
+
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/description.xml b/Roadie.Dlna/Server/Resources/description.xml
new file mode 100644
index 0000000..3e7c71c
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/description.xml
@@ -0,0 +1,77 @@
+
+
+
+ 1
+ 0
+
+
+
+ DMS-1.50
+
+ M-DMS-1.50
+
+ urn:schemas-upnp-org:device:MediaServer:1
+ Roadie
+ https://github.com/sphildreth/roadie
+ Roadie Music Server
+
+
+ https://github.com/sphildreth/roadie/
+
+ smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec
+ smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec
+
+
+ image/jpeg
+ 48
+ 48
+ 24
+ /icon/small.jpg
+
+
+ image/png
+ 48
+ 48
+ 24
+ /icon/small.png
+
+
+ image/png
+ 120
+ 120
+ 24
+ /icon/large.png
+
+
+ image/jpeg
+ 120
+ 120
+ 24
+ /icon/large.jpg
+
+
+
+
+ urn:schemas-upnp-org:service:ContentDirectory:1
+ urn:upnp-org:serviceId:ContentDirectory
+ /contentDirectory.xml
+ /serviceControl
+
+
+
+ urn:schemas-upnp-org:service:ConnectionManager:1
+ urn:upnp-org:serviceId:ConnectionManager
+ /connectionManager.xml
+ /serviceControl
+
+
+
+ urn:schemas-upnp-org:service:X_MS_MediaReceiverRegistrar:1
+ urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar
+ /MSMediaReceiverRegistrar.xml
+ /serviceControl
+
+
+
+
+
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Resources/favicon.ico b/Roadie.Dlna/Server/Resources/favicon.ico
new file mode 100644
index 0000000..3599ec6
Binary files /dev/null and b/Roadie.Dlna/Server/Resources/favicon.ico differ
diff --git a/Roadie.Dlna/Server/Resources/large.jpg b/Roadie.Dlna/Server/Resources/large.jpg
new file mode 100644
index 0000000..2e60719
Binary files /dev/null and b/Roadie.Dlna/Server/Resources/large.jpg differ
diff --git a/Roadie.Dlna/Server/Resources/large.png b/Roadie.Dlna/Server/Resources/large.png
new file mode 100644
index 0000000..026646a
Binary files /dev/null and b/Roadie.Dlna/Server/Resources/large.png differ
diff --git a/Roadie.Dlna/Server/Resources/small.jpg b/Roadie.Dlna/Server/Resources/small.jpg
new file mode 100644
index 0000000..5a74762
Binary files /dev/null and b/Roadie.Dlna/Server/Resources/small.jpg differ
diff --git a/Roadie.Dlna/Server/Resources/small.png b/Roadie.Dlna/Server/Resources/small.png
new file mode 100644
index 0000000..6a016b2
Binary files /dev/null and b/Roadie.Dlna/Server/Resources/small.png differ
diff --git a/Roadie.Dlna/Server/Resources/x_featurelist.xml b/Roadie.Dlna/Server/Resources/x_featurelist.xml
new file mode 100644
index 0000000..4789192
--- /dev/null
+++ b/Roadie.Dlna/Server/Resources/x_featurelist.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Responses/FileResponse.cs b/Roadie.Dlna/Server/Responses/FileResponse.cs
new file mode 100644
index 0000000..ae43e5f
--- /dev/null
+++ b/Roadie.Dlna/Server/Responses/FileResponse.cs
@@ -0,0 +1,29 @@
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class FileResponse : IResponse
+ {
+ private readonly FileInfo body;
+
+ public Stream Body => body.OpenRead();
+
+ public IHeaders Headers { get; } = new ResponseHeaders();
+
+ public HttpCode Status { get; }
+
+ public FileResponse(HttpCode aStatus, FileInfo aBody)
+ : this(aStatus, "text/html; charset=utf-8", aBody)
+ {
+ }
+
+ public FileResponse(HttpCode aStatus, string aMime, FileInfo aBody)
+ {
+ Status = aStatus;
+ body = aBody;
+
+ Headers["Content-Type"] = aMime;
+ Headers["Content-Length"] = body.Length.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Responses/ItemResponse.cs b/Roadie.Dlna/Server/Responses/ItemResponse.cs
new file mode 100644
index 0000000..88d8891
--- /dev/null
+++ b/Roadie.Dlna/Server/Responses/ItemResponse.cs
@@ -0,0 +1,74 @@
+using Roadie.Dlna.Server.Metadata;
+using System;
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class ItemResponse : IResponse
+ {
+ private readonly Headers headers;
+
+ private readonly IMediaResource item;
+
+ public Stream Body => item.CreateContentStream();
+
+ public IHeaders Headers => headers;
+
+ public HttpCode Status { get; } = HttpCode.Ok;
+
+ public ItemResponse(string prefix, IRequest request, IMediaResource item,
+ string transferMode = "Streaming")
+ {
+ this.item = item;
+ headers = new ResponseHeaders(!(item is IMediaCoverResource));
+ var meta = item as IMetaInfo;
+ if (meta != null)
+ {
+ headers.Add("Content-Length", meta.InfoSize.ToString());
+ headers.Add("Last-Modified", meta.InfoDate.ToString("R"));
+ }
+ headers.Add("Accept-Ranges", "bytes");
+ headers.Add("Content-Type", DlnaMaps.Mime[item.Type]);
+ if (request.Headers.ContainsKey("getcontentFeatures.dlna.org"))
+ {
+ try
+ {
+ headers.Add(
+ "contentFeatures.dlna.org",
+ item.MediaType == DlnaMediaTypes.Image
+ ? $"DLNA.ORG_PN={item.PN};DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={DlnaMaps.DefaultInteractive}"
+ : $"DLNA.ORG_PN={item.PN};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={DlnaMaps.DefaultStreaming}"
+ );
+ }
+ catch (NotSupportedException)
+ {
+ }
+ catch (NotImplementedException)
+ {
+ }
+ }
+ if (request.Headers.ContainsKey("getCaptionInfo.sec"))
+ {
+ var mvi = item as IMetaVideoItem;
+ if (mvi != null && mvi.Subtitle.HasSubtitle)
+ {
+ var surl =
+ $"http://{request.LocalEndPoint.Address}:{request.LocalEndPoint.Port}{prefix}subtitle/{item.Id}/st.srt";
+ headers.Add("CaptionInfo.sec", surl);
+ }
+ }
+ if (request.Headers.ContainsKey("getMediaInfo.sec"))
+ {
+ var md = item as IMetaDuration;
+ if (md?.MetaDuration != null)
+ {
+ headers.Add(
+ "MediaInfo.sec",
+ $"SEC_Duration={md.MetaDuration.Value.TotalMilliseconds};"
+ );
+ }
+ }
+ headers.Add("transferMode.dlna.org", transferMode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Responses/Redirect.cs b/Roadie.Dlna/Server/Responses/Redirect.cs
new file mode 100644
index 0000000..c8f59b9
--- /dev/null
+++ b/Roadie.Dlna/Server/Responses/Redirect.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class Redirect : StringResponse
+ {
+ internal Redirect(string uri)
+ : this(HttpCode.TemporaryRedirect, uri)
+ {
+ }
+
+ internal Redirect(Uri uri)
+ : this(HttpCode.TemporaryRedirect, uri)
+ {
+ }
+
+ internal Redirect(IRequest request, string path)
+ : this(HttpCode.TemporaryRedirect, request, path)
+ {
+ }
+
+ internal Redirect(HttpCode code, string uri)
+ : base(code, "text/plain", "Redirecting...")
+ {
+ Headers.Add("Location", uri);
+ }
+
+ internal Redirect(HttpCode code, Uri uri)
+ : this(code, uri.AbsoluteUri)
+ {
+ }
+
+ internal Redirect(HttpCode code, IRequest request, string path)
+ : this(code, $"http://{request.LocalEndPoint}{path}")
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Responses/ResourceResponse.cs b/Roadie.Dlna/Server/Responses/ResourceResponse.cs
new file mode 100644
index 0000000..77c636e
--- /dev/null
+++ b/Roadie.Dlna/Server/Responses/ResourceResponse.cs
@@ -0,0 +1,36 @@
+using Roadie.Dlna.Utility;
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class ResourceResponse : IResponse
+ {
+ private readonly byte[] resource;
+
+ public Stream Body => new MemoryStream(resource);
+
+ public IHeaders Headers { get; } = new ResponseHeaders();
+
+ public HttpCode Status { get; }
+
+ public ResourceResponse(HttpCode aStatus, string type, string aResource)
+ {
+ Status = aStatus;
+ try
+ {
+ resource = ResourceHelper.GetResourceData(aResource);
+
+ Headers["Content-Type"] = type;
+ var len = resource?.Length.ToString() ?? "0";
+ Headers["Content-Length"] = len;
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to prepare resource { aResource }, Ex [{ ex }]");
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Responses/StringResponse.cs b/Roadie.Dlna/Server/Responses/StringResponse.cs
new file mode 100644
index 0000000..f45f805
--- /dev/null
+++ b/Roadie.Dlna/Server/Responses/StringResponse.cs
@@ -0,0 +1,30 @@
+using System.IO;
+using System.Text;
+
+namespace Roadie.Dlna.Server
+{
+ internal class StringResponse : IResponse
+ {
+ private readonly string body;
+
+ public Stream Body => new MemoryStream(Encoding.UTF8.GetBytes(body));
+
+ public IHeaders Headers { get; } = new ResponseHeaders();
+
+ public HttpCode Status { get; }
+
+ public StringResponse(HttpCode aStatus, string aBody)
+ : this(aStatus, "text/html; charset=utf-8", aBody)
+ {
+ }
+
+ public StringResponse(HttpCode aStatus, string aMime, string aBody)
+ {
+ Status = aStatus;
+ body = aBody;
+
+ Headers["Content-Type"] = aMime;
+ Headers["Content-Length"] = Encoding.UTF8.GetByteCount(body).ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Ssdp/Datagram.cs b/Roadie.Dlna/Server/Ssdp/Datagram.cs
new file mode 100644
index 0000000..ec29387
--- /dev/null
+++ b/Roadie.Dlna/Server/Ssdp/Datagram.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Roadie.Dlna.Server.Ssdp
+{
+ internal sealed class Datagram
+ {
+ public readonly IPEndPoint EndPoint;
+
+ public readonly IPAddress LocalAddress;
+
+ public readonly string Message;
+
+ public readonly bool Sticky;
+
+ public uint SendCount { get; private set; }
+
+ public Datagram(IPEndPoint endPoint, IPAddress localAddress,
+ string message, bool sticky)
+ {
+ EndPoint = endPoint;
+ LocalAddress = localAddress;
+ Message = message;
+ Sticky = sticky;
+ SendCount = 0;
+ }
+
+ public void Send()
+ {
+ var msg = Encoding.ASCII.GetBytes(Message);
+ try
+ {
+ var client = new UdpClient();
+ client.Client.Bind(new IPEndPoint(LocalAddress, 0));
+ client.Ttl = 10;
+ client.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 10);
+ client.BeginSend(msg, msg.Length, EndPoint, result =>
+ {
+ try
+ {
+ client.EndSend(result);
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine(ex);
+ }
+ finally
+ {
+ try
+ {
+ client.Close();
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ }, null);
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine(ex);
+ }
+ ++SendCount;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Ssdp/SsdpHandler.cs b/Roadie.Dlna/Server/Ssdp/SsdpHandler.cs
new file mode 100644
index 0000000..4e19273
--- /dev/null
+++ b/Roadie.Dlna/Server/Ssdp/SsdpHandler.cs
@@ -0,0 +1,343 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Timers;
+using Timer = System.Timers.Timer;
+
+namespace Roadie.Dlna.Server.Ssdp
+{
+ internal sealed class SsdpHandler : IDisposable
+ {
+ internal static readonly IPEndPoint BroadEndp =
+ new IPEndPoint(IPAddress.Parse("255.255.255.255"), SSDP_PORT);
+
+ private const int DATAGRAMS_PER_MESSAGE = 3;
+
+ private const string SSDP_ADDR = "239.255.255.250";
+
+ private const int SSDP_PORT = 1900;
+
+ private static readonly Random random = new Random();
+
+ private static readonly IPEndPoint ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDP_ADDR), SSDP_PORT);
+
+ private static readonly IPAddress ssdpIP = IPAddress.Parse(SSDP_ADDR);
+
+ private readonly UdpClient client = new UdpClient();
+
+ private readonly AutoResetEvent datagramPosted = new AutoResetEvent(false);
+
+ private readonly Dictionary> devices = new Dictionary>();
+
+ private readonly ConcurrentQueue messageQueue = new ConcurrentQueue();
+
+ private readonly Timer notificationTimer = new Timer(60000);
+
+ private readonly Timer queueTimer = new Timer(1000);
+
+ private bool running = true;
+
+ private ILogger Logger { get; }
+
+ private UpnpDevice[] Devices
+ {
+ get
+ {
+ UpnpDevice[] devs;
+ lock (devices)
+ {
+ devs = devices.Values.SelectMany(i => i).ToArray();
+ }
+ return devs;
+ }
+ }
+
+ public SsdpHandler(ILogger logger)
+ {
+ Logger = logger;
+
+ notificationTimer.Elapsed += Tick;
+ notificationTimer.Enabled = true;
+
+ queueTimer.Elapsed += ProcessQueue;
+
+ client.Client.UseOnlyOverlappedIO = true;
+ client.Client.SetSocketOption(
+ SocketOptionLevel.Socket,
+ SocketOptionName.ReuseAddress,
+ true
+ );
+ client.ExclusiveAddressUse = false;
+ client.Client.Bind(new IPEndPoint(IPAddress.Any, SSDP_PORT));
+ client.JoinMulticastGroup(ssdpIP, 10);
+ Logger.LogTrace("SSDP service started");
+ Receive();
+ }
+
+ public void Dispose()
+ {
+ Logger.LogTrace("Disposing SSDP");
+ running = false;
+ while (messageQueue.Count != 0)
+ {
+ datagramPosted.WaitOne();
+ }
+
+ client.DropMulticastGroup(ssdpIP);
+
+ notificationTimer.Enabled = false;
+ queueTimer.Enabled = false;
+ notificationTimer.Dispose();
+ queueTimer.Dispose();
+ datagramPosted.Dispose();
+ }
+
+ internal void NotifyAll()
+ {
+ Logger.LogTrace("NotifyAll");
+ foreach (var d in Devices)
+ {
+ NotifyDevice(d, "alive", false);
+ }
+ }
+
+ internal void NotifyDevice(UpnpDevice dev, string type, bool sticky)
+ {
+ Logger.LogTrace("NotifyDevice");
+ var headers = new RawHeaders
+ {
+ {"HOST", "239.255.255.250:1900"},
+ {"CACHE-CONTROL", "max-age = 600"},
+ {"LOCATION", dev.Descriptor.ToString()},
+ {"SERVER", HttpServer.Signature},
+ {"NTS", "ssdp:" + type},
+ {"NT", dev.Type},
+ {"USN", dev.USN}
+ };
+
+ SendDatagram(
+ ssdpEndp,
+ dev.Address,
+ $"NOTIFY * HTTP/1.1\r\n{headers.HeaderBlock}\r\n",
+ sticky
+ );
+ // Some buggy network equipment will swallow multicast packets, so lets
+ // cheat, increase the odds, by sending to broadcast.
+ SendDatagram(
+ BroadEndp,
+ dev.Address,
+ $"NOTIFY * HTTP/1.1\r\n{headers.HeaderBlock}\r\n",
+ sticky
+ );
+ Logger.LogTrace($"{dev.USN} said {type}");
+ }
+
+ internal void RegisterNotification(Guid uuid, Uri descriptor,
+ IPAddress address)
+ {
+ List list;
+ lock (devices)
+ {
+ if (!devices.TryGetValue(uuid, out list))
+ {
+ devices.Add(uuid, list = new List());
+ }
+ }
+ list.AddRange(new[]
+ {
+ "upnp:rootdevice", "urn:schemas-upnp-org:device:MediaServer:1",
+ "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1",
+ "urn:schemas-upnp-org:service:X_MS_MediaReceiverRegistrar:1", "uuid:" + uuid
+ }.Select(t => new UpnpDevice(uuid, t, descriptor, address)));
+
+ NotifyAll();
+ Logger.LogTrace($"Registered mount {uuid}, {address}");
+ }
+
+ internal void RespondToSearch(IPEndPoint endpoint, string req)
+ {
+ if (req == "ssdp:all")
+ {
+ req = null;
+ }
+
+ Logger.LogTrace("RespondToSearch {endpoint} {req}");
+ foreach (var d in Devices)
+ {
+ if (!string.IsNullOrEmpty(req) && req != d.Type)
+ {
+ continue;
+ }
+ SendSearchResponse(endpoint, d);
+ }
+ }
+
+ internal void UnregisterNotification(Guid uuid)
+ {
+ List dl;
+ lock (devices)
+ {
+ if (!devices.TryGetValue(uuid, out dl))
+ {
+ return;
+ }
+ devices.Remove(uuid);
+ }
+ foreach (var d in dl)
+ {
+ NotifyDevice(d, "byebye", true);
+ }
+ Logger.LogTrace("Unregistered mount {uuid}");
+ }
+
+ private void ProcessQueue(object sender, ElapsedEventArgs e)
+ {
+ while (messageQueue.Count != 0)
+ {
+ Datagram msg;
+ if (!messageQueue.TryPeek(out msg))
+ {
+ continue;
+ }
+ if (msg != null && (running || msg.Sticky))
+ {
+ msg.Send();
+ if (msg.SendCount > DATAGRAMS_PER_MESSAGE)
+ {
+ messageQueue.TryDequeue(out msg);
+ }
+ break;
+ }
+ messageQueue.TryDequeue(out msg);
+ }
+ datagramPosted.Set();
+ queueTimer.Enabled = messageQueue.Count != 0;
+ queueTimer.Interval = random.Next(25, running ? 75 : 50);
+ }
+
+ private void Receive()
+ {
+ try
+ {
+ client.BeginReceive(ReceiveCallback, null);
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ private void ReceiveCallback(IAsyncResult result)
+ {
+ try
+ {
+ var endpoint = new IPEndPoint(IPAddress.None, SSDP_PORT);
+ var received = client.EndReceive(result, ref endpoint);
+ if (received == null)
+ {
+ throw new IOException("Didn't receive anything");
+ }
+ if (received.Length == 0)
+ {
+ throw new IOException("Didn't receive any bytes");
+ }
+
+ //Logger.LogTrace($"{endpoint} - SSDP Received a datagram");
+
+ using (var reader = new StreamReader(new MemoryStream(received), Encoding.ASCII))
+ {
+ var proto = reader.ReadLine();
+ if (proto == null)
+ {
+ throw new IOException("Couldn't read protocol line");
+ }
+ proto = proto.Trim();
+ if (string.IsNullOrEmpty(proto))
+ {
+ throw new IOException("Invalid protocol line");
+ }
+ var method = proto.Split(new[] { ' ' }, 2)[0];
+ var headers = new Headers();
+ for (var line = reader.ReadLine();
+ line != null;
+ line = reader.ReadLine())
+ {
+ line = line.Trim();
+ if (string.IsNullOrEmpty(line))
+ {
+ break;
+ }
+ var parts = line.Split(new[] { ':' }, 2);
+ headers[parts[0]] = parts[1].Trim();
+ }
+ // Logger.LogTrace($"{endpoint} - Datagram method: {method}");
+ if (method == "M-SEARCH")
+ {
+ RespondToSearch(endpoint, headers["st"]);
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ Logger.LogTrace($"Failed to read SSDP message Ex [{ ex }]");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogTrace($"Failed to read SSDP message Ex [{ ex }]");
+ }
+ Receive();
+ }
+
+ private void SendDatagram(IPEndPoint endpoint, IPAddress address,
+ string message, bool sticky)
+ {
+ if (!running)
+ {
+ return;
+ }
+ var dgram = new Datagram(endpoint, address, message, sticky);
+ if (messageQueue.Count == 0)
+ {
+ dgram.Send();
+ }
+ messageQueue.Enqueue(dgram);
+ queueTimer.Enabled = true;
+ }
+
+ private void SendSearchResponse(IPEndPoint endpoint, UpnpDevice dev)
+ {
+ var headers = new RawHeaders
+ {
+ {"CACHE-CONTROL", "max-age = 600"},
+ {"DATE", DateTime.Now.ToString("R")},
+ {"EXT", string.Empty},
+ {"LOCATION", dev.Descriptor.ToString()},
+ {"SERVER", HttpServer.Signature},
+ {"ST", dev.Type},
+ {"USN", dev.USN}
+ };
+
+ SendDatagram(
+ endpoint,
+ dev.Address,
+ $"HTTP/1.1 200 OK\r\n{headers.HeaderBlock}\r\n",
+ false
+ );
+ Logger.LogTrace($"{dev.Address}, {endpoint} - Responded to a {dev.Type} request");
+ }
+
+ private void Tick(object sender, ElapsedEventArgs e)
+ {
+ Logger.LogTrace("Sending SSDP notifications!");
+ notificationTimer.Interval = random.Next(60000, 120000);
+ NotifyAll();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/AudioResourceDecorator.cs b/Roadie.Dlna/Server/Types/AudioResourceDecorator.cs
new file mode 100644
index 0000000..cfa3f7c
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/AudioResourceDecorator.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ internal class AudioResourceDecorator
+ : MediaResourceDecorator
+ {
+ public virtual string MetaAlbum => Resource.MetaAlbum;
+
+ public virtual string MetaArtist => Resource.MetaArtist;
+
+ public virtual string MetaDescription => Resource.MetaDescription;
+
+ public virtual TimeSpan? MetaDuration => Resource.MetaDuration;
+
+ public virtual string MetaGenre => Resource.MetaGenre;
+
+ public virtual string MetaPerformer => Resource.MetaPerformer;
+
+ public virtual int? MetaTrack => Resource.MetaTrack;
+
+ public AudioResourceDecorator(IMediaAudioResource resource) : base(resource)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/DlnaFlags.cs b/Roadie.Dlna/Server/Types/DlnaFlags.cs
new file mode 100644
index 0000000..7cd95ac
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/DlnaFlags.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ [Flags]
+ internal enum DlnaFlags : ulong
+ {
+ BackgroundTransferMode = 1 << 22,
+ ByteBasedSeek = 1 << 29,
+ ConnectionStall = 1 << 21,
+ DlnaV15 = 1 << 20,
+ InteractiveTransferMode = 1 << 23,
+ PlayContainer = 1 << 28,
+ RtspPause = 1 << 25,
+ S0Increase = 1 << 27,
+ SenderPaced = 1L << 31,
+ SnIncrease = 1 << 26,
+ StreamingTransferMode = 1 << 24,
+ TimeBasedSeek = 1 << 30
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/DlnaMaps.cs b/Roadie.Dlna/Server/Types/DlnaMaps.cs
new file mode 100644
index 0000000..243ff42
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/DlnaMaps.cs
@@ -0,0 +1,397 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Roadie.Dlna.Server
+{
+ public static class DlnaMaps
+ {
+ public static readonly Dictionary> AllPN = new Dictionary>
+ {
+ {
+ DlnaMime.AudioAAC, new List
+ {
+ "AAC"
+ }
+ },
+ {
+ DlnaMime.AudioFLAC, new List
+ {
+ "FLAC"
+ }
+ },
+ {
+ DlnaMime.AudioMP2, new List
+ {
+ "MP2_MPS"
+ }
+ },
+ {
+ DlnaMime.AudioMP3, new List
+ {
+ "MP3"
+ }
+ },
+ {
+ DlnaMime.AudioRAW, new List
+ {
+ "LPCM"
+ }
+ },
+ {
+ DlnaMime.AudioVORBIS, new List
+ {
+ "OGG"
+ }
+ },
+ {
+ DlnaMime.ImageGIF, new List
+ {
+ "GIF",
+ "GIF_LRG",
+ "GIF_MED",
+ "GIF_SM"
+ }
+ },
+ {
+ DlnaMime.ImageJPEG, new List
+ {
+ "JPEG",
+ "JPEG_LRG",
+ "JPEG_MED",
+ "JPEG_SM",
+ "JPEG_TN"
+ }
+ },
+ {
+ DlnaMime.ImagePNG, new List
+ {
+ "PNG",
+ "PNG_LRG",
+ "PNG_MED",
+ "PNG_SM",
+ "PNG_TN"
+ }
+ },
+ {
+ DlnaMime.SubtitleSRT, new List
+ {
+ "SRT"
+ }
+ },
+ {
+ DlnaMime.Video3GPP, new List
+ {
+ "MPEG4_P2_3GPP_SP_L0B_AMR",
+ "AVC_3GPP_BL_QCIF15_AAC",
+ "MPEG4_H263_3GPP_P0_L10_AMR",
+ "MPEG4_H263_MP4_P0_L10_AAC",
+ "MPEG4_P2_3GPP_SP_L0B_AAC"
+ }
+ },
+ {
+ DlnaMime.VideoAVC, new List
+ {
+ "AVC_MP4_MP_SD_AAC_MULT5",
+ "AVC_MP4_HP_HD_AAC",
+ "AVC_MP4_HP_HD_DTS",
+ "AVC_MP4_LPCM",
+ "AVC_MP4_MP_SD_AC3",
+ "AVC_MP4_MP_SD_DTS",
+ "AVC_MP4_MP_SD_MPEG1_L3",
+ "AVC_TS_HD_50_LPCM_T",
+ "AVC_TS_HD_DTS_ISO",
+ "AVC_TS_HD_DTS_T",
+ "AVC_TS_HP_HD_MPEG1_L2_ISO",
+ "AVC_TS_HP_HD_MPEG1_L2_T",
+ "AVC_TS_HP_SD_MPEG1_L2_ISO",
+ "AVC_TS_HP_SD_MPEG1_L2_T",
+ "AVC_TS_MP_HD_AAC_MULT5",
+ "AVC_TS_MP_HD_AAC_MULT5_ISO",
+ "AVC_TS_MP_HD_AAC_MULT5_T",
+ "AVC_TS_MP_HD_AC3",
+ "AVC_TS_MP_HD_AC3_ISO",
+ "AVC_TS_MP_HD_AC3_T",
+ "AVC_TS_MP_HD_MPEG1_L3",
+ "AVC_TS_MP_HD_MPEG1_L3_ISO",
+ "AVC_TS_MP_HD_MPEG1_L3_T",
+ "AVC_TS_MP_SD_AAC_MULT5",
+ "AVC_TS_MP_SD_AAC_MULT5_ISO",
+ "AVC_TS_MP_SD_AAC_MULT5_T",
+ "AVC_TS_MP_SD_AC3",
+ "AVC_TS_MP_SD_AC3_ISO",
+ "AVC_TS_MP_SD_AC3_T",
+ "AVC_TS_MP_SD_MPEG1_L3",
+ "AVC_TS_MP_SD_MPEG1_L3_ISO",
+ "AVC_TS_MP_SD_MPEG1_L3_T"
+ }
+ },
+ {
+ DlnaMime.VideoAVI, new List
+ {
+ "AVI"
+ }
+ },
+ {
+ DlnaMime.VideoFLV, new List
+ {
+ "FLV"
+ }
+ },
+ {
+ DlnaMime.VideoMKV, new List
+ {
+ "MATROSKA"
+ }
+ },
+ {
+ DlnaMime.VideoMPEG, new List
+ {
+ "MPEG1",
+ "MPEG_PS_PAL",
+ "MPEG_PS_NTSC",
+ "MPEG_TS_SD_EU",
+ "MPEG_TS_SD_EU_T",
+ "MPEG_TS_SD_EU_ISO",
+ "MPEG_TS_SD_NA",
+ "MPEG_TS_SD_NA_T",
+ "MPEG_TS_SD_NA_ISO",
+ "MPEG_TS_SD_KO",
+ "MPEG_TS_SD_KO_T",
+ "MPEG_TS_SD_KO_ISO",
+ "MPEG_TS_JP_T"
+ }
+ },
+ {
+ DlnaMime.VideoOGV, new List
+ {
+ "OGV"
+ }
+ },
+ {
+ DlnaMime.VideoWMV, new List
+ {
+ "WMV_FULL",
+ "WMV_BASE",
+ "WMVHIGH_FULL",
+ "WMVHIGH_BASE",
+ "WMVHIGH_PRO",
+ "WMVMED_FULL",
+ "WMVMED_BASE",
+ "WMVMED_PRO",
+ "VC1_ASF_AP_L1_WMA",
+ "VC1_ASF_AP_L2_WMA",
+ "VC1_ASF_AP_L3_WMA"
+ }
+ }
+ };
+
+ public static readonly Dictionary> Dlna2Ext =
+ new Dictionary>();
+
+ public static readonly Dictionary Ext2Dlna =
+ new Dictionary();
+
+ public static readonly Dictionary Ext2Media =
+ new Dictionary();
+
+ public static readonly Dictionary MainPN = GenerateMainPN();
+
+ public static readonly Dictionary> Media2Ext =
+ new Dictionary>();
+
+ public static readonly Dictionary Mime = new Dictionary
+ {
+ {DlnaMime.AudioAAC, "audio/aac"},
+ {DlnaMime.AudioFLAC, "audio/flac"},
+ {DlnaMime.AudioMP2, "audio/mpeg"},
+ {DlnaMime.AudioMP3, "audio/mpeg"},
+ {DlnaMime.AudioRAW, "audio/L16;rate=44100;channels=2"},
+ {DlnaMime.AudioVORBIS, "audio/ogg"},
+ {DlnaMime.ImageGIF, "image/gif"},
+ {DlnaMime.ImageJPEG, "image/jpeg"},
+ {DlnaMime.ImagePNG, "image/png"},
+ {DlnaMime.SubtitleSRT, "smi/caption"},
+ {DlnaMime.Video3GPP, "video/3gpp"},
+ {DlnaMime.VideoAVC, "video/mp4"},
+ {DlnaMime.VideoAVI, "video/avi"},
+ {DlnaMime.VideoFLV, "video/flv"},
+ {DlnaMime.VideoMKV, "video/x-matroska"},
+ {DlnaMime.VideoMPEG, "video/mpeg"},
+ {DlnaMime.VideoOGV, "video/ogg"},
+ {DlnaMime.VideoWMV, "video/x-ms-wmv"}
+ };
+
+ public static readonly string ProtocolInfo = GenerateProtocolInfo();
+
+ internal static readonly string DefaultInteractive = FlagsToString(
+ DlnaFlags.InteractiveTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.ConnectionStall |
+ DlnaFlags.ByteBasedSeek |
+ DlnaFlags.DlnaV15
+ );
+
+ internal static readonly string DefaultStreaming = FlagsToString(
+ DlnaFlags.StreamingTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.ConnectionStall |
+ DlnaFlags.ByteBasedSeek |
+ DlnaFlags.DlnaV15
+ );
+
+ private static readonly string[] ext3GPP =
+ {"3gp", "3gpp"};
+
+ private static readonly string[] extAAC =
+ {"aac", "mp4a", "m4a"};
+
+ private static readonly string[] extAVC =
+ {"avc", "mp4", "m4v", "mov"};
+
+ private static readonly string[] extAVI =
+ {"avi", "divx", "xvid"};
+
+ private static readonly string[] extFLAC =
+ {"flac"};
+
+ private static readonly string[] extFLV =
+ {"flv"};
+
+ private static readonly string[] extGIF =
+ {"gif"};
+
+ private static readonly string[] extJPEG =
+ {"jpg", "jpe", "jpeg", "jif", "jfif"};
+
+ private static readonly string[] extMKV =
+ {"mkv", "matroska", "mk3d", "webm"};
+
+ private static readonly string[] extMP2 =
+ {"mp2"};
+
+ private static readonly string[] extMP3 =
+ {"mp3", "mp3p", "mp3x", "mp3a", "mpa"};
+
+ private static readonly string[] extMPEG =
+ {"mpg", "mpe", "mpeg", "mpg2", "mpeg2", "ts", "vob", "m2v"};
+
+ private static readonly string[] extOGV =
+ {"ogm", "ogv"};
+
+ private static readonly string[] extPNG =
+ {"png"};
+
+ private static readonly string[] extRAWAUDIO =
+ {"wav"};
+
+ private static readonly string[] extVORBIS =
+ {"ogg", "oga"};
+
+ private static readonly string[] extWMV =
+ {"wmv", "asf", "wma", "wmf"};
+
+ static DlnaMaps()
+ {
+ var extToDLNA = new[]
+ {
+ new
+ {t = DlnaMime.AudioAAC, e = extAAC},
+ new
+ {t = DlnaMime.AudioFLAC, e = extFLAC},
+ new
+ {t = DlnaMime.AudioMP2, e = extMP2},
+ new
+ {t = DlnaMime.AudioMP3, e = extMP3},
+ new
+ {t = DlnaMime.AudioRAW, e = extRAWAUDIO},
+ new
+ {t = DlnaMime.AudioVORBIS, e = extVORBIS},
+ new
+ {t = DlnaMime.ImageGIF, e = extGIF},
+ new
+ {t = DlnaMime.ImageJPEG, e = extJPEG},
+ new
+ {t = DlnaMime.ImagePNG, e = extPNG},
+ new
+ {t = DlnaMime.Video3GPP, e = ext3GPP},
+ new
+ {t = DlnaMime.VideoAVC, e = extAVC},
+ new
+ {t = DlnaMime.VideoAVI, e = extAVI},
+ new
+ {t = DlnaMime.VideoFLV, e = extFLV},
+ new
+ {t = DlnaMime.VideoMKV, e = extMKV},
+ new
+ {t = DlnaMime.VideoMPEG, e = extMPEG},
+ new
+ {t = DlnaMime.VideoOGV, e = extOGV},
+ new
+ {t = DlnaMime.VideoWMV, e = extWMV}
+ };
+
+ foreach (var i in extToDLNA)
+ {
+ var t = i.t;
+ foreach (var e in i.e)
+ {
+ Ext2Dlna.Add(e.ToUpperInvariant(), t);
+ }
+ Dlna2Ext.Add(i.t, new List(i.e));
+ }
+
+ InitMedia(
+ new[] { ext3GPP, extAVI, extAVC, extFLV, extMKV, extMPEG, extOGV, extWMV },
+ DlnaMediaTypes.Video);
+ InitMedia(
+ new[] { extJPEG, extPNG, extGIF },
+ DlnaMediaTypes.Image);
+ InitMedia(
+ new[] { extAAC, extFLAC, extMP2, extMP3, extRAWAUDIO, extVORBIS },
+ DlnaMediaTypes.Audio);
+ }
+
+ public static Dictionary GenerateMainPN()
+ {
+ return AllPN.ToDictionary(p => p.Key, p => p.Value.FirstOrDefault());
+ }
+
+ internal static string FlagsToString(DlnaFlags flags)
+ {
+ return $"{(ulong)flags:X8}{0:D24}";
+ }
+
+ private static string GenerateProtocolInfo()
+ {
+ var pns = (from p in AllPN
+ let mime = Mime[p.Key]
+ from pn in p.Value
+ select
+ string.Format("http-get:*:{1}:DLNA.ORG_PN={0};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={2}", pn,
+ mime, DefaultStreaming)).ToList();
+ return string.Join(",", pns);
+ }
+
+ private static void InitMedia(string[][] k, DlnaMediaTypes t)
+ {
+ foreach (var i in k)
+ {
+ var e = (from ext in i
+ select ext.ToUpperInvariant()).ToList();
+ try
+ {
+ Media2Ext.Add(t, e);
+ }
+ catch (ArgumentException)
+ {
+ Media2Ext[t].AddRange(e);
+ }
+ foreach (var ext in e)
+ {
+ Ext2Media.Add(ext.ToUpperInvariant(), t);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/DlnaMediaTypes.cs b/Roadie.Dlna/Server/Types/DlnaMediaTypes.cs
new file mode 100644
index 0000000..14b48b8
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/DlnaMediaTypes.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Roadie.Dlna.Server
+{
+ [Flags]
+ public enum DlnaMediaTypes
+ {
+ Audio = 1 << 2,
+ Image = 1 << 1,
+ Video = 1 << 0,
+ All = ~(-1 << 3)
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/DlnaMime.cs b/Roadie.Dlna/Server/Types/DlnaMime.cs
new file mode 100644
index 0000000..2f44fcb
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/DlnaMime.cs
@@ -0,0 +1,24 @@
+namespace Roadie.Dlna.Server
+{
+ public enum DlnaMime
+ {
+ AudioAAC,
+ AudioFLAC,
+ AudioMP2,
+ AudioMP3,
+ AudioRAW,
+ AudioVORBIS,
+ ImageGIF,
+ ImageJPEG,
+ ImagePNG,
+ SubtitleSRT,
+ Video3GPP,
+ VideoAVC,
+ VideoAVI,
+ VideoFLV,
+ VideoMKV,
+ VideoMPEG,
+ VideoOGV,
+ VideoWMV
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/Extensions.cs b/Roadie.Dlna/Server/Types/Extensions.cs
new file mode 100644
index 0000000..329fdc4
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/Extensions.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Roadie.Dlna.Server
+{
+ public static class Extensions
+ {
+ public static IEnumerable GetExtensions(this DlnaMediaTypes types)
+ {
+ return (from i in DlnaMaps.Media2Ext
+ where types.HasFlag(i.Key)
+ select i.Value).SelectMany(i => i);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/Headers.cs b/Roadie.Dlna/Server/Types/Headers.cs
new file mode 100644
index 0000000..3f63914
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/Headers.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server
+{
+ public class Headers : IHeaders
+ {
+ private static readonly Regex validator = new Regex(
+ @"^[a-z\d][a-z\d_.-]+$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ private readonly bool asIs;
+
+ private readonly Dictionary dict =
+ new Dictionary();
+
+ public int Count => dict.Count;
+
+ public string HeaderBlock
+ {
+ get
+ {
+ var hb = new StringBuilder();
+ foreach (var h in this)
+ {
+ hb.AppendFormat("{0}: {1}\r\n", h.Key, h.Value);
+ }
+ return hb.ToString();
+ }
+ }
+
+ public Stream HeaderStream => new MemoryStream(Encoding.ASCII.GetBytes(HeaderBlock));
+
+ public bool IsReadOnly => false;
+
+ public ICollection Keys => dict.Keys;
+
+ public ICollection Values => dict.Values;
+
+ public string this[string key]
+ {
+ get { return dict[Normalize(key)]; }
+ set { dict[Normalize(key)] = value; }
+ }
+
+ public Headers()
+ : this(false)
+ {
+ }
+
+ protected Headers(bool asIs)
+ {
+ this.asIs = asIs;
+ }
+
+ public void Add(KeyValuePair item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ public void Add(string key, string value)
+ {
+ dict.Add(Normalize(key), value);
+ }
+
+ public void Clear()
+ {
+ dict.Clear();
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ var p = new KeyValuePair(
+ Normalize(item.Key), item.Value);
+ return dict.Contains(p);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return dict.ContainsKey(Normalize(key));
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return dict.GetEnumerator();
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return dict.GetEnumerator();
+ }
+
+ public bool Remove(string key)
+ {
+ return dict.Remove(Normalize(key));
+ }
+
+ public bool Remove(KeyValuePair item)
+ {
+ return Remove(item.Key);
+ }
+
+ public override string ToString()
+ {
+ return $"({string.Join(", ", from x in dict select $"{x.Key}={x.Value}")})";
+ }
+
+ public bool TryGetValue(string key, out string value)
+ {
+ return dict.TryGetValue(Normalize(key), out value);
+ }
+
+ private string Normalize(string header)
+ {
+ if (!asIs)
+ {
+ header = header.ToUpperInvariant();
+ }
+ header = header.Trim();
+ if (!validator.IsMatch(header))
+ {
+ throw new ArgumentException("Invalid header: " + header);
+ }
+ return header;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/HtmlTools.cs b/Roadie.Dlna/Server/Types/HtmlTools.cs
new file mode 100644
index 0000000..2c0f9c0
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/HtmlTools.cs
@@ -0,0 +1,65 @@
+using Roadie.Dlna.Utility;
+using System.Reflection;
+using System.Xml;
+
+namespace Roadie.Dlna.Server
+{
+ internal static class HtmlTools
+ {
+ public static XmlElement CreateHtmlArticle(string title)
+ {
+ title += " – Roadie Music Server";
+
+ var document = new XmlDocument();
+ document.AppendChild(document.CreateDocumentType(
+ "html", null, null, null));
+
+ document.AppendChild(document.EL("html"));
+
+ var head = document.EL("head");
+ document.DocumentElement?.AppendChild(head);
+ head.AppendChild(document.EL("title", title));
+ head.AppendChild(document.EL(
+ "link",
+ new AttributeCollection
+ {
+ {"rel", "stylesheet"},
+ {"type", "text/css"},
+ {"href", "/static/browse.css"}
+ }));
+
+ var body = document.EL("body");
+ document.DocumentElement?.AppendChild(body);
+
+ var article = document.EL("article");
+ body.AppendChild(article);
+
+ var header = document.EL("header");
+ header.AppendChild(document.EL("h1", title));
+ article.AppendChild(header);
+
+ var footer = document.EL("footer");
+ footer.AppendChild(document.EL(
+ "img",
+ new AttributeCollection { { "src", "/icon/small.png" } }
+ ));
+ footer.AppendChild(document.EL("h3",
+ $"Roadie Music Server: roadie/{Assembly.GetExecutingAssembly().GetName().Version.Major}.{Assembly.GetExecutingAssembly().GetName().Version.Minor}"));
+ footer.AppendChild(document.EL(
+ "p",
+ new AttributeCollection { { "class", "desc" } },
+ "A powerful API server."
+ ));
+ footer.AppendChild(document.EL(
+ "a",
+ new AttributeCollection
+ {
+ {"href", "https://github.com/sphildreth/roadie/"}
+ },
+ "Fork me on GitHub")
+ );
+ body.AppendChild(footer);
+ return article;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/HttpException.cs b/Roadie.Dlna/Server/Types/HttpException.cs
new file mode 100644
index 0000000..2cfe726
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/HttpException.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Roadie.Dlna.Server
+{
+ [Serializable]
+ public class HttpException : Exception
+ {
+ public HttpException()
+ {
+ }
+
+ public HttpException(string message)
+ : base(message)
+ {
+ }
+
+ public HttpException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ protected HttpException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/HttpStatusException.cs b/Roadie.Dlna/Server/Types/HttpStatusException.cs
new file mode 100644
index 0000000..69c2a50
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/HttpStatusException.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Roadie.Dlna.Server
+{
+ [Serializable]
+ public sealed class HttpStatusException : HttpException
+ {
+ public HttpCode Code { get; private set; }
+
+ public HttpStatusException()
+ {
+ }
+
+ public HttpStatusException(HttpCode code)
+ : base(HttpPhrases.Phrases[code])
+ {
+ Code = code;
+ }
+
+ public HttpStatusException(string message)
+ : base(message)
+ {
+ Code = HttpCode.None;
+ }
+
+ public HttpStatusException(HttpCode code, Exception innerException)
+ : base(HttpPhrases.Phrases[code], innerException)
+ {
+ Code = code;
+ }
+
+ public HttpStatusException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ Code = HttpCode.None;
+ }
+
+ private HttpStatusException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/Identifiers.cs b/Roadie.Dlna/Server/Types/Identifiers.cs
new file mode 100644
index 0000000..3ef65dd
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/Identifiers.cs
@@ -0,0 +1,160 @@
+using Roadie.Dlna.Server.Comparers;
+using Roadie.Dlna.Server.Views;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Roadie.Dlna.Server
+{
+ public sealed class Identifiers
+ {
+ public const string GENERAL_ROOT = "0";
+
+ public const string SAMSUNG_AUDIO = "A";
+
+ public const string SAMSUNG_IMAGES = "I";
+
+ public const string SAMSUNG_VIDEO = "V";
+
+ private static readonly Random idGen = new Random();
+
+ private readonly IItemComparer comparer;
+ private readonly List filters = new List();
+
+ private readonly Dictionary hardRefs =
+ new Dictionary();
+
+ private readonly Dictionary ids =
+ new Dictionary();
+
+ private readonly bool order;
+
+ private readonly List views = new List();
+
+ private Dictionary paths =
+ new Dictionary();
+
+ public bool HasViews => views.Count != 0;
+
+ public IEnumerable Resources => (from i in ids.Values
+ where i.Target is IMediaResource
+ select i).ToList();
+
+ public Identifiers(IItemComparer comparer, bool order)
+ {
+ this.comparer = comparer;
+ this.order = order;
+ }
+
+ public void AddView(string name)
+ {
+ try
+ {
+ var view = ViewRepository.Lookup(name);
+ views.Add(view);
+ var filter = view as IFilteredView;
+ if (filter != null)
+ {
+ filters.Add(filter);
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to add view Ex [{ ex }]");
+ throw;
+ }
+ }
+
+ public bool Allowed(IMediaResource item)
+ {
+ return filters.All(f => f.Allowed(item));
+ }
+
+ public void Cleanup()
+ {
+ GC.Collect();
+ var pc = paths.Count;
+ var ic = ids.Count;
+ var npaths = new Dictionary();
+ foreach (var p in paths)
+ {
+ if (ids[p.Value].Target == null)
+ {
+ ids.Remove(p.Value);
+ }
+ else
+ {
+ npaths.Add(p.Key, p.Value);
+ }
+ }
+ paths = npaths;
+ Trace.WriteLine($"Cleanup complete: ids (evicted) {ids.Count} ({ (ic - ids.Count) }), paths {paths.Count} ({ (pc - paths.Count)})");
+ }
+
+ public IMediaItem GetItemById(string id)
+ {
+ return ids[id].Target as IMediaItem;
+ }
+
+ public IMediaItem GetItemByPath(string path)
+ {
+ string id;
+ if (!paths.TryGetValue(path, out id))
+ {
+ return null;
+ }
+ return GetItemById(id);
+ }
+
+ public IMediaFolder RegisterFolder(string id, IMediaFolder item)
+ {
+ var rv = item;
+ RegisterFolderTree(rv);
+ foreach (var v in views)
+ {
+ rv = v.Transform(rv);
+ RegisterFolderTree(rv);
+ }
+ rv.Cleanup();
+ ids[id] = new WeakReference(rv);
+ hardRefs[id] = rv;
+ rv.Id = id;
+ rv.Sort(comparer, order);
+ return rv;
+ }
+
+ private void RegisterFolderTree(IMediaFolder folder)
+ {
+ foreach (var f in folder.ChildFolders)
+ {
+ RegisterFolderTree(f);
+ }
+ foreach (var i in folder.ChildItems)
+ {
+ RegisterPath(i);
+ }
+ RegisterPath(folder);
+ }
+
+ private void RegisterPath(IMediaItem item)
+ {
+ var path = item.Path;
+ string id;
+ if (!paths.ContainsKey(path))
+ {
+ while (ids.ContainsKey(id = idGen.Next(1000, int.MaxValue).ToString("X8")))
+ {
+ }
+ paths[path] = id;
+ }
+ else
+ {
+ id = paths[path];
+ }
+ ids[id] = new WeakReference(item);
+
+ item.Id = id;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/MediaResourceDecorator.cs b/Roadie.Dlna/Server/Types/MediaResourceDecorator.cs
new file mode 100644
index 0000000..952227d
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/MediaResourceDecorator.cs
@@ -0,0 +1,61 @@
+using Roadie.Dlna.Server.Metadata;
+using System;
+using System.IO;
+
+namespace Roadie.Dlna.Server
+{
+ internal class MediaResourceDecorator : IMediaResource, IMetaInfo
+ where T : IMediaResource, IMetaInfo
+ {
+ protected T Resource;
+
+ public virtual IMediaCoverResource Cover => Resource.Cover;
+
+ public string Id
+ {
+ get { return Resource.Id; }
+ set { Resource.Id = value; }
+ }
+
+ public DateTime InfoDate => Resource.InfoDate;
+
+ public long? InfoSize => Resource.InfoSize;
+
+ public virtual DlnaMediaTypes MediaType => Resource.MediaType;
+
+ public string Path => Resource.Path;
+
+ public virtual string PN => Resource.PN;
+
+ public virtual IHeaders Properties => Resource.Properties;
+
+ public virtual string Title => Resource.Title;
+
+ public DlnaMime Type => Resource.Type;
+
+ public MediaResourceDecorator(T resource)
+ {
+ Resource = resource;
+ }
+
+ public virtual int CompareTo(IMediaItem other)
+ {
+ return Resource.CompareTo(other);
+ }
+
+ public virtual Stream CreateContentStream()
+ {
+ return Resource.CreateContentStream();
+ }
+
+ public bool Equals(IMediaItem other)
+ {
+ return Resource.Equals(other);
+ }
+
+ public string ToComparableTitle()
+ {
+ return Resource.ToComparableTitle();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/MediaTypes.cs b/Roadie.Dlna/Server/Types/MediaTypes.cs
new file mode 100644
index 0000000..e69de29
diff --git a/Roadie.Dlna/Server/Types/RawHeaders.cs b/Roadie.Dlna/Server/Types/RawHeaders.cs
new file mode 100644
index 0000000..e8932f4
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/RawHeaders.cs
@@ -0,0 +1,10 @@
+namespace Roadie.Dlna.Server
+{
+ public class RawHeaders : Headers
+ {
+ public RawHeaders()
+ : base(true)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/SubTitle.cs b/Roadie.Dlna/Server/Types/SubTitle.cs
new file mode 100644
index 0000000..2394275
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/SubTitle.cs
@@ -0,0 +1,184 @@
+using Roadie.Dlna.Utility;
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace Roadie.Dlna.Server
+{
+ [Serializable]
+ public sealed class Subtitle : IMediaResource
+ {
+ [NonSerialized]
+ private static readonly string[] exts =
+ {
+ ".srt", ".SRT",
+ ".ass", ".ASS",
+ ".ssa", ".SSA",
+ ".sub", ".SUB",
+ ".vtt", ".VTT"
+ };
+
+ [NonSerialized] private byte[] encodedText;
+
+ private string text;
+
+ public IMediaCoverResource Cover
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public bool HasSubtitle => !string.IsNullOrWhiteSpace(text);
+
+ public string Id
+ {
+ get { return Path; }
+ set { throw new NotImplementedException(); }
+ }
+
+ public DateTime InfoDate => DateTime.UtcNow;
+
+ public long? InfoSize
+ {
+ get
+ {
+ try
+ {
+ using (var s = CreateContentStream())
+ {
+ return s.Length;
+ }
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+ }
+
+ public DlnaMediaTypes MediaType
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public string Path => "ad-hoc-subtitle:";
+
+ public string PN => DlnaMaps.MainPN[Type];
+
+ public IHeaders Properties
+ {
+ get
+ {
+ var rv = new RawHeaders { { "Type", Type.ToString() } };
+ if (InfoSize.HasValue)
+ {
+ rv.Add("SizeRaw", InfoSize.ToString());
+ rv.Add("Size", InfoSize.Value.FormatFileSize());
+ }
+ rv.Add("Date", InfoDate.ToString(CultureInfo.InvariantCulture));
+ rv.Add("DateO", InfoDate.ToString("o"));
+ return rv;
+ }
+ }
+
+ public string Title
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public DlnaMime Type => DlnaMime.SubtitleSRT;
+
+ public Subtitle()
+ {
+ }
+
+ public Subtitle(FileInfo file)
+ {
+ Load(file);
+ }
+
+ public Subtitle(string text)
+ {
+ this.text = text;
+ }
+
+ public int CompareTo(IMediaItem other)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Stream CreateContentStream()
+ {
+ if (!HasSubtitle)
+ {
+ throw new NotSupportedException();
+ }
+ if (encodedText == null)
+ {
+ encodedText = Encoding.UTF8.GetBytes(text);
+ }
+ return new MemoryStream(encodedText, false);
+ }
+
+ public bool Equals(IMediaItem other)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string ToComparableTitle()
+ {
+ throw new NotImplementedException();
+ }
+
+ private void Load(FileInfo file)
+ {
+ try
+ {
+ // Try external
+ foreach (var i in exts)
+ {
+ var sti = new FileInfo(
+ System.IO.Path.ChangeExtension(file.FullName, i));
+ try
+ {
+ if (!sti.Exists)
+ {
+ sti = new FileInfo(file.FullName + i);
+ }
+ if (!sti.Exists)
+ {
+ continue;
+ }
+ text = FFmpeg.GetSubtitleSubrip(sti);
+ Trace.WriteLine($"Loaded subtitle from {sti.FullName}");
+ }
+ catch (NotSupportedException)
+ {
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to get subtitle from {sti.FullName} Ex [{ ex }]");
+ }
+ }
+ try
+ {
+ text = FFmpeg.GetSubtitleSubrip(file);
+ Trace.WriteLine($"Loaded subtitle from {file.FullName}");
+ }
+ catch (NotSupportedException ex)
+ {
+ Trace.WriteLine($"Subtitle not supported {file.FullName} Ex [{ ex }]");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to get subtitle from {file.FullName} Ex [{ ex }]");
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to load subtitle for {file.FullName} Ex [{ ex }]");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/UpnpDevice.cs b/Roadie.Dlna/Server/Types/UpnpDevice.cs
new file mode 100644
index 0000000..492ed28
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/UpnpDevice.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Net;
+
+namespace Roadie.Dlna.Server
+{
+ internal sealed class UpnpDevice
+ {
+ public readonly IPAddress Address;
+
+ public readonly Uri Descriptor;
+
+ public readonly string Type;
+
+ public readonly string USN;
+
+ public readonly Guid UUID;
+
+ public UpnpDevice(Guid uuid, string type, Uri descriptor,
+ IPAddress address)
+ {
+ UUID = uuid;
+ Type = type;
+ Descriptor = descriptor;
+ Address = address;
+
+ if (Type.StartsWith("uuid:", StringComparison.Ordinal))
+ {
+ USN = Type;
+ }
+ else
+ {
+ USN = $"uuid:{UUID}::{Type}";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/VirtualClonedFolder.cs b/Roadie.Dlna/Server/Types/VirtualClonedFolder.cs
new file mode 100644
index 0000000..c76a6e4
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/VirtualClonedFolder.cs
@@ -0,0 +1,59 @@
+namespace Roadie.Dlna.Server
+{
+ public sealed class VirtualClonedFolder : VirtualFolder
+ {
+ private readonly IMediaFolder clone;
+
+ private readonly DlnaMediaTypes types;
+
+ public VirtualClonedFolder(IMediaFolder parent)
+ : this(parent, parent.Id, parent.Id, DlnaMediaTypes.All)
+ {
+ }
+
+ public VirtualClonedFolder(IMediaFolder parent, string name)
+ : this(parent, name, name, DlnaMediaTypes.All)
+ {
+ }
+
+ public VirtualClonedFolder(IMediaFolder parent, string name,
+ DlnaMediaTypes types)
+ : this(parent, name, name, types)
+ {
+ }
+
+ private VirtualClonedFolder(IMediaFolder parent, string name, string id,
+ DlnaMediaTypes types)
+ : base(parent, name, id)
+ {
+ this.types = types;
+ Id = id;
+ clone = parent;
+ CloneFolder(this, parent);
+ Cleanup();
+ }
+
+ public override void Cleanup()
+ {
+ base.Cleanup();
+ clone.Cleanup();
+ }
+
+ private void CloneFolder(VirtualFolder parent, IMediaFolder folder)
+ {
+ foreach (var f in folder.ChildFolders)
+ {
+ var vf = new VirtualFolder(parent, f.Title, f.Id);
+ parent.AdoptFolder(vf);
+ CloneFolder(vf, f);
+ }
+ foreach (var i in folder.ChildItems)
+ {
+ if ((types & i.MediaType) == i.MediaType)
+ {
+ parent.AddResource(i);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Types/VirtualFolder.cs b/Roadie.Dlna/Server/Types/VirtualFolder.cs
new file mode 100644
index 0000000..1b4a991
--- /dev/null
+++ b/Roadie.Dlna/Server/Types/VirtualFolder.cs
@@ -0,0 +1,214 @@
+using Roadie.Dlna.Utility;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Roadie.Dlna.Server
+{
+ public class VirtualFolder : IMediaFolder
+ {
+ protected List Folders = new List();
+
+ protected List Resources = new List();
+
+ private static readonly StringComparer comparer =
+ new NaturalStringComparer(true);
+
+ private readonly List merged = new List();
+ private string comparableTitle;
+ private string path;
+
+ public IEnumerable AllItems
+ {
+ get
+ {
+ return Folders.SelectMany(f => ((VirtualFolder)f).AllItems).Concat(Resources);
+ }
+ }
+
+ public int ChildCount => Folders.Count + Resources.Count;
+
+ public IEnumerable ChildFolders => Folders;
+
+ public IEnumerable ChildItems => Resources;
+
+ public int FullChildCount => Resources.Count + (from f in Folders select f.FullChildCount).Sum();
+
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+
+ public IMediaFolder Parent { get; set; }
+
+ public virtual string Path
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+ var p = string.IsNullOrEmpty(Id) ? Name : Id;
+ if (Parent != null)
+ {
+ var vp = Parent as VirtualFolder;
+ path = $"{(vp != null ? vp.Path : Parent.Id)}/v:{p}";
+ }
+ else
+ {
+ path = p;
+ }
+ return path;
+ }
+ }
+
+ public IHeaders Properties
+ {
+ get
+ {
+ var rv = new RawHeaders { { "Title", Title } };
+ return rv;
+ }
+ }
+
+ public virtual string Title => Name;
+
+ public VirtualFolder()
+ {
+ }
+
+ public VirtualFolder(IMediaFolder parent, string name)
+ : this(parent, name, name)
+ {
+ }
+
+ public VirtualFolder(IMediaFolder parent, string name, string id)
+ {
+ Parent = parent;
+ Id = id;
+ Name = name;
+ }
+
+ public void AddResource(IMediaResource res)
+ {
+ Resources.Add(res);
+ }
+
+ public void AddFolder(IMediaFolder folder)
+ {
+ Folders.Add(folder);
+ }
+
+ public void AddFolders(IEnumerable folders)
+ {
+ Folders.AddRange(folders);
+ }
+
+ public void AdoptFolder(IMediaFolder folder)
+ {
+ if (folder == null)
+ {
+ throw new ArgumentNullException(nameof(folder));
+ }
+ var vf = folder.Parent as VirtualFolder;
+ vf?.ReleaseFolder(folder);
+ folder.Parent = this;
+ if (!Folders.Contains(folder))
+ {
+ Folders.Add(folder);
+ }
+ }
+
+ public virtual void Cleanup()
+ {
+ foreach (var m in merged)
+ {
+ m.Cleanup();
+ }
+ foreach (var f in Folders.ToList())
+ {
+ f.Cleanup();
+ }
+ if (ChildCount != 0)
+ {
+ return;
+ }
+ var vp = Parent as VirtualFolder;
+ vp?.ReleaseFolder(this);
+ }
+
+ public int CompareTo(IMediaItem other)
+ {
+ if (other == null)
+ {
+ return 1;
+ }
+ return comparer.Compare(Title, other.Title);
+ }
+
+ public bool Equals(IMediaItem other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+ return Title.Equals(other.Title);
+ }
+
+ public void Merge(IMediaFolder folder)
+ {
+ if (folder == null)
+ {
+ throw new ArgumentNullException(nameof(folder));
+ }
+ merged.Add(folder);
+ foreach (var item in folder.ChildItems)
+ {
+ AddResource(item);
+ }
+ foreach (var cf in folder.ChildFolders)
+ {
+ var ownFolder = (from f in Folders
+ where f is VirtualFolder && f.Title == cf.Title
+ select f as VirtualFolder
+ ).FirstOrDefault();
+ if (ownFolder == null)
+ {
+ ownFolder = new VirtualFolder(this, cf.Title, cf.Id);
+ AdoptFolder(ownFolder);
+ }
+ ownFolder.Merge(cf);
+ }
+ }
+
+ public void ReleaseFolder(IMediaFolder folder)
+ {
+ Folders.Remove(folder);
+ }
+
+ public bool RemoveResource(IMediaResource res)
+ {
+ return Resources.Remove(res);
+ }
+
+ public void Sort(IComparer sortComparer, bool descending)
+ {
+ foreach (var f in Folders)
+ {
+ f.Sort(sortComparer, descending);
+ }
+ Folders.Sort(sortComparer);
+ Resources.Sort(sortComparer);
+ if (descending)
+ {
+ Folders.Reverse();
+ Resources.Reverse();
+ }
+ }
+
+ public string ToComparableTitle()
+ {
+ return comparableTitle ?? (comparableTitle = Title.StemCompareBase());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/BaseView.cs b/Roadie.Dlna/Server/Views/BaseView.cs
new file mode 100644
index 0000000..38e75d2
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/BaseView.cs
@@ -0,0 +1,47 @@
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal abstract class BaseView : IView
+ {
+ public abstract string Description { get; }
+
+ public abstract string Name { get; }
+
+ public override string ToString()
+ {
+ return $"{Name} - {Description}";
+ }
+
+ public abstract IMediaFolder Transform(IMediaFolder oldRoot);
+
+ protected static void MergeFolders(VirtualFolder aFrom, VirtualFolder aTo)
+ {
+ var merges = from f in aFrom.ChildFolders
+ join t in aTo.ChildFolders on f.Title.ToUpper() equals t.Title.ToUpper()
+ where f != t
+ select new
+ {
+ f = f as VirtualFolder,
+ t = t as VirtualFolder
+ };
+ foreach (var m in merges.ToList())
+ {
+ MergeFolders(m.f, m.t);
+ foreach (var c in m.f.ChildFolders.ToList())
+ {
+ m.t.AdoptFolder(c);
+ }
+ foreach (var c in m.f.ChildItems.ToList())
+ {
+ m.t.AddResource(c);
+ m.f.RemoveResource(c);
+ }
+ if (aFrom != aTo)
+ {
+ ((VirtualFolder)m.f.Parent).ReleaseFolder(m.f);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/ByTitleView.cs b/Roadie.Dlna/Server/Views/ByTitleView.cs
new file mode 100644
index 0000000..f62314d
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/ByTitleView.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class ByTitleView : BaseView
+ {
+ public override string Description => "Reorganizes files into folders by title";
+
+ public override string Name => "bytitle";
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ var root = new VirtualClonedFolder(oldRoot);
+ var titles = new SimpleKeyedVirtualFolder(root, "titles");
+ SortFolder(root, titles);
+ foreach (var i in root.ChildFolders.ToList())
+ {
+ root.ReleaseFolder(i);
+ }
+ foreach (var i in titles.ChildFolders.ToList())
+ {
+ if (i.ChildCount > 100)
+ {
+ Trace.WriteLine($"Partioning folder {i.Title}");
+ using (var prefixer = new Prefixer())
+ {
+ PartitionChildren(i as VirtualFolder, prefixer);
+ }
+ }
+ root.AdoptFolder(i);
+ }
+ return root;
+ }
+
+ private static string GetTitle(IMediaResource res)
+ {
+ var pre = res.ToComparableTitle();
+ if (string.IsNullOrEmpty(pre))
+ {
+ return "Unnamed";
+ }
+ return pre;
+ }
+
+ private static void SortFolder(VirtualFolder folder, SimpleKeyedVirtualFolder titles)
+ {
+ foreach (var f in folder.ChildFolders.ToList())
+ {
+ SortFolder(f as VirtualFolder, titles);
+ }
+
+ foreach (var c in folder.ChildItems.ToList())
+ {
+ var pre = GetTitle(c);
+ pre = pre[0].ToString().ToUpperInvariant();
+ titles.GetFolder(pre).AddResource(c);
+ folder.RemoveResource(c);
+ }
+ }
+
+ private void PartitionChildren(VirtualFolder folder, Prefixer prefixer, int startfrom = 1)
+ {
+ for (var wordcount = startfrom; ;)
+ {
+ var curwc = wordcount;
+ var groups = from i in folder.ChildItems.ToList()
+ let prefix = prefixer.GetWordPrefix(GetTitle(i), curwc)
+ where !string.IsNullOrWhiteSpace(prefix)
+ group i by prefix.ToLowerInvariant()
+ into g
+ let gcount = g.LongCount()
+ where gcount > 3
+ orderby g.LongCount() descending
+ select g;
+ var longest = groups.FirstOrDefault();
+ if (longest == null)
+ {
+ if (wordcount++ > 5)
+ {
+ return;
+ }
+ continue;
+ }
+ var newfolder = new VirtualFolder(folder, longest.Key);
+ foreach (var item in longest)
+ {
+ folder.RemoveResource(item);
+ newfolder.AddResource(item);
+ }
+ if (newfolder.ChildCount > 100)
+ {
+ PartitionChildren(newfolder, prefixer, wordcount + 1);
+ }
+ if (newfolder.ChildFolders.LongCount() == 1)
+ {
+ foreach (var f in newfolder.ChildFolders.ToList())
+ {
+ folder.AdoptFolder(f);
+ }
+ }
+ else
+ {
+ folder.AdoptFolder(newfolder);
+ }
+ }
+ }
+
+ private sealed class Prefixer : IDisposable
+ {
+ private static readonly Regex numbers = new Regex(@"[\d+._()\[\]+-]+", RegexOptions.Compiled);
+ private static readonly Regex wordsplit = new Regex(@"(\b[^\s]+\b)", RegexOptions.Compiled);
+ private readonly Dictionary cache = new Dictionary();
+
+ public void Dispose()
+ {
+ cache.Clear();
+ }
+
+ public string GetWordPrefix(string str, int wordcount)
+ {
+ string[] m;
+ var key = str.ToUpperInvariant();
+ if (!cache.TryGetValue(key, out m))
+ {
+ m = (from w in wordsplit.Matches(str).Cast()
+ let v = numbers.Replace(w.Value, "").Trim()
+ where !string.IsNullOrWhiteSpace(v)
+ select v).ToArray();
+ cache[key] = m;
+ }
+ if (m.Length < wordcount)
+ {
+ return null;
+ }
+ return string.Join(" ", m.Take(wordcount).ToArray());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/CascadedView.cs b/Roadie.Dlna/Server/Views/CascadedView.cs
new file mode 100644
index 0000000..fd64020
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/CascadedView.cs
@@ -0,0 +1,65 @@
+using Roadie.Dlna.Utility;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal abstract class CascadedView : BaseView, IConfigurable
+ {
+ private bool cascade = true;
+
+ public void SetParameters(ConfigParameters parameters)
+ {
+ cascade = !parameters.Has("no-cascade") && parameters.Get("cascade", cascade);
+ }
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ var root = new VirtualClonedFolder(oldRoot);
+ var sorted = new SimpleKeyedVirtualFolder(root, Name);
+ SortFolder(root, sorted);
+ Trace.WriteLine($"sort {sorted.ChildFolders.Count()} - {sorted.ChildItems.Count()}");
+ Trace.WriteLine($"root {root.ChildFolders.Count()} - {root.ChildItems.Count()}");
+ foreach (var f in sorted.ChildFolders.ToList())
+ {
+ if (f.ChildCount < 2)
+ {
+ foreach (var file in f.ChildItems)
+ {
+ root.AddResource(file);
+ }
+ continue;
+ }
+ var fsmi = f as VirtualFolder;
+ root.AdoptFolder(fsmi);
+ }
+ foreach (var f in sorted.ChildItems.ToList())
+ {
+ root.AddResource(f);
+ }
+ Trace.WriteLine($"merg {root.ChildFolders.Count()} - {root.ChildItems.Count()}");
+ MergeFolders(root, root);
+ Trace.WriteLine($"done {root.ChildFolders.Count()} - {root.ChildItems.Count()}");
+
+ if (!cascade || root.ChildFolders.LongCount() <= 50)
+ {
+ return root;
+ }
+ var cascaded = new DoubleKeyedVirtualFolder(root, "Series");
+ foreach (var i in root.ChildFolders.ToList())
+ {
+ var folder = cascaded.GetFolder(i.Title.StemCompareBase().Substring(0, 1).ToUpper());
+ folder.AdoptFolder(i);
+ }
+ foreach (var i in root.ChildItems.ToList())
+ {
+ var folder = cascaded.GetFolder(i.Title.StemCompareBase().Substring(0, 1).ToUpper());
+ folder.AddResource(i);
+ }
+ return cascaded;
+ }
+
+ protected abstract void SortFolder(IMediaFolder folder,
+ SimpleKeyedVirtualFolder series);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/DimensionView.cs b/Roadie.Dlna/Server/Views/DimensionView.cs
new file mode 100644
index 0000000..3d50863
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/DimensionView.cs
@@ -0,0 +1,85 @@
+using Roadie.Dlna.Server.Metadata;
+using Roadie.Dlna.Utility;
+using System;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal class DimensionView : FilteringView, IConfigurable
+ {
+ private uint? max;
+
+ private uint? maxHeight;
+
+ private uint? maxWidth;
+
+ private uint? min;
+
+ private uint? minHeight;
+
+ private uint? minWidth;
+
+ public override string Description => "Show only items of a certain dimension";
+
+ public override string Name => "dimension";
+
+ public override bool Allowed(IMediaResource res)
+ {
+ var i = res as IMetaResolution;
+ if (i?.MetaWidth == null || !i.MetaHeight.HasValue)
+ {
+ return false;
+ }
+ var w = i.MetaWidth.Value;
+ var h = i.MetaHeight.Value;
+ if (min.HasValue && Math.Min(w, h) < min.Value)
+ {
+ return false;
+ }
+ if (max.HasValue && Math.Max(w, h) > max.Value)
+ {
+ return false;
+ }
+ if (minWidth.HasValue && w < minWidth.Value)
+ {
+ return false;
+ }
+ if (maxWidth.HasValue && w > maxWidth.Value)
+ {
+ return false;
+ }
+ if (minHeight.HasValue && h < minHeight.Value)
+ {
+ return false;
+ }
+ if (maxHeight.HasValue && h > maxHeight.Value)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public void SetParameters(ConfigParameters parameters)
+ {
+ if (parameters == null)
+ {
+ throw new ArgumentNullException(nameof(parameters));
+ }
+ min = parameters.MaybeGet("min");
+ max = parameters.MaybeGet("max");
+ minWidth = parameters.MaybeGet("minwidth");
+ maxWidth = parameters.MaybeGet("maxwidth");
+ minHeight = parameters.MaybeGet("minheight");
+ maxHeight = parameters.MaybeGet("maxheight");
+ }
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ if (!min.HasValue && !max.HasValue && !minWidth.HasValue &&
+ !maxWidth.HasValue && !minHeight.HasValue && !maxHeight.HasValue)
+ {
+ return oldRoot;
+ }
+ return base.Transform(oldRoot);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/DoubleKeyedVirtualFolder.cs b/Roadie.Dlna/Server/Views/DoubleKeyedVirtualFolder.cs
new file mode 100644
index 0000000..6f75bf4
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/DoubleKeyedVirtualFolder.cs
@@ -0,0 +1,15 @@
+namespace Roadie.Dlna.Server.Views
+{
+ internal class DoubleKeyedVirtualFolder
+ : KeyedVirtualFolder
+ {
+ public DoubleKeyedVirtualFolder()
+ {
+ }
+
+ public DoubleKeyedVirtualFolder(IMediaFolder aParent, string aName)
+ : base(aParent, aName)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/FilterView.cs b/Roadie.Dlna/Server/Views/FilterView.cs
new file mode 100644
index 0000000..90d270b
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/FilterView.cs
@@ -0,0 +1,69 @@
+using Roadie.Dlna.Utility;
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal class FilterView : FilteringView, IConfigurable
+ {
+ private static readonly string[] escapes = "\\.+|[]{}()$#^".ToArray().Select(c => new string(c, 1)).ToArray();
+ private Regex filter;
+
+ public override string Description => "Show only files matching a specific filter";
+
+ public override string Name => "filter";
+
+ public override bool Allowed(IMediaResource res)
+ {
+ if (res == null)
+ {
+ throw new ArgumentNullException(nameof(res));
+ }
+ if (filter == null)
+ {
+ return true;
+ }
+ return filter.IsMatch(res.Title) || filter.IsMatch(res.Path);
+ }
+
+ public void SetParameters(ConfigParameters parameters)
+ {
+ if (parameters == null)
+ {
+ throw new ArgumentNullException(nameof(parameters));
+ }
+
+ var filters = from f in parameters.Keys
+ let e = Escape(f)
+ select e;
+ filter = new Regex(
+ string.Join("|", filters),
+ RegexOptions.Compiled | RegexOptions.IgnoreCase
+ );
+ Trace.WriteLine($"Using filter { filter.ToString() }");
+ }
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ if (filter == null)
+ {
+ return oldRoot;
+ }
+ return base.Transform(oldRoot);
+ }
+
+ private static string Escape(string str)
+ {
+ str = escapes.Aggregate(str, (current, cs) => current.Replace(cs, "\\" + cs));
+ if (str.Contains('*') || str.Contains("?"))
+ {
+ str = $"^{str}$";
+ str = str.Replace("*", ".*");
+ str = str.Replace("?", ".");
+ }
+ return str;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/FilteringView.cs b/Roadie.Dlna/Server/Views/FilteringView.cs
new file mode 100644
index 0000000..b246f94
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/FilteringView.cs
@@ -0,0 +1,32 @@
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal abstract class FilteringView : BaseView, IFilteredView
+ {
+ public abstract bool Allowed(IMediaResource item);
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ oldRoot = new VirtualClonedFolder(oldRoot);
+ ProcessFolder(oldRoot);
+ return oldRoot;
+ }
+
+ private void ProcessFolder(IMediaFolder root)
+ {
+ foreach (var f in root.ChildFolders)
+ {
+ ProcessFolder(f);
+ }
+ foreach (var f in root.ChildItems.ToList())
+ {
+ if (Allowed(f))
+ {
+ continue;
+ }
+ root.RemoveResource(f);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/FlattenView.cs b/Roadie.Dlna/Server/Views/FlattenView.cs
new file mode 100644
index 0000000..a701617
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/FlattenView.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class FlattenView : BaseView
+ {
+ public override string Description => "Removes empty intermediate folders and flattens folders with only few files";
+
+ public override string Name => "flatten";
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ var r = new VirtualClonedFolder(oldRoot);
+ var cross = from f in r.ChildFolders
+ from t in r.ChildFolders
+ where f != t
+ orderby f.Title, t.Title
+ select new
+ {
+ f = f as VirtualFolder,
+ t = t as VirtualFolder
+ };
+ foreach (var c in cross)
+ {
+ MergeFolders(c.f, c.t);
+ }
+
+ TransformInternal(r, r);
+ MergeFolders(r, r);
+ return r;
+ }
+
+ private static bool TransformInternal(VirtualFolder root,
+ VirtualFolder current)
+ {
+ foreach (var f in current.ChildFolders.ToList())
+ {
+ var vf = f as VirtualFolder;
+ if (TransformInternal(root, vf))
+ {
+ current.ReleaseFolder(vf);
+ }
+ }
+
+ if (current == root || current.ChildItems.Count() > 3)
+ {
+ return false;
+ }
+ var newParent = (VirtualFolder)current.Parent;
+ foreach (var c in current.ChildItems.ToList())
+ {
+ current.RemoveResource(c);
+ newParent.AddResource(c);
+ }
+
+ if (current.ChildCount != 0)
+ {
+ MergeFolders(current, newParent);
+ foreach (var f in current.ChildFolders.ToList())
+ {
+ newParent.AdoptFolder(f);
+ }
+ foreach (var f in current.ChildItems.ToList())
+ {
+ current.RemoveResource(f);
+ newParent.AddResource(f);
+ }
+ }
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/IFilteredView.cs b/Roadie.Dlna/Server/Views/IFilteredView.cs
new file mode 100644
index 0000000..4885151
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/IFilteredView.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Server.Views
+{
+ public interface IFilteredView : IView
+ {
+ bool Allowed(IMediaResource item);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/IView.cs b/Roadie.Dlna/Server/Views/IView.cs
new file mode 100644
index 0000000..da607fb
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/IView.cs
@@ -0,0 +1,8 @@
+using Roadie.Dlna.Utility;
+namespace Roadie.Dlna.Server.Views
+{
+ public interface IView : IRepositoryItem
+ {
+ IMediaFolder Transform(IMediaFolder oldRoot);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/KeyedVirtualFolder.cs b/Roadie.Dlna/Server/Views/KeyedVirtualFolder.cs
new file mode 100644
index 0000000..c9c4c5b
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/KeyedVirtualFolder.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal class KeyedVirtualFolder : VirtualFolder where T : VirtualFolder, new()
+ {
+ private readonly Dictionary keys = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
+
+ protected KeyedVirtualFolder() : this(null, null)
+ {
+ }
+
+ protected KeyedVirtualFolder(IMediaFolder aParent, string aName) : base(aParent, aName)
+ {
+ }
+
+ public T GetFolder(string key)
+ {
+ T rv;
+ if (!keys.TryGetValue(key, out rv))
+ {
+ rv = new T
+ {
+ Name = key,
+ Parent = this
+ };
+ Folders.Add(rv);
+ keys.Add(key, rv);
+ }
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/LargeView.cs b/Roadie.Dlna/Server/Views/LargeView.cs
new file mode 100644
index 0000000..e559595
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/LargeView.cs
@@ -0,0 +1,39 @@
+using Roadie.Dlna.Server.Metadata;
+using Roadie.Dlna.Utility;
+using System;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal class LargeView : FilteringView, IConfigurable
+ {
+ private long minSize = 300 * 1024 * 1024;
+
+ public override string Description => "Show only large files";
+
+ public override string Name => "large";
+
+ public override bool Allowed(IMediaResource res)
+ {
+ var i = res as IMetaInfo;
+ if (i == null)
+ {
+ return false;
+ }
+ return i.InfoSize.HasValue && i.InfoSize.Value >= minSize;
+ }
+
+ public void SetParameters(ConfigParameters parameters)
+ {
+ if (parameters == null)
+ {
+ throw new ArgumentNullException(nameof(parameters));
+ }
+
+ long min;
+ if (parameters.TryGet("size", out min))
+ {
+ minSize = min * 1024 * 1024;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/MusicView.cs b/Roadie.Dlna/Server/Views/MusicView.cs
new file mode 100644
index 0000000..7a4238f
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/MusicView.cs
@@ -0,0 +1,114 @@
+using Roadie.Dlna.Utility;
+using System.Globalization;
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class MusicView : BaseView
+ {
+ public override string Description => "Reorganizes files into a proper music collection";
+
+ public override string Name => "music";
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ var root = new VirtualClonedFolder(oldRoot);
+ var artists = new TripleKeyedVirtualFolder(root, "Artists");
+ var performers = new TripleKeyedVirtualFolder(root, "Performers");
+ var albums = new DoubleKeyedVirtualFolder(root, "Albums");
+ var genres = new SimpleKeyedVirtualFolder(root, "Genre");
+ var folders = new VirtualFolder(root, "Folders");
+ SortFolder(root, artists, performers, albums, genres);
+ foreach (var f in root.ChildFolders.ToList())
+ {
+ folders.AdoptFolder(f);
+ }
+ root.AdoptFolder(artists);
+ root.AdoptFolder(performers);
+ root.AdoptFolder(albums);
+ root.AdoptFolder(genres);
+ root.AdoptFolder(folders);
+ return root;
+ }
+
+ private static void LinkTriple(TripleKeyedVirtualFolder folder, IMediaAudioResource r, string key1,string key2)
+ {
+ if (string.IsNullOrWhiteSpace(key1))
+ {
+ return;
+ }
+ if (string.IsNullOrWhiteSpace(key2))
+ {
+ return;
+ }
+ var targetFolder = folder
+ .GetFolder(key1.StemCompareBase().First().ToString().ToUpper(CultureInfo.CurrentUICulture))
+ .GetFolder(key1.StemNameBase());
+ targetFolder
+ .GetFolder(key2.StemNameBase())
+ .AddResource(r);
+ var allRes = new AlbumInTitleAudioResource(r);
+ targetFolder
+ .GetFolder("All Albums")
+ .AddResource(allRes);
+ }
+
+ private static void SortFolder(VirtualFolder folder, TripleKeyedVirtualFolder artists, TripleKeyedVirtualFolder performers,
+ DoubleKeyedVirtualFolder albums, SimpleKeyedVirtualFolder genres)
+ {
+ foreach (var f in folder.ChildFolders.ToList())
+ {
+ SortFolder(f as VirtualFolder, artists, performers, albums, genres);
+ }
+ foreach (var i in folder.ChildItems.ToList())
+ {
+ var ai = i as IMediaAudioResource;
+ if (ai == null)
+ {
+ continue;
+ }
+ var album = ai.MetaAlbum ?? "Unspecified album";
+ albums.GetFolder(album.StemCompareBase()
+ .First()
+ .ToString()
+ .ToUpper(CultureInfo.CurrentUICulture))
+ .GetFolder(album.StemNameBase())
+ .AddResource(i);
+ LinkTriple(artists, ai, ai.MetaArtist, album);
+ LinkTriple(performers, ai, ai.MetaPerformer, album);
+ var genre = ai.MetaGenre;
+ if (genre != null)
+ {
+ genres.GetFolder(genre.StemNameBase()).AddResource(i);
+ }
+ }
+ }
+
+ private class AlbumInTitleAudioResource : AudioResourceDecorator
+ {
+ public override string Title
+ {
+ get
+ {
+ var album = MetaAlbum;
+ if (!string.IsNullOrWhiteSpace(album))
+ {
+ return $"{album} — {base.Title}";
+ }
+ return base.Title;
+ }
+ }
+
+ public AlbumInTitleAudioResource(IMediaAudioResource resource) : base(resource)
+ {
+ }
+ }
+
+ private class TripleKeyedVirtualFolder : KeyedVirtualFolder
+ {
+ public TripleKeyedVirtualFolder(IMediaFolder aParent, string aName) : base(aParent, aName)
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/NewView.cs b/Roadie.Dlna/Server/Views/NewView.cs
new file mode 100644
index 0000000..16b68cb
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/NewView.cs
@@ -0,0 +1,42 @@
+using Roadie.Dlna.Server.Metadata;
+using Roadie.Dlna.Utility;
+using System;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal class NewView : FilteringView, IConfigurable
+ {
+ private DateTime minDate = DateTime.Now.AddDays(-7.0);
+
+ public override string Description => "Show only new files";
+
+ public override string Name => "new";
+
+ public override bool Allowed(IMediaResource res)
+ {
+ var i = res as IMetaInfo;
+ if (i == null)
+ {
+ return false;
+ }
+ return i.InfoDate >= minDate;
+ }
+
+ public void SetParameters(ConfigParameters parameters)
+ {
+ if (parameters == null)
+ {
+ throw new ArgumentNullException(nameof(parameters));
+ }
+
+ foreach (var v in parameters.GetValuesForKey("date"))
+ {
+ DateTime min;
+ if (DateTime.TryParse(v, out min))
+ {
+ minDate = min;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/PlainView.cs b/Roadie.Dlna/Server/Views/PlainView.cs
new file mode 100644
index 0000000..af4209e
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/PlainView.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Linq;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class PlainView : BaseView
+ {
+ public override string Description => "Mushes all files together into the root folder";
+
+ public override string Name => "plain";
+
+ public override IMediaFolder Transform(IMediaFolder oldRoot)
+ {
+ if (oldRoot == null)
+ {
+ throw new ArgumentNullException(nameof(oldRoot));
+ }
+ var rv = new VirtualFolder(null, oldRoot.Title, oldRoot.Id);
+ EatAll(rv, oldRoot);
+ return rv;
+ }
+
+ private static void EatAll(IMediaFolder root, IMediaFolder folder)
+ {
+ foreach (var f in folder.ChildFolders.ToList())
+ {
+ EatAll(root, f);
+ }
+ foreach (var c in folder.ChildItems.ToList())
+ {
+ root.AddResource(c);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/SeriesView.cs b/Roadie.Dlna/Server/Views/SeriesView.cs
new file mode 100644
index 0000000..33e88ec
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/SeriesView.cs
@@ -0,0 +1,52 @@
+using Roadie.Dlna.Utility;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class SeriesView : CascadedView
+ {
+ private static readonly Regex regSeries = new Regex(
+ @"^(.+?)(?:s\d+[\s_-]*e\d+|" + // S01E10
+ @"\d+[\s_-]*x[\s_-]*\d+|" + // 1x01
+ @"\b[\s-_]*(?:19|20|21)[0-9]{2}[\s._-](?:0[1-9]|1[012])[\s._-](?:0[1-9]|[12][0-9]|3[01])|" + // 2014.02.20
+ @"\b[\s-_]*(?:0[1-9]|[12][0-9]|3[01])[\s._-](?:0[1-9]|1[012])[\s._-](?:19|20|21)[0-9]{2}|" + // 20.02.2014 (sane)
+ @"\b[\s-_]*(?:0[1-9]|1[012])[\s._-](?:0[1-9]|[12][0-9]|3[01])[\s._-](?:19|20|21)[0-9]{2}|" + // 02.20.2014 (US)
+ @"\b[1-9](?:0[1-9]|[1-3]\d)\b)", // 101
+ RegexOptions.Compiled | RegexOptions.IgnoreCase
+ );
+
+ public override string Description => "Try to determine (TV) series from title and categorize accordingly";
+
+ public override string Name => "series";
+
+ protected override void SortFolder(IMediaFolder folder,
+ SimpleKeyedVirtualFolder series)
+ {
+ foreach (var f in folder.ChildFolders.ToList())
+ {
+ SortFolder(f, series);
+ }
+ foreach (var i in folder.ChildItems.ToList())
+ {
+ var title = i.Title;
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ continue;
+ }
+ var m = regSeries.Match(title);
+ if (!m.Success)
+ {
+ continue;
+ }
+ var ser = m.Groups[1].Value;
+ if (string.IsNullOrEmpty(ser))
+ {
+ continue;
+ }
+ series.GetFolder(ser.StemNameBase()).AddResource(i);
+ folder.RemoveResource(i);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/SimpleKeyedVirtualFolder.cs b/Roadie.Dlna/Server/Views/SimpleKeyedVirtualFolder.cs
new file mode 100644
index 0000000..85d0fc5
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/SimpleKeyedVirtualFolder.cs
@@ -0,0 +1,14 @@
+namespace Roadie.Dlna.Server.Views
+{
+ internal class SimpleKeyedVirtualFolder : KeyedVirtualFolder
+ {
+ public SimpleKeyedVirtualFolder()
+ {
+ }
+
+ public SimpleKeyedVirtualFolder(IMediaFolder aParent, string aName)
+ : base(aParent, aName)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/SiteView.cs b/Roadie.Dlna/Server/Views/SiteView.cs
new file mode 100644
index 0000000..fbb7aad
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/SiteView.cs
@@ -0,0 +1,71 @@
+using Roadie.Dlna.Utility;
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Server.Views
+{
+ internal sealed class SiteView : CascadedView
+ {
+ private static readonly Regex regNumberStrip = new Regex(@"\d+$", RegexOptions.Compiled);
+
+ private static readonly Regex regSites = new Regex(
+ @"^[\[\(](?.+?)[\]\)]|" +
+ @"^(?.+?)\s+-|" +
+ @"^(?.+?)[\[\]\(\)._-]|" +
+ @"^(?.+?)\s",
+ RegexOptions.Compiled
+ );
+
+ private static readonly Regex regWord = new Regex(@"\w", RegexOptions.Compiled);
+
+ public override string Description => "Try to determine websites from title and categorize accordingly";
+
+ public override string Name => "sites";
+
+ protected override void SortFolder(IMediaFolder folder,
+ SimpleKeyedVirtualFolder series)
+ {
+ foreach (var f in folder.ChildFolders.ToList())
+ {
+ SortFolder(f, series);
+ }
+ foreach (var i in folder.ChildItems.ToList())
+ {
+ try
+ {
+ var title = i.Title;
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ throw new Exception("No title");
+ }
+ var m = regSites.Match(title);
+ if (!m.Success)
+ {
+ throw new Exception("No match");
+ }
+ var site = m.Groups["site"].Value;
+ if (string.IsNullOrEmpty(site))
+ {
+ throw new Exception("No site");
+ }
+ site = site.Replace(" ", "").Replace("\t", "").Replace("-", "");
+ site = regNumberStrip.Replace(site, string.Empty).TrimEnd();
+ if (!regWord.IsMatch(site))
+ {
+ throw new Exception("Not a site");
+ }
+ folder.RemoveResource(i);
+ series.GetFolder(site.StemNameBase()).AddResource(i);
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"{ex.Message} - {i.Title}");
+ folder.RemoveResource(i);
+ series.AddResource(i);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Server/Views/ViewRepository.cs b/Roadie.Dlna/Server/Views/ViewRepository.cs
new file mode 100644
index 0000000..e42b303
--- /dev/null
+++ b/Roadie.Dlna/Server/Views/ViewRepository.cs
@@ -0,0 +1,7 @@
+using Roadie.Dlna.Utility;
+namespace Roadie.Dlna.Server.Views
+{
+ public sealed class ViewRepository : Repository
+ {
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/IThumbnail.cs b/Roadie.Dlna/Thumbnails/IThumbnail.cs
new file mode 100644
index 0000000..b9d3342
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/IThumbnail.cs
@@ -0,0 +1,11 @@
+namespace Roadie.Dlna.Thumbnails
+{
+ public interface IThumbnail
+ {
+ int Height { get; }
+
+ int Width { get; }
+
+ byte[] GetData();
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/IThumbnailLoader.cs b/Roadie.Dlna/Thumbnails/IThumbnailLoader.cs
new file mode 100644
index 0000000..00172dd
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/IThumbnailLoader.cs
@@ -0,0 +1,12 @@
+using Roadie.Dlna.Server;
+using System.IO;
+
+namespace Roadie.Dlna.Thumbnails
+{
+ internal interface IThumbnailLoader
+ {
+ DlnaMediaTypes Handling { get; }
+
+ MemoryStream GetThumbnail(object item, ref int width, ref int height);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/ImageThumbnailLoader.cs b/Roadie.Dlna/Thumbnails/ImageThumbnailLoader.cs
new file mode 100644
index 0000000..468757b
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/ImageThumbnailLoader.cs
@@ -0,0 +1,56 @@
+using Roadie.Dlna.Server;
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+
+namespace Roadie.Dlna.Thumbnails
+{
+ internal sealed class ImageThumbnailLoader : IThumbnailLoader
+ {
+ public DlnaMediaTypes Handling => DlnaMediaTypes.Image;
+
+ public MemoryStream GetThumbnail(object item, ref int width,
+ ref int height)
+ {
+ Image img;
+ var stream = item as Stream;
+ if (stream != null)
+ {
+ img = Image.FromStream(stream);
+ }
+ else
+ {
+ var fi = item as FileInfo;
+ if (fi != null)
+ {
+ img = Image.FromFile(fi.FullName);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+ using (img)
+ {
+ using (var scaled = ThumbnailMaker.ResizeImage(
+ img, width, height, ThumbnailMakerBorder.Borderless))
+ {
+ width = scaled.Width;
+ height = scaled.Height;
+ var rv = new MemoryStream();
+ try
+ {
+ scaled.Save(rv, ImageFormat.Jpeg);
+ return rv;
+ }
+ catch (Exception)
+ {
+ rv.Dispose();
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/Thumbnail.cs b/Roadie.Dlna/Thumbnails/Thumbnail.cs
new file mode 100644
index 0000000..57b1669
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/Thumbnail.cs
@@ -0,0 +1,23 @@
+namespace Roadie.Dlna.Thumbnails
+{
+ internal sealed class Thumbnail : IThumbnail
+ {
+ private readonly byte[] data;
+
+ public int Height { get; }
+
+ public int Width { get; }
+
+ internal Thumbnail(int width, int height, byte[] data)
+ {
+ Width = width;
+ Height = height;
+ this.data = data;
+ }
+
+ public byte[] GetData()
+ {
+ return data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/ThumbnailMaker.cs b/Roadie.Dlna/Thumbnails/ThumbnailMaker.cs
new file mode 100644
index 0000000..3d8fef3
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/ThumbnailMaker.cs
@@ -0,0 +1,208 @@
+using Roadie.Dlna.Server;
+using Roadie.Dlna.Utility;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace Roadie.Dlna.Thumbnails
+{
+ public sealed class ThumbnailMaker
+ {
+ private static readonly LeastRecentlyUsedDictionary cache = new LeastRecentlyUsedDictionary(1 << 11);
+
+ private static readonly Dictionary> thumbers = BuildThumbnailers();
+
+ public IThumbnail GetThumbnail(FileSystemInfo file, int width, int height)
+ {
+ if (file == null)
+ {
+ throw new ArgumentNullException(nameof(file));
+ }
+ var ext = file.Extension.ToUpperInvariant().Substring(1);
+ var mediaType = DlnaMaps.Ext2Media[ext];
+
+ var key = file.FullName;
+ byte[] rv;
+ if (GetThumbnailFromCache(ref key, ref width, ref height, out rv))
+ {
+ return new Thumbnail(width, height, rv);
+ }
+
+ rv = GetThumbnailInternal(key, file, mediaType, ref width, ref height);
+ return new Thumbnail(width, height, rv);
+ }
+
+ public IThumbnail GetThumbnail(string key, DlnaMediaTypes type,
+ Stream stream, int width, int height)
+ {
+ byte[] rv;
+ if (GetThumbnailFromCache(ref key, ref width, ref height, out rv))
+ {
+ return new Thumbnail(width, height, rv);
+ }
+ rv = GetThumbnailInternal(key, stream, type, ref width, ref height);
+ return new Thumbnail(width, height, rv);
+ }
+
+ internal static Image ResizeImage(Image image, int width, int height,
+ ThumbnailMakerBorder border)
+ {
+ var nw = (float)image.Width;
+ var nh = (float)image.Height;
+ if (nw > width)
+ {
+ nh = width * nh / nw;
+ nw = width;
+ }
+ if (nh > height)
+ {
+ nw = height * nw / nh;
+ nh = height;
+ }
+
+ var result = new Bitmap(
+ border == ThumbnailMakerBorder.Bordered ? width : (int)nw,
+ border == ThumbnailMakerBorder.Bordered ? height : (int)nh
+ );
+ try
+ {
+ try
+ {
+ result.SetResolution(image.HorizontalResolution, image.VerticalResolution);
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to set resolution Ex [{ ex }]");
+ }
+ using (var graphics = Graphics.FromImage(result))
+ {
+ if (result.Width > image.Width && result.Height > image.Height)
+ {
+ graphics.CompositingQuality =
+ CompositingQuality.HighQuality;
+ graphics.InterpolationMode =
+ InterpolationMode.High;
+ }
+ else
+ {
+ graphics.CompositingQuality =
+ CompositingQuality.HighSpeed;
+ graphics.InterpolationMode = InterpolationMode.Bicubic;
+ }
+ var rect = new Rectangle(
+ (int)(result.Width - nw) / 2,
+ (int)(result.Height - nh) / 2,
+ (int)nw, (int)nh
+ );
+ graphics.SmoothingMode = SmoothingMode.HighSpeed;
+ graphics.FillRectangle(
+ Brushes.Black, new Rectangle(0, 0, result.Width, result.Height));
+ graphics.DrawImage(image, rect);
+ }
+ return result;
+ }
+ catch (Exception)
+ {
+ result.Dispose();
+ throw;
+ }
+ }
+
+ private static Dictionary> BuildThumbnailers()
+ {
+ var types = Enum.GetValues(typeof(DlnaMediaTypes));
+ var buildThumbnailers = types.Cast().ToDictionary(i => i, i => new List());
+ var a = Assembly.GetExecutingAssembly();
+ foreach (var t in a.GetTypes())
+ {
+ if (t.GetInterface("IThumbnailLoader") == null)
+ {
+ continue;
+ }
+ var ctor = t.GetConstructor(new Type[] { });
+ var thumber = ctor?.Invoke(new object[] { }) as IThumbnailLoader;
+ if (thumber == null)
+ {
+ continue;
+ }
+ foreach (DlnaMediaTypes i in types)
+ {
+ if (thumber.Handling.HasFlag(i))
+ {
+ buildThumbnailers[i].Add(thumber);
+ }
+ }
+ }
+ return buildThumbnailers;
+ }
+
+ private static bool GetThumbnailFromCache(ref string key, ref int width,
+ ref int height, out byte[] rv)
+ {
+ key = $"{width}x{height} {key}";
+ lock (cache)
+ {
+ CacheItem ci;
+ if (cache.TryGetValue(key, out ci))
+ {
+ rv = ci.Data;
+ width = ci.Width;
+ height = ci.Height;
+ return true;
+ }
+ }
+ rv = null;
+ return false;
+ }
+
+ private byte[] GetThumbnailInternal(string key, object item,
+ DlnaMediaTypes type, ref int width,
+ ref int height)
+ {
+ var thumbnailers = thumbers[type];
+ var rw = width;
+ var rh = height;
+ foreach (var thumber in thumbnailers)
+ {
+ try
+ {
+ using (var i = thumber.GetThumbnail(item, ref width, ref height))
+ {
+ var rv = i.ToArray();
+ lock (cache)
+ {
+ cache[key] = new CacheItem(rv, rw, rh);
+ }
+ return rv;
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"{thumber.GetType()} failed to thumbnail a resource Ex [{ ex }]");
+ }
+ }
+ throw new ArgumentException("Not a supported resource");
+ }
+
+ private struct CacheItem
+ {
+ public readonly byte[] Data;
+
+ public readonly int Height;
+
+ public readonly int Width;
+
+ public CacheItem(byte[] aData, int aWidth, int aHeight)
+ {
+ Data = aData;
+ Width = aWidth;
+ Height = aHeight;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Thumbnails/ThumbnailMakerBorder.cs b/Roadie.Dlna/Thumbnails/ThumbnailMakerBorder.cs
new file mode 100644
index 0000000..6e09375
--- /dev/null
+++ b/Roadie.Dlna/Thumbnails/ThumbnailMakerBorder.cs
@@ -0,0 +1,8 @@
+namespace Roadie.Dlna.Thumbnails
+{
+ internal enum ThumbnailMakerBorder
+ {
+ Bordered,
+ Borderless
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/AttributeCollection.cs b/Roadie.Dlna/Utility/AttributeCollection.cs
new file mode 100644
index 0000000..a7a03cb
--- /dev/null
+++ b/Roadie.Dlna/Utility/AttributeCollection.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Roadie.Dlna.Utility
+{
+ using Attribute = KeyValuePair;
+
+ public class AttributeCollection : IEnumerable
+ {
+ private readonly IList list = new List();
+
+ public int Count => list.Count;
+
+ public ICollection Keys => (from i in list
+ select i.Key).ToList();
+
+ public ICollection Values => (from i in list
+ select i.Value).ToList();
+
+ public void Add(Attribute item)
+ {
+ list.Add(item);
+ }
+
+ public void Add(string key, string value)
+ {
+ list.Add(new Attribute(key, value));
+ }
+
+ public void Clear()
+ {
+ list.Clear();
+ }
+
+ public bool Contains(Attribute item)
+ {
+ return list.Contains(item);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return list.GetEnumerator();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return list.GetEnumerator();
+ }
+
+ public IEnumerable GetValuesForKey(string key)
+ {
+ return GetValuesForKey(key, StringComparer.CurrentCultureIgnoreCase);
+ }
+
+ public IEnumerable GetValuesForKey(string key, StringComparer comparer)
+ {
+ return from i in list
+ where comparer.Equals(i.Key, key)
+ select i.Value;
+ }
+
+ public bool Has(string key)
+ {
+ return Has(key, StringComparer.CurrentCultureIgnoreCase);
+ }
+
+ public bool Has(string key, StringComparer comparer)
+ {
+ return list.Any(e => comparer.Equals(key, e.Key));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/BaseSortPart.cs b/Roadie.Dlna/Utility/BaseSortPart.cs
new file mode 100644
index 0000000..3169357
--- /dev/null
+++ b/Roadie.Dlna/Utility/BaseSortPart.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace Roadie.Dlna.Utility
+{
+ internal abstract class BaseSortPart : IComparable
+ {
+ private readonly Type type;
+
+ protected BaseSortPart()
+ {
+ type = GetType();
+ }
+
+ public int CompareTo(BaseSortPart other)
+ {
+ if (other == null)
+ {
+ return 1;
+ }
+ if (type != other.type)
+ {
+ if (type == typeof(StringSortPart))
+ {
+ return 1;
+ }
+ return -1;
+ }
+ var sp = other as StringSortPart;
+ if (sp != null)
+ {
+ return ((StringSortPart)this).CompareTo(sp);
+ }
+ return ((NumericSortPart)this).CompareTo(
+ (NumericSortPart)other);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/ConcatenatedStream.cs b/Roadie.Dlna/Utility/ConcatenatedStream.cs
new file mode 100644
index 0000000..40238a5
--- /dev/null
+++ b/Roadie.Dlna/Utility/ConcatenatedStream.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Roadie.Dlna.Utility
+{
+ public sealed class ConcatenatedStream : Stream
+ {
+ private readonly Queue streams = new Queue();
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override long Length
+ {
+ get { throw new NotSupportedException(); }
+ }
+
+ public override long Position
+ {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+
+ public void AddStream(Stream stream)
+ {
+ streams.Enqueue(stream);
+ }
+
+ public override void Close()
+ {
+ foreach (var stream in streams)
+ {
+ stream.Close();
+ stream.Dispose();
+ }
+ streams.Clear();
+ base.Close();
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (streams.Count == 0)
+ {
+ return 0;
+ }
+
+ var read = streams.Peek().Read(buffer, offset, count);
+ if (read < count)
+ {
+ var sndRead = streams.Peek().Read(buffer, offset + read, count - read);
+ if (sndRead <= 0)
+ {
+ streams.Dequeue().Dispose();
+ return read + Read(buffer, offset + read, count - read);
+ }
+ read += sndRead;
+ }
+ return read;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/ConfigParameters.cs b/Roadie.Dlna/Utility/ConfigParameters.cs
new file mode 100644
index 0000000..8b458f2
--- /dev/null
+++ b/Roadie.Dlna/Utility/ConfigParameters.cs
@@ -0,0 +1,105 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Roadie.Dlna.Utility
+{
+ public class ConfigParameters : AttributeCollection
+ {
+ public ConfigParameters()
+ {
+ }
+
+ public ConfigParameters(string parameters)
+ {
+ foreach (var valuesplit in parameters.Split(',').Select(p => p.Split(new[] { '=' }, 2)))
+ {
+ Add(valuesplit[0], valuesplit.Length == 2 ? valuesplit[1] : null);
+ }
+ }
+
+ public TValue Get(string key, TValue defaultValue) where TValue : struct
+ {
+ return Get(key, defaultValue, StringComparer.CurrentCultureIgnoreCase);
+ }
+
+ public TValue Get(string key, TValue defaultValue, StringComparer comparer)
+ where TValue : struct
+ {
+ TValue rv;
+ return TryGet(key, out rv, comparer) ? rv : defaultValue;
+ }
+
+ public TValue? MaybeGet(string key) where TValue : struct
+ {
+ return MaybeGet(key, StringComparer.CurrentCultureIgnoreCase);
+ }
+
+ public TValue? MaybeGet(string key, StringComparer comparer) where TValue : struct
+ {
+ TValue? rv = null;
+ TValue attempt;
+ if (TryGet(key, out attempt, comparer))
+ {
+ rv = attempt;
+ }
+ return rv;
+ }
+
+ public bool TryGet(string key, out TValue rv) where TValue : struct
+ {
+ return TryGet(key, out rv, StringComparer.CurrentCultureIgnoreCase);
+ }
+
+ public bool TryGet(string key, out TValue rv, StringComparer comparer) where TValue : struct
+ {
+ rv = new TValue();
+ var convertible = rv as IConvertible;
+ if (convertible == null)
+ {
+ throw new NotSupportedException("Not convertible");
+ }
+ switch (convertible.GetTypeCode())
+ {
+ case TypeCode.Boolean:
+ foreach (var val in GetValuesForKey(key, comparer))
+ {
+ try
+ {
+ rv = (TValue)(object)Formatting.Booley(val);
+ return true;
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ break;
+
+ case TypeCode.Object:
+ throw new NotSupportedException("Non pod types are not supported");
+ default:
+ var conv = TypeDescriptor.GetConverter(typeof(TValue));
+ foreach (var val in GetValuesForKey(key, comparer))
+ {
+ try
+ {
+ var converted = conv.ConvertFromString(val);
+ if (converted == null)
+ {
+ continue;
+ }
+ rv = (TValue)converted;
+ return true;
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ break;
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/Ffmpeg.cs b/Roadie.Dlna/Utility/Ffmpeg.cs
new file mode 100644
index 0000000..d76adb8
--- /dev/null
+++ b/Roadie.Dlna/Utility/Ffmpeg.cs
@@ -0,0 +1,343 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Utility
+{
+ using InfoCache = LeastRecentlyUsedDictionary>;
+
+ public static class FFmpeg
+ {
+ private static readonly DirectoryInfo[] specialLocations =
+ {
+ GetFFMpegFolder(Environment.SpecialFolder.CommonProgramFiles),
+ GetFFMpegFolder(Environment.SpecialFolder.CommonProgramFilesX86),
+ GetFFMpegFolder(Environment.SpecialFolder.ProgramFiles),
+ GetFFMpegFolder(Environment.SpecialFolder.ProgramFilesX86),
+ GetFFMpegFolder(Environment.SpecialFolder.UserProfile),
+ new DirectoryInfo(Environment.GetFolderPath(
+ Environment.SpecialFolder.UserProfile))
+ };
+
+ private static readonly InfoCache infoCache = new InfoCache(500);
+
+ private static readonly Regex regAssStrip = new Regex(@"^,+", RegexOptions.Compiled);
+
+ private static readonly Regex regDuration = new Regex(@"Duration: ([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?", RegexOptions.Compiled);
+
+ private static readonly Regex regDimensions = new Regex(@"Video: .+ ([0-9]{2,})x([0-9]{2,}) ", RegexOptions.Compiled);
+
+ public static readonly string FFmpegExecutable = FindExecutable("ffmpeg");
+
+ private static DirectoryInfo GetFFMpegFolder(Environment.SpecialFolder folder)
+ {
+ return new DirectoryInfo(
+ Path.Combine(Environment.GetFolderPath(folder), "ffmpeg"));
+ }
+
+ private static string FindExecutable(string executable)
+ {
+ var os = Environment.OSVersion.Platform.ToString().ToUpperInvariant();
+ var isWin = os.Contains("WIN");
+ if (isWin)
+ {
+ executable += ".exe";
+ }
+ var places = new List();
+ var assemblyLoc = Assembly.GetExecutingAssembly().Location;
+ if (assemblyLoc != null)
+ {
+ places.Add(new FileInfo(assemblyLoc).Directory);
+ }
+ try
+ {
+ var ffhome = @"C:\tools\ffmpeg"; // Environment.GetEnvironmentVariable("FFMPEG_HOME");
+ if (!string.IsNullOrWhiteSpace(ffhome))
+ {
+ places.Add(new DirectoryInfo(ffhome));
+ }
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ foreach (var l in specialLocations)
+ {
+ try
+ {
+ places.Add(l);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ var envpath = Environment.GetEnvironmentVariable("PATH");
+ if (!string.IsNullOrWhiteSpace(envpath))
+ {
+ foreach (var p in envpath.
+ Split(isWin ? ';' : ':'))
+ {
+ try
+ {
+ places.Add(new DirectoryInfo(p.Trim()));
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ }
+
+ foreach (var i in places)
+ {
+ Trace.WriteLine($"Searching {i.FullName}");
+ if (!i.Exists)
+ {
+ continue;
+ }
+ var folders = new[]
+ {
+ i,
+ new DirectoryInfo(Path.Combine(i.FullName, "bin"))
+ };
+ foreach (var di in folders)
+ {
+ try
+ {
+ var r = di.GetFiles(executable, SearchOption.TopDirectoryOnly);
+ if (r.Length != 0)
+ {
+ var rv = r[0];
+ Trace.WriteLine($"Found {executable} at {rv.FullName}");
+ return rv.FullName;
+ }
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ }
+ Trace.WriteLine($"Did not find {executable}");
+ return null;
+ }
+
+ private static IDictionary IdentifyFileInternal(
+ FileInfo file)
+ {
+ if (FFmpegExecutable == null)
+ {
+ throw new NotSupportedException();
+ }
+ if (file == null)
+ {
+ throw new ArgumentNullException(nameof(file));
+ }
+ IDictionary rv;
+ if (infoCache.TryGetValue(file, out rv))
+ {
+ return rv;
+ }
+ try
+ {
+ return IdentifyInternalFromProcess(file);
+ }
+ catch (Exception ex)
+ {
+ throw new NotSupportedException(ex.Message, ex);
+ }
+ }
+
+ private static IDictionary IdentifyInternalFromProcess(
+ FileInfo file)
+ {
+ using (var p = new Process())
+ {
+ var sti = p.StartInfo;
+#if !DEBUG
+ sti.CreateNoWindow = true;
+#endif
+ sti.UseShellExecute = false;
+ sti.FileName = FFmpegExecutable;
+ sti.Arguments = $"-i \"{file.FullName}\"";
+ sti.LoadUserProfile = false;
+ sti.RedirectStandardError = true;
+ p.Start();
+ IDictionary rv = new Dictionary();
+
+ using (var reader = new StreamReader(StreamManager.GetStream()))
+ {
+ using (var pump = new StreamPump(
+ p.StandardError.BaseStream, reader.BaseStream, 4096))
+ {
+ pump.Pump(null);
+ if (!p.WaitForExit(3000))
+ {
+ throw new NotSupportedException("ffmpeg timed out");
+ }
+ if (!pump.Wait(1000))
+ {
+ throw new NotSupportedException("ffmpeg pump timed out");
+ }
+ reader.BaseStream.Seek(0, SeekOrigin.Begin);
+
+ var output = reader.ReadToEnd();
+ var match = regDuration.Match(output);
+ if (match.Success)
+ {
+ int h, m, s;
+ if (int.TryParse(match.Groups[1].Value, out h) &&
+ int.TryParse(match.Groups[2].Value, out m) &&
+ int.TryParse(match.Groups[3].Value, out s))
+ {
+ int ms;
+ if (match.Groups.Count < 5 ||
+ !int.TryParse(match.Groups[4].Value, out ms))
+ {
+ ms = 0;
+ }
+ var ts = new TimeSpan(0, h, m, s, ms * 10);
+ var tss = ts.TotalSeconds.ToString(
+ CultureInfo.InvariantCulture);
+ rv.Add("LENGTH", tss);
+ }
+ }
+ match = regDimensions.Match(output);
+ if (match.Success)
+ {
+ int w, h;
+ if (int.TryParse(match.Groups[1].Value, out w) &&
+ int.TryParse(match.Groups[2].Value, out h))
+ {
+ rv.Add("VIDEO_WIDTH", w.ToString());
+ rv.Add("VIDEO_HEIGHT", h.ToString());
+ }
+ }
+ }
+ }
+ if (rv.Count == 0)
+ {
+ throw new NotSupportedException("File not supported");
+ }
+ return rv;
+ }
+ }
+
+ public static Size GetFileDimensions(FileInfo file)
+ {
+ string sw, sh;
+ int w, h;
+ if (IdentifyFile(file).TryGetValue("VIDEO_WIDTH", out sw)
+ && IdentifyFile(file).TryGetValue("VIDEO_HEIGHT", out sh)
+ && int.TryParse(sw, out w)
+ && int.TryParse(sh, out h)
+ && w > 0 && h > 0)
+ {
+ return new Size(w, h);
+ }
+ throw new NotSupportedException();
+ }
+
+ public static double GetFileDuration(FileInfo file)
+ {
+ string sl;
+ if (IdentifyFile(file).TryGetValue("LENGTH", out sl))
+ {
+ double dur;
+ var valid = double.TryParse(
+ sl, NumberStyles.AllowDecimalPoint,
+ CultureInfo.GetCultureInfo("en-US", "en"), out dur);
+ if (valid && dur > 0)
+ {
+ return dur;
+ }
+ }
+ throw new NotSupportedException();
+ }
+
+ public static string GetSubtitleSubrip(FileInfo file)
+ {
+ if (FFmpegExecutable == null)
+ {
+ throw new NotSupportedException();
+ }
+ if (file == null)
+ {
+ throw new ArgumentNullException(nameof(file));
+ }
+ try
+ {
+ using (var p = new Process())
+ {
+ var sti = p.StartInfo;
+#if !DEBUG
+ sti.CreateNoWindow = true;
+#endif
+ sti.UseShellExecute = false;
+ sti.FileName = FFmpegExecutable;
+ sti.Arguments = $"-i \"{file.FullName}\" -map s:0 -f srt pipe:";
+ sti.LoadUserProfile = false;
+ sti.RedirectStandardOutput = true;
+ p.Start();
+
+ var lastPosition = 0L;
+ using (var reader = new StreamReader(StreamManager.GetStream()))
+ {
+ using (var pump = new StreamPump(
+ p.StandardOutput.BaseStream, reader.BaseStream, 100))
+ {
+ pump.Pump(null);
+ while (!p.WaitForExit(20000))
+ {
+ if (lastPosition != reader.BaseStream.Position)
+ {
+ lastPosition = reader.BaseStream.Position;
+ continue;
+ }
+ p.Kill();
+ throw new NotSupportedException("ffmpeg timed out");
+ }
+ if (!pump.Wait(2000))
+ {
+ throw new NotSupportedException("ffmpeg pump timed out");
+ }
+ reader.BaseStream.Seek(0, SeekOrigin.Begin);
+
+ var rv = string.Empty;
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ rv += regAssStrip.Replace(line.Trim(), string.Empty) + "\n";
+ }
+ if (!string.IsNullOrWhiteSpace(rv))
+ {
+ return rv;
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new NotSupportedException(ex.Message, ex);
+ }
+ throw new NotSupportedException(
+ "File does not contain a valid subtitle");
+ }
+
+ public static IDictionary IdentifyFile(FileInfo file)
+ {
+ if (FFmpegExecutable != null)
+ {
+ return IdentifyFileInternal(file);
+ }
+ throw new NotSupportedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/Formatting.cs b/Roadie.Dlna/Utility/Formatting.cs
new file mode 100644
index 0000000..c553461
--- /dev/null
+++ b/Roadie.Dlna/Utility/Formatting.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class Formatting
+ {
+ private static readonly Regex respace =
+ new Regex(@"[.+]+", RegexOptions.Compiled);
+
+ private static readonly Regex sanitizer = new Regex(
+ @"\b(?:the|an?|ein(?:e[rs]?)?|der|die|das)\b",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ );
+
+ private static readonly Regex trim = new Regex(
+ @"\s+|^[._+)}\]-]+|[._+({\[-]+$",
+ RegexOptions.Compiled
+ );
+
+ private static readonly Regex trimmore =
+ new Regex(@"^[^\d\w]+|[^\d\w]+$", RegexOptions.Compiled);
+
+ public static bool Booley(string maybeBoolean)
+ {
+ if (maybeBoolean == null)
+ {
+ throw new ArgumentNullException(nameof(maybeBoolean));
+ }
+ maybeBoolean = maybeBoolean.Trim();
+ var sc = StringComparer.CurrentCultureIgnoreCase;
+ return sc.Equals("yes", maybeBoolean) || sc.Equals("1", maybeBoolean) || sc.Equals("true", maybeBoolean);
+ }
+
+ public static string FormatFileSize(this long size)
+ {
+ if (size < 900)
+ {
+ return $"{size} B";
+ }
+ var ds = size / 1024.0;
+ if (ds < 900)
+ {
+ return $"{ds:F2} KB";
+ }
+ ds /= 1024.0;
+ if (ds < 900)
+ {
+ return $"{ds:F2} MB";
+ }
+ ds /= 1024.0;
+ if (ds < 900)
+ {
+ return $"{ds:F3} GB";
+ }
+ ds /= 1024.0;
+ if (ds < 900)
+ {
+ return $"{ds:F3} TB";
+ }
+ ds /= 1024.0;
+ return $"{ds:F4} PB";
+ }
+
+ public static string GetSystemName()
+ {
+ return System.Environment.MachineName;
+ }
+
+ public static string StemCompareBase(this string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var san = trimmore.Replace(
+ sanitizer.Replace(name, string.Empty),
+ string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(san))
+ {
+ return name;
+ }
+ return san.StemNameBase();
+ }
+
+ public static string StemNameBase(this string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (!name.Contains(" "))
+ {
+ name = name.Replace('_', ' ');
+ if (!name.Contains(" "))
+ {
+ name = name.Replace('-', ' ');
+ }
+ name = respace.Replace(name, " ");
+ }
+ var ws = name;
+ string wsprev;
+ do
+ {
+ wsprev = ws;
+ ws = trim.Replace(wsprev.Trim(), " ").Trim();
+ } while (wsprev != ws);
+ if (string.IsNullOrWhiteSpace(ws))
+ {
+ return name;
+ }
+ return ws;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/HttpMethod.cs b/Roadie.Dlna/Utility/HttpMethod.cs
new file mode 100644
index 0000000..808bfd3
--- /dev/null
+++ b/Roadie.Dlna/Utility/HttpMethod.cs
@@ -0,0 +1,8 @@
+namespace Roadie.Dlna.Utility
+{
+ public enum HttpMethod
+ {
+ GET,
+ HEAD
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/HttpStream.cs b/Roadie.Dlna/Utility/HttpStream.cs
new file mode 100644
index 0000000..29bfa2b
--- /dev/null
+++ b/Roadie.Dlna/Utility/HttpStream.cs
@@ -0,0 +1,312 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.IO;
+using System.Net;
+using System.Reflection;
+
+namespace Roadie.Dlna.Utility
+{
+ public class HttpStream : Stream, IDisposable
+ {
+ public static readonly string UserAgent = GenerateUserAgent();
+ private const int BUFFER_SIZE = 1 << 10;
+
+ private const long SMALL_SEEK = 1 << 9;
+
+ private const int TIMEOUT = 30000;
+ private readonly Uri Referrer;
+
+ private readonly Uri StreamUri;
+
+ private Stream bufferedStream;
+
+ private long? length;
+
+ private long position;
+
+ private HttpWebRequest request;
+
+ private HttpWebResponse response;
+
+ private Stream responseStream;
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek
+ {
+ get
+ {
+ if (Length <= 0)
+ {
+ return false;
+ }
+
+ EnsureResponse();
+ var ranges = response.Headers.Get("Accept-Ranges");
+ return string.IsNullOrEmpty(ranges)
+ || !string.Equals(ranges, "none", StringComparison.InvariantCultureIgnoreCase);
+ }
+ }
+
+ public override bool CanTimeout => true;
+ public override bool CanWrite => false;
+
+ public string ContentType
+ {
+ get
+ {
+ EnsureResponse();
+ return response.ContentType;
+ }
+ }
+
+ public DateTime LastModified
+ {
+ get
+ {
+ EnsureResponse();
+ return response.LastModified;
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ if (!length.HasValue)
+ {
+ OpenAt(0, HttpMethod.HEAD);
+ length = response.ContentLength;
+ }
+ if (length.Value < 0)
+ {
+ throw new IOException("Stream does not feature a length");
+ }
+ return length.Value;
+ }
+ }
+
+ public override long Position
+ {
+ get { return position; }
+ set { Seek(value, SeekOrigin.Begin); }
+ }
+
+ public Uri Uri => new Uri(StreamUri.ToString());
+ private ILogger Logger { get; }
+
+ public HttpStream(ILogger logger, Uri uri, Uri referrer)
+ {
+ if (uri == null)
+ {
+ throw new ArgumentNullException(nameof(uri));
+ }
+ StreamUri = uri;
+ Referrer = referrer;
+ Logger = logger;
+ }
+
+ public override void Close()
+ {
+ bufferedStream?.Close();
+ responseStream?.Close();
+ response?.Close();
+ base.Close();
+ }
+
+ public new void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public override void Flush()
+ {
+ Dispose(true);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ try
+ {
+ if (responseStream == null)
+ {
+ OpenAt(position, HttpMethod.GET);
+ }
+ var read = bufferedStream.Read(buffer, offset, count);
+ if (read > 0)
+ {
+ position += read;
+ }
+ return read;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to read", ex);
+ throw;
+ }
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ Logger.LogDebug("Seek to {0}, {1} requested", offset, origin);
+ var np = 0L;
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ np = offset;
+ break;
+
+ case SeekOrigin.Current:
+ np = position + offset;
+ break;
+
+ case SeekOrigin.End:
+ np = Length + np;
+ break;
+ }
+ if (np < 0 || np >= Length)
+ {
+ throw new IOException("Invalid seek; out of stream bounds");
+ }
+ var off = position - np;
+ if (off == 0)
+ {
+ Logger.LogDebug("No seek required");
+ }
+ else
+ {
+ if (response != null && off > 0 && off < SMALL_SEEK)
+ {
+ var buf = new byte[off];
+ bufferedStream.Read(buf, 0, (int)off);
+ Logger.LogDebug("Did a small seek of {0}", off);
+ }
+ else
+ {
+ OpenAt(np, HttpMethod.GET);
+ Logger.LogDebug("Did a long seek of {0}", off);
+ }
+ }
+ position = np;
+ Logger.LogDebug("Successfully sought to {0}", position);
+ return position;
+ }
+
+ public override void SetLength(long value)
+ {
+ length = value;
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (bufferedStream != null)
+ {
+ bufferedStream.Dispose();
+ bufferedStream = null;
+ }
+ if (responseStream != null)
+ {
+ responseStream.Dispose();
+ responseStream = null;
+ }
+ response = null;
+ request = null;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ protected void OpenAt(long offset, HttpMethod method)
+ {
+ if (offset < 0)
+ {
+ throw new IOException("Position cannot be negative");
+ }
+ if (offset > 0 && method == HttpMethod.HEAD)
+ {
+ throw new ArgumentException(
+ "Cannot use a position (seek) with HEAD request");
+ }
+ Close();
+ Dispose();
+
+ request = (HttpWebRequest)WebRequest.Create(Uri);
+ request.Method = method.ToString();
+ if (Referrer != null)
+ {
+ request.Referer = Referrer.ToString();
+ }
+ request.AllowAutoRedirect = true;
+ request.Timeout = TIMEOUT * 1000;
+ request.UserAgent = UserAgent;
+ if (offset > 0)
+ {
+ request.AddRange(offset);
+ }
+ response = (HttpWebResponse)request.GetResponse();
+ if (method != HttpMethod.HEAD)
+ {
+ responseStream = response.GetResponseStream();
+ if (responseStream == null)
+ {
+ throw new IOException("Didn't get a response stream");
+ }
+ bufferedStream = new BufferedStream(responseStream, BUFFER_SIZE);
+ }
+ if (offset > 0 && response.StatusCode != HttpStatusCode.PartialContent)
+ {
+ throw new IOException(
+ "Failed to open the http stream at a specific position");
+ }
+ if (offset == 0 && response.StatusCode != HttpStatusCode.OK)
+ {
+ throw new IOException("Failed to open the http stream");
+ }
+ Logger.LogInformation("Opened {0} {1} at {2}", method, Uri, offset);
+ }
+
+ private static string GenerateUserAgent()
+ {
+ var os = Environment.OSVersion;
+ string pstring;
+ switch (os.Platform)
+ {
+ case PlatformID.Win32NT:
+ case PlatformID.Win32S:
+ case PlatformID.Win32Windows:
+ pstring = "WIN";
+ break;
+
+ default:
+ pstring = "Unix";
+ break;
+ }
+ return string.Format(
+ "roadie/{4}.{5} ({0}{1} {2}.{3}) like curl/7.3 like wget/1.0",
+ pstring,
+ IntPtr.Size * 8,
+ os.Version.Major,
+ os.Version.Minor,
+ Assembly.GetExecutingAssembly().GetName().Version.Major,
+ Assembly.GetExecutingAssembly().GetName().Version.Minor
+ );
+ }
+
+ private void EnsureResponse()
+ {
+ if (response != null)
+ {
+ return;
+ }
+ OpenAt(0, HttpMethod.HEAD);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/IConfigurable.cs b/Roadie.Dlna/Utility/IConfigurable.cs
new file mode 100644
index 0000000..b23b69a
--- /dev/null
+++ b/Roadie.Dlna/Utility/IConfigurable.cs
@@ -0,0 +1,7 @@
+namespace Roadie.Dlna.Utility
+{
+ public interface IConfigurable
+ {
+ void SetParameters(ConfigParameters parameters);
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/IP.cs b/Roadie.Dlna/Utility/IP.cs
new file mode 100644
index 0000000..9b24708
--- /dev/null
+++ b/Roadie.Dlna/Utility/IP.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class IP
+ {
+ private static bool warned;
+
+ public static IEnumerable AllIPAddresses
+ {
+ get
+ {
+ try
+ {
+ return GetIPsDefault().ToArray();
+ }
+ catch (Exception ex)
+ {
+ if (!warned)
+ {
+ Trace.WriteLine($"Failed to retrieve IP addresses the usual way, falling back to naive mode, ex [{ ex }]");
+ warned = true;
+ }
+ return GetIPsFallback();
+ }
+ }
+ }
+
+ public static IEnumerable ExternalIPAddresses => from i in AllIPAddresses
+ where !IPAddress.IsLoopback(i)
+ select i;
+
+ private static IEnumerable GetIPsDefault()
+ {
+ var returned = false;
+ foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
+ {
+ var props = adapter.GetIPProperties();
+ var gateways = from ga in props.GatewayAddresses
+ where !ga.Address.Equals(IPAddress.Any)
+ select true;
+ if (!gateways.Any())
+ {
+ Trace.WriteLine("Skipping {props}. No gateways");
+ continue;
+ }
+ Trace.WriteLine($"Using {props}");
+ foreach (var uni in props.UnicastAddresses)
+ {
+ var address = uni.Address;
+ if (address.AddressFamily != AddressFamily.InterNetwork)
+ {
+ Trace.WriteLine($"Skipping {address}. Not IPv4");
+ continue;
+ }
+ Trace.WriteLine($"Found {address}");
+ returned = true;
+ yield return address;
+ }
+ }
+ if (!returned)
+ {
+ throw new ApplicationException("No IP");
+ }
+ }
+
+ private static IEnumerable GetIPsFallback()
+ {
+ var returned = false;
+ foreach (var i in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
+ {
+ if (i.AddressFamily == AddressFamily.InterNetwork)
+ {
+ Trace.WriteLine($"Found {i}");
+ returned = true;
+ yield return i;
+ }
+ }
+ if (!returned)
+ {
+ throw new ApplicationException("No IP");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/IRepositoryItem.cs b/Roadie.Dlna/Utility/IRepositoryItem.cs
new file mode 100644
index 0000000..79729e0
--- /dev/null
+++ b/Roadie.Dlna/Utility/IRepositoryItem.cs
@@ -0,0 +1,9 @@
+namespace Roadie.Dlna.Utility
+{
+ public interface IRepositoryItem
+ {
+ string Description { get; }
+
+ string Name { get; }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/LeastRecentlyUsedDictionary.cs b/Roadie.Dlna/Utility/LeastRecentlyUsedDictionary.cs
new file mode 100644
index 0000000..42a1580
--- /dev/null
+++ b/Roadie.Dlna/Utility/LeastRecentlyUsedDictionary.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Roadie.Dlna.Utility
+{
+ public sealed class LeastRecentlyUsedDictionary : IDictionary
+ {
+ private readonly ConcurrentDictionary>> items = new ConcurrentDictionary>>();
+
+ private readonly LinkedList> order = new LinkedList>();
+
+ private readonly uint toDrop;
+
+ public uint Capacity { get; }
+
+ public int Count => items.Count;
+
+ public bool IsReadOnly => false;
+
+ public ICollection Keys => items.Keys;
+
+ public ICollection Values => (from i in items.Values
+ select i.Value.Value).ToList();
+
+ public TValue this[TKey key]
+ {
+ get { return items[key].Value.Value; }
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ set
+ {
+ Remove(key);
+ Add(key, value);
+ }
+ }
+
+ public LeastRecentlyUsedDictionary(uint capacity)
+ {
+ Capacity = capacity;
+ toDrop = Math.Min(10, (uint)(capacity * 0.07));
+ }
+
+ public LeastRecentlyUsedDictionary(int capacity)
+ : this((uint)capacity)
+ {
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public void Add(KeyValuePair item)
+ {
+ AddAndPop(item);
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public void Add(TKey key, TValue value)
+ {
+ AddAndPop(new KeyValuePair(key, value));
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public TValue AddAndPop(KeyValuePair item)
+ {
+ LinkedListNode> node;
+ lock (order)
+ {
+ node = order.AddFirst(item);
+ }
+ items.TryAdd(item.Key, node);
+ return MaybeDropSome();
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public TValue AddAndPop(TKey key, TValue value)
+ {
+ return AddAndPop(new KeyValuePair(key, value));
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public void Clear()
+ {
+ items.Clear();
+ lock (order)
+ {
+ order.Clear();
+ }
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ return items.ContainsKey(item.Key);
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return items.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return items.GetEnumerator();
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return items.Select(i => i.Value.Value).GetEnumerator();
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public bool Remove(TKey key)
+ {
+ LinkedListNode> node;
+ if (items.TryRemove(key, out node))
+ {
+ lock (order)
+ {
+ order.Remove(node);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ public bool Remove(KeyValuePair item)
+ {
+ LinkedListNode> node;
+ if (items.TryRemove(item.Key, out node))
+ {
+ lock (order)
+ {
+ order.Remove(node);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public bool TryGetValue(TKey key, out TValue value)
+ {
+ LinkedListNode> node;
+ if (items.TryGetValue(key, out node))
+ {
+ value = node.Value.Value;
+ return true;
+ }
+ value = default(TValue);
+ return false;
+ }
+
+ private TValue MaybeDropSome()
+ {
+ if (Count <= Capacity)
+ {
+ return default(TValue);
+ }
+ lock (order)
+ {
+ var rv = default(TValue);
+ for (var i = 0; i < toDrop; ++i)
+ {
+ LinkedListNode> item;
+ if (items.TryRemove(order.Last.Value.Key, out item))
+ {
+ rv = item.Value.Value;
+ }
+ order.RemoveLast();
+ }
+ return rv;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/MoreDom.cs b/Roadie.Dlna/Utility/MoreDom.cs
new file mode 100644
index 0000000..c86fd0f
--- /dev/null
+++ b/Roadie.Dlna/Utility/MoreDom.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Xml;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class MoreDom
+ {
+ public static XmlElement EL(this XmlDocument doc, string name)
+ {
+ return EL(doc, name, null, null);
+ }
+
+ public static XmlElement EL(this XmlDocument doc, string name,
+ AttributeCollection attributes)
+ {
+ return EL(doc, name, attributes, null);
+ }
+
+ public static XmlElement EL(this XmlDocument doc, string name, string text)
+ {
+ return EL(doc, name, null, text);
+ }
+
+ public static XmlElement EL(this XmlDocument doc, string name,
+ AttributeCollection attributes, string text)
+ {
+ if (doc == null)
+ {
+ throw new ArgumentNullException(nameof(doc));
+ }
+ var rv = doc.CreateElement(name);
+ if (text != null)
+ {
+ rv.InnerText = text;
+ }
+ if (attributes != null)
+ {
+ foreach (var i in attributes)
+ {
+ rv.SetAttribute(i.Key, i.Value);
+ }
+ }
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/NaturalStringComparer.cs b/Roadie.Dlna/Utility/NaturalStringComparer.cs
new file mode 100644
index 0000000..d74274c
--- /dev/null
+++ b/Roadie.Dlna/Utility/NaturalStringComparer.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+
+namespace Roadie.Dlna.Utility
+{
+ using PartsCache = LeastRecentlyUsedDictionary;
+
+ public sealed class NaturalStringComparer : StringComparer
+ {
+ private static readonly StringComparer comparer =
+ CurrentCultureIgnoreCase;
+
+ private readonly PartsCache partsCache = new PartsCache(5000);
+
+ private readonly bool stemBase;
+
+ public static IComparer Comparer { get; } = new NaturalStringComparer();
+
+ public NaturalStringComparer()
+ : this(false)
+ {
+ }
+
+ public NaturalStringComparer(bool stemBase)
+ {
+ this.stemBase = stemBase;
+ }
+
+ public override int Compare(string x, string y)
+ {
+ if (stemBase)
+ {
+ x = x.StemCompareBase();
+ y = y.StemCompareBase();
+ }
+ if (x == y || InvariantCulture.Compare(x, y) == 0)
+ {
+ return 0;
+ }
+ var p1 = Split(x);
+ var p2 = Split(y);
+
+ int rv;
+ var e = Math.Min(p1.Length, p2.Length);
+ for (var i = 0; i < e; ++i)
+ {
+ rv = p1[i].CompareTo(p2[i]);
+ if (rv != 0)
+ {
+ return rv;
+ }
+ }
+ rv = p1.Length.CompareTo(p2.Length);
+ if (rv == 0)
+ {
+ return comparer.Compare(x, y);
+ }
+ return rv;
+ }
+
+ public override bool Equals(string x, string y)
+ {
+ return Compare(x, y) == 0;
+ }
+
+ public override int GetHashCode(string obj)
+ {
+ return comparer.GetHashCode(obj);
+ }
+
+ private BaseSortPart[] Split(string str)
+ {
+ BaseSortPart[] rv;
+ lock (partsCache)
+ {
+ if (partsCache.TryGetValue(str, out rv))
+ {
+ return rv;
+ }
+ }
+
+ var parts = new List();
+ var num = false;
+ var start = 0;
+ for (var i = 0; i < str.Length; ++i)
+ {
+ var c = str[i];
+ var cnum = c >= '0' && c <= '9';
+ if (cnum == num)
+ {
+ continue;
+ }
+ if (i != 0)
+ {
+ var p = str.Substring(start, i - start).Trim();
+ if (num)
+ {
+ parts.Add(new NumericSortPart(p));
+ }
+ else
+ {
+ if (!string.IsNullOrWhiteSpace(p))
+ {
+ parts.Add(new StringSortPart(p, comparer));
+ }
+ }
+ }
+ num = cnum;
+ start = i;
+ }
+ var pe = str.Substring(start).Trim();
+ if (!string.IsNullOrWhiteSpace(pe))
+ {
+ if (num)
+ {
+ parts.Add(new NumericSortPart(pe));
+ }
+ else
+ {
+ parts.Add(new StringSortPart(pe, comparer));
+ }
+ }
+
+ rv = parts.ToArray();
+ lock (partsCache)
+ {
+ partsCache[str] = rv;
+ }
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/NumericSortPart.cs b/Roadie.Dlna/Utility/NumericSortPart.cs
new file mode 100644
index 0000000..1ae4228
--- /dev/null
+++ b/Roadie.Dlna/Utility/NumericSortPart.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Roadie.Dlna.Utility
+{
+ internal sealed class NumericSortPart : BaseSortPart, IComparable
+ {
+ private readonly int len;
+
+ private readonly ulong val;
+
+ public NumericSortPart(string s)
+ {
+ val = ulong.Parse(s);
+ len = s.Length;
+ }
+
+ public int CompareTo(NumericSortPart other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+ var rv = val.CompareTo(other.val);
+ if (rv == 0)
+ {
+ return len.CompareTo(other.len);
+ }
+ return rv;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/ProductInformation.cs b/Roadie.Dlna/Utility/ProductInformation.cs
new file mode 100644
index 0000000..851f90d
--- /dev/null
+++ b/Roadie.Dlna/Utility/ProductInformation.cs
@@ -0,0 +1,71 @@
+using System.IO;
+using System.Reflection;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class ProductInformation
+ {
+ public static string Company
+ {
+ get
+ {
+ var attributes = Assembly.GetEntryAssembly().GetCustomAttributes(
+ typeof(AssemblyCompanyAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return string.Empty;
+ }
+ return ((AssemblyCompanyAttribute)attributes[0]).Company;
+ }
+ }
+
+ public static string Copyright
+ {
+ get
+ {
+ var attributes = Assembly.GetEntryAssembly().GetCustomAttributes(
+ typeof(AssemblyCopyrightAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return string.Empty;
+ }
+ return ((AssemblyCopyrightAttribute)attributes[0]).Copyright;
+ }
+ }
+
+ public static string ProductVersion
+ {
+ get
+ {
+ var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(
+ typeof(AssemblyInformationalVersionAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return string.Empty;
+ }
+ var infoVersionAttr =
+ (AssemblyInformationalVersionAttribute)attributes[0];
+ return infoVersionAttr.InformationalVersion;
+ }
+ }
+
+ public static string Title
+ {
+ get
+ {
+ var attributes = Assembly.GetEntryAssembly().GetCustomAttributes(
+ typeof(AssemblyTitleAttribute), false);
+ if (attributes.Length > 0)
+ {
+ var titleAttribute = (AssemblyTitleAttribute)attributes[0];
+ if (!string.IsNullOrWhiteSpace(titleAttribute.Title))
+ {
+ return titleAttribute.Title;
+ }
+ }
+ return Path.GetFileNameWithoutExtension(
+ Assembly.GetExecutingAssembly().CodeBase);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/Repository.cs b/Roadie.Dlna/Utility/Repository.cs
new file mode 100644
index 0000000..6d3cf35
--- /dev/null
+++ b/Roadie.Dlna/Utility/Repository.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Roadie.Dlna.Utility
+{
+ public abstract class Repository where TInterface : class, IRepositoryItem
+ {
+ private static readonly Dictionary items = BuildRepository();
+
+ public static IDictionary ListItems()
+ {
+ return items.Values.ToDictionary(v => v.Name, v => v);
+ }
+
+ public static TInterface Lookup(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Invalid repository name", nameof(name));
+ }
+ var argsplit = name.Split(new[] { ':' }, 2);
+ name = argsplit[0].ToUpperInvariant().Trim();
+ TInterface result;
+ if (!items.TryGetValue(name, out result))
+ {
+ throw new RepositoryLookupException(name);
+ }
+ if (argsplit.Length == 1 || !(result is IConfigurable))
+ {
+ return result;
+ }
+ var parameters = new ConfigParameters(argsplit[1]);
+ if (parameters.Count == 0)
+ {
+ return result;
+ }
+ var ctor = result.GetType().GetConstructor(new Type[] { });
+ if (ctor == null)
+ {
+ throw new RepositoryLookupException(name);
+ }
+ try
+ {
+ var item = ctor.Invoke(new object[] { }) as TInterface;
+ if (item == null)
+ {
+ throw new RepositoryLookupException(name);
+ }
+ var configItem = item as IConfigurable;
+ configItem?.SetParameters(parameters);
+ return item;
+ }
+ catch (Exception ex)
+ {
+ throw new RepositoryLookupException($"Cannot construct repository item: {ex.Message}", ex);
+ }
+ }
+
+ private static Dictionary BuildRepository()
+ {
+ var found = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
+ var type = typeof(TInterface).Name;
+ var a = typeof(TInterface).Assembly;
+ foreach (var t in a.GetTypes())
+ {
+ if (t.GetInterface(type) == null)
+ {
+ continue;
+ }
+ var ctor = t.GetConstructor(new Type[] { });
+ if (ctor == null)
+ {
+ continue;
+ }
+ try
+ {
+ var item = ctor.Invoke(new object[] { }) as TInterface;
+ if (item == null)
+ {
+ continue;
+ }
+ found.Add(item.Name, item);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ return found;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/RepositoryLookupException.cs b/Roadie.Dlna/Utility/RepositoryLookupException.cs
new file mode 100644
index 0000000..2fe836b
--- /dev/null
+++ b/Roadie.Dlna/Utility/RepositoryLookupException.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Roadie.Dlna.Utility
+{
+ [Serializable]
+ public sealed class RepositoryLookupException : ArgumentException
+ {
+ public string Key { get; private set; }
+
+ public RepositoryLookupException()
+ {
+ }
+
+ public RepositoryLookupException(string key)
+ : base($"Failed to lookup {key}")
+ {
+ Key = key;
+ }
+
+ public RepositoryLookupException(string message, Exception inner)
+ : base(message, inner)
+ {
+ }
+
+ public RepositoryLookupException(string message, string paramName) : base(message, paramName)
+ {
+ }
+
+ public RepositoryLookupException(string message, string paramName, Exception innerException) : base(message, paramName, innerException)
+ {
+ }
+
+ private RepositoryLookupException(SerializationInfo info,
+ StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/ResourceHelper.cs b/Roadie.Dlna/Utility/ResourceHelper.cs
new file mode 100644
index 0000000..a133d7b
--- /dev/null
+++ b/Roadie.Dlna/Utility/ResourceHelper.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class ResourceHelper
+ {
+ private static object _sybcRoot = new object();
+
+ private static Dictionary _cache = new Dictionary();
+
+ public static byte[] GetResourceData(string resource)
+ {
+ lock (_sybcRoot)
+ {
+ if (!_cache.ContainsKey(resource))
+ {
+ var pathToResourceFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Server", "Resources", resource);
+ _cache.Add(resource, File.ReadAllBytes(pathToResourceFile));
+ }
+ return _cache[resource];
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/StreamManager.cs b/Roadie.Dlna/Utility/StreamManager.cs
new file mode 100644
index 0000000..d8adda0
--- /dev/null
+++ b/Roadie.Dlna/Utility/StreamManager.cs
@@ -0,0 +1,20 @@
+using Microsoft.IO;
+using System.IO;
+
+namespace Roadie.Dlna.Utility
+{
+ public static class StreamManager
+ {
+ private static readonly RecyclableMemoryStreamManager manager = new RecyclableMemoryStreamManager();
+
+ public static MemoryStream GetStream()
+ {
+ return manager.GetStream();
+ }
+
+ public static MemoryStream GetStream(string tag)
+ {
+ return manager.GetStream(tag);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/StreamPump.cs b/Roadie.Dlna/Utility/StreamPump.cs
new file mode 100644
index 0000000..4172387
--- /dev/null
+++ b/Roadie.Dlna/Utility/StreamPump.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Roadie.Dlna.Utility
+{
+ public sealed class StreamPump : IDisposable
+ {
+ private readonly byte[] buffer;
+
+ private readonly SemaphoreSlim sem = new SemaphoreSlim(0, 1);
+
+ public Stream Input { get; }
+
+ public Stream Output { get; }
+
+ public StreamPump(Stream inputStream, Stream outputStream, int bufferSize)
+ {
+ buffer = new byte[bufferSize];
+ Input = inputStream;
+ Output = outputStream;
+ }
+
+ public void Dispose()
+ {
+ sem.Dispose();
+ }
+
+ public void Pump(StreamPumpCallback callback)
+ {
+ try
+ {
+ Input.BeginRead(buffer, 0, buffer.Length, readResult =>
+ {
+ try
+ {
+ var read = Input.EndRead(readResult);
+ if (read <= 0)
+ {
+ Finish(StreamPumpResult.Delivered, callback);
+ return;
+ }
+
+ try
+ {
+ Output.BeginWrite(buffer, 0, read, writeResult =>
+ {
+ try
+ {
+ Output.EndWrite(writeResult);
+ Pump(callback);
+ }
+ catch (Exception)
+ {
+ Finish(StreamPumpResult.Aborted, callback);
+ }
+ }, null);
+ }
+ catch (Exception)
+ {
+ Finish(StreamPumpResult.Aborted, callback);
+ }
+ }
+ catch (Exception)
+ {
+ Finish(StreamPumpResult.Aborted, callback);
+ }
+ }, null);
+ }
+ catch (Exception)
+ {
+ Finish(StreamPumpResult.Aborted, callback);
+ }
+ }
+
+ public bool Wait(int timeout)
+ {
+ return sem.Wait(timeout);
+ }
+
+ private void Finish(StreamPumpResult result, StreamPumpCallback callback)
+ {
+ //https://stackoverflow.com/a/55516918/74071
+ var task = Task.Run(() => callback(this, result));
+ task.Wait();
+
+ try
+ {
+ sem.Release();
+ }
+ catch (ObjectDisposedException)
+ {
+ // ignore
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"StreamPump.Finish Ex [{ ex.Message }]");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/StreamPumpCallback.cs b/Roadie.Dlna/Utility/StreamPumpCallback.cs
new file mode 100644
index 0000000..6fbdb04
--- /dev/null
+++ b/Roadie.Dlna/Utility/StreamPumpCallback.cs
@@ -0,0 +1,4 @@
+namespace Roadie.Dlna.Utility
+{
+ public delegate void StreamPumpCallback(StreamPump pump, StreamPumpResult result);
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/StreamPumpResult.cs b/Roadie.Dlna/Utility/StreamPumpResult.cs
new file mode 100644
index 0000000..68a471e
--- /dev/null
+++ b/Roadie.Dlna/Utility/StreamPumpResult.cs
@@ -0,0 +1,8 @@
+namespace Roadie.Dlna.Utility
+{
+ public enum StreamPumpResult
+ {
+ Aborted,
+ Delivered
+ }
+}
\ No newline at end of file
diff --git a/Roadie.Dlna/Utility/StringSortPart.cs b/Roadie.Dlna/Utility/StringSortPart.cs
new file mode 100644
index 0000000..91440e5
--- /dev/null
+++ b/Roadie.Dlna/Utility/StringSortPart.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Roadie.Dlna.Utility
+{
+ internal sealed class StringSortPart : BaseSortPart, IComparable
+ {
+ private readonly StringComparer comparer;
+
+ private readonly string str;
+
+ internal StringSortPart(string str, StringComparer comparer)
+ {
+ this.str = str;
+ this.comparer = comparer;
+ }
+
+ public int CompareTo(StringSortPart other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+ return comparer.Compare(str, other.str);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Roadie.sln b/Roadie.sln
index c13fa31..a9e641c 100644
--- a/Roadie.sln
+++ b/Roadie.sln
@@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Api.Hubs", "Roadie.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inspector", "Inspector\Inspector.csproj", "{9A0831DC-343A-4E0C-8617-AF62426F3BA8}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Dlna", "Roadie.Dlna\Roadie.Dlna.csproj", "{FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Dlna.Services", "Roadie.Dlna.Services\Roadie.Dlna.Services.csproj", "{7345CBBD-0D21-43E6-9435-DBCDBDFB4516}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -76,12 +80,28 @@ Global
{E740C89E-3363-4577-873B-0871823E252C}.Release|x64.Build.0 = Release|x64
{9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|x64.ActiveCfg = Debug|Any CPU
- {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|x64.Build.0 = Debug|Any CPU
+ {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|x64.ActiveCfg = Debug|x64
+ {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Debug|x64.Build.0 = Debug|x64
{9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|Any CPU.Build.0 = Release|Any CPU
- {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|x64.ActiveCfg = Release|Any CPU
- {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|x64.Build.0 = Release|Any CPU
+ {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|x64.ActiveCfg = Release|x64
+ {9A0831DC-343A-4E0C-8617-AF62426F3BA8}.Release|x64.Build.0 = Release|x64
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Debug|x64.ActiveCfg = Debug|x64
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Debug|x64.Build.0 = Debug|x64
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Release|x64.ActiveCfg = Release|x64
+ {FF2D3B18-DC47-4FAA-8F14-CB3B1B3C81D2}.Release|x64.Build.0 = Release|x64
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Debug|x64.ActiveCfg = Debug|x64
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Debug|x64.Build.0 = Debug|x64
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Release|x64.ActiveCfg = Release|x64
+ {7345CBBD-0D21-43E6-9435-DBCDBDFB4516}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE