ItemUpdateController.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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()
  146. .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
  147. .OrderBy(c => c.DisplayName)
  148. .ToArray()
  149. };
  150. if (!item.IsVirtualItem
  151. && item is not ICollectionFolder
  152. && item is not UserView
  153. && item is not AggregateFolder
  154. && item is not LiveTvChannel
  155. && item is not IItemByName
  156. && item.SourceType == SourceType.Library)
  157. {
  158. var inheritedContentType = _libraryManager.GetInheritedContentType(item);
  159. var configuredContentType = _libraryManager.GetConfiguredContentType(item);
  160. if (inheritedContentType is null || configuredContentType is not null)
  161. {
  162. info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
  163. info.ContentType = configuredContentType;
  164. if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
  165. {
  166. info.ContentTypeOptions = info.ContentTypeOptions
  167. .Where(i => string.IsNullOrWhiteSpace(i.Value)
  168. || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
  169. .ToArray();
  170. }
  171. }
  172. }
  173. return info;
  174. }
  175. /// <summary>
  176. /// Updates an item's content type.
  177. /// </summary>
  178. /// <param name="itemId">The item id.</param>
  179. /// <param name="contentType">The content type of the item.</param>
  180. /// <response code="204">Item content type updated.</response>
  181. /// <response code="404">Item not found.</response>
  182. /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
  183. [HttpPost("Items/{itemId}/ContentType")]
  184. [ProducesResponseType(StatusCodes.Status204NoContent)]
  185. [ProducesResponseType(StatusCodes.Status404NotFound)]
  186. public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
  187. {
  188. var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
  189. if (item is null)
  190. {
  191. return NotFound();
  192. }
  193. var path = item.ContainingFolderPath;
  194. var types = _serverConfigurationManager.Configuration.ContentTypes
  195. .Where(i => !string.IsNullOrWhiteSpace(i.Name))
  196. .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
  197. .ToList();
  198. if (!string.IsNullOrWhiteSpace(contentType))
  199. {
  200. types.Add(new NameValuePair
  201. {
  202. Name = path,
  203. Value = contentType
  204. });
  205. }
  206. _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
  207. _serverConfigurationManager.SaveConfiguration();
  208. return NoContent();
  209. }
  210. private async Task UpdateItem(BaseItemDto request, BaseItem item)
  211. {
  212. item.Name = request.Name;
  213. item.ForcedSortName = request.ForcedSortName;
  214. item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
  215. item.CriticRating = request.CriticRating;
  216. item.CommunityRating = request.CommunityRating;
  217. item.IndexNumber = request.IndexNumber;
  218. item.ParentIndexNumber = request.ParentIndexNumber;
  219. item.Overview = request.Overview;
  220. item.Genres = request.Genres;
  221. if (item is Episode episode)
  222. {
  223. episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
  224. episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
  225. episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
  226. }
  227. if (request.Height is not null && item is LiveTvChannel channel)
  228. {
  229. channel.Height = request.Height.Value;
  230. }
  231. if (request.Taglines is not null)
  232. {
  233. item.Tagline = request.Taglines.FirstOrDefault();
  234. }
  235. if (request.Studios is not null)
  236. {
  237. item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
  238. }
  239. if (request.DateCreated.HasValue)
  240. {
  241. item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
  242. }
  243. item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
  244. item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
  245. item.ProductionYear = request.ProductionYear;
  246. request.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
  247. item.OfficialRating = request.OfficialRating;
  248. item.CustomRating = request.CustomRating;
  249. var currentTags = item.Tags;
  250. var newTags = request.Tags;
  251. var removedTags = currentTags.Except(newTags).ToList();
  252. var addedTags = newTags.Except(currentTags).ToList();
  253. item.Tags = newTags;
  254. if (item is Series rseries)
  255. {
  256. foreach (var season in rseries.Children.OfType<Season>())
  257. {
  258. if (!season.LockedFields.Contains(MetadataField.OfficialRating))
  259. {
  260. season.OfficialRating = request.OfficialRating;
  261. }
  262. season.CustomRating = request.CustomRating;
  263. if (!season.LockedFields.Contains(MetadataField.Tags))
  264. {
  265. season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  266. }
  267. season.OnMetadataChanged();
  268. await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  269. foreach (var ep in season.Children.OfType<Episode>())
  270. {
  271. if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
  272. {
  273. ep.OfficialRating = request.OfficialRating;
  274. }
  275. ep.CustomRating = request.CustomRating;
  276. if (!ep.LockedFields.Contains(MetadataField.Tags))
  277. {
  278. ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  279. }
  280. ep.OnMetadataChanged();
  281. await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  282. }
  283. }
  284. }
  285. else if (item is Season season)
  286. {
  287. foreach (var ep in season.Children.OfType<Episode>())
  288. {
  289. if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
  290. {
  291. ep.OfficialRating = request.OfficialRating;
  292. }
  293. ep.CustomRating = request.CustomRating;
  294. if (!ep.LockedFields.Contains(MetadataField.Tags))
  295. {
  296. ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  297. }
  298. ep.OnMetadataChanged();
  299. await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  300. }
  301. }
  302. else if (item is MusicAlbum album)
  303. {
  304. foreach (BaseItem track in album.Children)
  305. {
  306. if (!track.LockedFields.Contains(MetadataField.OfficialRating))
  307. {
  308. track.OfficialRating = request.OfficialRating;
  309. }
  310. track.CustomRating = request.CustomRating;
  311. if (!track.LockedFields.Contains(MetadataField.Tags))
  312. {
  313. track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  314. }
  315. track.OnMetadataChanged();
  316. await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  317. }
  318. }
  319. if (request.ProductionLocations is not null)
  320. {
  321. item.ProductionLocations = request.ProductionLocations;
  322. }
  323. item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
  324. item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
  325. if (item is IHasDisplayOrder hasDisplayOrder)
  326. {
  327. hasDisplayOrder.DisplayOrder = request.DisplayOrder;
  328. }
  329. if (item is IHasAspectRatio hasAspectRatio)
  330. {
  331. hasAspectRatio.AspectRatio = request.AspectRatio;
  332. }
  333. item.IsLocked = request.LockData ?? false;
  334. if (request.LockedFields is not null)
  335. {
  336. item.LockedFields = request.LockedFields;
  337. }
  338. // Only allow this for series. Runtimes for media comes from ffprobe.
  339. if (item is Series)
  340. {
  341. item.RunTimeTicks = request.RunTimeTicks;
  342. }
  343. foreach (var pair in request.ProviderIds.ToList())
  344. {
  345. if (string.IsNullOrEmpty(pair.Value))
  346. {
  347. request.ProviderIds.Remove(pair.Key);
  348. }
  349. }
  350. item.ProviderIds = request.ProviderIds;
  351. if (item is Video video)
  352. {
  353. video.Video3DFormat = request.Video3DFormat;
  354. }
  355. if (request.AlbumArtists is not null)
  356. {
  357. if (item is IHasAlbumArtist hasAlbumArtists)
  358. {
  359. hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
  360. }
  361. }
  362. if (request.ArtistItems is not null)
  363. {
  364. if (item is IHasArtist hasArtists)
  365. {
  366. hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
  367. }
  368. }
  369. switch (item)
  370. {
  371. case Audio song:
  372. song.Album = request.Album;
  373. break;
  374. case MusicVideo musicVideo:
  375. musicVideo.Album = request.Album;
  376. break;
  377. case Series series:
  378. {
  379. series.Status = GetSeriesStatus(request);
  380. if (request.AirDays is not null)
  381. {
  382. series.AirDays = request.AirDays;
  383. series.AirTime = request.AirTime;
  384. }
  385. break;
  386. }
  387. }
  388. }
  389. private SeriesStatus? GetSeriesStatus(BaseItemDto item)
  390. {
  391. if (string.IsNullOrEmpty(item.Status))
  392. {
  393. return null;
  394. }
  395. return Enum.Parse<SeriesStatus>(item.Status, true);
  396. }
  397. private DateTime NormalizeDateTime(DateTime val)
  398. {
  399. return DateTime.SpecifyKind(val, DateTimeKind.Utc);
  400. }
  401. private List<NameValuePair> GetContentTypeOptions(bool isForItem)
  402. {
  403. var list = new List<NameValuePair>();
  404. if (isForItem)
  405. {
  406. list.Add(new NameValuePair
  407. {
  408. Name = "Inherit",
  409. Value = string.Empty
  410. });
  411. }
  412. list.Add(new NameValuePair
  413. {
  414. Name = "Movies",
  415. Value = "movies"
  416. });
  417. list.Add(new NameValuePair
  418. {
  419. Name = "Music",
  420. Value = "music"
  421. });
  422. list.Add(new NameValuePair
  423. {
  424. Name = "Shows",
  425. Value = "tvshows"
  426. });
  427. if (!isForItem)
  428. {
  429. list.Add(new NameValuePair
  430. {
  431. Name = "Books",
  432. Value = "books"
  433. });
  434. }
  435. list.Add(new NameValuePair
  436. {
  437. Name = "HomeVideos",
  438. Value = "homevideos"
  439. });
  440. list.Add(new NameValuePair
  441. {
  442. Name = "MusicVideos",
  443. Value = "musicvideos"
  444. });
  445. list.Add(new NameValuePair
  446. {
  447. Name = "Photos",
  448. Value = "photos"
  449. });
  450. if (!isForItem)
  451. {
  452. list.Add(new NameValuePair
  453. {
  454. Name = "MixedContent",
  455. Value = string.Empty
  456. });
  457. }
  458. foreach (var val in list)
  459. {
  460. val.Name = _localizationManager.GetLocalizedString(val.Name);
  461. }
  462. return list;
  463. }
  464. }