ImageService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Common.Extensions;
  9. using MediaBrowser.Controller.Configuration;
  10. using MediaBrowser.Controller.Drawing;
  11. using MediaBrowser.Controller.Dto;
  12. using MediaBrowser.Controller.Entities;
  13. using MediaBrowser.Controller.Library;
  14. using MediaBrowser.Controller.Net;
  15. using MediaBrowser.Controller.Providers;
  16. using MediaBrowser.Model.Drawing;
  17. using MediaBrowser.Model.Entities;
  18. using MediaBrowser.Model.IO;
  19. using MediaBrowser.Model.Net;
  20. using MediaBrowser.Model.Services;
  21. using Microsoft.Extensions.Logging;
  22. using Microsoft.Net.Http.Headers;
  23. using User = Jellyfin.Data.Entities.User;
  24. namespace MediaBrowser.Api.Images
  25. {
  26. [Route("/Items/{Id}/Images/{Type}", "GET")]
  27. [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
  28. [Route("/Items/{Id}/Images/{Type}", "HEAD")]
  29. [Route("/Items/{Id}/Images/{Type}/{Index}", "HEAD")]
  30. [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "GET")]
  31. [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "HEAD")]
  32. public class GetItemImage : ImageRequest
  33. {
  34. /// <summary>
  35. /// Gets or sets the id.
  36. /// </summary>
  37. /// <value>The id.</value>
  38. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path")]
  39. public Guid Id { get; set; }
  40. }
  41. /// <summary>
  42. /// Class GetPersonImage.
  43. /// </summary>
  44. [Route("/Artists/{Name}/Images/{Type}", "GET")]
  45. [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")]
  46. [Route("/Genres/{Name}/Images/{Type}", "GET")]
  47. [Route("/Genres/{Name}/Images/{Type}/{Index}", "GET")]
  48. [Route("/MusicGenres/{Name}/Images/{Type}", "GET")]
  49. [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "GET")]
  50. [Route("/Persons/{Name}/Images/{Type}", "GET")]
  51. [Route("/Persons/{Name}/Images/{Type}/{Index}", "GET")]
  52. [Route("/Studios/{Name}/Images/{Type}", "GET")]
  53. [Route("/Studios/{Name}/Images/{Type}/{Index}", "GET")]
  54. ////[Route("/Years/{Year}/Images/{Type}", "GET")]
  55. ////[Route("/Years/{Year}/Images/{Type}/{Index}", "GET")]
  56. [Route("/Artists/{Name}/Images/{Type}", "HEAD")]
  57. [Route("/Artists/{Name}/Images/{Type}/{Index}", "HEAD")]
  58. [Route("/Genres/{Name}/Images/{Type}", "HEAD")]
  59. [Route("/Genres/{Name}/Images/{Type}/{Index}", "HEAD")]
  60. [Route("/MusicGenres/{Name}/Images/{Type}", "HEAD")]
  61. [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "HEAD")]
  62. [Route("/Persons/{Name}/Images/{Type}", "HEAD")]
  63. [Route("/Persons/{Name}/Images/{Type}/{Index}", "HEAD")]
  64. [Route("/Studios/{Name}/Images/{Type}", "HEAD")]
  65. [Route("/Studios/{Name}/Images/{Type}/{Index}", "HEAD")]
  66. ////[Route("/Years/{Year}/Images/{Type}", "HEAD")]
  67. ////[Route("/Years/{Year}/Images/{Type}/{Index}", "HEAD")]
  68. public class GetItemByNameImage : ImageRequest
  69. {
  70. /// <summary>
  71. /// Gets or sets the name.
  72. /// </summary>
  73. /// <value>The name.</value>
  74. [ApiMember(Name = "Name", Description = "Item name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
  75. public string Name { get; set; }
  76. }
  77. /// <summary>
  78. /// Class GetUserImage.
  79. /// </summary>
  80. [Route("/Users/{Id}/Images/{Type}", "GET")]
  81. [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")]
  82. [Route("/Users/{Id}/Images/{Type}", "HEAD")]
  83. [Route("/Users/{Id}/Images/{Type}/{Index}", "HEAD")]
  84. public class GetUserImage : ImageRequest
  85. {
  86. /// <summary>
  87. /// Gets or sets the id.
  88. /// </summary>
  89. /// <value>The id.</value>
  90. [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
  91. public Guid Id { get; set; }
  92. }
  93. /// <summary>
  94. /// Class ImageService.
  95. /// </summary>
  96. public class ImageService : BaseApiService
  97. {
  98. private readonly IUserManager _userManager;
  99. private readonly ILibraryManager _libraryManager;
  100. private readonly IProviderManager _providerManager;
  101. private readonly IImageProcessor _imageProcessor;
  102. private readonly IFileSystem _fileSystem;
  103. private readonly IAuthorizationContext _authContext;
  104. /// <summary>
  105. /// Initializes a new instance of the <see cref="ImageService" /> class.
  106. /// </summary>
  107. public ImageService(
  108. ILogger<ImageService> logger,
  109. IServerConfigurationManager serverConfigurationManager,
  110. IHttpResultFactory httpResultFactory,
  111. IUserManager userManager,
  112. ILibraryManager libraryManager,
  113. IProviderManager providerManager,
  114. IImageProcessor imageProcessor,
  115. IFileSystem fileSystem,
  116. IAuthorizationContext authContext)
  117. : base(logger, serverConfigurationManager, httpResultFactory)
  118. {
  119. _userManager = userManager;
  120. _libraryManager = libraryManager;
  121. _providerManager = providerManager;
  122. _imageProcessor = imageProcessor;
  123. _fileSystem = fileSystem;
  124. _authContext = authContext;
  125. }
  126. /// <summary>
  127. /// Gets the specified request.
  128. /// </summary>
  129. /// <param name="request">The request.</param>
  130. /// <returns>System.Object.</returns>
  131. public object Get(GetItemImage request)
  132. {
  133. return GetImage(request, request.Id, null, false);
  134. }
  135. /// <summary>
  136. /// Gets the specified request.
  137. /// </summary>
  138. /// <param name="request">The request.</param>
  139. /// <returns>System.Object.</returns>
  140. public object Head(GetItemImage request)
  141. {
  142. return GetImage(request, request.Id, null, true);
  143. }
  144. /// <summary>
  145. /// Gets the specified request.
  146. /// </summary>
  147. /// <param name="request">The request.</param>
  148. /// <returns>System.Object.</returns>
  149. public object Get(GetUserImage request)
  150. {
  151. var item = _userManager.GetUserById(request.Id);
  152. return GetImage(request, item, false);
  153. }
  154. public object Head(GetUserImage request)
  155. {
  156. var item = _userManager.GetUserById(request.Id);
  157. return GetImage(request, item, true);
  158. }
  159. public object Get(GetItemByNameImage request)
  160. {
  161. var type = GetPathValue(0).ToString();
  162. var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
  163. return GetImage(request, item.Id, item, false);
  164. }
  165. public object Head(GetItemByNameImage request)
  166. {
  167. var type = GetPathValue(0).ToString();
  168. var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
  169. return GetImage(request, item.Id, item, true);
  170. }
  171. /// <summary>
  172. /// Gets the image.
  173. /// </summary>
  174. /// <param name="request">The request.</param>
  175. /// <param name="item">The item.</param>
  176. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  177. /// <returns>System.Object.</returns>
  178. /// <exception cref="ResourceNotFoundException"></exception>
  179. public Task<object> GetImage(ImageRequest request, Guid itemId, BaseItem item, bool isHeadRequest)
  180. {
  181. if (request.PercentPlayed.HasValue)
  182. {
  183. if (request.PercentPlayed.Value <= 0)
  184. {
  185. request.PercentPlayed = null;
  186. }
  187. else if (request.PercentPlayed.Value >= 100)
  188. {
  189. request.PercentPlayed = null;
  190. request.AddPlayedIndicator = true;
  191. }
  192. }
  193. if (request.PercentPlayed.HasValue)
  194. {
  195. request.UnplayedCount = null;
  196. }
  197. if (request.UnplayedCount.HasValue
  198. && request.UnplayedCount.Value <= 0)
  199. {
  200. request.UnplayedCount = null;
  201. }
  202. if (item == null)
  203. {
  204. item = _libraryManager.GetItemById(itemId);
  205. if (item == null)
  206. {
  207. throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N", CultureInfo.InvariantCulture)));
  208. }
  209. }
  210. var imageInfo = GetImageInfo(request, item);
  211. if (imageInfo == null)
  212. {
  213. throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
  214. }
  215. bool cropWhitespace;
  216. if (request.CropWhitespace.HasValue)
  217. {
  218. cropWhitespace = request.CropWhitespace.Value;
  219. }
  220. else
  221. {
  222. cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
  223. }
  224. var outputFormats = GetOutputFormats(request);
  225. TimeSpan? cacheDuration = null;
  226. if (!string.IsNullOrEmpty(request.Tag))
  227. {
  228. cacheDuration = TimeSpan.FromDays(365);
  229. }
  230. var responseHeaders = new Dictionary<string, string>
  231. {
  232. {"transferMode.dlna.org", "Interactive"},
  233. {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
  234. };
  235. return GetImageResult(
  236. item,
  237. itemId,
  238. request,
  239. imageInfo,
  240. cropWhitespace,
  241. outputFormats,
  242. cacheDuration,
  243. responseHeaders,
  244. isHeadRequest);
  245. }
  246. public Task<object> GetImage(ImageRequest request, User user, bool isHeadRequest)
  247. {
  248. var imageInfo = GetImageInfo(request, user);
  249. TimeSpan? cacheDuration = null;
  250. if (!string.IsNullOrEmpty(request.Tag))
  251. {
  252. cacheDuration = TimeSpan.FromDays(365);
  253. }
  254. var responseHeaders = new Dictionary<string, string>
  255. {
  256. {"transferMode.dlna.org", "Interactive"},
  257. {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
  258. };
  259. var outputFormats = GetOutputFormats(request);
  260. return GetImageResult(user.Id,
  261. request,
  262. imageInfo,
  263. outputFormats,
  264. cacheDuration,
  265. responseHeaders,
  266. isHeadRequest);
  267. }
  268. private async Task<object> GetImageResult(
  269. Guid itemId,
  270. ImageRequest request,
  271. ItemImageInfo info,
  272. IReadOnlyCollection<ImageFormat> supportedFormats,
  273. TimeSpan? cacheDuration,
  274. IDictionary<string, string> headers,
  275. bool isHeadRequest)
  276. {
  277. info.Type = ImageType.Profile;
  278. var options = new ImageProcessingOptions
  279. {
  280. CropWhiteSpace = true,
  281. Height = request.Height,
  282. ImageIndex = request.Index ?? 0,
  283. Image = info,
  284. Item = null, // Hack alert
  285. ItemId = itemId,
  286. MaxHeight = request.MaxHeight,
  287. MaxWidth = request.MaxWidth,
  288. Quality = request.Quality ?? 100,
  289. Width = request.Width,
  290. AddPlayedIndicator = request.AddPlayedIndicator,
  291. PercentPlayed = 0,
  292. UnplayedCount = request.UnplayedCount,
  293. Blur = request.Blur,
  294. BackgroundColor = request.BackgroundColor,
  295. ForegroundLayer = request.ForegroundLayer,
  296. SupportedOutputFormats = supportedFormats
  297. };
  298. var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
  299. headers[HeaderNames.Vary] = HeaderNames.Accept;
  300. return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  301. {
  302. CacheDuration = cacheDuration,
  303. ResponseHeaders = headers,
  304. ContentType = imageResult.Item2,
  305. DateLastModified = imageResult.Item3,
  306. IsHeadRequest = isHeadRequest,
  307. Path = imageResult.Item1,
  308. FileShare = FileShare.Read
  309. }).ConfigureAwait(false);
  310. }
  311. private async Task<object> GetImageResult(
  312. BaseItem item,
  313. Guid itemId,
  314. ImageRequest request,
  315. ItemImageInfo image,
  316. bool cropwhitespace,
  317. IReadOnlyCollection<ImageFormat> supportedFormats,
  318. TimeSpan? cacheDuration,
  319. IDictionary<string, string> headers,
  320. bool isHeadRequest)
  321. {
  322. if (!image.IsLocalFile)
  323. {
  324. item ??= _libraryManager.GetItemById(itemId);
  325. image = await _libraryManager.ConvertImageToLocal(item, image, request.Index ?? 0).ConfigureAwait(false);
  326. }
  327. var options = new ImageProcessingOptions
  328. {
  329. CropWhiteSpace = cropwhitespace,
  330. Height = request.Height,
  331. ImageIndex = request.Index ?? 0,
  332. Image = image,
  333. Item = item,
  334. ItemId = itemId,
  335. MaxHeight = request.MaxHeight,
  336. MaxWidth = request.MaxWidth,
  337. Quality = request.Quality ?? 100,
  338. Width = request.Width,
  339. AddPlayedIndicator = request.AddPlayedIndicator,
  340. PercentPlayed = request.PercentPlayed ?? 0,
  341. UnplayedCount = request.UnplayedCount,
  342. Blur = request.Blur,
  343. BackgroundColor = request.BackgroundColor,
  344. ForegroundLayer = request.ForegroundLayer,
  345. SupportedOutputFormats = supportedFormats
  346. };
  347. var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
  348. headers[HeaderNames.Vary] = HeaderNames.Accept;
  349. return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  350. {
  351. CacheDuration = cacheDuration,
  352. ResponseHeaders = headers,
  353. ContentType = imageResult.Item2,
  354. DateLastModified = imageResult.Item3,
  355. IsHeadRequest = isHeadRequest,
  356. Path = imageResult.Item1,
  357. FileShare = FileShare.Read
  358. }).ConfigureAwait(false);
  359. }
  360. private ImageFormat[] GetOutputFormats(ImageRequest request)
  361. {
  362. if (!string.IsNullOrWhiteSpace(request.Format)
  363. && Enum.TryParse(request.Format, true, out ImageFormat format))
  364. {
  365. return new[] { format };
  366. }
  367. return GetClientSupportedFormats();
  368. }
  369. private ImageFormat[] GetClientSupportedFormats()
  370. {
  371. var supportedFormats = Request.AcceptTypes ?? Array.Empty<string>();
  372. if (supportedFormats.Length > 0)
  373. {
  374. for (int i = 0; i < supportedFormats.Length; i++)
  375. {
  376. int index = supportedFormats[i].IndexOf(';');
  377. if (index != -1)
  378. {
  379. supportedFormats[i] = supportedFormats[i].Substring(0, index);
  380. }
  381. }
  382. }
  383. var acceptParam = Request.QueryString["accept"];
  384. var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
  385. if (!supportsWebP)
  386. {
  387. var userAgent = Request.UserAgent ?? string.Empty;
  388. if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
  389. userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
  390. {
  391. supportsWebP = true;
  392. }
  393. }
  394. var formats = new List<ImageFormat>(4);
  395. if (supportsWebP)
  396. {
  397. formats.Add(ImageFormat.Webp);
  398. }
  399. formats.Add(ImageFormat.Jpg);
  400. formats.Add(ImageFormat.Png);
  401. if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
  402. {
  403. formats.Add(ImageFormat.Gif);
  404. }
  405. return formats.ToArray();
  406. }
  407. private bool SupportsFormat(IEnumerable<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
  408. {
  409. var mimeType = "image/" + format;
  410. if (requestAcceptTypes.Contains(mimeType))
  411. {
  412. return true;
  413. }
  414. if (acceptAll && requestAcceptTypes.Contains("*/*"))
  415. {
  416. return true;
  417. }
  418. return string.Equals(Request.QueryString["accept"], format, StringComparison.OrdinalIgnoreCase);
  419. }
  420. /// <summary>
  421. /// Gets the image path.
  422. /// </summary>
  423. /// <param name="request">The request.</param>
  424. /// <param name="item">The item.</param>
  425. /// <returns>System.String.</returns>
  426. private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
  427. {
  428. var index = request.Index ?? 0;
  429. return item.GetImageInfo(request.Type, index);
  430. }
  431. private static ItemImageInfo GetImageInfo(ImageRequest request, User user)
  432. {
  433. var info = new ItemImageInfo
  434. {
  435. Path = user.ProfileImage.Path,
  436. Type = ImageType.Primary,
  437. DateModified = user.ProfileImage.LastModified,
  438. };
  439. if (request.Width.HasValue)
  440. {
  441. info.Width = request.Width.Value;
  442. }
  443. if (request.Height.HasValue)
  444. {
  445. info.Height = request.Height.Value;
  446. }
  447. return info;
  448. }
  449. /// <summary>
  450. /// Posts the image.
  451. /// </summary>
  452. /// <param name="entity">The entity.</param>
  453. /// <param name="inputStream">The input stream.</param>
  454. /// <param name="imageType">Type of the image.</param>
  455. /// <param name="mimeType">Type of the MIME.</param>
  456. /// <returns>Task.</returns>
  457. public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
  458. {
  459. var memoryStream = await GetMemoryStream(inputStream);
  460. // Handle image/png; charset=utf-8
  461. mimeType = mimeType.Split(';').FirstOrDefault();
  462. await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
  463. entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
  464. }
  465. private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
  466. {
  467. using var reader = new StreamReader(inputStream);
  468. var text = await reader.ReadToEndAsync().ConfigureAwait(false);
  469. var bytes = Convert.FromBase64String(text);
  470. return new MemoryStream(bytes)
  471. {
  472. Position = 0
  473. };
  474. }
  475. private async Task PostImage(User user, Stream inputStream, string mimeType)
  476. {
  477. var memoryStream = await GetMemoryStream(inputStream);
  478. // Handle image/png; charset=utf-8
  479. mimeType = mimeType.Split(';').FirstOrDefault();
  480. var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
  481. if (user.ProfileImage != null)
  482. {
  483. _userManager.ClearProfileImage(user);
  484. }
  485. user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
  486. await _providerManager
  487. .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
  488. .ConfigureAwait(false);
  489. await _userManager.UpdateUserAsync(user);
  490. }
  491. }
  492. }