LibraryController.cs 41 KB

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