ImageFromMediaLocationProvider.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities;
  4. using MediaBrowser.Controller.Entities.Movies;
  5. using MediaBrowser.Controller.Entities.TV;
  6. using MediaBrowser.Controller.Library;
  7. using MediaBrowser.Controller.Providers;
  8. using MediaBrowser.Model.Entities;
  9. using MediaBrowser.Model.Logging;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Globalization;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. namespace MediaBrowser.Providers
  18. {
  19. /// <summary>
  20. /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
  21. /// </summary>
  22. public class ImageFromMediaLocationProvider : BaseMetadataProvider
  23. {
  24. protected readonly IFileSystem FileSystem;
  25. public ImageFromMediaLocationProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
  26. : base(logManager, configurationManager)
  27. {
  28. FileSystem = fileSystem;
  29. }
  30. public override ItemUpdateType ItemUpdateType
  31. {
  32. get
  33. {
  34. return ItemUpdateType.ImageUpdate;
  35. }
  36. }
  37. /// <summary>
  38. /// Supportses the specified item.
  39. /// </summary>
  40. /// <param name="item">The item.</param>
  41. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  42. public override bool Supports(BaseItem item)
  43. {
  44. if (item.LocationType == LocationType.FileSystem)
  45. {
  46. if (item.ResolveArgs.IsDirectory)
  47. {
  48. return true;
  49. }
  50. return item.IsInMixedFolder && item.Parent != null && !(item is Episode);
  51. }
  52. if (item.LocationType == LocationType.Virtual)
  53. {
  54. var season = item as Season;
  55. if (season != null)
  56. {
  57. var series = season.Series;
  58. if (series != null && series.LocationType == LocationType.FileSystem)
  59. {
  60. return true;
  61. }
  62. }
  63. }
  64. return false;
  65. }
  66. protected override IEnumerable<BaseItem> GetItemsForFileStampComparison(BaseItem item)
  67. {
  68. var season = item as Season;
  69. if (season != null)
  70. {
  71. var list = new List<BaseItem>();
  72. if (season.LocationType == LocationType.FileSystem)
  73. {
  74. list.Add(season);
  75. }
  76. var series = season.Series;
  77. if (series != null && series.LocationType == LocationType.FileSystem)
  78. {
  79. list.Add(series);
  80. }
  81. return list;
  82. }
  83. return base.GetItemsForFileStampComparison(item);
  84. }
  85. /// <summary>
  86. /// Gets the priority.
  87. /// </summary>
  88. /// <value>The priority.</value>
  89. public override MetadataProviderPriority Priority
  90. {
  91. get { return MetadataProviderPriority.First; }
  92. }
  93. /// <summary>
  94. /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
  95. /// </summary>
  96. /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
  97. protected override bool RefreshOnFileSystemStampChange
  98. {
  99. get
  100. {
  101. return true;
  102. }
  103. }
  104. /// <summary>
  105. /// Gets the filestamp extensions.
  106. /// </summary>
  107. /// <value>The filestamp extensions.</value>
  108. protected override string[] FilestampExtensions
  109. {
  110. get
  111. {
  112. return BaseItem.SupportedImageExtensions;
  113. }
  114. }
  115. /// <summary>
  116. /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
  117. /// </summary>
  118. /// <param name="item">The item.</param>
  119. /// <param name="force">if set to <c>true</c> [force].</param>
  120. /// <param name="cancellationToken">The cancellation token.</param>
  121. /// <returns>Task{System.Boolean}.</returns>
  122. public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
  123. {
  124. cancellationToken.ThrowIfCancellationRequested();
  125. // Make sure current image paths still exist
  126. item.ValidateImages();
  127. cancellationToken.ThrowIfCancellationRequested();
  128. // Make sure current backdrop paths still exist
  129. item.ValidateBackdrops();
  130. var hasScreenshots = item as IHasScreenshots;
  131. if (hasScreenshots != null)
  132. {
  133. hasScreenshots.ValidateScreenshots();
  134. }
  135. cancellationToken.ThrowIfCancellationRequested();
  136. var args = GetResolveArgsContainingImages(item);
  137. PopulateBaseItemImages(item, args);
  138. SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
  139. return TrueTaskResult;
  140. }
  141. private ItemResolveArgs GetResolveArgsContainingImages(BaseItem item)
  142. {
  143. if (item.LocationType != LocationType.FileSystem)
  144. {
  145. return null;
  146. }
  147. if (item.IsInMixedFolder)
  148. {
  149. if (item.Parent == null)
  150. {
  151. return item.ResolveArgs;
  152. }
  153. return item.Parent.ResolveArgs;
  154. }
  155. return item.ResolveArgs;
  156. }
  157. /// <summary>
  158. /// Gets the image.
  159. /// </summary>
  160. /// <param name="item">The item.</param>
  161. /// <param name="args">The args.</param>
  162. /// <param name="filenameWithoutExtension">The filename without extension.</param>
  163. /// <returns>FileSystemInfo.</returns>
  164. protected virtual FileSystemInfo GetImage(BaseItem item, ItemResolveArgs args, string filenameWithoutExtension)
  165. {
  166. if (string.IsNullOrEmpty(item.MetaLocation))
  167. {
  168. return null;
  169. }
  170. return BaseItem.SupportedImageExtensions
  171. .Select(i => args.GetMetaFileByPath(GetFullImagePath(item, args, filenameWithoutExtension, i)))
  172. .FirstOrDefault(i => i != null);
  173. }
  174. protected virtual string GetFullImagePath(BaseItem item, ItemResolveArgs args, string filenameWithoutExtension, string extension)
  175. {
  176. var path = item.MetaLocation;
  177. if (item.IsInMixedFolder)
  178. {
  179. var pathFilenameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path);
  180. // If the image filename and path file name match, just look for an image using the same full path as the item
  181. if (string.Equals(pathFilenameWithoutExtension, filenameWithoutExtension))
  182. {
  183. return Path.ChangeExtension(item.Path, extension);
  184. }
  185. return Path.Combine(path, pathFilenameWithoutExtension + "-" + filenameWithoutExtension + extension);
  186. }
  187. return Path.Combine(path, filenameWithoutExtension + extension);
  188. }
  189. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  190. /// <summary>
  191. /// Fills in image paths based on files win the folder
  192. /// </summary>
  193. /// <param name="item">The item.</param>
  194. /// <param name="args">The args.</param>
  195. private void PopulateBaseItemImages(BaseItem item, ItemResolveArgs args)
  196. {
  197. PopulatePrimaryImage(item, args);
  198. // Logo Image
  199. var image = GetImage(item, args, "logo");
  200. if (image != null)
  201. {
  202. item.SetImagePath(ImageType.Logo, image.FullName);
  203. }
  204. // Clearart
  205. image = GetImage(item, args, "clearart");
  206. if (image != null)
  207. {
  208. item.SetImagePath(ImageType.Art, image.FullName);
  209. }
  210. // Disc
  211. image = GetImage(item, args, "disc") ??
  212. GetImage(item, args, "cdart");
  213. if (image != null)
  214. {
  215. item.SetImagePath(ImageType.Disc, image.FullName);
  216. }
  217. // Box Image
  218. image = GetImage(item, args, "box");
  219. if (image != null)
  220. {
  221. item.SetImagePath(ImageType.Box, image.FullName);
  222. }
  223. // BoxRear Image
  224. image = GetImage(item, args, "boxrear");
  225. if (image != null)
  226. {
  227. item.SetImagePath(ImageType.BoxRear, image.FullName);
  228. }
  229. // Thumbnail Image
  230. image = GetImage(item, args, "menu");
  231. if (image != null)
  232. {
  233. item.SetImagePath(ImageType.Menu, image.FullName);
  234. }
  235. PopulateBanner(item, args);
  236. PopulateThumb(item, args);
  237. // Backdrop Image
  238. PopulateBackdrops(item, args);
  239. PopulateScreenshots(item, args);
  240. }
  241. private void PopulatePrimaryImage(BaseItem item, ItemResolveArgs args)
  242. {
  243. // Primary Image
  244. var image = GetImage(item, args, "folder") ??
  245. GetImage(item, args, "poster") ??
  246. GetImage(item, args, "cover") ??
  247. GetImage(item, args, "default");
  248. // Support plex/xbmc convention
  249. if (image == null && item is Series)
  250. {
  251. image = GetImage(item, args, "show");
  252. }
  253. // Support plex/xbmc convention
  254. if (image == null)
  255. {
  256. // Supprt xbmc conventions
  257. var season = item as Season;
  258. if (season != null && item.IndexNumber.HasValue && season.Series.LocationType == LocationType.FileSystem)
  259. {
  260. image = GetSeasonImageFromSeriesFolder(season, "-poster");
  261. }
  262. }
  263. // Support plex/xbmc convention
  264. if (image == null && (item is Movie || item is MusicVideo || item is AdultVideo))
  265. {
  266. image = GetImage(item, args, "movie");
  267. }
  268. // Look for a file with the same name as the item
  269. if (image == null && !string.IsNullOrEmpty(item.Path))
  270. {
  271. var name = Path.GetFileNameWithoutExtension(item.Path);
  272. if (!string.IsNullOrEmpty(name))
  273. {
  274. image = GetImage(item, args, name) ??
  275. GetImage(item, args, name + "-poster");
  276. }
  277. }
  278. if (image != null)
  279. {
  280. item.SetImagePath(ImageType.Primary, image.FullName);
  281. }
  282. }
  283. /// <summary>
  284. /// Populates the banner.
  285. /// </summary>
  286. /// <param name="item">The item.</param>
  287. /// <param name="args">The args.</param>
  288. private void PopulateBanner(BaseItem item, ItemResolveArgs args)
  289. {
  290. // Banner Image
  291. var image = GetImage(item, args, "banner");
  292. if (image == null)
  293. {
  294. // Supprt xbmc conventions
  295. var season = item as Season;
  296. if (season != null && item.IndexNumber.HasValue && season.Series.LocationType == LocationType.FileSystem)
  297. {
  298. image = GetSeasonImageFromSeriesFolder(season, "-banner");
  299. }
  300. }
  301. if (image != null)
  302. {
  303. item.SetImagePath(ImageType.Banner, image.FullName);
  304. }
  305. }
  306. /// <summary>
  307. /// Populates the thumb.
  308. /// </summary>
  309. /// <param name="item">The item.</param>
  310. /// <param name="args">The args.</param>
  311. private void PopulateThumb(BaseItem item, ItemResolveArgs args)
  312. {
  313. // Thumbnail Image
  314. var image = GetImage(item, args, "thumb") ??
  315. GetImage(item, args, "landscape");
  316. if (image == null)
  317. {
  318. // Supprt xbmc conventions
  319. var season = item as Season;
  320. if (season != null && item.IndexNumber.HasValue && season.Series.LocationType == LocationType.FileSystem)
  321. {
  322. image = GetSeasonImageFromSeriesFolder(season, "-landscape");
  323. }
  324. }
  325. if (image != null)
  326. {
  327. item.SetImagePath(ImageType.Thumb, image.FullName);
  328. }
  329. }
  330. /// <summary>
  331. /// Populates the backdrops.
  332. /// </summary>
  333. /// <param name="item">The item.</param>
  334. /// <param name="args">The args.</param>
  335. private void PopulateBackdrops(BaseItem item, ItemResolveArgs args)
  336. {
  337. var backdropFiles = new List<string>();
  338. PopulateBackdrops(item, args, backdropFiles, "backdrop", "backdrop");
  339. // Support {name}-fanart.ext
  340. if (!string.IsNullOrEmpty(item.Path))
  341. {
  342. var name = Path.GetFileNameWithoutExtension(item.Path);
  343. if (!string.IsNullOrEmpty(name))
  344. {
  345. var image = GetImage(item, args, name + "-fanart");
  346. if (image != null)
  347. {
  348. backdropFiles.Add(image.FullName);
  349. }
  350. }
  351. }
  352. // Support plex/xbmc conventions
  353. PopulateBackdrops(item, args, backdropFiles, "fanart", "fanart-");
  354. PopulateBackdrops(item, args, backdropFiles, "background", "background-");
  355. PopulateBackdrops(item, args, backdropFiles, "art", "art-");
  356. var season = item as Season;
  357. if (season != null && item.IndexNumber.HasValue && season.Series.LocationType == LocationType.FileSystem)
  358. {
  359. var image = GetSeasonImageFromSeriesFolder(season, "-fanart");
  360. if (image != null)
  361. {
  362. backdropFiles.Add(image.FullName);
  363. }
  364. }
  365. if (item.LocationType == LocationType.FileSystem)
  366. {
  367. PopulateBackdropsFromExtraFanart(args, backdropFiles);
  368. }
  369. if (backdropFiles.Count > 0)
  370. {
  371. item.BackdropImagePaths = backdropFiles;
  372. }
  373. }
  374. private FileSystemInfo GetSeasonImageFromSeriesFolder(Season season, string imageSuffix)
  375. {
  376. var series = season.Series;
  377. var seriesFolderArgs = series.ResolveArgs;
  378. var seasonNumber = season.IndexNumber;
  379. string filename = null;
  380. FileSystemInfo image;
  381. if (seasonNumber.HasValue)
  382. {
  383. var seasonMarker = seasonNumber.Value == 0
  384. ? "-specials"
  385. : seasonNumber.Value.ToString("00", _usCulture);
  386. // Get this one directly from the file system since we have to go up a level
  387. filename = "season" + seasonMarker + imageSuffix;
  388. image = GetImage(series, seriesFolderArgs, filename);
  389. if (image != null && image.Exists)
  390. {
  391. return image;
  392. }
  393. }
  394. var previousFilename = filename;
  395. // Try using the season name
  396. filename = season.Name.ToLower().Replace(" ", string.Empty) + imageSuffix;
  397. if (!string.Equals(previousFilename, filename))
  398. {
  399. image = GetImage(series, seriesFolderArgs, filename);
  400. if (image != null && image.Exists)
  401. {
  402. return image;
  403. }
  404. }
  405. return null;
  406. }
  407. /// <summary>
  408. /// Populates the backdrops from extra fanart.
  409. /// </summary>
  410. /// <param name="args">The args.</param>
  411. /// <param name="backdrops">The backdrops.</param>
  412. private void PopulateBackdropsFromExtraFanart(ItemResolveArgs args, List<string> backdrops)
  413. {
  414. if (!args.IsDirectory)
  415. {
  416. return;
  417. }
  418. if (args.ContainsFileSystemEntryByName("extrafanart"))
  419. {
  420. var path = Path.Combine(args.Path, "extrafanart");
  421. var imageFiles = Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly)
  422. .Where(i =>
  423. {
  424. var extension = Path.GetExtension(i);
  425. if (string.IsNullOrEmpty(extension))
  426. {
  427. return false;
  428. }
  429. return BaseItem.SupportedImageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
  430. })
  431. .ToList();
  432. backdrops.AddRange(imageFiles);
  433. }
  434. }
  435. /// <summary>
  436. /// Populates the backdrops.
  437. /// </summary>
  438. /// <param name="item">The item.</param>
  439. /// <param name="args">The args.</param>
  440. /// <param name="backdropFiles">The backdrop files.</param>
  441. /// <param name="filename">The filename.</param>
  442. /// <param name="numberedSuffix">The numbered suffix.</param>
  443. private void PopulateBackdrops(BaseItem item, ItemResolveArgs args, List<string> backdropFiles, string filename, string numberedSuffix)
  444. {
  445. var image = GetImage(item, args, filename);
  446. if (image != null)
  447. {
  448. backdropFiles.Add(image.FullName);
  449. }
  450. var unfound = 0;
  451. for (var i = 1; i <= 20; i++)
  452. {
  453. // Backdrop Image
  454. image = GetImage(item, args, numberedSuffix + i);
  455. if (image != null)
  456. {
  457. backdropFiles.Add(image.FullName);
  458. }
  459. else
  460. {
  461. unfound++;
  462. if (unfound >= 3)
  463. {
  464. break;
  465. }
  466. }
  467. }
  468. }
  469. /// <summary>
  470. /// Populates the screenshots.
  471. /// </summary>
  472. /// <param name="item">The item.</param>
  473. /// <param name="args">The args.</param>
  474. private void PopulateScreenshots(BaseItem item, ItemResolveArgs args)
  475. {
  476. // Screenshot Image
  477. var image = GetImage(item, args, "screenshot");
  478. var screenshotFiles = new List<string>();
  479. if (image != null)
  480. {
  481. screenshotFiles.Add(image.FullName);
  482. }
  483. var unfound = 0;
  484. for (var i = 1; i <= 20; i++)
  485. {
  486. // Screenshot Image
  487. image = GetImage(item, args, "screenshot" + i);
  488. if (image != null)
  489. {
  490. screenshotFiles.Add(image.FullName);
  491. }
  492. else
  493. {
  494. unfound++;
  495. if (unfound >= 3)
  496. {
  497. break;
  498. }
  499. }
  500. }
  501. if (screenshotFiles.Count > 0)
  502. {
  503. var hasScreenshots = item as IHasScreenshots;
  504. if (hasScreenshots != null)
  505. {
  506. hasScreenshots.ScreenshotImagePaths = screenshotFiles;
  507. }
  508. }
  509. }
  510. protected FileSystemInfo GetImageFromLocation(string path, string filenameWithoutExtension)
  511. {
  512. try
  513. {
  514. var files = new DirectoryInfo(path)
  515. .EnumerateFiles()
  516. .Where(i =>
  517. {
  518. var fileName = Path.GetFileNameWithoutExtension(i.FullName);
  519. if (!string.Equals(fileName, filenameWithoutExtension, StringComparison.OrdinalIgnoreCase))
  520. {
  521. return false;
  522. }
  523. var ext = i.Extension;
  524. return !string.IsNullOrEmpty(ext) &&
  525. BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
  526. })
  527. .ToList();
  528. return BaseItem.SupportedImageExtensions
  529. .Select(ext => files.FirstOrDefault(i => string.Equals(ext, i.Extension, StringComparison.OrdinalIgnoreCase)))
  530. .FirstOrDefault(file => file != null);
  531. }
  532. catch (DirectoryNotFoundException)
  533. {
  534. return null;
  535. }
  536. }
  537. }
  538. }