LibraryController.cs 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Jellyfin.Api.Attributes;
  10. using Jellyfin.Api.Extensions;
  11. using Jellyfin.Api.Helpers;
  12. using Jellyfin.Api.ModelBinders;
  13. using Jellyfin.Api.Models.LibraryDtos;
  14. using Jellyfin.Data.Entities;
  15. using Jellyfin.Data.Enums;
  16. using Jellyfin.Database.Implementations.Enums;
  17. using Jellyfin.Extensions;
  18. using MediaBrowser.Common.Api;
  19. using MediaBrowser.Common.Extensions;
  20. using MediaBrowser.Controller.Configuration;
  21. using MediaBrowser.Controller.Dto;
  22. using MediaBrowser.Controller.Entities;
  23. using MediaBrowser.Controller.Entities.Audio;
  24. using MediaBrowser.Controller.Entities.Movies;
  25. using MediaBrowser.Controller.Entities.TV;
  26. using MediaBrowser.Controller.Library;
  27. using MediaBrowser.Controller.Providers;
  28. using MediaBrowser.Model.Activity;
  29. using MediaBrowser.Model.Configuration;
  30. using MediaBrowser.Model.Dto;
  31. using MediaBrowser.Model.Entities;
  32. using MediaBrowser.Model.Globalization;
  33. using MediaBrowser.Model.Net;
  34. using MediaBrowser.Model.Querying;
  35. using Microsoft.AspNetCore.Authorization;
  36. using Microsoft.AspNetCore.Http;
  37. using Microsoft.AspNetCore.Mvc;
  38. using Microsoft.Extensions.Logging;
  39. namespace Jellyfin.Api.Controllers;
  40. /// <summary>
  41. /// Library Controller.
  42. /// </summary>
  43. [Route("")]
  44. public class LibraryController : BaseJellyfinApiController
  45. {
  46. private readonly IProviderManager _providerManager;
  47. private readonly ILibraryManager _libraryManager;
  48. private readonly IUserManager _userManager;
  49. private readonly IDtoService _dtoService;
  50. private readonly IActivityManager _activityManager;
  51. private readonly ILocalizationManager _localization;
  52. private readonly ILibraryMonitor _libraryMonitor;
  53. private readonly ILogger<LibraryController> _logger;
  54. private readonly IServerConfigurationManager _serverConfigurationManager;
  55. /// <summary>
  56. /// Initializes a new instance of the <see cref="LibraryController"/> class.
  57. /// </summary>
  58. /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
  59. /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
  60. /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
  61. /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
  62. /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
  63. /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
  64. /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
  65. /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
  66. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  67. public LibraryController(
  68. IProviderManager providerManager,
  69. ILibraryManager libraryManager,
  70. IUserManager userManager,
  71. IDtoService dtoService,
  72. IActivityManager activityManager,
  73. ILocalizationManager localization,
  74. ILibraryMonitor libraryMonitor,
  75. ILogger<LibraryController> logger,
  76. IServerConfigurationManager serverConfigurationManager)
  77. {
  78. _providerManager = providerManager;
  79. _libraryManager = libraryManager;
  80. _userManager = userManager;
  81. _dtoService = dtoService;
  82. _activityManager = activityManager;
  83. _localization = localization;
  84. _libraryMonitor = libraryMonitor;
  85. _logger = logger;
  86. _serverConfigurationManager = serverConfigurationManager;
  87. }
  88. /// <summary>
  89. /// Get the original file of an item.
  90. /// </summary>
  91. /// <param name="itemId">The item id.</param>
  92. /// <response code="200">File stream returned.</response>
  93. /// <response code="404">Item not found.</response>
  94. /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
  95. [HttpGet("Items/{itemId}/File")]
  96. [Authorize]
  97. [ProducesResponseType(StatusCodes.Status200OK)]
  98. [ProducesResponseType(StatusCodes.Status404NotFound)]
  99. [ProducesFile("video/*", "audio/*")]
  100. public ActionResult GetFile([FromRoute, Required] Guid itemId)
  101. {
  102. var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
  103. if (item is null)
  104. {
  105. return NotFound();
  106. }
  107. return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
  108. }
  109. /// <summary>
  110. /// Gets critic review for an item.
  111. /// </summary>
  112. /// <response code="200">Critic reviews returned.</response>
  113. /// <returns>The list of critic reviews.</returns>
  114. [HttpGet("Items/{itemId}/CriticReviews")]
  115. [Authorize]
  116. [Obsolete("This endpoint is obsolete.")]
  117. [ProducesResponseType(StatusCodes.Status200OK)]
  118. public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
  119. {
  120. return new QueryResult<BaseItemDto>();
  121. }
  122. /// <summary>
  123. /// Get theme songs for an item.
  124. /// </summary>
  125. /// <param name="itemId">The item id.</param>
  126. /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
  127. /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
  128. /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
  129. /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
  130. /// <response code="200">Theme songs returned.</response>
  131. /// <response code="404">Item not found.</response>
  132. /// <returns>The item theme songs.</returns>
  133. [HttpGet("Items/{itemId}/ThemeSongs")]
  134. [Authorize]
  135. [ProducesResponseType(StatusCodes.Status200OK)]
  136. [ProducesResponseType(StatusCodes.Status404NotFound)]
  137. public ActionResult<ThemeMediaResult> GetThemeSongs(
  138. [FromRoute, Required] Guid itemId,
  139. [FromQuery] Guid? userId,
  140. [FromQuery] bool inheritFromParent = false,
  141. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
  142. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
  143. {
  144. userId = RequestHelpers.GetUserId(User, userId);
  145. var user = userId.IsNullOrEmpty()
  146. ? null
  147. : _userManager.GetUserById(userId.Value);
  148. var item = itemId.IsEmpty()
  149. ? (userId.IsNullOrEmpty()
  150. ? _libraryManager.RootFolder
  151. : _libraryManager.GetUserRootFolder())
  152. : _libraryManager.GetItemById<BaseItem>(itemId, user);
  153. if (item is null)
  154. {
  155. return NotFound();
  156. }
  157. sortOrder ??= [];
  158. sortBy ??= [];
  159. var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
  160. IReadOnlyList<BaseItem> themeItems;
  161. while (true)
  162. {
  163. themeItems = item.GetThemeSongs(user, orderBy);
  164. if (themeItems.Count > 0 || !inheritFromParent)
  165. {
  166. break;
  167. }
  168. var parent = item.GetParent();
  169. if (parent is null)
  170. {
  171. break;
  172. }
  173. item = parent;
  174. }
  175. var dtoOptions = new DtoOptions().AddClientFields(User);
  176. var items = themeItems
  177. .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
  178. .ToArray();
  179. return new ThemeMediaResult
  180. {
  181. Items = items,
  182. TotalRecordCount = items.Length,
  183. OwnerId = item.Id
  184. };
  185. }
  186. /// <summary>
  187. /// Get theme videos for an item.
  188. /// </summary>
  189. /// <param name="itemId">The item id.</param>
  190. /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
  191. /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
  192. /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
  193. /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
  194. /// <response code="200">Theme videos returned.</response>
  195. /// <response code="404">Item not found.</response>
  196. /// <returns>The item theme videos.</returns>
  197. [HttpGet("Items/{itemId}/ThemeVideos")]
  198. [Authorize]
  199. [ProducesResponseType(StatusCodes.Status200OK)]
  200. [ProducesResponseType(StatusCodes.Status404NotFound)]
  201. public ActionResult<ThemeMediaResult> GetThemeVideos(
  202. [FromRoute, Required] Guid itemId,
  203. [FromQuery] Guid? userId,
  204. [FromQuery] bool inheritFromParent = false,
  205. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
  206. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
  207. {
  208. userId = RequestHelpers.GetUserId(User, userId);
  209. var user = userId.IsNullOrEmpty()
  210. ? null
  211. : _userManager.GetUserById(userId.Value);
  212. var item = itemId.IsEmpty()
  213. ? (userId.IsNullOrEmpty()
  214. ? _libraryManager.RootFolder
  215. : _libraryManager.GetUserRootFolder())
  216. : _libraryManager.GetItemById<BaseItem>(itemId, user);
  217. if (item is null)
  218. {
  219. return NotFound();
  220. }
  221. sortOrder ??= [];
  222. sortBy ??= [];
  223. var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
  224. IEnumerable<BaseItem> themeItems;
  225. while (true)
  226. {
  227. themeItems = item.GetThemeVideos(user, orderBy);
  228. if (themeItems.Any() || !inheritFromParent)
  229. {
  230. break;
  231. }
  232. var parent = item.GetParent();
  233. if (parent is null)
  234. {
  235. break;
  236. }
  237. item = parent;
  238. }
  239. var dtoOptions = new DtoOptions().AddClientFields(User);
  240. var items = themeItems
  241. .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
  242. .ToArray();
  243. return new ThemeMediaResult
  244. {
  245. Items = items,
  246. TotalRecordCount = items.Length,
  247. OwnerId = item.Id
  248. };
  249. }
  250. /// <summary>
  251. /// Get theme songs and videos for an item.
  252. /// </summary>
  253. /// <param name="itemId">The item id.</param>
  254. /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
  255. /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
  256. /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
  257. /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
  258. /// <response code="200">Theme songs and videos returned.</response>
  259. /// <response code="404">Item not found.</response>
  260. /// <returns>The item theme videos.</returns>
  261. [HttpGet("Items/{itemId}/ThemeMedia")]
  262. [Authorize]
  263. [ProducesResponseType(StatusCodes.Status200OK)]
  264. public ActionResult<AllThemeMediaResult> GetThemeMedia(
  265. [FromRoute, Required] Guid itemId,
  266. [FromQuery] Guid? userId,
  267. [FromQuery] bool inheritFromParent = false,
  268. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
  269. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
  270. {
  271. var themeSongs = GetThemeSongs(
  272. itemId,
  273. userId,
  274. inheritFromParent,
  275. sortBy,
  276. sortOrder);
  277. var themeVideos = GetThemeVideos(
  278. itemId,
  279. userId,
  280. inheritFromParent,
  281. sortBy,
  282. sortOrder);
  283. if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }
  284. || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound })
  285. {
  286. return NotFound();
  287. }
  288. return new AllThemeMediaResult
  289. {
  290. ThemeSongsResult = themeSongs.Value,
  291. ThemeVideosResult = themeVideos.Value,
  292. SoundtrackSongsResult = new ThemeMediaResult()
  293. };
  294. }
  295. /// <summary>
  296. /// Starts a library scan.
  297. /// </summary>
  298. /// <response code="204">Library scan started.</response>
  299. /// <returns>A <see cref="NoContentResult"/>.</returns>
  300. [HttpPost("Library/Refresh")]
  301. [Authorize(Policy = Policies.RequiresElevation)]
  302. [ProducesResponseType(StatusCodes.Status204NoContent)]
  303. public async Task<ActionResult> RefreshLibrary()
  304. {
  305. try
  306. {
  307. await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
  308. }
  309. catch (Exception ex)
  310. {
  311. _logger.LogError(ex, "Error refreshing library");
  312. }
  313. return NoContent();
  314. }
  315. /// <summary>
  316. /// Deletes an item from the library and filesystem.
  317. /// </summary>
  318. /// <param name="itemId">The item id.</param>
  319. /// <response code="204">Item deleted.</response>
  320. /// <response code="401">Unauthorized access.</response>
  321. /// <response code="404">Item not found.</response>
  322. /// <returns>A <see cref="NoContentResult"/>.</returns>
  323. [HttpDelete("Items/{itemId}")]
  324. [Authorize]
  325. [ProducesResponseType(StatusCodes.Status204NoContent)]
  326. [ProducesResponseType(StatusCodes.Status401Unauthorized)]
  327. [ProducesResponseType(StatusCodes.Status404NotFound)]
  328. public ActionResult DeleteItem(Guid itemId)
  329. {
  330. var userId = User.GetUserId();
  331. var isApiKey = User.GetIsApiKey();
  332. var user = userId.IsEmpty() && isApiKey
  333. ? null
  334. : _userManager.GetUserById(userId);
  335. if (user is null && !isApiKey)
  336. {
  337. return NotFound();
  338. }
  339. var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
  340. if (item is null)
  341. {
  342. return NotFound();
  343. }
  344. if (user is not null && !item.CanDelete(user))
  345. {
  346. return Unauthorized("Unauthorized access");
  347. }
  348. _libraryManager.DeleteItem(
  349. item,
  350. new DeleteOptions { DeleteFileLocation = true },
  351. true);
  352. return NoContent();
  353. }
  354. /// <summary>
  355. /// Deletes items from the library and filesystem.
  356. /// </summary>
  357. /// <param name="ids">The item ids.</param>
  358. /// <response code="204">Items deleted.</response>
  359. /// <response code="401">Unauthorized access.</response>
  360. /// <returns>A <see cref="NoContentResult"/>.</returns>
  361. [HttpDelete("Items")]
  362. [Authorize]
  363. [ProducesResponseType(StatusCodes.Status204NoContent)]
  364. [ProducesResponseType(StatusCodes.Status401Unauthorized)]
  365. [ProducesResponseType(StatusCodes.Status404NotFound)]
  366. public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
  367. {
  368. var isApiKey = User.GetIsApiKey();
  369. var userId = User.GetUserId();
  370. var user = !isApiKey && !userId.IsEmpty()
  371. ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
  372. : null;
  373. if (!isApiKey && user is null)
  374. {
  375. return Unauthorized("Unauthorized access");
  376. }
  377. foreach (var i in ids)
  378. {
  379. var item = _libraryManager.GetItemById<BaseItem>(i, user);
  380. if (item is null)
  381. {
  382. return NotFound();
  383. }
  384. if (user is not null && !item.CanDelete(user))
  385. {
  386. return Unauthorized("Unauthorized access");
  387. }
  388. _libraryManager.DeleteItem(
  389. item,
  390. new DeleteOptions { DeleteFileLocation = true },
  391. true);
  392. }
  393. return NoContent();
  394. }
  395. /// <summary>
  396. /// Get item counts.
  397. /// </summary>
  398. /// <param name="userId">Optional. Get counts from a specific user's library.</param>
  399. /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
  400. /// <response code="200">Item counts returned.</response>
  401. /// <returns>Item counts.</returns>
  402. [HttpGet("Items/Counts")]
  403. [Authorize]
  404. [ProducesResponseType(StatusCodes.Status200OK)]
  405. public ActionResult<ItemCounts> GetItemCounts(
  406. [FromQuery] Guid? userId,
  407. [FromQuery] bool? isFavorite)
  408. {
  409. userId = RequestHelpers.GetUserId(User, userId);
  410. var user = userId.IsNullOrEmpty()
  411. ? null
  412. : _userManager.GetUserById(userId.Value);
  413. var counts = new ItemCounts
  414. {
  415. AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
  416. EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
  417. MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
  418. SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
  419. SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
  420. MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
  421. BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
  422. BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
  423. };
  424. return counts;
  425. }
  426. /// <summary>
  427. /// Gets all parents of an item.
  428. /// </summary>
  429. /// <param name="itemId">The item id.</param>
  430. /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
  431. /// <response code="200">Item parents returned.</response>
  432. /// <response code="404">Item not found.</response>
  433. /// <returns>Item parents.</returns>
  434. [HttpGet("Items/{itemId}/Ancestors")]
  435. [Authorize]
  436. [ProducesResponseType(StatusCodes.Status200OK)]
  437. [ProducesResponseType(StatusCodes.Status404NotFound)]
  438. public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
  439. {
  440. userId = RequestHelpers.GetUserId(User, userId);
  441. var user = userId.IsNullOrEmpty()
  442. ? null
  443. : _userManager.GetUserById(userId.Value);
  444. var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
  445. if (item is null)
  446. {
  447. return NotFound();
  448. }
  449. var baseItemDtos = new List<BaseItemDto>();
  450. var dtoOptions = new DtoOptions().AddClientFields(User);
  451. BaseItem? parent = item.GetParent();
  452. while (parent is not null)
  453. {
  454. if (user is not null)
  455. {
  456. parent = TranslateParentItem(parent, user);
  457. if (parent is null)
  458. {
  459. break;
  460. }
  461. }
  462. baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
  463. parent = parent.GetParent();
  464. }
  465. return baseItemDtos;
  466. }
  467. /// <summary>
  468. /// Gets a list of physical paths from virtual folders.
  469. /// </summary>
  470. /// <response code="200">Physical paths returned.</response>
  471. /// <returns>List of physical paths.</returns>
  472. [HttpGet("Library/PhysicalPaths")]
  473. [Authorize(Policy = Policies.RequiresElevation)]
  474. [ProducesResponseType(StatusCodes.Status200OK)]
  475. public ActionResult<IEnumerable<string>> GetPhysicalPaths()
  476. {
  477. return Ok(_libraryManager.RootFolder.Children
  478. .SelectMany(c => c.PhysicalLocations));
  479. }
  480. /// <summary>
  481. /// Gets all user media folders.
  482. /// </summary>
  483. /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
  484. /// <response code="200">Media folders returned.</response>
  485. /// <returns>List of user media folders.</returns>
  486. [HttpGet("Library/MediaFolders")]
  487. [Authorize(Policy = Policies.RequiresElevation)]
  488. [ProducesResponseType(StatusCodes.Status200OK)]
  489. public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
  490. {
  491. var items = _libraryManager.GetUserRootFolder().Children
  492. .Concat(_libraryManager.RootFolder.VirtualChildren)
  493. .Where(i => _libraryManager.GetLibraryOptions(i).Enabled)
  494. .OrderBy(i => i.SortName)
  495. .ToList();
  496. if (isHidden.HasValue)
  497. {
  498. var val = isHidden.Value;
  499. items = items.Where(i => i.IsHidden == val).ToList();
  500. }
  501. var dtoOptions = new DtoOptions().AddClientFields(User);
  502. var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
  503. return new QueryResult<BaseItemDto>(resultArray);
  504. }
  505. /// <summary>
  506. /// Reports that new episodes of a series have been added by an external source.
  507. /// </summary>
  508. /// <param name="tvdbId">The tvdbId.</param>
  509. /// <response code="204">Report success.</response>
  510. /// <returns>A <see cref="NoContentResult"/>.</returns>
  511. [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
  512. [HttpPost("Library/Series/Updated")]
  513. [Authorize]
  514. [ProducesResponseType(StatusCodes.Status204NoContent)]
  515. public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
  516. {
  517. var series = _libraryManager.GetItemList(new InternalItemsQuery
  518. {
  519. IncludeItemTypes = new[] { BaseItemKind.Series },
  520. DtoOptions = new DtoOptions(false)
  521. {
  522. EnableImages = false
  523. }
  524. }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
  525. foreach (var item in series)
  526. {
  527. _libraryMonitor.ReportFileSystemChanged(item.Path);
  528. }
  529. return NoContent();
  530. }
  531. /// <summary>
  532. /// Reports that new movies have been added by an external source.
  533. /// </summary>
  534. /// <param name="tmdbId">The tmdbId.</param>
  535. /// <param name="imdbId">The imdbId.</param>
  536. /// <response code="204">Report success.</response>
  537. /// <returns>A <see cref="NoContentResult"/>.</returns>
  538. [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
  539. [HttpPost("Library/Movies/Updated")]
  540. [Authorize]
  541. [ProducesResponseType(StatusCodes.Status204NoContent)]
  542. public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
  543. {
  544. var movies = _libraryManager.GetItemList(new InternalItemsQuery
  545. {
  546. IncludeItemTypes = new[] { BaseItemKind.Movie },
  547. DtoOptions = new DtoOptions(false)
  548. {
  549. EnableImages = false
  550. }
  551. });
  552. if (!string.IsNullOrWhiteSpace(imdbId))
  553. {
  554. movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
  555. }
  556. else if (!string.IsNullOrWhiteSpace(tmdbId))
  557. {
  558. movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
  559. }
  560. else
  561. {
  562. movies = new List<BaseItem>();
  563. }
  564. foreach (var item in movies)
  565. {
  566. _libraryMonitor.ReportFileSystemChanged(item.Path);
  567. }
  568. return NoContent();
  569. }
  570. /// <summary>
  571. /// Reports that new movies have been added by an external source.
  572. /// </summary>
  573. /// <param name="dto">The update paths.</param>
  574. /// <response code="204">Report success.</response>
  575. /// <returns>A <see cref="NoContentResult"/>.</returns>
  576. [HttpPost("Library/Media/Updated")]
  577. [Authorize]
  578. [ProducesResponseType(StatusCodes.Status204NoContent)]
  579. public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
  580. {
  581. foreach (var item in dto.Updates)
  582. {
  583. _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
  584. }
  585. return NoContent();
  586. }
  587. /// <summary>
  588. /// Downloads item media.
  589. /// </summary>
  590. /// <param name="itemId">The item id.</param>
  591. /// <response code="200">Media downloaded.</response>
  592. /// <response code="404">Item not found.</response>
  593. /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
  594. /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
  595. [HttpGet("Items/{itemId}/Download")]
  596. [Authorize(Policy = Policies.Download)]
  597. [ProducesResponseType(StatusCodes.Status200OK)]
  598. [ProducesResponseType(StatusCodes.Status404NotFound)]
  599. [ProducesFile("video/*", "audio/*")]
  600. public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
  601. {
  602. var userId = User.GetUserId();
  603. var user = userId.IsEmpty()
  604. ? null
  605. : _userManager.GetUserById(userId);
  606. var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
  607. if (item is null)
  608. {
  609. return NotFound();
  610. }
  611. if (user is not null)
  612. {
  613. if (!item.CanDownload(user))
  614. {
  615. throw new ArgumentException("Item does not support downloading");
  616. }
  617. }
  618. else
  619. {
  620. if (!item.CanDownload())
  621. {
  622. throw new ArgumentException("Item does not support downloading");
  623. }
  624. }
  625. if (user is not null)
  626. {
  627. await LogDownloadAsync(item, user).ConfigureAwait(false);
  628. }
  629. // Quotes are valid in linux. They'll possibly cause issues here.
  630. var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
  631. return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
  632. }
  633. /// <summary>
  634. /// Gets similar items.
  635. /// </summary>
  636. /// <param name="itemId">The item id.</param>
  637. /// <param name="excludeArtistIds">Exclude artist ids.</param>
  638. /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
  639. /// <param name="limit">Optional. The maximum number of records to return.</param>
  640. /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
  641. /// <response code="200">Similar items returned.</response>
  642. /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
  643. [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
  644. [HttpGet("Items/{itemId}/Similar")]
  645. [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
  646. [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
  647. [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
  648. [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
  649. [Authorize]
  650. [ProducesResponseType(StatusCodes.Status200OK)]
  651. public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
  652. [FromRoute, Required] Guid itemId,
  653. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
  654. [FromQuery] Guid? userId,
  655. [FromQuery] int? limit,
  656. [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
  657. {
  658. userId = RequestHelpers.GetUserId(User, userId);
  659. var user = userId.IsNullOrEmpty()
  660. ? null
  661. : _userManager.GetUserById(userId.Value);
  662. var item = itemId.IsEmpty()
  663. ? (user is null
  664. ? _libraryManager.RootFolder
  665. : _libraryManager.GetUserRootFolder())
  666. : _libraryManager.GetItemById<BaseItem>(itemId, user);
  667. if (item is null)
  668. {
  669. return NotFound();
  670. }
  671. if (item is Episode || (item is IItemByName && item is not MusicArtist))
  672. {
  673. return new QueryResult<BaseItemDto>();
  674. }
  675. var dtoOptions = new DtoOptions { Fields = fields }
  676. .AddClientFields(User);
  677. var program = item as IHasProgramAttributes;
  678. bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
  679. bool? isSeries = item is Series || (program is not null && program.IsSeries);
  680. var includeItemTypes = new List<BaseItemKind>();
  681. if (isMovie.Value)
  682. {
  683. includeItemTypes.Add(BaseItemKind.Movie);
  684. if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
  685. {
  686. includeItemTypes.Add(BaseItemKind.Trailer);
  687. includeItemTypes.Add(BaseItemKind.LiveTvProgram);
  688. }
  689. }
  690. else if (isSeries.Value)
  691. {
  692. includeItemTypes.Add(BaseItemKind.Series);
  693. }
  694. else
  695. {
  696. // For non series and movie types these columns are typically null
  697. // isSeries = null;
  698. isMovie = null;
  699. includeItemTypes.Add(item.GetBaseItemKind());
  700. }
  701. var query = new InternalItemsQuery(user)
  702. {
  703. Genres = item.Genres,
  704. Limit = limit,
  705. IncludeItemTypes = includeItemTypes.ToArray(),
  706. DtoOptions = dtoOptions,
  707. EnableTotalRecordCount = !isMovie ?? true,
  708. EnableGroupByMetadataKey = isMovie ?? false,
  709. };
  710. // ExcludeArtistIds
  711. if (excludeArtistIds.Length != 0)
  712. {
  713. query.ExcludeArtistIds = excludeArtistIds;
  714. }
  715. var itemsResult = _libraryManager.GetItemList(query);
  716. var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
  717. return new QueryResult<BaseItemDto>(
  718. query.StartIndex,
  719. itemsResult.Count,
  720. returnList);
  721. }
  722. /// <summary>
  723. /// Gets the library options info.
  724. /// </summary>
  725. /// <param name="libraryContentType">Library content type.</param>
  726. /// <param name="isNewLibrary">Whether this is a new library.</param>
  727. /// <response code="200">Library options info returned.</response>
  728. /// <returns>Library options info.</returns>
  729. [HttpGet("Libraries/AvailableOptions")]
  730. [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
  731. [ProducesResponseType(StatusCodes.Status200OK)]
  732. public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
  733. [FromQuery] CollectionType? libraryContentType,
  734. [FromQuery] bool isNewLibrary = false)
  735. {
  736. var result = new LibraryOptionsResultDto();
  737. var types = GetRepresentativeItemTypes(libraryContentType);
  738. var typesList = types.ToList();
  739. var plugins = _providerManager.GetAllMetadataPlugins()
  740. .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
  741. .OrderBy(i => typesList.IndexOf(i.ItemType))
  742. .ToList();
  743. result.MetadataSavers = plugins
  744. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
  745. .Select(i => new LibraryOptionInfoDto
  746. {
  747. Name = i.Name,
  748. DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
  749. })
  750. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  751. .ToArray();
  752. result.MetadataReaders = plugins
  753. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
  754. .Select(i => new LibraryOptionInfoDto
  755. {
  756. Name = i.Name,
  757. DefaultEnabled = true
  758. })
  759. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  760. .ToArray();
  761. result.SubtitleFetchers = plugins
  762. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
  763. .Select(i => new LibraryOptionInfoDto
  764. {
  765. Name = i.Name,
  766. DefaultEnabled = true
  767. })
  768. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  769. .ToArray();
  770. result.LyricFetchers = plugins
  771. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LyricFetcher))
  772. .Select(i => new LibraryOptionInfoDto
  773. {
  774. Name = i.Name,
  775. DefaultEnabled = true
  776. })
  777. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  778. .ToArray();
  779. result.MediaSegmentProviders = plugins
  780. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MediaSegmentProvider))
  781. .Select(i => new LibraryOptionInfoDto
  782. {
  783. Name = i.Name,
  784. DefaultEnabled = true
  785. })
  786. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  787. .ToArray();
  788. var typeOptions = new List<LibraryTypeOptionsDto>();
  789. foreach (var type in types)
  790. {
  791. TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
  792. typeOptions.Add(new LibraryTypeOptionsDto
  793. {
  794. Type = type,
  795. MetadataFetchers = plugins
  796. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  797. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
  798. .Select(i => new LibraryOptionInfoDto
  799. {
  800. Name = i.Name,
  801. DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
  802. })
  803. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  804. .ToArray(),
  805. ImageFetchers = plugins
  806. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  807. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
  808. .Select(i => new LibraryOptionInfoDto
  809. {
  810. Name = i.Name,
  811. DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
  812. })
  813. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  814. .ToArray(),
  815. SupportedImageTypes = plugins
  816. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  817. .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
  818. .Distinct()
  819. .ToArray(),
  820. DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
  821. });
  822. }
  823. result.TypeOptions = typeOptions.ToArray();
  824. return result;
  825. }
  826. private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
  827. {
  828. var query = new InternalItemsQuery(user)
  829. {
  830. IncludeItemTypes = new[] { itemKind },
  831. Limit = 0,
  832. Recursive = true,
  833. IsVirtualItem = false,
  834. IsFavorite = isFavorite,
  835. DtoOptions = new DtoOptions(false)
  836. {
  837. EnableImages = false
  838. }
  839. };
  840. return _libraryManager.GetItemsResult(query).TotalRecordCount;
  841. }
  842. private BaseItem? TranslateParentItem(BaseItem item, User user)
  843. {
  844. return item.GetParent() is AggregateFolder
  845. ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
  846. .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
  847. : item;
  848. }
  849. private async Task LogDownloadAsync(BaseItem item, User user)
  850. {
  851. try
  852. {
  853. await _activityManager.CreateAsync(new ActivityLog(
  854. string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
  855. "UserDownloadingContent",
  856. User.GetUserId())
  857. {
  858. ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
  859. ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
  860. }).ConfigureAwait(false);
  861. }
  862. catch
  863. {
  864. // Logged at lower levels
  865. }
  866. }
  867. private static string[] GetRepresentativeItemTypes(CollectionType? contentType)
  868. {
  869. return contentType switch
  870. {
  871. CollectionType.boxsets => new[] { "BoxSet" },
  872. CollectionType.playlists => new[] { "Playlist" },
  873. CollectionType.movies => new[] { "Movie" },
  874. CollectionType.tvshows => new[] { "Series", "Season", "Episode" },
  875. CollectionType.books => new[] { "Book" },
  876. CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
  877. CollectionType.homevideos => new[] { "Video", "Photo" },
  878. CollectionType.photos => new[] { "Video", "Photo" },
  879. CollectionType.musicvideos => new[] { "MusicVideo" },
  880. _ => new[] { "Series", "Season", "Episode", "Movie" }
  881. };
  882. }
  883. private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
  884. {
  885. if (isNewLibrary)
  886. {
  887. return false;
  888. }
  889. var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
  890. .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
  891. .ToArray();
  892. return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
  893. }
  894. private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
  895. {
  896. if (isNewLibrary)
  897. {
  898. if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
  899. {
  900. return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
  901. || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
  902. || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
  903. }
  904. return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
  905. || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
  906. || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
  907. }
  908. var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type);
  909. return metadataOptions is null || !metadataOptions.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
  910. }
  911. private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
  912. {
  913. if (isNewLibrary)
  914. {
  915. if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
  916. {
  917. return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
  918. && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
  919. && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
  920. && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
  921. }
  922. return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
  923. || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
  924. || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
  925. || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
  926. }
  927. var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type);
  928. return metadataOptions is null || !metadataOptions.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
  929. }
  930. }