Plugin.cs 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.Composition;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using MediaBrowser.Common.Extensions;
  8. using MediaBrowser.Common.Net;
  9. using MediaBrowser.Common.Plugins;
  10. using MediaBrowser.Controller.Entities;
  11. using MediaBrowser.Controller.Entities.Audio;
  12. using MediaBrowser.Controller.Entities.TV;
  13. using MediaBrowser.Model.Entities;
  14. using MediaBrowser.Plugins.Dlna.Configuration;
  15. namespace MediaBrowser.Plugins.Dlna
  16. {
  17. /// <summary>
  18. /// Class Plugin
  19. /// </summary>
  20. [Export(typeof(IPlugin))]
  21. public class Plugin : BasePlugin<PluginConfiguration>
  22. {
  23. //these are Neptune values, they probably belong in the managed wrapper somewhere, but they aren't
  24. //techincally theres 50 to 100 of these values, but these 3 seem to be the most useful
  25. private const int NEP_Failure = -1;
  26. private const int NEP_NotImplemented = -2012;
  27. private const int NEP_Success = 0;
  28. private Platinum.UPnP _Upnp;
  29. private Platinum.MediaConnect _PlatinumServer;
  30. private User _CurrentUser;
  31. /// <summary>
  32. /// Gets the name.
  33. /// </summary>
  34. /// <value>The name.</value>
  35. public override string Name
  36. {
  37. get { return "DLNA Server"; }
  38. }
  39. /// <summary>
  40. /// Gets the description.
  41. /// </summary>
  42. /// <value>The description.</value>
  43. public override string Description
  44. {
  45. get { return "DLNA Server"; }
  46. }
  47. /// <summary>
  48. /// Gets the instance.
  49. /// </summary>
  50. /// <value>The instance.</value>
  51. public static Plugin Instance { get; private set; }
  52. /// <summary>
  53. /// Initializes a new instance of the <see cref="Plugin" /> class.
  54. /// </summary>
  55. public Plugin()
  56. : base()
  57. {
  58. Instance = this;
  59. }
  60. /// <summary>
  61. /// Initializes the on server.
  62. /// </summary>
  63. /// <param name="isFirstRun">if set to <c>true</c> [is first run].</param>
  64. protected override void InitializeOnServer(bool isFirstRun)
  65. {
  66. base.InitializeOnServer(isFirstRun);
  67. Kernel.ReloadCompleted += Kernel_ReloadCompleted;
  68. AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
  69. }
  70. /// <summary>
  71. /// Handles the AssemblyResolve event of the CurrentDomain control.
  72. /// </summary>
  73. /// <param name="sender">The source of the event.</param>
  74. /// <param name="args">The <see cref="ResolveEventArgs" /> instance containing the event data.</param>
  75. /// <returns>Assembly.</returns>
  76. Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
  77. {
  78. var askedAssembly = new AssemblyName(args.Name);
  79. var resourcePath = "MediaBrowser.Plugins.Dlna.Assemblies." + askedAssembly.Name + ".dll";
  80. using (var stream = GetType().Assembly.GetManifestResourceStream(resourcePath))
  81. {
  82. if (stream != null)
  83. {
  84. Logger.Info("Loading assembly from resource {0}", resourcePath);
  85. using (var memoryStream = new MemoryStream())
  86. {
  87. stream.CopyTo(memoryStream);
  88. memoryStream.Position = 0;
  89. return Assembly.Load(memoryStream.ToArray());
  90. }
  91. }
  92. }
  93. return null;
  94. }
  95. /// <summary>
  96. /// Disposes the on server.
  97. /// </summary>
  98. /// <param name="dispose">if set to <c>true</c> [dispose].</param>
  99. protected override void DisposeOnServer(bool dispose)
  100. {
  101. if (dispose)
  102. {
  103. Kernel.ReloadCompleted -= Kernel_ReloadCompleted;
  104. AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
  105. DisposeDlnaServer();
  106. }
  107. base.DisposeOnServer(dispose);
  108. }
  109. /// <summary>
  110. /// Handles the ReloadCompleted event of the Kernel control.
  111. /// </summary>
  112. /// <param name="sender">The source of the event.</param>
  113. /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
  114. void Kernel_ReloadCompleted(object sender, EventArgs e)
  115. {
  116. InitializeDlnaServer();
  117. }
  118. /// <summary>
  119. /// Initializes the dlna server.
  120. /// </summary>
  121. private void InitializeDlnaServer()
  122. {
  123. this.SetupUPnPServer();
  124. }
  125. /// <summary>
  126. /// Disposes the dlna server.
  127. /// </summary>
  128. private void DisposeDlnaServer()
  129. {
  130. this.CleanupUPnPServer();
  131. }
  132. private void SetupUPnPServer()
  133. {
  134. this._Upnp = new Platinum.UPnP();
  135. // Will need a config setting to set the friendly name of the upnp server
  136. //this._PlatinumServer = new Platinum.MediaConnect("MB3 UPnP", "MB3UPnP", 1901);
  137. if (this.Configuration.DlnaPortNumber.HasValue)
  138. this._PlatinumServer = new Platinum.MediaConnect(this.Configuration.FriendlyDlnaName, "MB3UPnP", this.Configuration.DlnaPortNumber.Value);
  139. else
  140. this._PlatinumServer = new Platinum.MediaConnect(this.Configuration.FriendlyDlnaName);
  141. this._PlatinumServer.BrowseMetadata += new Platinum.MediaConnect.BrowseMetadataDelegate(server_BrowseMetadata);
  142. this._PlatinumServer.BrowseDirectChildren += new Platinum.MediaConnect.BrowseDirectChildrenDelegate(server_BrowseDirectChildren);
  143. this._PlatinumServer.ProcessFileRequest += new Platinum.MediaConnect.ProcessFileRequestDelegate(server_ProcessFileRequest);
  144. this._PlatinumServer.SearchContainer += new Platinum.MediaConnect.SearchContainerDelegate(server_SearchContainer);
  145. this._Upnp.AddDeviceHost(this._PlatinumServer);
  146. this._Upnp.Start();
  147. }
  148. private void CleanupUPnPServer()
  149. {
  150. if (this._Upnp != null && this._Upnp.Running)
  151. this._Upnp.Stop();
  152. if (this._PlatinumServer != null)
  153. {
  154. this._PlatinumServer.BrowseMetadata -= new Platinum.MediaConnect.BrowseMetadataDelegate(server_BrowseMetadata);
  155. this._PlatinumServer.BrowseDirectChildren -= new Platinum.MediaConnect.BrowseDirectChildrenDelegate(server_BrowseDirectChildren);
  156. this._PlatinumServer.ProcessFileRequest -= new Platinum.MediaConnect.ProcessFileRequestDelegate(server_ProcessFileRequest);
  157. this._PlatinumServer.SearchContainer -= new Platinum.MediaConnect.SearchContainerDelegate(server_SearchContainer);
  158. this._PlatinumServer.Dispose();
  159. this._PlatinumServer = null;
  160. }
  161. if (this._Upnp != null)
  162. {
  163. this._Upnp.Dispose();
  164. this._Upnp = null;
  165. }
  166. }
  167. private int server_BrowseMetadata(Platinum.Action action, string object_id, string filter, int starting_index, int requested_count, string sort_criteria, Platinum.HttpRequestContext context)
  168. {
  169. Logger.Info("BrowseMetadata Entered - Parameters: action:{0} object_id:{1} filter:{2} starting_index:{3} requested_count:{4} sort_criteria:{5} context:{6}",
  170. action.ToLogString(), object_id, filter, starting_index, requested_count, sort_criteria, context.ToLogString());
  171. //nothing much seems to call BrowseMetadata so far
  172. //but perhaps that is because we aren't handing out the correct info for the client to call this... I don't know
  173. //PS3 calls it
  174. //Parameters: action:Action Name:Browse Description:Platinum.ActionDescription Arguments: object_id:0
  175. //filter:@id,upnp:class,res,res@protocolInfo,res@av:authenticationUri,res@size,dc:title,upnp:albumArtURI,res@dlna:ifoFileURI,res@protection,res@bitrate,res@duration,res@sampleFrequency,res@bitsPerSample,res@nrAudioChannels,res@resolution,res@colorDepth,dc:date,av:dateTime,upnp:artist,upnp:album,upnp:genre,dc:contributer,upnp:storageFree,upnp:storageUsed,upnp:originalTrackNumber,dc:publisher,dc:language,dc:region,dc:description,upnp:toc,@childCount,upnp:albumArtURI@dlna:profileID,res@dlna:cleartextSize
  176. //starting_index:0 requested_count:1 sort_criteria: context:HttpRequestContext LocalAddress:HttpRequestContext.SocketAddress IP:192.168.1.56 Port:1845 RemoteAddress:HttpRequestContext.SocketAddress IP:192.168.1.40 Port:49277 Request:http://192.168.1.56:1845/ContentDirectory/7c6b1b90-872b-2cda-3c5c-21a0e430ce5e/control.xml Signature:PS3
  177. if (object_id == "0")
  178. {
  179. var root = new Platinum.MediaContainer();
  180. root.Title = "Root";
  181. root.ObjectID = "0";
  182. root.ParentID = "-1";
  183. root.Class = new Platinum.ObjectClass("object.container.storageFolder", "");
  184. var didl = Platinum.Didl.header + root.ToDidl(filter) + Platinum.Didl.footer;
  185. action.SetArgumentValue("Result", didl);
  186. action.SetArgumentValue("NumberReturned", "1");
  187. action.SetArgumentValue("TotalMatches", "1");
  188. // update ID may be wrong here, it should be the one of the container?
  189. action.SetArgumentValue("UpdateId", "1");
  190. return NEP_Success;
  191. }
  192. else
  193. {
  194. return NEP_Failure;
  195. }
  196. }
  197. private int server_BrowseDirectChildren(Platinum.Action action, String object_id, String filter, Int32 starting_index, Int32 requested_count, String sort_criteria, Platinum.HttpRequestContext context)
  198. {
  199. Logger.Info("BrowseDirectChildren Entered - Parameters: action:{0} object_id:{1} filter:{2} starting_index:{3} requested_count:{4} sort_criteria:{5} context:{6}",
  200. action.ToLogString(), object_id, filter, starting_index, requested_count, sort_criteria, context.ToLogString());
  201. //WMP doesn't care how many results we return and what type they are
  202. //Xbox360 Music App is unknown, it calls SearchContainer and stops, not sure what will happen once we return it results
  203. //XBox360 Video App has fairly specific filter string and it need it - if you serve it music (mp3), it'll put music in the video list, so we have to do our own filtering
  204. //XBox360 Video App
  205. // action: "Browse"
  206. // object_id: "15"
  207. // filter: "dc:title,res,res@protection,res@duration,res@bitrate,upnp:genre,upnp:actor,res@microsoft:codec"
  208. // starting_index: 0
  209. // requested_count: 100
  210. // sort_criteria: "+upnp:class,+dc:title"
  211. //
  212. //the wierd thing about the filter is that there isn't much in it that says "only give me video"... except...
  213. //if we look at the doc available here: http://msdn.microsoft.com/en-us/library/windows/hardware/gg487545.aspx which describes the way WMP does its DLNA serving (not clienting)
  214. //doc is also a search cached google docs here: https://docs.google.com/viewer?a=v&q=cache:tnrdpTFCc84J:download.microsoft.com/download/0/0/b/00bba048-35e6-4e5b-a3dc-36da83cbb0d1/NetCompat_WMP11.docx+&hl=en&gl=au&pid=bl&srcid=ADGEESiBSKE1ZJeWmgYVOkKmRJuYaSL3_50KL1o6Ugp28JL1Ytq-2QbEeu6xFD8rbWcX35ZG4d7qPQnzqURGR5vig79S2Arj5umQNPnLeJo1k5_iWYbqPejeMHwwAv0ATmq2ynoZCBNL&sig=AHIEtbQ2qZJ8xMXLZYBWHHerezzXShKoVg
  215. //it describes object 15 as beeing Root/Video/Folders which can contain object.storageFolder
  216. //so perhaps thats what is saying 'only give me video'
  217. //I'm just not sure if those folders listed with object IDs are all well known across clients or if these ones are WMP specific
  218. //if they are device specific but also significant, then that might explain why Plex goes to the trouble of having configurable client device profiles for its DLNA server
  219. var didl = Platinum.Didl.header;
  220. IEnumerable<BaseItem> children = null;
  221. // I need to ask someone on the MB team if there's a better way to do this, it seems like it
  222. //could get pretty expensive to get ALL children all the time
  223. //if it's our only option perhaps we should cache results locally or something similar
  224. children = this.CurrentUser.RootFolder.GetRecursiveChildren(this.CurrentUser);
  225. //children = children.Filter(Extensions.FilterType.Music | Extensions.FilterType.Video).Page(starting_index, requested_count);
  226. int itemCount = 0;
  227. if (children != null)
  228. {
  229. foreach (var child in children)
  230. {
  231. using (var item = BaseItemToMediaItem(child, context))
  232. {
  233. if (item != null)
  234. {
  235. string test;
  236. test = item.ToDidl(filter);
  237. didl += item.ToDidl(filter);
  238. itemCount++;
  239. }
  240. }
  241. }
  242. didl += Platinum.Didl.footer;
  243. action.SetArgumentValue("Result", didl);
  244. action.SetArgumentValue("NumberReturned", itemCount.ToString());
  245. action.SetArgumentValue("TotalMatches", itemCount.ToString());
  246. // update ID may be wrong here, it should be the one of the container?
  247. action.SetArgumentValue("UpdateId", "1");
  248. return NEP_Success;
  249. }
  250. return NEP_Failure;
  251. }
  252. private int server_ProcessFileRequest(Platinum.HttpRequestContext context, Platinum.HttpResponse response)
  253. {
  254. Logger.Info("ProcessFileRequest Entered - Parameters: context:{0} response:{1}",
  255. context.ToLogString(), response);
  256. Uri uri = context.Request.URI;
  257. var id = uri.AbsolutePath.TrimStart('/');
  258. Guid itemID;
  259. if (Guid.TryParseExact(id, "D", out itemID))
  260. {
  261. var item = this.CurrentUser.RootFolder.FindItemById(itemID, this.CurrentUser);
  262. if (item != null)
  263. {
  264. //this is how the Xbox 360 Video app asks for artwork, it tacks this query string onto its request
  265. //?albumArt=true
  266. if (uri.Query == "?albumArt=true")
  267. {
  268. if (!string.IsNullOrWhiteSpace(item.PrimaryImagePath))
  269. //let see if we can serve artwork like this to the Xbox 360 Video App
  270. Platinum.MediaServer.SetResponseFilePath(context, response, Kernel.HttpServerUrlPrefix.Replace("+", context.LocalAddress.ip) + "/api/image?id=" + item.Id.ToString() + "&type=primary");
  271. //Platinum.MediaServer.SetResponseFilePath(context, response, item.PrimaryImagePath);
  272. }
  273. else
  274. Platinum.MediaServer.SetResponseFilePath(context, response, item.Path);
  275. //this does not work for WMP
  276. //Platinum.MediaServer.SetResponseFilePath(context, response, Kernel.HttpServerUrlPrefix.Replace("+", context.LocalAddress.ip) + "/api/video.ts?id=" + item.Id.ToString());
  277. return NEP_Success;
  278. }
  279. }
  280. return NEP_Failure;
  281. }
  282. private int server_SearchContainer(Platinum.Action action, string object_id, string searchCriteria, string filter, int starting_index, int requested_count, string sort_criteria, Platinum.HttpRequestContext context)
  283. {
  284. Logger.Info("SearchContainer Entered - Parameters: action:{0} object_id:{1} searchCriteria:{7} filter:{2} starting_index:{3} requested_count:{4} sort_criteria:{5} context:{6}",
  285. action.ToLogString(), object_id, filter, starting_index, requested_count, sort_criteria, context.ToLogString(), searchCriteria);
  286. //Doesn't call search at all:
  287. // XBox360 Video App
  288. //Calls search but does not require it to be implemented:
  289. // WMP, probably uses it just for its "images" section
  290. //Calls search Seems to require it:
  291. // XBox360 Music App
  292. //WMP
  293. // action: "Search"
  294. // object_id: "0"
  295. // searchCriteria: "upnp:class derivedfrom \"object.item.imageItem\" and @refID exists false"
  296. // filter: "*"
  297. // starting_index: 0
  298. // requested_count: 200
  299. // sort_criteria: "-dc:date"
  300. //XBox360 Music App
  301. // action: "Search"
  302. // object_id: "7"
  303. // searchCriteria: "(upnp:class = \"object.container.album.musicAlbum\")"
  304. // filter: "dc:title,upnp:artist"
  305. // starting_index: 0
  306. // requested_count: 1000
  307. // sort_criteria: "+dc:title"
  308. //
  309. //XBox360 Music App seems to work souly using SearchContainer and ProcessFileRequest
  310. //I think the current resource Uri's aren't going to work because it seems to require an extension like .mp3 to work, but this requires further testing
  311. //When hitting the Album tab of the app it's searching criteria is object.container.album.musicAlbum
  312. //this means it wants albums put into containers, I thought Platinum might do this for us, but it doesn't
  313. var didl = Platinum.Didl.header;
  314. IEnumerable<BaseItem> children = null;
  315. // I need to ask someone on the MB team if there's a better way to do this, it seems like it
  316. //could get pretty expensive to get ALL children all the time
  317. //if it's our only option perhaps we should cache results locally or something similar
  318. children = this.CurrentUser.RootFolder.GetRecursiveChildren(this.CurrentUser);
  319. //children = children.Filter(Extensions.FilterType.Music | Extensions.FilterType.Video).Page(starting_index, requested_count);
  320. //var test = GetFilterFromCriteria(searchCriteria);
  321. children = children.Where(GetBaseItemMatchFromCriteria(searchCriteria));
  322. int itemCount = 0;
  323. if (children != null)
  324. {
  325. Platinum.MediaItem item = null;
  326. foreach (var child in children)
  327. {
  328. item = BaseItemToMediaItem(child, context);
  329. if (item != null)
  330. {
  331. item.ParentID = string.Empty;
  332. didl += item.ToDidl(filter);
  333. itemCount++;
  334. }
  335. }
  336. didl += Platinum.Didl.footer;
  337. action.SetArgumentValue("Result", didl);
  338. action.SetArgumentValue("NumberReturned", itemCount.ToString());
  339. action.SetArgumentValue("TotalMatches", itemCount.ToString());
  340. // update ID may be wrong here, it should be the one of the container?
  341. action.SetArgumentValue("UpdateId", "1");
  342. return NEP_Success;
  343. }
  344. return NEP_Failure;
  345. }
  346. private Platinum.MediaItem BaseItemToMediaItem(BaseItem child, Platinum.HttpRequestContext context)
  347. {
  348. Platinum.MediaItem result = null;
  349. Platinum.MediaResource resource = null;
  350. if (child.IsFolder)
  351. {
  352. //DLNA is a fairly flat system, there doesn't appear to be much room in the system for folders so far
  353. //I haven't tested too many DLNA clients yet tho
  354. result = null;
  355. //item = new Platinum.MediaItem();
  356. //item.Class = new Platinum.ObjectClass("object.container.storageFolder", "");
  357. }
  358. else if (child is Episode)
  359. {
  360. result = MediaItemHelper.GetMediaItem((Episode)child);
  361. resource = MediaItemHelper.GetMediaResource((Episode)child);
  362. }
  363. else if (child is Video)
  364. {
  365. result = MediaItemHelper.GetMediaItem((Video)child);
  366. resource = MediaItemHelper.GetMediaResource((Video)child);
  367. }
  368. else if (child is Audio)
  369. {
  370. result = MediaItemHelper.GetMediaItem((Audio)child);
  371. resource = MediaItemHelper.GetMediaResource((Audio)child);
  372. }
  373. if (result != null)
  374. {
  375. //have a go at finding the mime type
  376. var mimeType = string.Empty;
  377. if (child.Path != null && Path.HasExtension(child.Path))
  378. mimeType = MimeTypes.GetMimeType(child.Path);
  379. resource.ProtoInfo = Platinum.ProtocolInfo.GetProtocolInfoFromMimeType(mimeType, true, context);
  380. // get list of ips and make sure the ip the request came from is used for the first resource returned
  381. // this ensures that clients which look only at the first resource will be able to reach the item
  382. IEnumerable<String> ips = GetUPnPIPAddresses(context); //.Distinct();
  383. // iterate through all ips and create a resource for each
  384. // I think we need extensions (".mp3" type extensions) on these for Xbox360 Video and Music apps to work
  385. //resource.URI = new Uri(Kernel.HttpServerUrlPrefix + "/api/video.ts?id=" + child.Id.ToString("D")).ToString();
  386. //result.AddResource(resource);
  387. foreach (String ip in ips)
  388. {
  389. //doesn't work for WMP
  390. //resource.URI = new Uri(Kernel.HttpServerUrlPrefix.Replace("+", ip) + "/api/video.ts?id=" + child.Id.ToString()).ToString();
  391. resource.URI = new Uri("http://" + ip + ":" + context.LocalAddress.port + "/" + child.Id.ToString("D")).ToString();
  392. result.AddResource(resource);
  393. }
  394. MediaItemHelper.AddAlbumArtInfoToMediaItem(result, child, Kernel.HttpServerUrlPrefix, ips);
  395. }
  396. return result;
  397. }
  398. private void AddResourcesToMediaItem(Platinum.MediaItem item, BaseItem child, Platinum.HttpRequestContext context)
  399. {
  400. Platinum.MediaResource resource = null;
  401. if (child is Video)
  402. {
  403. var videoChild = (Video)child;
  404. resource = new Platinum.MediaResource();
  405. if (videoChild.DefaultVideoStream != null)
  406. {
  407. //Bitrate is Bytes per second
  408. if (videoChild.DefaultVideoStream.BitRate.HasValue)
  409. resource.Bitrate = (uint)videoChild.DefaultVideoStream.BitRate;
  410. //not sure if we know Colour Depth
  411. //resource.ColorDepth
  412. if (videoChild.DefaultVideoStream.Channels.HasValue)
  413. resource.NbAudioChannels = (uint)videoChild.DefaultVideoStream.Channels.Value;
  414. //resource.Protection
  415. //resource.ProtoInfo
  416. //we must know resolution, I'm just not sure how to get it
  417. //resource.Resolution
  418. //I'm not sure what this actually means, is it Sample Rate
  419. if (videoChild.DefaultVideoStream.SampleRate.HasValue)
  420. resource.SampleFrequency = (uint)videoChild.DefaultVideoStream.SampleRate.Value;
  421. //file size?
  422. //resource.Size
  423. }
  424. }
  425. else if (child is Audio)
  426. {
  427. }
  428. // get list of ips and make sure the ip the request came from is used for the first resource returned
  429. // this ensures that clients which look only at the first resource will be able to reach the item
  430. List<String> ips = GetUPnPIPAddresses(context);
  431. // iterate through all ips and create a resource for each
  432. // I think we need extensions (".mp3" type extensions) on these for Xbox360 Video and Music apps to work
  433. foreach (String ip in ips)
  434. {
  435. resource.URI = new Uri("http://" + ip + ":" + context.LocalAddress.port + "/" + child.Id.ToString("D")).ToString();
  436. item.AddResource(resource);
  437. }
  438. }
  439. /// <summary>
  440. /// Gets a list of valid IP Addresses that the UPnP server is using
  441. /// </summary>
  442. /// <param name="context"></param>
  443. /// <returns></returns>
  444. private List<String> GetUPnPIPAddresses(Platinum.HttpRequestContext context)
  445. {
  446. // get list of ips and make sure the ip the request came from is used for the first resource returned
  447. // this ensures that clients which look only at the first resource will be able to reach the item
  448. List<String> result = Platinum.UPnP.GetIpAddresses(true); //if this call is expensive we could cache the results
  449. String localIP = context.LocalAddress.ip;
  450. if (localIP != "0.0.0.0")
  451. {
  452. result.Remove(localIP);
  453. result.Insert(0, localIP);
  454. }
  455. return result;
  456. }
  457. /// <summary>
  458. /// Gets the MB User with a user name that matches the user name configured in the plugin config
  459. /// </summary>
  460. /// <returns>MediaBrowser.Controller.Entities.User</returns>
  461. private User CurrentUser
  462. {
  463. get
  464. {
  465. if (this._CurrentUser == null)
  466. {
  467. //this looks like a lot of processing but it really isn't
  468. //its mostly gaurding against no users or no matching user existing
  469. var serverKernel = Controller.Kernel.Instance;
  470. if (serverKernel.Users.Any())
  471. {
  472. if (string.IsNullOrWhiteSpace(this.Configuration.UserName))
  473. this._CurrentUser = serverKernel.Users.First();
  474. else
  475. {
  476. this._CurrentUser = serverKernel.Users.FirstOrDefault(i => string.Equals(i.Name, this.Configuration.UserName, StringComparison.OrdinalIgnoreCase));
  477. if (this._CurrentUser == null)
  478. {
  479. //log and return first user
  480. this._CurrentUser = serverKernel.Users.First();
  481. Logger.Error("Configured user: \"{0}\" not found. Using first user found: \"{1}\" instead", this.Configuration.UserName, this._CurrentUser.Name);
  482. }
  483. }
  484. }
  485. else
  486. {
  487. Logger.Fatal("No users in the system");
  488. this._CurrentUser = null;
  489. }
  490. }
  491. return this._CurrentUser;
  492. }
  493. }
  494. #region "A Search Idea"
  495. //this is just an idea of how we might do some search
  496. //it's a bit lackluster in places and might be overkill in others
  497. //all in all it might not be a good idea, but I thought I'd see how it felt
  498. private Func<BaseItem, bool> GetBaseItemMatchFromCriteria(string searchCriteria)
  499. {
  500. //WMP Search when clicking Music:
  501. //"upnp:class derivedfrom \"object.item.audioItem\" and @refID exists false"
  502. //WMP Search when clicking Videos:
  503. //"upnp:class derivedfrom \"object.item.videoItem\" and @refID exists false"
  504. //WMP Search when clicking Pictures:
  505. //"upnp:class derivedfrom \"object.item.imageItem\" and @refID exists false"
  506. //WMP Search when clicking Recorded TV:
  507. //"upnp:class derivedfrom \"object.item.videoItem\" and @refID exists false"
  508. //we really need a syntax tree parser here
  509. //but the requests never seem to get more complex than "'Condition One' And 'Condition Two'"
  510. //something like Rosylin would be fun but it'd be serious overkill
  511. //the syntax seems to be very clear and there are only a handful of valid constructs
  512. //so this very basic parsing will provide some support for now
  513. Queue<string> criteriaQueue = new Queue<string>(searchCriteria.Split(' '));
  514. Func<BaseItem, bool> result = null;
  515. var currentMainOperatorIsAnd = false;
  516. //loop through in order and process - do not parallelise, order is important
  517. while (criteriaQueue.Any())
  518. {
  519. Func<BaseItem, bool> currentFilter = null;
  520. var metadataElement = criteriaQueue.Dequeue();
  521. var criteriaOperator = criteriaQueue.Dequeue();
  522. var value = criteriaQueue.Dequeue();
  523. if (value.StartsWith("\"") || value.StartsWith("\\\""))
  524. while (!value.EndsWith("\""))
  525. {
  526. value += criteriaQueue.Dequeue();
  527. }
  528. value = value.Trim();
  529. if (string.Equals(metadataElement, "upnp:class", StringComparison.OrdinalIgnoreCase))
  530. currentFilter = GetUpnpClassFilter(criteriaOperator, value);
  531. else if (string.Equals(metadataElement, "@refID", StringComparison.OrdinalIgnoreCase))
  532. {
  533. //not entirely sure what refID is for
  534. //Platinum has ReferenceID which I assume is the same thing, but we're not using it yet
  535. }
  536. else
  537. {
  538. //fail??
  539. }
  540. if (currentFilter != null)
  541. {
  542. if (result == null)
  543. result = currentFilter;
  544. else
  545. if (currentMainOperatorIsAnd)
  546. result = (i) => result(i) && currentFilter(i);
  547. else
  548. result = (i) => result(i) || currentFilter(i);
  549. }
  550. if (criteriaQueue.Any())
  551. {
  552. var op = criteriaQueue.Dequeue();
  553. if (string.Equals(op, "and", StringComparison.OrdinalIgnoreCase))
  554. currentMainOperatorIsAnd = true;
  555. else
  556. currentMainOperatorIsAnd = false;
  557. }
  558. }
  559. return result;
  560. }
  561. private Func<BaseItem, bool> GetUpnpClassFilter(string criteriaOperator, string value)
  562. {
  563. //"upnp:class derivedfrom \"object.item.videoItem\" "
  564. //"(upnp:class = \"object.container.album.musicAlbum\")"
  565. //only two options are valid for criteria
  566. // =, derivedfrom
  567. //there are only a few values we care about
  568. //object.item.videoItem
  569. //object.item.audioItem
  570. //object.container.storageFolder
  571. if (string.Equals(criteriaOperator, "=", StringComparison.OrdinalIgnoreCase))
  572. {
  573. if (value.Contains("object.item.videoItem"))
  574. return (i) => (i is Video);
  575. else if (value.Contains("object.item.audioItem"))
  576. return (i) => (i is Audio);
  577. else if (value.Contains("object.container.storageFolder"))
  578. return (i) => (i is Folder);
  579. else
  580. //something has gone wrong, don't filter anything
  581. return (i) => true;
  582. }
  583. else if (string.Equals(criteriaOperator, "derivedfrom", StringComparison.OrdinalIgnoreCase))
  584. {
  585. if (value.Contains("object.item.videoItem"))
  586. return (i) => (i is Video);
  587. else if (value.Contains("object.item.audioItem"))
  588. return (i) => (i is Audio);
  589. else if (value.Contains("object.container.storageFolder"))
  590. return (i) => (i is Folder);
  591. else
  592. //something has gone wrong, don't filter anything
  593. return (i) => true;
  594. }
  595. else
  596. {
  597. //something has gone wrong, don't filter anything
  598. return (i) => true;
  599. }
  600. }
  601. #endregion
  602. public override void UpdateConfiguration(Model.Plugins.BasePluginConfiguration configuration)
  603. {
  604. base.UpdateConfiguration(configuration);
  605. var config = (PluginConfiguration)configuration;
  606. this.CleanupUPnPServer();
  607. this._CurrentUser = null;
  608. this.SetupUPnPServer();
  609. }
  610. }
  611. internal static class MediaItemHelper
  612. {
  613. internal static Platinum.MediaResource GetMediaResource(Video item)
  614. {
  615. var result = GetMediaResource((BaseItem)item);
  616. if (item.DefaultVideoStream != null)
  617. {
  618. //Bitrate is Bytes per second
  619. if (item.DefaultVideoStream.BitRate.HasValue)
  620. result.Bitrate = (uint)item.DefaultVideoStream.BitRate;
  621. //not sure if we know Colour Depth
  622. //resource.ColorDepth
  623. if (item.DefaultVideoStream.Channels.HasValue)
  624. result.NbAudioChannels = (uint)item.DefaultVideoStream.Channels.Value;
  625. //resource.Protection
  626. //resource.ProtoInfo
  627. //we must know resolution, I'm just not sure how to get it
  628. //resource.Resolution
  629. //I'm not sure what this actually means, is it Sample Rate
  630. if (item.DefaultVideoStream.SampleRate.HasValue)
  631. result.SampleFrequency = (uint)item.DefaultVideoStream.SampleRate.Value;
  632. //file size?
  633. //resource.Size
  634. ////to do subtitles for clients that can deal with external subtitles (like srt)
  635. ////we will have to do something like this
  636. //IEnumerable<String> ips = GetUPnPIPAddresses(context);
  637. //foreach (var st in videoChild.MediaStreams)
  638. //{
  639. // if (st.Type == MediaStreamType.Subtitle)
  640. // {
  641. // Platinum.MediaResource subtitleResource = new Platinum.MediaResource();
  642. // subtitleResource.ProtoInfo = Platinum.ProtocolInfo.GetProtocolInfo(st.Path, with_dlna_extension: false);
  643. // foreach (String ip in ips)
  644. // {
  645. // //we'll need to figure out which of these options works for whick players
  646. // //either serve them ourselves
  647. // resource.URI = new Uri("http://" + ip + ":" + context.LocalAddress.port + "/" + child.Id.ToString("D")).ToString();
  648. // //or get the web api to serve them directly
  649. // resource.URI = new Uri(Kernel.HttpServerUrlPrefix.Replace("+", ip) + "/api/video?id=" + child.Id.ToString() + "&type=Subtitle").ToString();
  650. // result.AddResource(resource);
  651. // }
  652. // }
  653. //}
  654. }
  655. return result;
  656. }
  657. internal static Platinum.MediaItem GetMediaItem(Video item)
  658. {
  659. var result = GetMediaItem((BaseItem)item);
  660. result.Title = GetTitle(item);
  661. return result;
  662. }
  663. internal static Platinum.MediaResource GetMediaResource(Episode item)
  664. {
  665. //there's nothing specific about an episode that requires extra Resources
  666. return GetMediaResource((Video)item);
  667. }
  668. internal static Platinum.MediaItem GetMediaItem(Episode item)
  669. {
  670. var result = GetMediaItem((Video)item);
  671. if (item.IndexNumber.HasValue)
  672. result.Recorded.EpisodeNumber = (uint)item.IndexNumber.Value;
  673. if (item.Series != null && item.Series.Name != null)
  674. result.Recorded.SeriesTitle = item.Series.Name;
  675. result.Recorded.ProgramTitle = item.Name == null ? string.Empty : item.Name;
  676. return result;
  677. }
  678. internal static Platinum.MediaResource GetMediaResource(Audio item)
  679. {
  680. //there's nothing specific about an audio item that requires extra Resources
  681. return GetMediaResource((BaseItem)item);
  682. }
  683. internal static Platinum.MediaItem GetMediaItem(Audio item)
  684. {
  685. var result = GetMediaItem((BaseItem)item);
  686. result.Title = GetTitle(item);
  687. result.People.AddArtist(new Platinum.PersonRole(item.Artist));
  688. result.People.Contributor = item.AlbumArtist;
  689. result.Affiliation.Album = item.Album;
  690. return result;
  691. }
  692. internal static Platinum.MediaResource GetMediaResource(BaseItem item)
  693. {
  694. var result = new Platinum.MediaResource();
  695. //duration is in seconds
  696. if (item.RunTimeTicks.HasValue)
  697. result.Duration = (uint)TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
  698. return result;
  699. }
  700. internal static Platinum.MediaItem GetMediaItem(BaseItem item)
  701. {
  702. var result = new Platinum.MediaItem();
  703. result.ObjectID = item.Id.ToString();
  704. //if (child.Parent != null)
  705. // result.ParentID = child.Parent.Id.ToString();
  706. result.Class = item.GetPlatinumClassObject();
  707. result.Description.Date = item.PremiereDate.HasValue ? item.PremiereDate.Value.ToString() : string.Empty;
  708. result.Description.Language = item.Language == null ? string.Empty : item.Language;
  709. result.Description.DescriptionText = "this is DescriptionText";
  710. result.Description.LongDescriptionText = item.Overview == null ? string.Empty : item.Overview;
  711. result.Description.Rating = item.CommunityRating.ToString();
  712. if (item.Genres != null)
  713. {
  714. foreach (var genre in item.Genres)
  715. {
  716. result.Affiliation.AddGenre(genre);
  717. }
  718. }
  719. if (item.People != null)
  720. {
  721. foreach (var person in item.People)
  722. {
  723. if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
  724. result.People.AddActor(new Platinum.PersonRole(person.Name, person.Role == null ? string.Empty : person.Role));
  725. else if (string.Equals(person.Type, PersonType.MusicArtist, StringComparison.OrdinalIgnoreCase))
  726. {
  727. result.People.AddArtist(new Platinum.PersonRole(person.Name, "MusicArtist"));
  728. result.People.AddArtist(new Platinum.PersonRole(person.Name, "Performer"));
  729. }
  730. else if (string.Equals(person.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
  731. result.People.AddAuthors(new Platinum.PersonRole(person.Name, "Composer"));
  732. else if (string.Equals(person.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
  733. result.People.AddAuthors(new Platinum.PersonRole(person.Name, "Writer"));
  734. else if (string.Equals(person.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase))
  735. {
  736. result.People.AddAuthors(new Platinum.PersonRole(person.Name, "Director"));
  737. result.People.Director = result.People.Director + " " + person.Name;
  738. }
  739. else
  740. result.People.AddArtist(new Platinum.PersonRole(person.Name, person.Type == null ? string.Empty : person.Type));
  741. }
  742. }
  743. return result;
  744. }
  745. internal static void AddAlbumArtInfoToMediaItem(Platinum.MediaItem item, BaseItem child, string httpServerUrlPrefix, IEnumerable<String> ips)
  746. {
  747. foreach (var ip in ips)
  748. {
  749. AddAlbumArtInfoToMediaItem(item, child, httpServerUrlPrefix, ip);
  750. }
  751. }
  752. private static void AddAlbumArtInfoToMediaItem(Platinum.MediaItem item, BaseItem child, string httpServerUrlPrefix, string ip)
  753. {
  754. //making the artwork a direct hit to the MediaBrowser server instead of via the DLNA plugin works for WMP
  755. item.Extra.AddAlbumArtInfo(new Platinum.AlbumArtInfo(httpServerUrlPrefix.Replace("+", ip) + "/api/image?id=" + child.Id.ToString() + "&type=primary"));
  756. }
  757. /// <summary>
  758. /// Gets the title.
  759. /// </summary>
  760. /// <param name="video">The video.</param>
  761. /// <returns>System.String.</returns>
  762. private static string GetTitle(Video video)
  763. {
  764. //we have to be extremely careful with all string handling
  765. //if we set a null reference to a Platinum string it will not marshall to native correctly and things got very bad very quickly
  766. var title = video.Name == null ? string.Empty : video.Name;
  767. var episode = video as Episode;
  768. if (episode != null)
  769. {
  770. if (episode.Season != null)
  771. {
  772. title = string.Format("{0}-{1}", episode.Season.Name, title);
  773. }
  774. if (episode.Series != null)
  775. {
  776. title = string.Format("{0}-{1}", episode.Series.Name, title);
  777. }
  778. }
  779. return title;
  780. }
  781. /// <summary>
  782. /// Gets the title.
  783. /// </summary>
  784. /// <param name="audio">The audio.</param>
  785. /// <returns>System.String.</returns>
  786. private static string GetTitle(Audio audio)
  787. {
  788. return audio.Name == null ? string.Empty : audio.Name;
  789. }
  790. }
  791. internal static class Extensions
  792. {
  793. [Flags()]
  794. internal enum FilterType
  795. {
  796. Folder = 1,
  797. Music = 2,
  798. Video = 4
  799. }
  800. internal static IEnumerable<BaseItem> Filter(this IEnumerable<BaseItem> en, FilterType filter)
  801. {
  802. return en.Where(i => (
  803. (((filter & FilterType.Folder) == FilterType.Folder) && (i is Folder)) ||
  804. (((filter & FilterType.Music) == FilterType.Music) && (i is Audio)) ||
  805. (((filter & FilterType.Video) == FilterType.Video) && (i is Video)))
  806. );
  807. }
  808. internal static IEnumerable<BaseItem> Page(this IEnumerable<BaseItem> en, int starting_index, int requested_count)
  809. {
  810. return en.Skip(starting_index).Take(requested_count);
  811. }
  812. internal static Platinum.ObjectClass GetPlatinumClassObject(this BaseItem item)
  813. {
  814. if (item is Video)
  815. return new Platinum.ObjectClass("object.item.videoItem", "");
  816. else if (item is Audio)
  817. return new Platinum.ObjectClass("object.item.audioItem.musicTrack", "");
  818. else if (item is Folder)
  819. return new Platinum.ObjectClass("object.container.storageFolder", "");
  820. else
  821. return null;
  822. }
  823. internal static Platinum.ObjectClass GetPlatinumClassObject(this Folder item)
  824. {
  825. return new Platinum.ObjectClass("object.container.storageFolder", "");
  826. }
  827. internal static Platinum.ObjectClass GetPlatinumClassObject(this Audio item)
  828. {
  829. return new Platinum.ObjectClass("object.item.audioItem.musicTrack", "");
  830. }
  831. internal static Platinum.ObjectClass GetPlatinumClassObject(this Video item)
  832. {
  833. return new Platinum.ObjectClass("object.item.videoItem", "");
  834. }
  835. }
  836. internal static class LoggingExtensions
  837. {
  838. //provide some json-esque string that can be used for Verbose logging purposed
  839. internal static string ToLogString(this Platinum.Action item)
  840. {
  841. return string.Format(" {{ Name:\"{0}\", Description:\"{1}\", Arguments:{2} }} ",
  842. item.Name, item.Description.ToLogString(), item.Arguments.ToLogString());
  843. }
  844. internal static string ToLogString(this IEnumerable<Platinum.ActionArgumentDescription> items)
  845. {
  846. var result = "[";
  847. foreach (var arg in items)
  848. {
  849. result += (" " + arg.ToLogString());
  850. }
  851. result += " ]";
  852. return result;
  853. }
  854. internal static string ToLogString(this Platinum.ActionArgumentDescription item)
  855. {
  856. return string.Format(" {{ Name:\"{0}\", Direction:{1}, HasReturnValue:{2}, RelatedStateVariable:{3} }} ",
  857. item.Name, item.Direction, item.HasReturnValue, item.RelatedStateVariable.ToLogString());
  858. }
  859. internal static string ToLogString(this Platinum.StateVariable item)
  860. {
  861. return string.Format(" {{ Name:\"{0}\", DataType:{1}, DataTypeString:\"{2}\", Value:{3}, ValueString:\"{4}\" }} ",
  862. item.Name, item.DataType, item.DataTypeString, item.Value, item.ValueString);
  863. }
  864. internal static string ToLogString(this Platinum.ActionDescription item)
  865. {
  866. return string.Format(" {{ Name:\"{0}\", Arguments:{1} }} ",
  867. item.Name, item.Arguments.ToLogString());
  868. }
  869. internal static string ToLogString(this Platinum.HttpRequestContext item)
  870. {
  871. return string.Format(" {{ LocalAddress:{0}, RemoteAddress:{1}, Request:\"{2}\", Signature:{3} }}",
  872. item.LocalAddress.ToLogString(), item.RemoteAddress.ToLogString(), item.Request.URI.ToString(), item.Signature);
  873. }
  874. internal static string ToLogString(this Platinum.HttpRequestContext.SocketAddress item)
  875. {
  876. return string.Format("{{ IP:{0}, Port:{1} }}",
  877. item.ip, item.port);
  878. }
  879. }
  880. }