ItemUpdateController.cs 18 KB

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