ItemUpdateController.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Api.Constants;
  8. using Jellyfin.Data.Enums;
  9. using MediaBrowser.Common.Api;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Entities;
  12. using MediaBrowser.Controller.Entities.Audio;
  13. using MediaBrowser.Controller.Entities.TV;
  14. using MediaBrowser.Controller.Library;
  15. using MediaBrowser.Controller.LiveTv;
  16. using MediaBrowser.Controller.Providers;
  17. using MediaBrowser.Model.Dto;
  18. using MediaBrowser.Model.Entities;
  19. using MediaBrowser.Model.Globalization;
  20. using MediaBrowser.Model.IO;
  21. using Microsoft.AspNetCore.Authorization;
  22. using Microsoft.AspNetCore.Http;
  23. using Microsoft.AspNetCore.Mvc;
  24. namespace Jellyfin.Api.Controllers;
  25. /// <summary>
  26. /// Item update controller.
  27. /// </summary>
  28. [Route("")]
  29. [Authorize(Policy = Policies.RequiresElevation)]
  30. public class ItemUpdateController : BaseJellyfinApiController
  31. {
  32. private readonly ILibraryManager _libraryManager;
  33. private readonly IProviderManager _providerManager;
  34. private readonly ILocalizationManager _localizationManager;
  35. private readonly IFileSystem _fileSystem;
  36. private readonly IServerConfigurationManager _serverConfigurationManager;
  37. /// <summary>
  38. /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
  39. /// </summary>
  40. /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
  41. /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
  42. /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
  43. /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
  44. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  45. public ItemUpdateController(
  46. IFileSystem fileSystem,
  47. ILibraryManager libraryManager,
  48. IProviderManager providerManager,
  49. ILocalizationManager localizationManager,
  50. IServerConfigurationManager serverConfigurationManager)
  51. {
  52. _libraryManager = libraryManager;
  53. _providerManager = providerManager;
  54. _localizationManager = localizationManager;
  55. _fileSystem = fileSystem;
  56. _serverConfigurationManager = serverConfigurationManager;
  57. }
  58. /// <summary>
  59. /// Updates an item.
  60. /// </summary>
  61. /// <param name="itemId">The item id.</param>
  62. /// <param name="request">The new item properties.</param>
  63. /// <response code="204">Item updated.</response>
  64. /// <response code="404">Item not found.</response>
  65. /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
  66. [HttpPost("Items/{itemId}")]
  67. [ProducesResponseType(StatusCodes.Status204NoContent)]
  68. [ProducesResponseType(StatusCodes.Status404NotFound)]
  69. public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
  70. {
  71. var item = _libraryManager.GetItemById(itemId);
  72. if (item is null)
  73. {
  74. return NotFound();
  75. }
  76. var newLockData = request.LockData ?? false;
  77. var isLockedChanged = item.IsLocked != newLockData;
  78. var series = item as Series;
  79. var displayOrderChanged = series is not null && !string.Equals(
  80. series.DisplayOrder ?? string.Empty,
  81. request.DisplayOrder ?? string.Empty,
  82. StringComparison.OrdinalIgnoreCase);
  83. // Do this first so that metadata savers can pull the updates from the database.
  84. if (request.People is not null)
  85. {
  86. _libraryManager.UpdatePeople(
  87. item,
  88. request.People.Select(x => new PersonInfo
  89. {
  90. Name = x.Name,
  91. Role = x.Role,
  92. Type = x.Type
  93. }).ToList());
  94. }
  95. await UpdateItem(request, item).ConfigureAwait(false);
  96. item.OnMetadataChanged();
  97. await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  98. if (isLockedChanged && item.IsFolder)
  99. {
  100. var folder = (Folder)item;
  101. foreach (var child in folder.GetRecursiveChildren())
  102. {
  103. child.IsLocked = newLockData;
  104. await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  105. }
  106. }
  107. if (displayOrderChanged)
  108. {
  109. _providerManager.QueueRefresh(
  110. series!.Id,
  111. new MetadataRefreshOptions(new DirectoryService(_fileSystem))
  112. {
  113. MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
  114. ImageRefreshMode = MetadataRefreshMode.FullRefresh,
  115. ReplaceAllMetadata = true
  116. },
  117. RefreshPriority.High);
  118. }
  119. return NoContent();
  120. }
  121. /// <summary>
  122. /// Gets metadata editor info for an item.
  123. /// </summary>
  124. /// <param name="itemId">The item id.</param>
  125. /// <response code="200">Item metadata editor returned.</response>
  126. /// <response code="404">Item not found.</response>
  127. /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
  128. [HttpGet("Items/{itemId}/MetadataEditor")]
  129. [ProducesResponseType(StatusCodes.Status200OK)]
  130. [ProducesResponseType(StatusCodes.Status404NotFound)]
  131. public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
  132. {
  133. var item = _libraryManager.GetItemById(itemId);
  134. var info = new MetadataEditorInfo
  135. {
  136. ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
  137. ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
  138. Countries = _localizationManager.GetCountries().ToArray(),
  139. Cultures = _localizationManager.GetCultures().ToArray()
  140. };
  141. if (!item.IsVirtualItem
  142. && item is not ICollectionFolder
  143. && item is not UserView
  144. && item is not AggregateFolder
  145. && item is not LiveTvChannel
  146. && item is not IItemByName
  147. && item.SourceType == SourceType.Library)
  148. {
  149. var inheritedContentType = _libraryManager.GetInheritedContentType(item);
  150. var configuredContentType = _libraryManager.GetConfiguredContentType(item);
  151. if (inheritedContentType is null || configuredContentType is not null)
  152. {
  153. info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
  154. info.ContentType = configuredContentType;
  155. if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
  156. {
  157. info.ContentTypeOptions = info.ContentTypeOptions
  158. .Where(i => string.IsNullOrWhiteSpace(i.Value)
  159. || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
  160. .ToArray();
  161. }
  162. }
  163. }
  164. return info;
  165. }
  166. /// <summary>
  167. /// Updates an item's content type.
  168. /// </summary>
  169. /// <param name="itemId">The item id.</param>
  170. /// <param name="contentType">The content type of the item.</param>
  171. /// <response code="204">Item content type updated.</response>
  172. /// <response code="404">Item not found.</response>
  173. /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
  174. [HttpPost("Items/{itemId}/ContentType")]
  175. [ProducesResponseType(StatusCodes.Status204NoContent)]
  176. [ProducesResponseType(StatusCodes.Status404NotFound)]
  177. public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
  178. {
  179. var item = _libraryManager.GetItemById(itemId);
  180. if (item is null)
  181. {
  182. return NotFound();
  183. }
  184. var path = item.ContainingFolderPath;
  185. var types = _serverConfigurationManager.Configuration.ContentTypes
  186. .Where(i => !string.IsNullOrWhiteSpace(i.Name))
  187. .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
  188. .ToList();
  189. if (!string.IsNullOrWhiteSpace(contentType))
  190. {
  191. types.Add(new NameValuePair
  192. {
  193. Name = path,
  194. Value = contentType
  195. });
  196. }
  197. _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
  198. _serverConfigurationManager.SaveConfiguration();
  199. return NoContent();
  200. }
  201. private async Task UpdateItem(BaseItemDto request, BaseItem item)
  202. {
  203. item.Name = request.Name;
  204. item.ForcedSortName = request.ForcedSortName;
  205. item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
  206. item.CriticRating = request.CriticRating;
  207. item.CommunityRating = request.CommunityRating;
  208. item.IndexNumber = request.IndexNumber;
  209. item.ParentIndexNumber = request.ParentIndexNumber;
  210. item.Overview = request.Overview;
  211. item.Genres = request.Genres;
  212. if (item is Episode episode)
  213. {
  214. episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
  215. episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
  216. episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
  217. }
  218. if (request.Height is not null && item is LiveTvChannel channel)
  219. {
  220. channel.Height = request.Height.Value;
  221. }
  222. if (request.Taglines is not null)
  223. {
  224. item.Tagline = request.Taglines.FirstOrDefault();
  225. }
  226. if (request.Studios is not null)
  227. {
  228. item.Studios = request.Studios.Select(x => x.Name).ToArray();
  229. }
  230. if (request.DateCreated.HasValue)
  231. {
  232. item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
  233. }
  234. item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
  235. item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
  236. item.ProductionYear = request.ProductionYear;
  237. request.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
  238. item.OfficialRating = request.OfficialRating;
  239. item.CustomRating = request.CustomRating;
  240. var currentTags = item.Tags;
  241. var newTags = request.Tags;
  242. var removedTags = currentTags.Except(newTags).ToList();
  243. var addedTags = newTags.Except(currentTags).ToList();
  244. item.Tags = newTags;
  245. if (item is Series rseries)
  246. {
  247. foreach (Season season in rseries.Children)
  248. {
  249. season.OfficialRating = request.OfficialRating;
  250. season.CustomRating = request.CustomRating;
  251. season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
  252. season.OnMetadataChanged();
  253. await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  254. foreach (Episode ep in season.Children)
  255. {
  256. ep.OfficialRating = request.OfficialRating;
  257. ep.CustomRating = request.CustomRating;
  258. ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
  259. ep.OnMetadataChanged();
  260. await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  261. }
  262. }
  263. }
  264. else if (item is Season season)
  265. {
  266. foreach (Episode ep in season.Children)
  267. {
  268. ep.OfficialRating = request.OfficialRating;
  269. ep.CustomRating = request.CustomRating;
  270. ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
  271. ep.OnMetadataChanged();
  272. await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  273. }
  274. }
  275. else if (item is MusicAlbum album)
  276. {
  277. foreach (BaseItem track in album.Children)
  278. {
  279. track.OfficialRating = request.OfficialRating;
  280. track.CustomRating = request.CustomRating;
  281. track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
  282. track.OnMetadataChanged();
  283. await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  284. }
  285. }
  286. if (request.ProductionLocations is not null)
  287. {
  288. item.ProductionLocations = request.ProductionLocations;
  289. }
  290. item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
  291. item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
  292. if (item is IHasDisplayOrder hasDisplayOrder)
  293. {
  294. hasDisplayOrder.DisplayOrder = request.DisplayOrder;
  295. }
  296. if (item is IHasAspectRatio hasAspectRatio)
  297. {
  298. hasAspectRatio.AspectRatio = request.AspectRatio;
  299. }
  300. item.IsLocked = request.LockData ?? false;
  301. if (request.LockedFields is not null)
  302. {
  303. item.LockedFields = request.LockedFields;
  304. }
  305. // Only allow this for series. Runtimes for media comes from ffprobe.
  306. if (item is Series)
  307. {
  308. item.RunTimeTicks = request.RunTimeTicks;
  309. }
  310. foreach (var pair in request.ProviderIds.ToList())
  311. {
  312. if (string.IsNullOrEmpty(pair.Value))
  313. {
  314. request.ProviderIds.Remove(pair.Key);
  315. }
  316. }
  317. item.ProviderIds = request.ProviderIds;
  318. if (item is Video video)
  319. {
  320. video.Video3DFormat = request.Video3DFormat;
  321. }
  322. if (request.AlbumArtists is not null)
  323. {
  324. if (item is IHasAlbumArtist hasAlbumArtists)
  325. {
  326. hasAlbumArtists.AlbumArtists = request
  327. .AlbumArtists
  328. .Select(i => i.Name)
  329. .ToArray();
  330. }
  331. }
  332. if (request.ArtistItems is not null)
  333. {
  334. if (item is IHasArtist hasArtists)
  335. {
  336. hasArtists.Artists = request
  337. .ArtistItems
  338. .Select(i => i.Name)
  339. .ToArray();
  340. }
  341. }
  342. switch (item)
  343. {
  344. case Audio song:
  345. song.Album = request.Album;
  346. break;
  347. case MusicVideo musicVideo:
  348. musicVideo.Album = request.Album;
  349. break;
  350. case Series series:
  351. {
  352. series.Status = GetSeriesStatus(request);
  353. if (request.AirDays is not null)
  354. {
  355. series.AirDays = request.AirDays;
  356. series.AirTime = request.AirTime;
  357. }
  358. break;
  359. }
  360. }
  361. }
  362. private SeriesStatus? GetSeriesStatus(BaseItemDto item)
  363. {
  364. if (string.IsNullOrEmpty(item.Status))
  365. {
  366. return null;
  367. }
  368. return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
  369. }
  370. private DateTime NormalizeDateTime(DateTime val)
  371. {
  372. return DateTime.SpecifyKind(val, DateTimeKind.Utc);
  373. }
  374. private List<NameValuePair> GetContentTypeOptions(bool isForItem)
  375. {
  376. var list = new List<NameValuePair>();
  377. if (isForItem)
  378. {
  379. list.Add(new NameValuePair
  380. {
  381. Name = "Inherit",
  382. Value = string.Empty
  383. });
  384. }
  385. list.Add(new NameValuePair
  386. {
  387. Name = "Movies",
  388. Value = "movies"
  389. });
  390. list.Add(new NameValuePair
  391. {
  392. Name = "Music",
  393. Value = "music"
  394. });
  395. list.Add(new NameValuePair
  396. {
  397. Name = "Shows",
  398. Value = "tvshows"
  399. });
  400. if (!isForItem)
  401. {
  402. list.Add(new NameValuePair
  403. {
  404. Name = "Books",
  405. Value = "books"
  406. });
  407. }
  408. list.Add(new NameValuePair
  409. {
  410. Name = "HomeVideos",
  411. Value = "homevideos"
  412. });
  413. list.Add(new NameValuePair
  414. {
  415. Name = "MusicVideos",
  416. Value = "musicvideos"
  417. });
  418. list.Add(new NameValuePair
  419. {
  420. Name = "Photos",
  421. Value = "photos"
  422. });
  423. if (!isForItem)
  424. {
  425. list.Add(new NameValuePair
  426. {
  427. Name = "MixedContent",
  428. Value = string.Empty
  429. });
  430. }
  431. foreach (var val in list)
  432. {
  433. val.Name = _localizationManager.GetLocalizedString(val.Name);
  434. }
  435. return list;
  436. }
  437. }