SyncManager.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Extensions;
  3. using MediaBrowser.Controller.Channels;
  4. using MediaBrowser.Controller.Drawing;
  5. using MediaBrowser.Controller.Dto;
  6. using MediaBrowser.Controller.Entities;
  7. using MediaBrowser.Controller.Entities.Audio;
  8. using MediaBrowser.Controller.Entities.TV;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.LiveTv;
  11. using MediaBrowser.Controller.Sync;
  12. using MediaBrowser.Controller.TV;
  13. using MediaBrowser.Model.Dlna;
  14. using MediaBrowser.Model.Dto;
  15. using MediaBrowser.Model.Entities;
  16. using MediaBrowser.Model.Logging;
  17. using MediaBrowser.Model.Querying;
  18. using MediaBrowser.Model.Sync;
  19. using MediaBrowser.Model.Users;
  20. using MoreLinq;
  21. using System;
  22. using System.Collections.Generic;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Threading.Tasks;
  26. namespace MediaBrowser.Server.Implementations.Sync
  27. {
  28. public class SyncManager : ISyncManager
  29. {
  30. private readonly ILibraryManager _libraryManager;
  31. private readonly ISyncRepository _repo;
  32. private readonly IImageProcessor _imageProcessor;
  33. private readonly ILogger _logger;
  34. private readonly IUserManager _userManager;
  35. private readonly Func<IDtoService> _dtoService;
  36. private readonly IApplicationHost _appHost;
  37. private readonly ITVSeriesManager _tvSeriesManager;
  38. private ISyncProvider[] _providers = { };
  39. public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IApplicationHost appHost, ITVSeriesManager tvSeriesManager)
  40. {
  41. _libraryManager = libraryManager;
  42. _repo = repo;
  43. _imageProcessor = imageProcessor;
  44. _logger = logger;
  45. _userManager = userManager;
  46. _dtoService = dtoService;
  47. _appHost = appHost;
  48. _tvSeriesManager = tvSeriesManager;
  49. }
  50. public void AddParts(IEnumerable<ISyncProvider> providers)
  51. {
  52. _providers = providers.ToArray();
  53. }
  54. public async Task<SyncJobCreationResult> CreateJob(SyncJobRequest request)
  55. {
  56. var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager);
  57. var user = _userManager.GetUserById(request.UserId);
  58. var items = (await processor
  59. .GetItemsForSync(request.Category, request.ParentId, request.ItemIds, user, request.UnwatchedOnly).ConfigureAwait(false))
  60. .ToList();
  61. if (items.Any(i => !SupportsSync(i)))
  62. {
  63. throw new ArgumentException("Item does not support sync.");
  64. }
  65. if (string.IsNullOrWhiteSpace(request.Name))
  66. {
  67. if (request.ItemIds.Count == 1)
  68. {
  69. request.Name = GetDefaultName(_libraryManager.GetItemById(request.ItemIds[0]));
  70. }
  71. }
  72. if (string.IsNullOrWhiteSpace(request.Name))
  73. {
  74. throw new ArgumentException("Please supply a name for the sync job.");
  75. }
  76. var target = GetSyncTargets(request.UserId)
  77. .FirstOrDefault(i => string.Equals(request.TargetId, i.Id));
  78. if (target == null)
  79. {
  80. throw new ArgumentException("Sync target not found.");
  81. }
  82. var jobId = Guid.NewGuid().ToString("N");
  83. var job = new SyncJob
  84. {
  85. Id = jobId,
  86. Name = request.Name,
  87. TargetId = target.Id,
  88. UserId = request.UserId,
  89. UnwatchedOnly = request.UnwatchedOnly,
  90. ItemLimit = request.ItemLimit,
  91. RequestedItemIds = request.ItemIds ?? new List<string> { },
  92. DateCreated = DateTime.UtcNow,
  93. DateLastModified = DateTime.UtcNow,
  94. SyncNewContent = request.SyncNewContent,
  95. ItemCount = items.Count,
  96. Quality = request.Quality,
  97. Category = request.Category,
  98. ParentId = request.ParentId
  99. };
  100. // It's just a static list
  101. if (!items.Any(i => i.IsFolder || i is IItemByName))
  102. {
  103. job.SyncNewContent = false;
  104. }
  105. await _repo.Create(job).ConfigureAwait(false);
  106. await processor.EnsureJobItems(job).ConfigureAwait(false);
  107. return new SyncJobCreationResult
  108. {
  109. Job = GetJob(jobId)
  110. };
  111. }
  112. public Task UpdateJob(SyncJob job)
  113. {
  114. // Get fresh from the db and only update the fields that are supported to be changed.
  115. var instance = _repo.GetJob(job.Id);
  116. instance.Name = job.Name;
  117. instance.Quality = job.Quality;
  118. instance.UnwatchedOnly = job.UnwatchedOnly;
  119. instance.SyncNewContent = job.SyncNewContent;
  120. instance.ItemLimit = job.ItemLimit;
  121. return _repo.Update(instance);
  122. }
  123. public async Task<QueryResult<SyncJob>> GetJobs(SyncJobQuery query)
  124. {
  125. var result = _repo.GetJobs(query);
  126. foreach (var item in result.Items)
  127. {
  128. await FillMetadata(item).ConfigureAwait(false);
  129. }
  130. return result;
  131. }
  132. private async Task FillMetadata(SyncJob job)
  133. {
  134. var item = job.RequestedItemIds
  135. .Select(_libraryManager.GetItemById)
  136. .FirstOrDefault(i => i != null);
  137. if (item == null)
  138. {
  139. var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager);
  140. var user = _userManager.GetUserById(job.UserId);
  141. item = (await processor
  142. .GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false))
  143. .FirstOrDefault();
  144. }
  145. if (item != null)
  146. {
  147. var hasSeries = item as IHasSeries;
  148. if (hasSeries != null)
  149. {
  150. job.ParentName = hasSeries.SeriesName;
  151. }
  152. var hasAlbumArtist = item as IHasAlbumArtist;
  153. if (hasAlbumArtist != null)
  154. {
  155. job.ParentName = hasAlbumArtist.AlbumArtists.FirstOrDefault();
  156. }
  157. var primaryImage = item.GetImageInfo(ImageType.Primary, 0);
  158. var itemWithImage = item;
  159. if (primaryImage == null)
  160. {
  161. var parentWithImage = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary));
  162. if (parentWithImage != null)
  163. {
  164. itemWithImage = parentWithImage;
  165. primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0);
  166. }
  167. }
  168. if (primaryImage != null)
  169. {
  170. try
  171. {
  172. job.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary);
  173. job.PrimaryImageItemId = itemWithImage.Id.ToString("N");
  174. }
  175. catch (Exception ex)
  176. {
  177. _logger.ErrorException("Error getting image info", ex);
  178. }
  179. }
  180. }
  181. }
  182. private void FillMetadata(SyncJobItem jobItem)
  183. {
  184. var item = _libraryManager.GetItemById(jobItem.ItemId);
  185. if (item == null)
  186. {
  187. return;
  188. }
  189. var primaryImage = item.GetImageInfo(ImageType.Primary, 0);
  190. var itemWithImage = item;
  191. if (primaryImage == null)
  192. {
  193. var parentWithImage = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary));
  194. if (parentWithImage != null)
  195. {
  196. itemWithImage = parentWithImage;
  197. primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0);
  198. }
  199. }
  200. if (primaryImage != null)
  201. {
  202. try
  203. {
  204. jobItem.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary);
  205. jobItem.PrimaryImageItemId = itemWithImage.Id.ToString("N");
  206. }
  207. catch (Exception ex)
  208. {
  209. _logger.ErrorException("Error getting image info", ex);
  210. }
  211. }
  212. }
  213. public Task CancelJob(string id)
  214. {
  215. return _repo.DeleteJob(id);
  216. }
  217. public SyncJob GetJob(string id)
  218. {
  219. return _repo.GetJob(id);
  220. }
  221. public IEnumerable<SyncTarget> GetSyncTargets(string userId)
  222. {
  223. return _providers
  224. .SelectMany(i => GetSyncTargets(i, userId))
  225. .OrderBy(i => i.Name);
  226. }
  227. private IEnumerable<SyncTarget> GetSyncTargets(ISyncProvider provider, string userId)
  228. {
  229. return provider.GetSyncTargets(userId).Select(i => new SyncTarget
  230. {
  231. Name = i.Name,
  232. Id = GetSyncTargetId(provider, i)
  233. });
  234. }
  235. private string GetSyncTargetId(ISyncProvider provider, SyncTarget target)
  236. {
  237. var hasUniqueId = provider as IHasUniqueTargetIds;
  238. if (hasUniqueId != null)
  239. {
  240. return target.Id;
  241. }
  242. var providerId = GetSyncProviderId(provider);
  243. return (providerId + "-" + target.Id).GetMD5().ToString("N");
  244. }
  245. private ISyncProvider GetSyncProvider(SyncTarget target)
  246. {
  247. var providerId = target.Id.Split(new[] { '-' }, 2).First();
  248. return _providers.First(i => string.Equals(providerId, GetSyncProviderId(i)));
  249. }
  250. private string GetSyncProviderId(ISyncProvider provider)
  251. {
  252. return (provider.GetType().Name).GetMD5().ToString("N");
  253. }
  254. public bool SupportsSync(BaseItem item)
  255. {
  256. if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) ||
  257. string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ||
  258. string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase) ||
  259. string.Equals(item.MediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase) ||
  260. string.Equals(item.MediaType, MediaType.Book, StringComparison.OrdinalIgnoreCase))
  261. {
  262. if (item.LocationType == LocationType.Virtual)
  263. {
  264. return false;
  265. }
  266. if (!item.RunTimeTicks.HasValue)
  267. {
  268. return false;
  269. }
  270. var video = item as Video;
  271. if (video != null)
  272. {
  273. if (video.VideoType == VideoType.Iso)
  274. {
  275. return false;
  276. }
  277. if (video.IsStacked)
  278. {
  279. return false;
  280. }
  281. }
  282. var game = item as Game;
  283. if (game != null)
  284. {
  285. if (game.IsMultiPart)
  286. {
  287. return false;
  288. }
  289. }
  290. if (item is LiveTvChannel || item is IChannelItem || item is ILiveTvRecording)
  291. {
  292. return false;
  293. }
  294. // It would be nice to support these later
  295. if (item is Game || item is Book)
  296. {
  297. return false;
  298. }
  299. return true;
  300. }
  301. return item.LocationType == LocationType.FileSystem || item is Season || item is ILiveTvRecording;
  302. }
  303. private string GetDefaultName(BaseItem item)
  304. {
  305. return item.Name;
  306. }
  307. public DeviceProfile GetDeviceProfile(string targetId)
  308. {
  309. foreach (var provider in _providers)
  310. {
  311. foreach (var target in GetSyncTargets(provider, null))
  312. {
  313. if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase))
  314. {
  315. return provider.GetDeviceProfile(target);
  316. }
  317. }
  318. }
  319. return null;
  320. }
  321. public async Task ReportSyncJobItemTransferred(string id)
  322. {
  323. var jobItem = _repo.GetJobItem(id);
  324. jobItem.Status = SyncJobItemStatus.Synced;
  325. jobItem.Progress = 100;
  326. await _repo.Update(jobItem).ConfigureAwait(false);
  327. var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager);
  328. await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
  329. }
  330. public SyncJobItem GetJobItem(string id)
  331. {
  332. return _repo.GetJobItem(id);
  333. }
  334. public QueryResult<SyncJobItem> GetJobItems(SyncJobItemQuery query)
  335. {
  336. var result = _repo.GetJobItems(query);
  337. if (query.AddMetadata)
  338. {
  339. result.Items.ForEach(FillMetadata);
  340. }
  341. return result;
  342. }
  343. private SyncedItem GetJobItemInfo(SyncJobItem jobItem)
  344. {
  345. var job = _repo.GetJob(jobItem.JobId);
  346. var libraryItem = _libraryManager.GetItemById(jobItem.ItemId);
  347. var syncedItem = new SyncedItem
  348. {
  349. SyncJobId = jobItem.JobId,
  350. SyncJobItemId = jobItem.Id,
  351. ServerId = _appHost.SystemId,
  352. UserId = job.UserId
  353. };
  354. var dtoOptions = new DtoOptions();
  355. // Remove some bloat
  356. dtoOptions.Fields.Remove(ItemFields.MediaStreams);
  357. dtoOptions.Fields.Remove(ItemFields.IndexOptions);
  358. dtoOptions.Fields.Remove(ItemFields.MediaSourceCount);
  359. dtoOptions.Fields.Remove(ItemFields.OriginalPrimaryImageAspectRatio);
  360. dtoOptions.Fields.Remove(ItemFields.Path);
  361. dtoOptions.Fields.Remove(ItemFields.SeriesGenres);
  362. dtoOptions.Fields.Remove(ItemFields.Settings);
  363. dtoOptions.Fields.Remove(ItemFields.SyncInfo);
  364. syncedItem.Item = _dtoService().GetBaseItemDto(libraryItem, dtoOptions);
  365. // TODO: this should be the media source of the transcoded output
  366. syncedItem.Item.MediaSources = syncedItem.Item.MediaSources
  367. .Where(i => string.Equals(i.Id, jobItem.MediaSourceId))
  368. .ToList();
  369. var mediaSource = syncedItem.Item.MediaSources
  370. .FirstOrDefault(i => string.Equals(i.Id, jobItem.MediaSourceId));
  371. // This will be null for items that are not audio/video
  372. if (mediaSource == null)
  373. {
  374. syncedItem.OriginalFileName = Path.GetFileName(libraryItem.Path);
  375. }
  376. else
  377. {
  378. syncedItem.OriginalFileName = Path.GetFileName(mediaSource.Path);
  379. }
  380. return syncedItem;
  381. }
  382. public Task ReportOfflineAction(UserAction action)
  383. {
  384. return Task.FromResult(true);
  385. }
  386. public List<SyncedItem> GetReadySyncItems(string targetId)
  387. {
  388. var jobItemResult = GetJobItems(new SyncJobItemQuery
  389. {
  390. TargetId = targetId,
  391. Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Transferring }
  392. });
  393. return jobItemResult.Items.Select(GetJobItemInfo)
  394. .ToList();
  395. }
  396. public async Task<SyncDataResponse> SyncData(SyncDataRequest request)
  397. {
  398. var jobItemResult = GetJobItems(new SyncJobItemQuery
  399. {
  400. TargetId = request.TargetId,
  401. Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Synced }
  402. });
  403. var response = new SyncDataResponse();
  404. foreach (var jobItem in jobItemResult.Items)
  405. {
  406. if (request.LocalItemIds.Contains(jobItem.ItemId, StringComparer.OrdinalIgnoreCase))
  407. {
  408. var job = _repo.GetJob(jobItem.JobId);
  409. var user = _userManager.GetUserById(job.UserId);
  410. if (user == null)
  411. {
  412. // Tell the device to remove it since the user is gone now
  413. response.ItemIdsToRemove.Add(jobItem.ItemId);
  414. }
  415. else if (job.UnwatchedOnly)
  416. {
  417. var libraryItem = _libraryManager.GetItemById(jobItem.ItemId);
  418. if (IsLibraryItemAvailable(libraryItem))
  419. {
  420. if (libraryItem.IsPlayed(user) && libraryItem is Video)
  421. {
  422. // Tell the device to remove it since it has been played
  423. response.ItemIdsToRemove.Add(jobItem.ItemId);
  424. }
  425. }
  426. else
  427. {
  428. // Tell the device to remove it since it's no longer available
  429. response.ItemIdsToRemove.Add(jobItem.ItemId);
  430. }
  431. }
  432. }
  433. else
  434. {
  435. // Content is no longer on the device
  436. jobItem.Status = SyncJobItemStatus.RemovedFromDevice;
  437. await _repo.Update(jobItem).ConfigureAwait(false);
  438. }
  439. }
  440. response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
  441. return response;
  442. }
  443. private bool IsLibraryItemAvailable(BaseItem item)
  444. {
  445. if (item == null)
  446. {
  447. return false;
  448. }
  449. // TODO: Make sure it hasn't been deleted
  450. return true;
  451. }
  452. }
  453. }