LibraryController.cs 40 KB

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