LibraryController.cs 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  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.Enums;
  15. using Jellyfin.Database.Implementations.Entities;
  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();
  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();
  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();
  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();
  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. 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. Tags = item.Tags,
  704. Limit = limit,
  705. IncludeItemTypes = includeItemTypes.ToArray(),
  706. DtoOptions = dtoOptions,
  707. EnableTotalRecordCount = !isMovie ?? true,
  708. EnableGroupByMetadataKey = isMovie ?? false,
  709. ExcludeItemIds = [itemId],
  710. OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
  711. };
  712. // ExcludeArtistIds
  713. if (excludeArtistIds.Length != 0)
  714. {
  715. query.ExcludeArtistIds = excludeArtistIds;
  716. }
  717. var itemsResult = _libraryManager.GetItemList(query);
  718. var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
  719. return new QueryResult<BaseItemDto>(
  720. query.StartIndex,
  721. itemsResult.Count,
  722. returnList);
  723. }
  724. /// <summary>
  725. /// Gets the library options info.
  726. /// </summary>
  727. /// <param name="libraryContentType">Library content type.</param>
  728. /// <param name="isNewLibrary">Whether this is a new library.</param>
  729. /// <response code="200">Library options info returned.</response>
  730. /// <returns>Library options info.</returns>
  731. [HttpGet("Libraries/AvailableOptions")]
  732. [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
  733. [ProducesResponseType(StatusCodes.Status200OK)]
  734. public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
  735. [FromQuery] CollectionType? libraryContentType,
  736. [FromQuery] bool isNewLibrary = false)
  737. {
  738. var result = new LibraryOptionsResultDto();
  739. var types = GetRepresentativeItemTypes(libraryContentType);
  740. var typesList = types.ToList();
  741. var plugins = _providerManager.GetAllMetadataPlugins()
  742. .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
  743. .OrderBy(i => typesList.IndexOf(i.ItemType))
  744. .ToList();
  745. result.MetadataSavers = plugins
  746. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
  747. .Select(i => new LibraryOptionInfoDto
  748. {
  749. Name = i.Name,
  750. DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
  751. })
  752. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  753. .ToArray();
  754. result.MetadataReaders = plugins
  755. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
  756. .Select(i => new LibraryOptionInfoDto
  757. {
  758. Name = i.Name,
  759. DefaultEnabled = true
  760. })
  761. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  762. .ToArray();
  763. result.SubtitleFetchers = plugins
  764. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
  765. .Select(i => new LibraryOptionInfoDto
  766. {
  767. Name = i.Name,
  768. DefaultEnabled = true
  769. })
  770. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  771. .ToArray();
  772. result.LyricFetchers = plugins
  773. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LyricFetcher))
  774. .Select(i => new LibraryOptionInfoDto
  775. {
  776. Name = i.Name,
  777. DefaultEnabled = true
  778. })
  779. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  780. .ToArray();
  781. result.MediaSegmentProviders = plugins
  782. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MediaSegmentProvider))
  783. .Select(i => new LibraryOptionInfoDto
  784. {
  785. Name = i.Name,
  786. DefaultEnabled = true
  787. })
  788. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  789. .ToArray();
  790. var typeOptions = new List<LibraryTypeOptionsDto>();
  791. foreach (var type in types)
  792. {
  793. TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
  794. typeOptions.Add(new LibraryTypeOptionsDto
  795. {
  796. Type = type,
  797. MetadataFetchers = plugins
  798. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  799. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
  800. .Select(i => new LibraryOptionInfoDto
  801. {
  802. Name = i.Name,
  803. DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
  804. })
  805. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  806. .ToArray(),
  807. ImageFetchers = plugins
  808. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  809. .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
  810. .Select(i => new LibraryOptionInfoDto
  811. {
  812. Name = i.Name,
  813. DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
  814. })
  815. .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
  816. .ToArray(),
  817. SupportedImageTypes = plugins
  818. .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
  819. .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
  820. .Distinct()
  821. .ToArray(),
  822. DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
  823. });
  824. }
  825. result.TypeOptions = typeOptions.ToArray();
  826. return result;
  827. }
  828. private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
  829. {
  830. var query = new InternalItemsQuery(user)
  831. {
  832. IncludeItemTypes = new[] { itemKind },
  833. Limit = 0,
  834. Recursive = true,
  835. IsVirtualItem = false,
  836. IsFavorite = isFavorite,
  837. DtoOptions = new DtoOptions(false)
  838. {
  839. EnableImages = false
  840. }
  841. };
  842. return _libraryManager.GetItemsResult(query).TotalRecordCount;
  843. }
  844. private BaseItem? TranslateParentItem(BaseItem item, User user)
  845. {
  846. return item.GetParent() is AggregateFolder
  847. ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
  848. .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
  849. : item;
  850. }
  851. private async Task LogDownloadAsync(BaseItem item, User user)
  852. {
  853. try
  854. {
  855. await _activityManager.CreateAsync(new ActivityLog(
  856. string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
  857. "UserDownloadingContent",
  858. User.GetUserId())
  859. {
  860. ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
  861. ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
  862. }).ConfigureAwait(false);
  863. }
  864. catch
  865. {
  866. // Logged at lower levels
  867. }
  868. }
  869. private static string[] GetRepresentativeItemTypes(CollectionType? contentType)
  870. {
  871. return contentType switch
  872. {
  873. CollectionType.boxsets => new[] { "BoxSet" },
  874. CollectionType.playlists => new[] { "Playlist" },
  875. CollectionType.movies => new[] { "Movie" },
  876. CollectionType.tvshows => new[] { "Series", "Season", "Episode" },
  877. CollectionType.books => new[] { "Book" },
  878. CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
  879. CollectionType.homevideos => new[] { "Video", "Photo" },
  880. CollectionType.photos => new[] { "Video", "Photo" },
  881. CollectionType.musicvideos => new[] { "MusicVideo" },
  882. _ => new[] { "Series", "Season", "Episode", "Movie" }
  883. };
  884. }
  885. private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
  886. {
  887. if (isNewLibrary)
  888. {
  889. return false;
  890. }
  891. var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
  892. .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
  893. .ToArray();
  894. return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
  895. }
  896. private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
  897. {
  898. if (isNewLibrary)
  899. {
  900. if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
  901. {
  902. return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
  903. || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
  904. || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
  905. }
  906. return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
  907. || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
  908. || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
  909. }
  910. var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type);
  911. return metadataOptions is null || !metadataOptions.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
  912. }
  913. private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
  914. {
  915. if (isNewLibrary)
  916. {
  917. if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
  918. {
  919. return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
  920. && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
  921. && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
  922. && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
  923. }
  924. return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
  925. || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
  926. || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
  927. || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
  928. }
  929. var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type);
  930. return metadataOptions is null || !metadataOptions.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
  931. }
  932. }