EpisodeFileOrganizer.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.FileOrganization;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Providers;
  7. using MediaBrowser.Model.Entities;
  8. using MediaBrowser.Model.Extensions;
  9. using MediaBrowser.Model.FileOrganization;
  10. using MediaBrowser.Model.Logging;
  11. using MediaBrowser.Server.Implementations.Library;
  12. using MediaBrowser.Server.Implementations.Logging;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.Globalization;
  16. using System.IO;
  17. using System.Linq;
  18. using System.Threading;
  19. using System.Threading.Tasks;
  20. using CommonIO;
  21. namespace MediaBrowser.Server.Implementations.FileOrganization
  22. {
  23. public class EpisodeFileOrganizer
  24. {
  25. private readonly ILibraryMonitor _libraryMonitor;
  26. private readonly ILibraryManager _libraryManager;
  27. private readonly ILogger _logger;
  28. private readonly IFileSystem _fileSystem;
  29. private readonly IFileOrganizationService _organizationService;
  30. private readonly IServerConfigurationManager _config;
  31. private readonly IProviderManager _providerManager;
  32. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  33. public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager)
  34. {
  35. _organizationService = organizationService;
  36. _config = config;
  37. _fileSystem = fileSystem;
  38. _logger = logger;
  39. _libraryManager = libraryManager;
  40. _libraryMonitor = libraryMonitor;
  41. _providerManager = providerManager;
  42. }
  43. public Task<FileOrganizationResult> OrganizeEpisodeFile(string path, CancellationToken cancellationToken)
  44. {
  45. var options = _config.GetAutoOrganizeOptions().TvOptions;
  46. return OrganizeEpisodeFile(path, options, false, cancellationToken);
  47. }
  48. public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting, CancellationToken cancellationToken)
  49. {
  50. _logger.Info("Sorting file {0}", path);
  51. var result = new FileOrganizationResult
  52. {
  53. Date = DateTime.UtcNow,
  54. OriginalPath = path,
  55. OriginalFileName = Path.GetFileName(path),
  56. Type = FileOrganizerType.Episode,
  57. FileSize = new FileInfo(path).Length
  58. };
  59. var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
  60. var resolver = new Naming.TV.EpisodeResolver(namingOptions, new PatternsLogger());
  61. var episodeInfo = resolver.Resolve(path, false) ??
  62. new Naming.TV.EpisodeInfo();
  63. var seriesName = episodeInfo.SeriesName;
  64. if (!string.IsNullOrEmpty(seriesName))
  65. {
  66. var seasonNumber = episodeInfo.SeasonNumber;
  67. result.ExtractedSeasonNumber = seasonNumber;
  68. // Passing in true will include a few extra regex's
  69. var episodeNumber = episodeInfo.EpisodeNumber;
  70. result.ExtractedEpisodeNumber = episodeNumber;
  71. var premiereDate = episodeInfo.IsByDate ?
  72. new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) :
  73. (DateTime?)null;
  74. if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue))
  75. {
  76. if (episodeInfo.IsByDate)
  77. {
  78. _logger.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value);
  79. }
  80. else
  81. {
  82. _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber);
  83. }
  84. var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber;
  85. result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;
  86. await OrganizeEpisode(path,
  87. seriesName,
  88. seasonNumber,
  89. episodeNumber,
  90. endingEpisodeNumber,
  91. premiereDate,
  92. options,
  93. overwriteExisting,
  94. result,
  95. cancellationToken).ConfigureAwait(false);
  96. }
  97. else
  98. {
  99. var msg = string.Format("Unable to determine episode number from {0}", path);
  100. result.Status = FileSortingStatus.Failure;
  101. result.StatusMessage = msg;
  102. _logger.Warn(msg);
  103. }
  104. }
  105. else
  106. {
  107. var msg = string.Format("Unable to determine series name from {0}", path);
  108. result.Status = FileSortingStatus.Failure;
  109. result.StatusMessage = msg;
  110. _logger.Warn(msg);
  111. }
  112. var previousResult = _organizationService.GetResultBySourcePath(path);
  113. if (previousResult != null)
  114. {
  115. // Don't keep saving the same result over and over if nothing has changed
  116. if (previousResult.Status == result.Status && result.Status != FileSortingStatus.Success)
  117. {
  118. return previousResult;
  119. }
  120. }
  121. await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
  122. return result;
  123. }
  124. public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options, CancellationToken cancellationToken)
  125. {
  126. var result = _organizationService.GetResult(request.ResultId);
  127. var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId));
  128. await OrganizeEpisode(result.OriginalPath,
  129. series,
  130. request.SeasonNumber,
  131. request.EpisodeNumber,
  132. request.EndingEpisodeNumber,
  133. null,
  134. options,
  135. true,
  136. result,
  137. cancellationToken).ConfigureAwait(false);
  138. await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
  139. return result;
  140. }
  141. private Task OrganizeEpisode(string sourcePath,
  142. string seriesName,
  143. int? seasonNumber,
  144. int? episodeNumber,
  145. int? endingEpiosdeNumber,
  146. DateTime? premiereDate,
  147. TvFileOrganizationOptions options,
  148. bool overwriteExisting,
  149. FileOrganizationResult result,
  150. CancellationToken cancellationToken)
  151. {
  152. var series = GetMatchingSeries(seriesName, result);
  153. if (series == null)
  154. {
  155. var msg = string.Format("Unable to find series in library matching name {0}", seriesName);
  156. result.Status = FileSortingStatus.Failure;
  157. result.StatusMessage = msg;
  158. _logger.Warn(msg);
  159. return Task.FromResult(true);
  160. }
  161. if (!series.ProviderIds.Any())
  162. {
  163. var msg = string.Format("Series has not yet been identified: {0}. If you just added the series, please run a library scan or use the identify feature to identify it.", seriesName);
  164. result.Status = FileSortingStatus.Failure;
  165. result.StatusMessage = msg;
  166. _logger.Warn(msg);
  167. return Task.FromResult(true);
  168. }
  169. return OrganizeEpisode(sourcePath,
  170. series,
  171. seasonNumber,
  172. episodeNumber,
  173. endingEpiosdeNumber,
  174. premiereDate,
  175. options,
  176. overwriteExisting,
  177. result,
  178. cancellationToken);
  179. }
  180. private async Task OrganizeEpisode(string sourcePath,
  181. Series series,
  182. int? seasonNumber,
  183. int? episodeNumber,
  184. int? endingEpiosdeNumber,
  185. DateTime? premiereDate,
  186. TvFileOrganizationOptions options,
  187. bool overwriteExisting,
  188. FileOrganizationResult result,
  189. CancellationToken cancellationToken)
  190. {
  191. _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path);
  192. // Proceed to sort the file
  193. var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options, cancellationToken).ConfigureAwait(false);
  194. if (string.IsNullOrEmpty(newPath))
  195. {
  196. var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath);
  197. result.Status = FileSortingStatus.Failure;
  198. result.StatusMessage = msg;
  199. _logger.Warn(msg);
  200. return;
  201. }
  202. _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath);
  203. result.TargetPath = newPath;
  204. var fileExists = _fileSystem.FileExists(result.TargetPath);
  205. var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber);
  206. if (!overwriteExisting)
  207. {
  208. if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath))
  209. {
  210. _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath);
  211. result.Status = FileSortingStatus.SkippedExisting;
  212. result.StatusMessage = string.Empty;
  213. return;
  214. }
  215. if (fileExists || otherDuplicatePaths.Count > 0)
  216. {
  217. result.Status = FileSortingStatus.SkippedExisting;
  218. result.StatusMessage = string.Empty;
  219. result.DuplicatePaths = otherDuplicatePaths;
  220. return;
  221. }
  222. }
  223. PerformFileSorting(options, result);
  224. if (overwriteExisting)
  225. {
  226. var hasRenamedFiles = false;
  227. foreach (var path in otherDuplicatePaths)
  228. {
  229. _logger.Debug("Removing duplicate episode {0}", path);
  230. _libraryMonitor.ReportFileSystemChangeBeginning(path);
  231. var renameRelatedFiles = !hasRenamedFiles &&
  232. string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase);
  233. if (renameRelatedFiles)
  234. {
  235. hasRenamedFiles = true;
  236. }
  237. try
  238. {
  239. DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath);
  240. }
  241. catch (IOException ex)
  242. {
  243. _logger.ErrorException("Error removing duplicate episode", ex, path);
  244. }
  245. finally
  246. {
  247. _libraryMonitor.ReportFileSystemChangeComplete(path, true);
  248. }
  249. }
  250. }
  251. }
  252. private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath)
  253. {
  254. _fileSystem.DeleteFile(path);
  255. if (!renameRelatedFiles)
  256. {
  257. return;
  258. }
  259. // Now find other files
  260. var originalFilenameWithoutExtension = Path.GetFileNameWithoutExtension(path);
  261. var directory = Path.GetDirectoryName(path);
  262. if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory))
  263. {
  264. // Get all related files, e.g. metadata, images, etc
  265. var files = _fileSystem.GetFilePaths(directory)
  266. .Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase))
  267. .ToList();
  268. var targetFilenameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath);
  269. foreach (var file in files)
  270. {
  271. directory = Path.GetDirectoryName(file);
  272. var filename = Path.GetFileName(file);
  273. filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension,
  274. StringComparison.OrdinalIgnoreCase);
  275. var destination = Path.Combine(directory, filename);
  276. _fileSystem.MoveFile(file, destination);
  277. }
  278. }
  279. }
  280. private List<string> GetOtherDuplicatePaths(string targetPath,
  281. Series series,
  282. int? seasonNumber,
  283. int? episodeNumber,
  284. int? endingEpisodeNumber)
  285. {
  286. // TODO: Support date-naming?
  287. if (!seasonNumber.HasValue || episodeNumber.HasValue)
  288. {
  289. return new List<string>();
  290. }
  291. var episodePaths = series.GetRecursiveChildren()
  292. .OfType<Episode>()
  293. .Where(i =>
  294. {
  295. var locationType = i.LocationType;
  296. // Must be file system based and match exactly
  297. if (locationType != LocationType.Remote &&
  298. locationType != LocationType.Virtual &&
  299. i.ParentIndexNumber.HasValue &&
  300. i.ParentIndexNumber.Value == seasonNumber &&
  301. i.IndexNumber.HasValue &&
  302. i.IndexNumber.Value == episodeNumber)
  303. {
  304. if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue)
  305. {
  306. return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue &&
  307. endingEpisodeNumber.Value == i.IndexNumberEnd.Value;
  308. }
  309. return true;
  310. }
  311. return false;
  312. })
  313. .Select(i => i.Path)
  314. .ToList();
  315. var folder = Path.GetDirectoryName(targetPath);
  316. var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath);
  317. try
  318. {
  319. var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder)
  320. .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase));
  321. episodePaths.AddRange(filesOfOtherExtensions);
  322. }
  323. catch (DirectoryNotFoundException)
  324. {
  325. // No big deal. Maybe the season folder doesn't already exist.
  326. }
  327. return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase))
  328. .Distinct(StringComparer.OrdinalIgnoreCase)
  329. .ToList();
  330. }
  331. private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
  332. {
  333. _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath);
  334. _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath));
  335. var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath);
  336. try
  337. {
  338. if (targetAlreadyExists || options.CopyOriginalFile)
  339. {
  340. _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true);
  341. }
  342. else
  343. {
  344. _fileSystem.MoveFile(result.OriginalPath, result.TargetPath);
  345. }
  346. result.Status = FileSortingStatus.Success;
  347. result.StatusMessage = string.Empty;
  348. }
  349. catch (Exception ex)
  350. {
  351. var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath);
  352. result.Status = FileSortingStatus.Failure;
  353. result.StatusMessage = errorMsg;
  354. _logger.ErrorException(errorMsg, ex);
  355. return;
  356. }
  357. finally
  358. {
  359. _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true);
  360. }
  361. if (targetAlreadyExists && !options.CopyOriginalFile)
  362. {
  363. try
  364. {
  365. _fileSystem.DeleteFile(result.OriginalPath);
  366. }
  367. catch (Exception ex)
  368. {
  369. _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
  370. }
  371. }
  372. }
  373. private Series GetMatchingSeries(string seriesName, FileOrganizationResult result)
  374. {
  375. var parsedName = _libraryManager.ParseName(seriesName);
  376. var yearInName = parsedName.Year;
  377. var nameWithoutYear = parsedName.Name;
  378. result.ExtractedName = nameWithoutYear;
  379. result.ExtractedYear = yearInName;
  380. return _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series)
  381. .Cast<Series>()
  382. .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i))
  383. .Where(i => i.Item2 > 0)
  384. .OrderByDescending(i => i.Item2)
  385. .Select(i => i.Item1)
  386. .FirstOrDefault();
  387. }
  388. /// <summary>
  389. /// Gets the new path.
  390. /// </summary>
  391. /// <param name="sourcePath">The source path.</param>
  392. /// <param name="series">The series.</param>
  393. /// <param name="seasonNumber">The season number.</param>
  394. /// <param name="episodeNumber">The episode number.</param>
  395. /// <param name="endingEpisodeNumber">The ending episode number.</param>
  396. /// <param name="options">The options.</param>
  397. /// <returns>System.String.</returns>
  398. private async Task<string> GetNewPath(string sourcePath,
  399. Series series,
  400. int? seasonNumber,
  401. int? episodeNumber,
  402. int? endingEpisodeNumber,
  403. DateTime? premiereDate,
  404. TvFileOrganizationOptions options,
  405. CancellationToken cancellationToken)
  406. {
  407. var episodeInfo = new EpisodeInfo
  408. {
  409. IndexNumber = episodeNumber,
  410. IndexNumberEnd = endingEpisodeNumber,
  411. MetadataCountryCode = series.GetPreferredMetadataCountryCode(),
  412. MetadataLanguage = series.GetPreferredMetadataLanguage(),
  413. ParentIndexNumber = seasonNumber,
  414. SeriesProviderIds = series.ProviderIds,
  415. PremiereDate = premiereDate
  416. };
  417. var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo>
  418. {
  419. SearchInfo = episodeInfo
  420. }, cancellationToken).ConfigureAwait(false);
  421. var episode = searchResults.FirstOrDefault();
  422. string episodeName = string.Empty;
  423. if (episode == null)
  424. {
  425. var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
  426. _logger.Warn(msg);
  427. //throw new Exception(msg);
  428. }
  429. else
  430. {
  431. episodeName = episode.Name;
  432. }
  433. seasonNumber = seasonNumber ?? episode.ParentIndexNumber;
  434. episodeNumber = episodeNumber ?? episode.IndexNumber;
  435. var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options);
  436. // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256
  437. // Usually newPath would include the drive component, but use 256 to be sure
  438. var maxFilenameLength = 256 - newPath.Length;
  439. if (!newPath.EndsWith(@"\"))
  440. {
  441. // Remove 1 for missing backslash combining path and filename
  442. maxFilenameLength--;
  443. }
  444. // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt)
  445. maxFilenameLength -= 4;
  446. var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength);
  447. if (string.IsNullOrEmpty(episodeFileName))
  448. {
  449. // cause failure
  450. return string.Empty;
  451. }
  452. newPath = Path.Combine(newPath, episodeFileName);
  453. return newPath;
  454. }
  455. /// <summary>
  456. /// Gets the season folder path.
  457. /// </summary>
  458. /// <param name="series">The series.</param>
  459. /// <param name="seasonNumber">The season number.</param>
  460. /// <param name="options">The options.</param>
  461. /// <returns>System.String.</returns>
  462. private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options)
  463. {
  464. // If there's already a season folder, use that
  465. var season = series
  466. .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber)
  467. .FirstOrDefault();
  468. if (season != null)
  469. {
  470. return season.Path;
  471. }
  472. var path = series.Path;
  473. if (series.ContainsEpisodesWithoutSeasonFolders)
  474. {
  475. return path;
  476. }
  477. if (seasonNumber == 0)
  478. {
  479. return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName));
  480. }
  481. var seasonFolderName = options.SeasonFolderPattern
  482. .Replace("%s", seasonNumber.ToString(_usCulture))
  483. .Replace("%0s", seasonNumber.ToString("00", _usCulture))
  484. .Replace("%00s", seasonNumber.ToString("000", _usCulture));
  485. return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
  486. }
  487. private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength)
  488. {
  489. seriesName = _fileSystem.GetValidFilename(seriesName).Trim();
  490. if (string.IsNullOrEmpty(episodeTitle))
  491. {
  492. episodeTitle = string.Empty;
  493. }
  494. else
  495. {
  496. episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim();
  497. }
  498. var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
  499. var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
  500. var result = pattern.Replace("%sn", seriesName)
  501. .Replace("%s.n", seriesName.Replace(" ", "."))
  502. .Replace("%s_n", seriesName.Replace(" ", "_"))
  503. .Replace("%s", seasonNumber.ToString(_usCulture))
  504. .Replace("%0s", seasonNumber.ToString("00", _usCulture))
  505. .Replace("%00s", seasonNumber.ToString("000", _usCulture))
  506. .Replace("%ext", sourceExtension)
  507. .Replace("%en", "%#1")
  508. .Replace("%e.n", "%#2")
  509. .Replace("%e_n", "%#3");
  510. if (endingEpisodeNumber.HasValue)
  511. {
  512. result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture))
  513. .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture))
  514. .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture));
  515. }
  516. result = result.Replace("%e", episodeNumber.ToString(_usCulture))
  517. .Replace("%0e", episodeNumber.ToString("00", _usCulture))
  518. .Replace("%00e", episodeNumber.ToString("000", _usCulture));
  519. if (maxLength.HasValue && result.Contains("%#"))
  520. {
  521. // Substract 3 for the temp token length (%#1, %#2 or %#3)
  522. int maxRemainingTitleLength = maxLength.Value - result.Length + 3;
  523. string shortenedEpisodeTitle = string.Empty;
  524. if (maxRemainingTitleLength > 5)
  525. {
  526. // A title with fewer than 5 letters wouldn't be of much value
  527. shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length));
  528. }
  529. result = result.Replace("%#1", shortenedEpisodeTitle)
  530. .Replace("%#2", shortenedEpisodeTitle.Replace(" ", "."))
  531. .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_"));
  532. }
  533. if (maxLength.HasValue && result.Length > maxLength.Value)
  534. {
  535. // There may be cases where reducing the title length may still not be sufficient to
  536. // stay below maxLength
  537. var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength);
  538. _logger.Warn(msg);
  539. return string.Empty;
  540. }
  541. return result;
  542. }
  543. private bool IsSameEpisode(string sourcePath, string newPath)
  544. {
  545. try
  546. {
  547. var sourceFileInfo = new FileInfo(sourcePath);
  548. var destinationFileInfo = new FileInfo(newPath);
  549. if (sourceFileInfo.Length == destinationFileInfo.Length)
  550. {
  551. return true;
  552. }
  553. }
  554. catch (FileNotFoundException)
  555. {
  556. return false;
  557. }
  558. catch (DirectoryNotFoundException)
  559. {
  560. return false;
  561. }
  562. return false;
  563. }
  564. }
  565. }