ItemUpdateController.cs 17 KB

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