BaseItem.cs 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.IO;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.Localization;
  6. using MediaBrowser.Controller.Persistence;
  7. using MediaBrowser.Controller.Providers;
  8. using MediaBrowser.Controller.Resolvers;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.Logging;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Runtime.Serialization;
  16. using System.Text;
  17. using System.Threading;
  18. using System.Threading.Tasks;
  19. namespace MediaBrowser.Controller.Entities
  20. {
  21. /// <summary>
  22. /// Class BaseItem
  23. /// </summary>
  24. public abstract class BaseItem : IHasProviderIds
  25. {
  26. protected BaseItem()
  27. {
  28. Genres = new List<string>();
  29. TrailerUrls = new List<string>();
  30. Studios = new List<string>();
  31. People = new List<PersonInfo>();
  32. CriticReviews = new List<ItemReview>();
  33. Taglines = new List<string>();
  34. }
  35. /// <summary>
  36. /// The trailer folder name
  37. /// </summary>
  38. public const string TrailerFolderName = "trailers";
  39. public const string ThemeSongsFolderName = "theme-music";
  40. /// <summary>
  41. /// Gets or sets the name.
  42. /// </summary>
  43. /// <value>The name.</value>
  44. public virtual string Name { get; set; }
  45. /// <summary>
  46. /// Gets or sets the id.
  47. /// </summary>
  48. /// <value>The id.</value>
  49. public virtual Guid Id { get; set; }
  50. /// <summary>
  51. /// Gets or sets the path.
  52. /// </summary>
  53. /// <value>The path.</value>
  54. public virtual string Path { get; set; }
  55. /// <summary>
  56. /// Gets or sets the type of the location.
  57. /// </summary>
  58. /// <value>The type of the location.</value>
  59. public virtual LocationType LocationType
  60. {
  61. get
  62. {
  63. if (string.IsNullOrEmpty(Path))
  64. {
  65. return LocationType.Virtual;
  66. }
  67. return System.IO.Path.IsPathRooted(Path) ? LocationType.FileSystem : LocationType.Remote;
  68. }
  69. }
  70. /// <summary>
  71. /// This is just a helper for convenience
  72. /// </summary>
  73. /// <value>The primary image path.</value>
  74. [IgnoreDataMember]
  75. public string PrimaryImagePath
  76. {
  77. get { return GetImage(ImageType.Primary); }
  78. set { SetImage(ImageType.Primary, value); }
  79. }
  80. /// <summary>
  81. /// Gets or sets the images.
  82. /// </summary>
  83. /// <value>The images.</value>
  84. public virtual Dictionary<ImageType, string> Images { get; set; }
  85. /// <summary>
  86. /// Gets or sets the date created.
  87. /// </summary>
  88. /// <value>The date created.</value>
  89. public DateTime DateCreated { get; set; }
  90. /// <summary>
  91. /// Gets or sets the date modified.
  92. /// </summary>
  93. /// <value>The date modified.</value>
  94. public DateTime DateModified { get; set; }
  95. /// <summary>
  96. /// The logger
  97. /// </summary>
  98. public static ILogger Logger { get; set; }
  99. public static ILibraryManager LibraryManager { get; set; }
  100. public static IServerConfigurationManager ConfigurationManager { get; set; }
  101. public static IProviderManager ProviderManager { get; set; }
  102. /// <summary>
  103. /// Returns a <see cref="System.String" /> that represents this instance.
  104. /// </summary>
  105. /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
  106. public override string ToString()
  107. {
  108. return Name;
  109. }
  110. /// <summary>
  111. /// Returns true if this item should not attempt to fetch metadata
  112. /// </summary>
  113. /// <value><c>true</c> if [dont fetch meta]; otherwise, <c>false</c>.</value>
  114. [IgnoreDataMember]
  115. public virtual bool DontFetchMeta
  116. {
  117. get
  118. {
  119. if (Path != null)
  120. {
  121. return Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1;
  122. }
  123. return false;
  124. }
  125. }
  126. /// <summary>
  127. /// Determines whether the item has a saved local image of the specified name (jpg or png).
  128. /// </summary>
  129. /// <param name="name">The name.</param>
  130. /// <returns><c>true</c> if [has local image] [the specified item]; otherwise, <c>false</c>.</returns>
  131. /// <exception cref="System.ArgumentNullException">name</exception>
  132. public bool HasLocalImage(string name)
  133. {
  134. if (string.IsNullOrEmpty(name))
  135. {
  136. throw new ArgumentNullException("name");
  137. }
  138. return ResolveArgs.ContainsMetaFileByName(name + ".jpg") ||
  139. ResolveArgs.ContainsMetaFileByName(name + ".png");
  140. }
  141. /// <summary>
  142. /// Should be overridden to return the proper folder where metadata lives
  143. /// </summary>
  144. /// <value>The meta location.</value>
  145. [IgnoreDataMember]
  146. public virtual string MetaLocation
  147. {
  148. get
  149. {
  150. return Path ?? "";
  151. }
  152. }
  153. /// <summary>
  154. /// The _provider data
  155. /// </summary>
  156. private Dictionary<Guid, BaseProviderInfo> _providerData;
  157. /// <summary>
  158. /// Holds persistent data for providers like last refresh date.
  159. /// Providers can use this to determine if they need to refresh.
  160. /// The BaseProviderInfo class can be extended to hold anything a provider may need.
  161. /// Keyed by a unique provider ID.
  162. /// </summary>
  163. /// <value>The provider data.</value>
  164. public Dictionary<Guid, BaseProviderInfo> ProviderData
  165. {
  166. get
  167. {
  168. return _providerData ?? (_providerData = new Dictionary<Guid, BaseProviderInfo>());
  169. }
  170. set
  171. {
  172. _providerData = value;
  173. }
  174. }
  175. /// <summary>
  176. /// The _file system stamp
  177. /// </summary>
  178. private Guid? _fileSystemStamp;
  179. /// <summary>
  180. /// Gets a directory stamp, in the form of a string, that can be used for
  181. /// comparison purposes to determine if the file system entries for this item have changed.
  182. /// </summary>
  183. /// <value>The file system stamp.</value>
  184. [IgnoreDataMember]
  185. public Guid FileSystemStamp
  186. {
  187. get
  188. {
  189. if (!_fileSystemStamp.HasValue)
  190. {
  191. _fileSystemStamp = GetFileSystemStamp();
  192. }
  193. return _fileSystemStamp.Value;
  194. }
  195. }
  196. /// <summary>
  197. /// Gets the type of the media.
  198. /// </summary>
  199. /// <value>The type of the media.</value>
  200. [IgnoreDataMember]
  201. public virtual string MediaType
  202. {
  203. get
  204. {
  205. return null;
  206. }
  207. }
  208. /// <summary>
  209. /// Gets a directory stamp, in the form of a string, that can be used for
  210. /// comparison purposes to determine if the file system entries for this item have changed.
  211. /// </summary>
  212. /// <returns>Guid.</returns>
  213. private Guid GetFileSystemStamp()
  214. {
  215. // If there's no path or the item is a file, there's nothing to do
  216. if (LocationType != LocationType.FileSystem || !ResolveArgs.IsDirectory)
  217. {
  218. return Guid.Empty;
  219. }
  220. var sb = new StringBuilder();
  221. // Record the name of each file
  222. // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order
  223. foreach (var file in ResolveArgs.FileSystemChildren.OrderBy(f => f.Name))
  224. {
  225. sb.Append(file.Name);
  226. }
  227. foreach (var file in ResolveArgs.MetadataFiles.OrderBy(f => f.Name))
  228. {
  229. sb.Append(file.Name);
  230. }
  231. return sb.ToString().GetMD5();
  232. }
  233. /// <summary>
  234. /// The _resolve args
  235. /// </summary>
  236. private ItemResolveArgs _resolveArgs;
  237. /// <summary>
  238. /// The _resolve args initialized
  239. /// </summary>
  240. private bool _resolveArgsInitialized;
  241. /// <summary>
  242. /// The _resolve args sync lock
  243. /// </summary>
  244. private object _resolveArgsSyncLock = new object();
  245. /// <summary>
  246. /// We attach these to the item so that we only ever have to hit the file system once
  247. /// (this includes the children of the containing folder)
  248. /// Use ResolveArgs.FileSystemDictionary to check for the existence of files instead of File.Exists
  249. /// </summary>
  250. /// <value>The resolve args.</value>
  251. [IgnoreDataMember]
  252. public ItemResolveArgs ResolveArgs
  253. {
  254. get
  255. {
  256. try
  257. {
  258. LazyInitializer.EnsureInitialized(ref _resolveArgs, ref _resolveArgsInitialized, ref _resolveArgsSyncLock, () => CreateResolveArgs());
  259. }
  260. catch (IOException ex)
  261. {
  262. Logger.ErrorException("Error creating resolve args for ", ex, Path);
  263. throw;
  264. }
  265. return _resolveArgs;
  266. }
  267. set
  268. {
  269. _resolveArgs = value;
  270. _resolveArgsInitialized = value != null;
  271. // Null this out so that it can be lazy loaded again
  272. _fileSystemStamp = null;
  273. }
  274. }
  275. /// <summary>
  276. /// Resets the resolve args.
  277. /// </summary>
  278. /// <param name="pathInfo">The path info.</param>
  279. public void ResetResolveArgs(FileSystemInfo pathInfo)
  280. {
  281. ResolveArgs = CreateResolveArgs(pathInfo);
  282. }
  283. /// <summary>
  284. /// Creates ResolveArgs on demand
  285. /// </summary>
  286. /// <param name="pathInfo">The path info.</param>
  287. /// <returns>ItemResolveArgs.</returns>
  288. /// <exception cref="System.IO.IOException">Unable to retrieve file system info for + path</exception>
  289. protected internal virtual ItemResolveArgs CreateResolveArgs(FileSystemInfo pathInfo = null)
  290. {
  291. var path = Path;
  292. // non file-system entries will not have a path
  293. if (LocationType != LocationType.FileSystem || string.IsNullOrEmpty(path))
  294. {
  295. return new ItemResolveArgs(ConfigurationManager.ApplicationPaths);
  296. }
  297. if (UseParentPathToCreateResolveArgs)
  298. {
  299. path = System.IO.Path.GetDirectoryName(path);
  300. }
  301. pathInfo = pathInfo ?? FileSystem.GetFileSystemInfo(path);
  302. if (pathInfo == null || !pathInfo.Exists)
  303. {
  304. throw new IOException("Unable to retrieve file system info for " + path);
  305. }
  306. var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths)
  307. {
  308. FileInfo = pathInfo,
  309. Path = path,
  310. Parent = Parent
  311. };
  312. // Gather child folder and files
  313. if (args.IsDirectory)
  314. {
  315. var isPhysicalRoot = args.IsPhysicalRoot;
  316. // When resolving the root, we need it's grandchildren (children of user views)
  317. var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
  318. args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, Logger, flattenFolderDepth: flattenFolderDepth, args: args, resolveShortcuts: isPhysicalRoot || args.IsVf);
  319. }
  320. //update our dates
  321. EntityResolutionHelper.EnsureDates(this, args);
  322. return args;
  323. }
  324. /// <summary>
  325. /// Some subclasses will stop resolving at a directory and point their Path to a file within. This will help ensure the on-demand resolve args are identical to the
  326. /// original ones.
  327. /// </summary>
  328. /// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
  329. [IgnoreDataMember]
  330. protected virtual bool UseParentPathToCreateResolveArgs
  331. {
  332. get
  333. {
  334. return false;
  335. }
  336. }
  337. /// <summary>
  338. /// Gets or sets the name of the forced sort.
  339. /// </summary>
  340. /// <value>The name of the forced sort.</value>
  341. public string ForcedSortName { get; set; }
  342. private string _sortName;
  343. /// <summary>
  344. /// Gets or sets the name of the sort.
  345. /// </summary>
  346. /// <value>The name of the sort.</value>
  347. [IgnoreDataMember]
  348. public string SortName
  349. {
  350. get
  351. {
  352. return ForcedSortName ?? _sortName ?? (_sortName = CreateSortName());
  353. }
  354. }
  355. /// <summary>
  356. /// Creates the name of the sort.
  357. /// </summary>
  358. /// <returns>System.String.</returns>
  359. protected virtual string CreateSortName()
  360. {
  361. if (Name == null) return null; //some items may not have name filled in properly
  362. var sortable = Name.Trim().ToLower();
  363. sortable = ConfigurationManager.Configuration.SortRemoveCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), string.Empty));
  364. sortable = ConfigurationManager.Configuration.SortReplaceCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), " "));
  365. foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
  366. {
  367. var searchLower = search.ToLower();
  368. // Remove from beginning if a space follows
  369. if (sortable.StartsWith(searchLower + " "))
  370. {
  371. sortable = sortable.Remove(0, searchLower.Length + 1);
  372. }
  373. // Remove from middle if surrounded by spaces
  374. sortable = sortable.Replace(" " + searchLower + " ", " ");
  375. // Remove from end if followed by a space
  376. if (sortable.EndsWith(" " + searchLower))
  377. {
  378. sortable = sortable.Remove(sortable.Length - (searchLower.Length + 1));
  379. }
  380. }
  381. return sortable;
  382. }
  383. /// <summary>
  384. /// Gets or sets the parent.
  385. /// </summary>
  386. /// <value>The parent.</value>
  387. [IgnoreDataMember]
  388. public Folder Parent { get; set; }
  389. /// <summary>
  390. /// Gets the collection folder parent.
  391. /// </summary>
  392. /// <value>The collection folder parent.</value>
  393. [IgnoreDataMember]
  394. public Folder CollectionFolder
  395. {
  396. get
  397. {
  398. if (this is AggregateFolder)
  399. {
  400. return null;
  401. }
  402. if (IsFolder)
  403. {
  404. var iCollectionFolder = this as ICollectionFolder;
  405. if (iCollectionFolder != null)
  406. {
  407. return (Folder)this;
  408. }
  409. }
  410. var parent = Parent;
  411. while (parent != null)
  412. {
  413. var iCollectionFolder = parent as ICollectionFolder;
  414. if (iCollectionFolder != null)
  415. {
  416. return parent;
  417. }
  418. parent = parent.Parent;
  419. }
  420. return null;
  421. }
  422. }
  423. /// <summary>
  424. /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
  425. /// </summary>
  426. /// <value>The premiere date.</value>
  427. public DateTime? PremiereDate { get; set; }
  428. /// <summary>
  429. /// Gets or sets the end date.
  430. /// </summary>
  431. /// <value>The end date.</value>
  432. public DateTime? EndDate { get; set; }
  433. /// <summary>
  434. /// Gets or sets the display type of the media.
  435. /// </summary>
  436. /// <value>The display type of the media.</value>
  437. public virtual string DisplayMediaType { get; set; }
  438. /// <summary>
  439. /// Gets or sets the backdrop image paths.
  440. /// </summary>
  441. /// <value>The backdrop image paths.</value>
  442. public List<string> BackdropImagePaths { get; set; }
  443. /// <summary>
  444. /// Gets or sets the screenshot image paths.
  445. /// </summary>
  446. /// <value>The screenshot image paths.</value>
  447. public List<string> ScreenshotImagePaths { get; set; }
  448. /// <summary>
  449. /// Gets or sets the official rating.
  450. /// </summary>
  451. /// <value>The official rating.</value>
  452. public virtual string OfficialRating { get; set; }
  453. /// <summary>
  454. /// Gets or sets the custom rating.
  455. /// </summary>
  456. /// <value>The custom rating.</value>
  457. public virtual string CustomRating { get; set; }
  458. /// <summary>
  459. /// Gets or sets the language.
  460. /// </summary>
  461. /// <value>The language.</value>
  462. public string Language { get; set; }
  463. /// <summary>
  464. /// Gets or sets the overview.
  465. /// </summary>
  466. /// <value>The overview.</value>
  467. public string Overview { get; set; }
  468. /// <summary>
  469. /// Gets or sets the taglines.
  470. /// </summary>
  471. /// <value>The taglines.</value>
  472. public List<string> Taglines { get; set; }
  473. /// <summary>
  474. /// Gets or sets the people.
  475. /// </summary>
  476. /// <value>The people.</value>
  477. public List<PersonInfo> People { get; set; }
  478. /// <summary>
  479. /// Override this if you need to combine/collapse person information
  480. /// </summary>
  481. /// <value>All people.</value>
  482. [IgnoreDataMember]
  483. public virtual IEnumerable<PersonInfo> AllPeople
  484. {
  485. get { return People; }
  486. }
  487. /// <summary>
  488. /// Gets or sets the studios.
  489. /// </summary>
  490. /// <value>The studios.</value>
  491. public virtual List<string> Studios { get; set; }
  492. /// <summary>
  493. /// Gets or sets the genres.
  494. /// </summary>
  495. /// <value>The genres.</value>
  496. public virtual List<string> Genres { get; set; }
  497. /// <summary>
  498. /// Gets or sets the home page URL.
  499. /// </summary>
  500. /// <value>The home page URL.</value>
  501. public string HomePageUrl { get; set; }
  502. /// <summary>
  503. /// Gets or sets the budget.
  504. /// </summary>
  505. /// <value>The budget.</value>
  506. public double? Budget { get; set; }
  507. /// <summary>
  508. /// Gets or sets the revenue.
  509. /// </summary>
  510. /// <value>The revenue.</value>
  511. public double? Revenue { get; set; }
  512. /// <summary>
  513. /// Gets or sets the production locations.
  514. /// </summary>
  515. /// <value>The production locations.</value>
  516. public List<string> ProductionLocations { get; set; }
  517. /// <summary>
  518. /// Gets or sets the critic rating.
  519. /// </summary>
  520. /// <value>The critic rating.</value>
  521. public float? CriticRating { get; set; }
  522. /// <summary>
  523. /// Gets or sets the critic rating summary.
  524. /// </summary>
  525. /// <value>The critic rating summary.</value>
  526. public string CriticRatingSummary { get; set; }
  527. /// <summary>
  528. /// Gets or sets the community rating.
  529. /// </summary>
  530. /// <value>The community rating.</value>
  531. public float? CommunityRating { get; set; }
  532. /// <summary>
  533. /// Gets or sets the run time ticks.
  534. /// </summary>
  535. /// <value>The run time ticks.</value>
  536. public long? RunTimeTicks { get; set; }
  537. /// <summary>
  538. /// Gets or sets the aspect ratio.
  539. /// </summary>
  540. /// <value>The aspect ratio.</value>
  541. public string AspectRatio { get; set; }
  542. /// <summary>
  543. /// Gets or sets the production year.
  544. /// </summary>
  545. /// <value>The production year.</value>
  546. public virtual int? ProductionYear { get; set; }
  547. /// <summary>
  548. /// If the item is part of a series, this is it's number in the series.
  549. /// This could be episode number, album track number, etc.
  550. /// </summary>
  551. /// <value>The index number.</value>
  552. public int? IndexNumber { get; set; }
  553. /// <summary>
  554. /// For an episode this could be the season number, or for a song this could be the disc number.
  555. /// </summary>
  556. /// <value>The parent index number.</value>
  557. public int? ParentIndexNumber { get; set; }
  558. /// <summary>
  559. /// Gets or sets the critic reviews.
  560. /// </summary>
  561. /// <value>The critic reviews.</value>
  562. public List<ItemReview> CriticReviews { get; set; }
  563. /// <summary>
  564. /// The _local trailers
  565. /// </summary>
  566. private List<Trailer> _localTrailers;
  567. /// <summary>
  568. /// The _local trailers initialized
  569. /// </summary>
  570. private bool _localTrailersInitialized;
  571. /// <summary>
  572. /// The _local trailers sync lock
  573. /// </summary>
  574. private object _localTrailersSyncLock = new object();
  575. /// <summary>
  576. /// Gets the local trailers.
  577. /// </summary>
  578. /// <value>The local trailers.</value>
  579. [IgnoreDataMember]
  580. public List<Trailer> LocalTrailers
  581. {
  582. get
  583. {
  584. LazyInitializer.EnsureInitialized(ref _localTrailers, ref _localTrailersInitialized, ref _localTrailersSyncLock, LoadLocalTrailers);
  585. return _localTrailers;
  586. }
  587. private set
  588. {
  589. _localTrailers = value;
  590. if (value == null)
  591. {
  592. _localTrailersInitialized = false;
  593. }
  594. }
  595. }
  596. private List<Audio.Audio> _themeSongs;
  597. private bool _themeSongsInitialized;
  598. private object _themeSongsSyncLock = new object();
  599. [IgnoreDataMember]
  600. public List<Audio.Audio> ThemeSongs
  601. {
  602. get
  603. {
  604. LazyInitializer.EnsureInitialized(ref _themeSongs, ref _themeSongsInitialized, ref _themeSongsSyncLock, LoadThemeSongs);
  605. return _themeSongs;
  606. }
  607. private set
  608. {
  609. _themeSongs = value;
  610. if (value == null)
  611. {
  612. _themeSongsInitialized = false;
  613. }
  614. }
  615. }
  616. /// <summary>
  617. /// Loads local trailers from the file system
  618. /// </summary>
  619. /// <returns>List{Video}.</returns>
  620. private List<Trailer> LoadLocalTrailers()
  621. {
  622. ItemResolveArgs resolveArgs;
  623. try
  624. {
  625. resolveArgs = ResolveArgs;
  626. }
  627. catch (IOException ex)
  628. {
  629. Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
  630. return new List<Trailer>();
  631. }
  632. if (!resolveArgs.IsDirectory)
  633. {
  634. return new List<Trailer>();
  635. }
  636. var folder = resolveArgs.GetFileSystemEntryByName(TrailerFolderName);
  637. // Path doesn't exist. No biggie
  638. if (folder == null)
  639. {
  640. return new List<Trailer>();
  641. }
  642. IEnumerable<FileSystemInfo> files;
  643. try
  644. {
  645. files = new DirectoryInfo(folder.FullName).EnumerateFiles();
  646. }
  647. catch (IOException ex)
  648. {
  649. Logger.ErrorException("Error loading trailers for {0}", ex, Name);
  650. return new List<Trailer>();
  651. }
  652. return LibraryManager.ResolvePaths<Trailer>(files, null).Select(video =>
  653. {
  654. // Try to retrieve it from the db. If we don't find it, use the resolved version
  655. var dbItem = LibraryManager.RetrieveItem(video.Id) as Trailer;
  656. if (dbItem != null)
  657. {
  658. dbItem.ResolveArgs = video.ResolveArgs;
  659. video = dbItem;
  660. }
  661. return video;
  662. }).ToList();
  663. }
  664. /// <summary>
  665. /// Loads the theme songs.
  666. /// </summary>
  667. /// <returns>List{Audio.Audio}.</returns>
  668. private List<Audio.Audio> LoadThemeSongs()
  669. {
  670. ItemResolveArgs resolveArgs;
  671. try
  672. {
  673. resolveArgs = ResolveArgs;
  674. }
  675. catch (IOException ex)
  676. {
  677. Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
  678. return new List<Audio.Audio>();
  679. }
  680. if (!resolveArgs.IsDirectory)
  681. {
  682. return new List<Audio.Audio>();
  683. }
  684. var folder = resolveArgs.GetFileSystemEntryByName(ThemeSongsFolderName);
  685. // Path doesn't exist. No biggie
  686. if (folder == null)
  687. {
  688. return new List<Audio.Audio>();
  689. }
  690. IEnumerable<FileSystemInfo> files;
  691. try
  692. {
  693. files = new DirectoryInfo(folder.FullName).EnumerateFiles();
  694. }
  695. catch (IOException ex)
  696. {
  697. Logger.ErrorException("Error loading theme songs for {0}", ex, Name);
  698. return new List<Audio.Audio>();
  699. }
  700. return LibraryManager.ResolvePaths<Audio.Audio>(files, null).Select(audio =>
  701. {
  702. // Try to retrieve it from the db. If we don't find it, use the resolved version
  703. var dbItem = LibraryManager.RetrieveItem(audio.Id) as Audio.Audio;
  704. if (dbItem != null)
  705. {
  706. dbItem.ResolveArgs = audio.ResolveArgs;
  707. audio = dbItem;
  708. }
  709. return audio;
  710. }).ToList();
  711. }
  712. /// <summary>
  713. /// Overrides the base implementation to refresh metadata for local trailers
  714. /// </summary>
  715. /// <param name="cancellationToken">The cancellation token.</param>
  716. /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
  717. /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
  718. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  719. /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
  720. /// <returns>true if a provider reports we changed</returns>
  721. public virtual async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
  722. {
  723. if (resetResolveArgs)
  724. {
  725. ResolveArgs = null;
  726. }
  727. // Lazy load these again
  728. LocalTrailers = null;
  729. ThemeSongs = null;
  730. // Refresh for the item
  731. var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders);
  732. cancellationToken.ThrowIfCancellationRequested();
  733. // Refresh metadata for local trailers
  734. var trailerTasks = LocalTrailers.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
  735. var themeSongTasks = ThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
  736. cancellationToken.ThrowIfCancellationRequested();
  737. // Await the trailer tasks
  738. await Task.WhenAll(trailerTasks).ConfigureAwait(false);
  739. await Task.WhenAll(themeSongTasks).ConfigureAwait(false);
  740. cancellationToken.ThrowIfCancellationRequested();
  741. // Get the result from the item task
  742. var changed = await itemRefreshTask.ConfigureAwait(false);
  743. if (changed || forceSave)
  744. {
  745. cancellationToken.ThrowIfCancellationRequested();
  746. await LibraryManager.SaveItem(this, cancellationToken).ConfigureAwait(false);
  747. }
  748. return changed;
  749. }
  750. /// <summary>
  751. /// Clear out all metadata properties. Extend for sub-classes.
  752. /// </summary>
  753. public virtual void ClearMetaValues()
  754. {
  755. Images = null;
  756. ForcedSortName = null;
  757. PremiereDate = null;
  758. BackdropImagePaths = null;
  759. OfficialRating = null;
  760. CustomRating = null;
  761. Overview = null;
  762. Taglines.Clear();
  763. Language = null;
  764. Studios.Clear();
  765. Genres.Clear();
  766. CommunityRating = null;
  767. RunTimeTicks = null;
  768. AspectRatio = null;
  769. ProductionYear = null;
  770. ProviderIds = null;
  771. DisplayMediaType = GetType().Name;
  772. ResolveArgs = null;
  773. }
  774. /// <summary>
  775. /// Gets or sets the trailer URL.
  776. /// </summary>
  777. /// <value>The trailer URL.</value>
  778. public List<string> TrailerUrls { get; set; }
  779. /// <summary>
  780. /// Gets or sets the provider ids.
  781. /// </summary>
  782. /// <value>The provider ids.</value>
  783. public Dictionary<string, string> ProviderIds { get; set; }
  784. /// <summary>
  785. /// Override this to false if class should be ignored for indexing purposes
  786. /// </summary>
  787. /// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
  788. [IgnoreDataMember]
  789. public virtual bool IncludeInIndex
  790. {
  791. get { return true; }
  792. }
  793. /// <summary>
  794. /// Override this to true if class should be grouped under a container in indicies
  795. /// The container class should be defined via IndexContainer
  796. /// </summary>
  797. /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
  798. [IgnoreDataMember]
  799. public virtual bool GroupInIndex
  800. {
  801. get { return false; }
  802. }
  803. /// <summary>
  804. /// Override this to return the folder that should be used to construct a container
  805. /// for this item in an index. GroupInIndex should be true as well.
  806. /// </summary>
  807. /// <value>The index container.</value>
  808. [IgnoreDataMember]
  809. public virtual Folder IndexContainer
  810. {
  811. get { return null; }
  812. }
  813. /// <summary>
  814. /// Gets the user data key.
  815. /// </summary>
  816. /// <returns>System.String.</returns>
  817. public virtual string GetUserDataKey()
  818. {
  819. return Id.ToString();
  820. }
  821. /// <summary>
  822. /// Determines if a given user has access to this item
  823. /// </summary>
  824. /// <param name="user">The user.</param>
  825. /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
  826. /// <exception cref="System.ArgumentNullException"></exception>
  827. public bool IsParentalAllowed(User user)
  828. {
  829. if (user == null)
  830. {
  831. throw new ArgumentNullException("user");
  832. }
  833. if (user.Configuration.MaxParentalRating == null)
  834. {
  835. return true;
  836. }
  837. return Ratings.Level(CustomRating ?? OfficialRating) <= user.Configuration.MaxParentalRating.Value;
  838. }
  839. /// <summary>
  840. /// Determines if this folder should be visible to a given user.
  841. /// Default is just parental allowed. Can be overridden for more functionality.
  842. /// </summary>
  843. /// <param name="user">The user.</param>
  844. /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
  845. /// <exception cref="System.ArgumentNullException">user</exception>
  846. public virtual bool IsVisible(User user)
  847. {
  848. if (user == null)
  849. {
  850. throw new ArgumentNullException("user");
  851. }
  852. return IsParentalAllowed(user);
  853. }
  854. /// <summary>
  855. /// Finds the particular item by searching through our parents and, if not found there, loading from repo
  856. /// </summary>
  857. /// <param name="id">The id.</param>
  858. /// <returns>BaseItem.</returns>
  859. /// <exception cref="System.ArgumentException"></exception>
  860. protected BaseItem FindParentItem(Guid id)
  861. {
  862. if (id == Guid.Empty)
  863. {
  864. throw new ArgumentException();
  865. }
  866. var parent = Parent;
  867. while (parent != null && !parent.IsRoot)
  868. {
  869. if (parent.Id == id) return parent;
  870. parent = parent.Parent;
  871. }
  872. //not found - load from repo
  873. return LibraryManager.RetrieveItem(id);
  874. }
  875. /// <summary>
  876. /// Gets a value indicating whether this instance is folder.
  877. /// </summary>
  878. /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
  879. [IgnoreDataMember]
  880. public virtual bool IsFolder
  881. {
  882. get
  883. {
  884. return false;
  885. }
  886. }
  887. /// <summary>
  888. /// Determine if we have changed vs the passed in copy
  889. /// </summary>
  890. /// <param name="copy">The copy.</param>
  891. /// <returns><c>true</c> if the specified copy has changed; otherwise, <c>false</c>.</returns>
  892. /// <exception cref="System.ArgumentNullException"></exception>
  893. public virtual bool HasChanged(BaseItem copy)
  894. {
  895. if (copy == null)
  896. {
  897. throw new ArgumentNullException();
  898. }
  899. var changed = copy.DateModified != DateModified;
  900. if (changed)
  901. {
  902. Logger.Debug(Name + " changed - original creation: " + DateCreated + " new creation: " + copy.DateCreated + " original modified: " + DateModified + " new modified: " + copy.DateModified);
  903. }
  904. return changed;
  905. }
  906. /// <summary>
  907. /// Determines if the item is considered new based on user settings
  908. /// </summary>
  909. /// <returns><c>true</c> if [is recently added] [the specified user]; otherwise, <c>false</c>.</returns>
  910. /// <exception cref="System.ArgumentNullException"></exception>
  911. public bool IsRecentlyAdded()
  912. {
  913. return (DateTime.UtcNow - DateCreated).TotalDays < ConfigurationManager.Configuration.RecentItemDays;
  914. }
  915. /// <summary>
  916. /// Adds a person to the item
  917. /// </summary>
  918. /// <param name="person">The person.</param>
  919. /// <exception cref="System.ArgumentNullException"></exception>
  920. public void AddPerson(PersonInfo person)
  921. {
  922. if (person == null)
  923. {
  924. throw new ArgumentNullException("person");
  925. }
  926. if (string.IsNullOrWhiteSpace(person.Name))
  927. {
  928. throw new ArgumentNullException();
  929. }
  930. // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
  931. if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
  932. {
  933. var existing = People.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase));
  934. if (existing != null)
  935. {
  936. existing.Type = PersonType.GuestStar;
  937. return;
  938. }
  939. }
  940. if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
  941. {
  942. // Only add actors if there isn't an existing one of type Actor or GuestStar
  943. if (!People.Any(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))))
  944. {
  945. People.Add(person);
  946. }
  947. }
  948. else
  949. {
  950. // Check for dupes based on the combination of Name and Type
  951. if (!People.Any(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(person.Type, StringComparison.OrdinalIgnoreCase)))
  952. {
  953. People.Add(person);
  954. }
  955. }
  956. }
  957. /// <summary>
  958. /// Adds a studio to the item
  959. /// </summary>
  960. /// <param name="name">The name.</param>
  961. /// <exception cref="System.ArgumentNullException"></exception>
  962. public void AddStudio(string name)
  963. {
  964. if (string.IsNullOrWhiteSpace(name))
  965. {
  966. throw new ArgumentNullException("name");
  967. }
  968. if (!Studios.Contains(name, StringComparer.OrdinalIgnoreCase))
  969. {
  970. Studios.Add(name);
  971. }
  972. }
  973. /// <summary>
  974. /// Adds a tagline to the item
  975. /// </summary>
  976. /// <param name="name">The name.</param>
  977. /// <exception cref="System.ArgumentNullException"></exception>
  978. public void AddTagline(string name)
  979. {
  980. if (string.IsNullOrWhiteSpace(name))
  981. {
  982. throw new ArgumentNullException("name");
  983. }
  984. if (!Taglines.Contains(name, StringComparer.OrdinalIgnoreCase))
  985. {
  986. Taglines.Add(name);
  987. }
  988. }
  989. /// <summary>
  990. /// Adds a TrailerUrl to the item
  991. /// </summary>
  992. /// <param name="url">The URL.</param>
  993. /// <exception cref="System.ArgumentNullException"></exception>
  994. public void AddTrailerUrl(string url)
  995. {
  996. if (string.IsNullOrWhiteSpace(url))
  997. {
  998. throw new ArgumentNullException("url");
  999. }
  1000. if (TrailerUrls == null)
  1001. {
  1002. TrailerUrls = new List<string>();
  1003. }
  1004. if (!TrailerUrls.Contains(url, StringComparer.OrdinalIgnoreCase))
  1005. {
  1006. TrailerUrls.Add(url);
  1007. }
  1008. }
  1009. /// <summary>
  1010. /// Adds a genre to the item
  1011. /// </summary>
  1012. /// <param name="name">The name.</param>
  1013. /// <exception cref="System.ArgumentNullException"></exception>
  1014. public void AddGenre(string name)
  1015. {
  1016. if (string.IsNullOrWhiteSpace(name))
  1017. {
  1018. throw new ArgumentNullException("name");
  1019. }
  1020. if (!Genres.Contains(name, StringComparer.OrdinalIgnoreCase))
  1021. {
  1022. Genres.Add(name);
  1023. }
  1024. }
  1025. /// <summary>
  1026. /// Adds the production location.
  1027. /// </summary>
  1028. /// <param name="location">The location.</param>
  1029. /// <exception cref="System.ArgumentNullException">location</exception>
  1030. public void AddProductionLocation(string location)
  1031. {
  1032. if (string.IsNullOrWhiteSpace(location))
  1033. {
  1034. throw new ArgumentNullException("location");
  1035. }
  1036. if (ProductionLocations == null)
  1037. {
  1038. ProductionLocations = new List<string>();
  1039. }
  1040. if (!ProductionLocations.Contains(location, StringComparer.OrdinalIgnoreCase))
  1041. {
  1042. ProductionLocations.Add(location);
  1043. }
  1044. }
  1045. /// <summary>
  1046. /// Marks the item as either played or unplayed
  1047. /// </summary>
  1048. /// <param name="user">The user.</param>
  1049. /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
  1050. /// <param name="userManager">The user manager.</param>
  1051. /// <returns>Task.</returns>
  1052. /// <exception cref="System.ArgumentNullException"></exception>
  1053. public virtual async Task SetPlayedStatus(User user, bool wasPlayed, IUserDataRepository userManager)
  1054. {
  1055. if (user == null)
  1056. {
  1057. throw new ArgumentNullException();
  1058. }
  1059. var key = GetUserDataKey();
  1060. var data = await userManager.GetUserData(user.Id, key).ConfigureAwait(false);
  1061. if (wasPlayed)
  1062. {
  1063. data.PlayCount = Math.Max(data.PlayCount, 1);
  1064. if (!data.LastPlayedDate.HasValue)
  1065. {
  1066. data.LastPlayedDate = DateTime.UtcNow;
  1067. }
  1068. }
  1069. else
  1070. {
  1071. //I think it is okay to do this here.
  1072. // if this is only called when a user is manually forcing something to un-played
  1073. // then it probably is what we want to do...
  1074. data.PlayCount = 0;
  1075. data.PlaybackPositionTicks = 0;
  1076. data.LastPlayedDate = null;
  1077. }
  1078. data.Played = wasPlayed;
  1079. await userManager.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
  1080. }
  1081. /// <summary>
  1082. /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
  1083. /// </summary>
  1084. /// <returns>Task.</returns>
  1085. public virtual Task ChangedExternally()
  1086. {
  1087. return RefreshMetadata(CancellationToken.None);
  1088. }
  1089. /// <summary>
  1090. /// Finds a parent of a given type
  1091. /// </summary>
  1092. /// <typeparam name="T"></typeparam>
  1093. /// <returns>``0.</returns>
  1094. public T FindParent<T>()
  1095. where T : Folder
  1096. {
  1097. var parent = Parent;
  1098. while (parent != null)
  1099. {
  1100. var result = parent as T;
  1101. if (result != null)
  1102. {
  1103. return result;
  1104. }
  1105. parent = parent.Parent;
  1106. }
  1107. return null;
  1108. }
  1109. /// <summary>
  1110. /// Gets an image
  1111. /// </summary>
  1112. /// <param name="type">The type.</param>
  1113. /// <returns>System.String.</returns>
  1114. /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
  1115. public string GetImage(ImageType type)
  1116. {
  1117. if (type == ImageType.Backdrop)
  1118. {
  1119. throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
  1120. }
  1121. if (type == ImageType.Screenshot)
  1122. {
  1123. throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
  1124. }
  1125. if (Images == null)
  1126. {
  1127. return null;
  1128. }
  1129. string val;
  1130. Images.TryGetValue(type, out val);
  1131. return val;
  1132. }
  1133. /// <summary>
  1134. /// Gets an image
  1135. /// </summary>
  1136. /// <param name="type">The type.</param>
  1137. /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
  1138. /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
  1139. public bool HasImage(ImageType type)
  1140. {
  1141. if (type == ImageType.Backdrop)
  1142. {
  1143. throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
  1144. }
  1145. if (type == ImageType.Screenshot)
  1146. {
  1147. throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
  1148. }
  1149. return !string.IsNullOrEmpty(GetImage(type));
  1150. }
  1151. /// <summary>
  1152. /// Sets an image
  1153. /// </summary>
  1154. /// <param name="type">The type.</param>
  1155. /// <param name="path">The path.</param>
  1156. /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
  1157. public void SetImage(ImageType type, string path)
  1158. {
  1159. if (type == ImageType.Backdrop)
  1160. {
  1161. throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
  1162. }
  1163. if (type == ImageType.Screenshot)
  1164. {
  1165. throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
  1166. }
  1167. var typeKey = type;
  1168. // If it's null remove the key from the dictionary
  1169. if (string.IsNullOrEmpty(path))
  1170. {
  1171. if (Images != null)
  1172. {
  1173. if (Images.ContainsKey(typeKey))
  1174. {
  1175. Images.Remove(typeKey);
  1176. }
  1177. }
  1178. }
  1179. else
  1180. {
  1181. // Ensure it exists
  1182. if (Images == null)
  1183. {
  1184. Images = new Dictionary<ImageType, string>();
  1185. }
  1186. Images[typeKey] = path;
  1187. }
  1188. }
  1189. /// <summary>
  1190. /// Deletes the image.
  1191. /// </summary>
  1192. /// <param name="type">The type.</param>
  1193. /// <returns>Task.</returns>
  1194. public async Task DeleteImage(ImageType type)
  1195. {
  1196. if (!HasImage(type))
  1197. {
  1198. return;
  1199. }
  1200. // Delete the source file
  1201. File.Delete(GetImage(type));
  1202. // Remove it from the item
  1203. SetImage(type, null);
  1204. // Refresh metadata
  1205. await RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
  1206. }
  1207. }
  1208. }