MissingEpisodeProvider.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.Localization;
  6. using MediaBrowser.Controller.Providers;
  7. using MediaBrowser.Model.Entities;
  8. using MediaBrowser.Model.Logging;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Globalization;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Text;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using System.Xml;
  18. namespace MediaBrowser.Providers.TV
  19. {
  20. class MissingEpisodeProvider
  21. {
  22. private readonly IServerConfigurationManager _config;
  23. private readonly ILogger _logger;
  24. private readonly ILibraryManager _libraryManager;
  25. private readonly ILocalizationManager _localization;
  26. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  27. public MissingEpisodeProvider(ILogger logger, IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization)
  28. {
  29. _logger = logger;
  30. _config = config;
  31. _libraryManager = libraryManager;
  32. _localization = localization;
  33. }
  34. public async Task Run(IEnumerable<IGrouping<string, Series>> series, CancellationToken cancellationToken)
  35. {
  36. foreach (var seriesGroup in series)
  37. {
  38. try
  39. {
  40. await Run(seriesGroup, cancellationToken).ConfigureAwait(false);
  41. }
  42. catch (OperationCanceledException)
  43. {
  44. break;
  45. }
  46. catch (DirectoryNotFoundException)
  47. {
  48. _logger.Warn("Series files missing for series id {0}", seriesGroup.Key);
  49. }
  50. catch (Exception ex)
  51. {
  52. _logger.ErrorException("Error in missing episode provider for series id {0}", ex, seriesGroup.Key);
  53. }
  54. }
  55. }
  56. private async Task Run(IGrouping<string, Series> group, CancellationToken cancellationToken)
  57. {
  58. var tvdbId = group.Key;
  59. var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
  60. var episodeFiles = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly)
  61. .Select(Path.GetFileNameWithoutExtension)
  62. .Where(i => i.StartsWith("episode-", StringComparison.OrdinalIgnoreCase))
  63. .ToList();
  64. var episodeLookup = episodeFiles
  65. .Select(i =>
  66. {
  67. var parts = i.Split('-');
  68. if (parts.Length == 3)
  69. {
  70. int seasonNumber;
  71. if (int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out seasonNumber))
  72. {
  73. int episodeNumber;
  74. if (int.TryParse(parts[2], NumberStyles.Integer, UsCulture, out episodeNumber))
  75. {
  76. return new Tuple<int, int>(seasonNumber, episodeNumber);
  77. }
  78. }
  79. }
  80. return new Tuple<int, int>(-1, -1);
  81. })
  82. .Where(i => i.Item1 != -1 && i.Item2 != -1)
  83. .ToList();
  84. var hasBadData = HasInvalidContent(group);
  85. var anySeasonsRemoved = await RemoveObsoleteOrMissingSeasons(group, episodeLookup)
  86. .ConfigureAwait(false);
  87. var anyEpisodesRemoved = await RemoveObsoleteOrMissingEpisodes(group, episodeLookup)
  88. .ConfigureAwait(false);
  89. var hasNewEpisodes = false;
  90. var hasNewSeasons = false;
  91. foreach (var series in group)
  92. {
  93. hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false);
  94. }
  95. if (_config.Configuration.EnableInternetProviders)
  96. {
  97. var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase));
  98. if (seriesConfig == null || !seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase))
  99. {
  100. hasNewEpisodes = await AddMissingEpisodes(group.ToList(), hasBadData, seriesDataPath, episodeLookup, cancellationToken)
  101. .ConfigureAwait(false);
  102. }
  103. }
  104. if (hasNewSeasons || hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
  105. {
  106. foreach (var series in group)
  107. {
  108. var directoryService = new DirectoryService();
  109. await series.RefreshMetadata(new MetadataRefreshOptions(directoryService)
  110. {
  111. }, cancellationToken).ConfigureAwait(false);
  112. await series.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(directoryService), true)
  113. .ConfigureAwait(false);
  114. }
  115. }
  116. }
  117. /// <summary>
  118. /// Returns true if a series has any seasons or episodes without season or episode numbers
  119. /// If this data is missing no virtual items will be added in order to prevent possible duplicates
  120. /// </summary>
  121. /// <param name="group"></param>
  122. /// <returns></returns>
  123. private bool HasInvalidContent(IEnumerable<Series> group)
  124. {
  125. var allItems = group.ToList().SelectMany(i => i.RecursiveChildren).ToList();
  126. return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) ||
  127. allItems.OfType<Episode>().Any(i =>
  128. {
  129. if (!i.ParentIndexNumber.HasValue)
  130. {
  131. return true;
  132. }
  133. // You could have episodes under season 0 with no number
  134. return false;
  135. });
  136. }
  137. /// <summary>
  138. /// For series with episodes directly under the series folder, this adds dummy seasons to enable regular browsing and metadata
  139. /// </summary>
  140. /// <param name="series"></param>
  141. /// <param name="cancellationToken"></param>
  142. /// <returns></returns>
  143. private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken)
  144. {
  145. var episodesInSeriesFolder = series.RecursiveChildren
  146. .OfType<Episode>()
  147. .Where(i => !i.IsInSeasonFolder)
  148. .ToList();
  149. var hasChanges = false;
  150. // Loop through the unique season numbers
  151. foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1)
  152. .Where(i => i >= 0)
  153. .Distinct()
  154. .ToList())
  155. {
  156. var hasSeason = series.Children.OfType<Season>()
  157. .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
  158. if (!hasSeason)
  159. {
  160. await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
  161. hasChanges = true;
  162. }
  163. }
  164. // Unknown season - create a dummy season to put these under
  165. if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue))
  166. {
  167. var hasSeason = series.Children.OfType<Season>()
  168. .Any(i => !i.IndexNumber.HasValue);
  169. if (!hasSeason)
  170. {
  171. await AddSeason(series, null, cancellationToken).ConfigureAwait(false);
  172. hasChanges = true;
  173. }
  174. }
  175. return hasChanges;
  176. }
  177. /// <summary>
  178. /// Adds the missing episodes.
  179. /// </summary>
  180. /// <param name="series">The series.</param>
  181. /// <param name="seriesHasBadData">if set to <c>true</c> [series has bad data].</param>
  182. /// <param name="seriesDataPath">The series data path.</param>
  183. /// <param name="episodeLookup">The episode lookup.</param>
  184. /// <param name="cancellationToken">The cancellation token.</param>
  185. /// <returns>Task.</returns>
  186. private async Task<bool> AddMissingEpisodes(List<Series> series,
  187. bool seriesHasBadData,
  188. string seriesDataPath,
  189. IEnumerable<Tuple<int, int>> episodeLookup,
  190. CancellationToken cancellationToken)
  191. {
  192. var existingEpisodes = (from s in series
  193. let seasonOffset = TvdbSeriesProvider.GetSeriesOffset(s.ProviderIds) ?? ((s.AnimeSeriesIndex ?? 1) - 1)
  194. from c in s.RecursiveChildren.OfType<Episode>()
  195. select new Tuple<int, Episode>((c.ParentIndexNumber ?? 0) + seasonOffset, c))
  196. .ToList();
  197. var lookup = episodeLookup as IList<Tuple<int, int>> ?? episodeLookup.ToList();
  198. var seasonCounts = (from e in lookup
  199. group e by e.Item1 into g select g)
  200. .ToDictionary(g => g.Key, g => g.Count());
  201. var hasChanges = false;
  202. foreach (var tuple in lookup)
  203. {
  204. if (tuple.Item1 <= 0)
  205. {
  206. // Ignore season zeros
  207. continue;
  208. }
  209. if (tuple.Item2 <= 0)
  210. {
  211. // Ignore episode zeros
  212. continue;
  213. }
  214. var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple);
  215. if (existingEpisode != null)
  216. {
  217. continue;
  218. }
  219. var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2);
  220. if (!airDate.HasValue)
  221. {
  222. continue;
  223. }
  224. var now = DateTime.UtcNow;
  225. var targetSeries = DetermineAppropriateSeries(series, tuple.Item1);
  226. var seasonOffset = TvdbSeriesProvider.GetSeriesOffset(targetSeries.ProviderIds) ?? ((targetSeries.AnimeSeriesIndex ?? 1) - 1);
  227. if (airDate.Value < now)
  228. {
  229. // Be conservative here to avoid creating missing episodes for ones they already have
  230. if (!seriesHasBadData)
  231. {
  232. // tvdb has a lot of nearly blank episodes
  233. _logger.Info("Creating virtual missing episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
  234. await AddEpisode(targetSeries, tuple.Item1 - seasonOffset, tuple.Item2, cancellationToken).ConfigureAwait(false);
  235. hasChanges = true;
  236. }
  237. }
  238. else if (airDate.Value > now)
  239. {
  240. // tvdb has a lot of nearly blank episodes
  241. _logger.Info("Creating virtual unaired episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
  242. await AddEpisode(targetSeries, tuple.Item1 - seasonOffset, tuple.Item2, cancellationToken).ConfigureAwait(false);
  243. hasChanges = true;
  244. }
  245. }
  246. return hasChanges;
  247. }
  248. private Series DetermineAppropriateSeries(IEnumerable<Series> series, int seasonNumber)
  249. {
  250. var seriesAndOffsets = series.Select(s => new { Series = s, SeasonOffset = TvdbSeriesProvider.GetSeriesOffset(s.ProviderIds) ?? ((s.AnimeSeriesIndex ?? 1) - 1) }).ToList();
  251. var bestMatch = seriesAndOffsets.FirstOrDefault(s => s.Series.RecursiveChildren.OfType<Season>().Any(season => (season.IndexNumber + s.SeasonOffset) == seasonNumber)) ??
  252. seriesAndOffsets.FirstOrDefault(s => s.Series.RecursiveChildren.OfType<Season>().Any(season => (season.IndexNumber + s.SeasonOffset) == 1)) ??
  253. seriesAndOffsets.OrderBy(s => s.Series.RecursiveChildren.OfType<Season>().Select(season => season.IndexNumber + s.SeasonOffset).Min()).First();
  254. return bestMatch.Series;
  255. }
  256. /// <summary>
  257. /// Removes the virtual entry after a corresponding physical version has been added
  258. /// </summary>
  259. private async Task<bool> RemoveObsoleteOrMissingEpisodes(IEnumerable<Series> series,
  260. IEnumerable<Tuple<int, int>> episodeLookup)
  261. {
  262. var existingEpisodes = (from s in series
  263. let seasonOffset = TvdbSeriesProvider.GetSeriesOffset(s.ProviderIds) ?? ((s.AnimeSeriesIndex ?? 1) - 1)
  264. from c in s.RecursiveChildren.OfType<Episode>()
  265. select new { SeasonOffset = seasonOffset, Episode = c })
  266. .ToList();
  267. var physicalEpisodes = existingEpisodes
  268. .Where(i => i.Episode.LocationType != LocationType.Virtual)
  269. .ToList();
  270. var virtualEpisodes = existingEpisodes
  271. .Where(i => i.Episode.LocationType == LocationType.Virtual)
  272. .ToList();
  273. var episodesToRemove = virtualEpisodes
  274. .Where(i =>
  275. {
  276. if (i.Episode.IndexNumber.HasValue && i.Episode.ParentIndexNumber.HasValue)
  277. {
  278. var seasonNumber = i.Episode.ParentIndexNumber.Value + i.SeasonOffset;
  279. var episodeNumber = i.Episode.IndexNumber.Value;
  280. // If there's a physical episode with the same season and episode number, delete it
  281. if (physicalEpisodes.Any(p =>
  282. p.Episode.ParentIndexNumber.HasValue && (p.Episode.ParentIndexNumber.Value + p.SeasonOffset) == seasonNumber &&
  283. p.Episode.ContainsEpisodeNumber(episodeNumber)))
  284. {
  285. return true;
  286. }
  287. // If the episode no longer exists in the remote lookup, delete it
  288. if (!episodeLookup.Any(e => e.Item1 == seasonNumber && e.Item2 == episodeNumber))
  289. {
  290. return true;
  291. }
  292. return false;
  293. }
  294. return true;
  295. })
  296. .ToList();
  297. var hasChanges = false;
  298. foreach (var episodeToRemove in episodesToRemove.Select(e => e.Episode))
  299. {
  300. _logger.Info("Removing missing/unaired episode {0} {1}x{2}", episodeToRemove.Series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
  301. await _libraryManager.DeleteItem(episodeToRemove).ConfigureAwait(false);
  302. hasChanges = true;
  303. }
  304. return hasChanges;
  305. }
  306. /// <summary>
  307. /// Removes the obsolete or missing seasons.
  308. /// </summary>
  309. /// <param name="series">The series.</param>
  310. /// <param name="episodeLookup">The episode lookup.</param>
  311. /// <returns>Task{System.Boolean}.</returns>
  312. private async Task<bool> RemoveObsoleteOrMissingSeasons(IEnumerable<Series> series,
  313. IEnumerable<Tuple<int, int>> episodeLookup)
  314. {
  315. var existingSeasons = (from s in series
  316. let seasonOffset = TvdbSeriesProvider.GetSeriesOffset(s.ProviderIds) ?? ((s.AnimeSeriesIndex ?? 1) - 1)
  317. from c in s.Children.OfType<Season>()
  318. select new { SeasonOffset = seasonOffset, Season = c })
  319. .ToList();
  320. var physicalSeasons = existingSeasons
  321. .Where(i => i.Season.LocationType != LocationType.Virtual)
  322. .ToList();
  323. var virtualSeasons = existingSeasons
  324. .Where(i => i.Season.LocationType == LocationType.Virtual)
  325. .ToList();
  326. var seasonsToRemove = virtualSeasons
  327. .Where(i =>
  328. {
  329. if (i.Season.IndexNumber.HasValue)
  330. {
  331. var seasonNumber = i.Season.IndexNumber.Value + i.SeasonOffset;
  332. // If there's a physical season with the same number, delete it
  333. if (physicalSeasons.Any(p => p.Season.IndexNumber.HasValue && (p.Season.IndexNumber.Value + p.SeasonOffset) == seasonNumber))
  334. {
  335. return true;
  336. }
  337. // If the season no longer exists in the remote lookup, delete it
  338. if (episodeLookup.All(e => e.Item1 != seasonNumber))
  339. {
  340. return true;
  341. }
  342. return false;
  343. }
  344. // Season does not have a number
  345. // Remove if there are no episodes directly in series without a season number
  346. return i.Season.Series.RecursiveChildren.OfType<Episode>().All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder);
  347. })
  348. .ToList();
  349. var hasChanges = false;
  350. foreach (var seasonToRemove in seasonsToRemove.Select(s => s.Season))
  351. {
  352. _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber);
  353. await _libraryManager.DeleteItem(seasonToRemove).ConfigureAwait(false);
  354. hasChanges = true;
  355. }
  356. return hasChanges;
  357. }
  358. /// <summary>
  359. /// Adds the episode.
  360. /// </summary>
  361. /// <param name="series">The series.</param>
  362. /// <param name="seasonNumber">The season number.</param>
  363. /// <param name="episodeNumber">The episode number.</param>
  364. /// <param name="cancellationToken">The cancellation token.</param>
  365. /// <returns>Task.</returns>
  366. private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
  367. {
  368. var season = series.Children.OfType<Season>()
  369. .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
  370. if (season == null)
  371. {
  372. season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
  373. }
  374. var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture));
  375. var episode = new Episode
  376. {
  377. Name = name,
  378. IndexNumber = episodeNumber,
  379. ParentIndexNumber = seasonNumber,
  380. Parent = season,
  381. DisplayMediaType = typeof(Episode).Name,
  382. Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Episode))
  383. };
  384. await season.AddChild(episode, cancellationToken).ConfigureAwait(false);
  385. await episode.RefreshMetadata(new MetadataRefreshOptions
  386. {
  387. }, cancellationToken).ConfigureAwait(false);
  388. }
  389. /// <summary>
  390. /// Adds the season.
  391. /// </summary>
  392. /// <param name="series">The series.</param>
  393. /// <param name="seasonNumber">The season number.</param>
  394. /// <param name="cancellationToken">The cancellation token.</param>
  395. /// <returns>Task{Season}.</returns>
  396. private async Task<Season> AddSeason(Series series,
  397. int? seasonNumber,
  398. CancellationToken cancellationToken)
  399. {
  400. var seasonName = seasonNumber == 0 ?
  401. _config.Configuration.SeasonZeroDisplayName :
  402. (seasonNumber.HasValue ? string.Format(_localization.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value.ToString(UsCulture)) : _localization.GetLocalizedString("NameSeasonUnknown"));
  403. _logger.Info("Creating Season {0} entry for {1}", seasonName, series.Name);
  404. var season = new Season
  405. {
  406. Name = seasonName,
  407. IndexNumber = seasonNumber,
  408. Parent = series,
  409. DisplayMediaType = typeof(Season).Name,
  410. Id = (series.Id + (seasonNumber ?? -1).ToString(UsCulture) + seasonName).GetMBId(typeof(Season))
  411. };
  412. await series.AddChild(season, cancellationToken).ConfigureAwait(false);
  413. await season.RefreshMetadata(new MetadataRefreshOptions(), cancellationToken).ConfigureAwait(false);
  414. return season;
  415. }
  416. /// <summary>
  417. /// Gets the existing episode.
  418. /// </summary>
  419. /// <param name="existingEpisodes">The existing episodes.</param>
  420. /// <param name="seasonCounts"></param>
  421. /// <param name="tuple">The tuple.</param>
  422. /// <returns>Episode.</returns>
  423. private Episode GetExistingEpisode(IList<Tuple<int, Episode>> existingEpisodes, Dictionary<int, int> seasonCounts, Tuple<int, int> tuple)
  424. {
  425. var s = tuple.Item1;
  426. var e = tuple.Item2;
  427. while (true)
  428. {
  429. var episode = GetExistingEpisode(existingEpisodes, s, e);
  430. if (episode != null)
  431. return episode;
  432. s--;
  433. if (seasonCounts.ContainsKey(s))
  434. e += seasonCounts[s];
  435. else
  436. break;
  437. }
  438. return null;
  439. }
  440. private static Episode GetExistingEpisode(IEnumerable<Tuple<int, Episode>> existingEpisodes, int season, int episode)
  441. {
  442. return existingEpisodes
  443. .Where(i => i.Item1 == season && i.Item2.ContainsEpisodeNumber(episode))
  444. .Select(i => i.Item2)
  445. .FirstOrDefault();
  446. }
  447. /// <summary>
  448. /// Gets the air date.
  449. /// </summary>
  450. /// <param name="seriesDataPath">The series data path.</param>
  451. /// <param name="seasonNumber">The season number.</param>
  452. /// <param name="episodeNumber">The episode number.</param>
  453. /// <returns>System.Nullable{DateTime}.</returns>
  454. private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber)
  455. {
  456. // First open up the tvdb xml file and make sure it has valid data
  457. var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(UsCulture), episodeNumber.ToString(UsCulture));
  458. var xmlPath = Path.Combine(seriesDataPath, filename);
  459. DateTime? airDate = null;
  460. // It appears the best way to filter out invalid entries is to only include those with valid air dates
  461. using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
  462. {
  463. // Use XmlReader for best performance
  464. using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
  465. {
  466. CheckCharacters = false,
  467. IgnoreProcessingInstructions = true,
  468. IgnoreComments = true,
  469. ValidationType = ValidationType.None
  470. }))
  471. {
  472. reader.MoveToContent();
  473. // Loop through each element
  474. while (reader.Read())
  475. {
  476. if (reader.NodeType == XmlNodeType.Element)
  477. {
  478. switch (reader.Name)
  479. {
  480. case "EpisodeName":
  481. {
  482. var val = reader.ReadElementContentAsString();
  483. if (string.IsNullOrWhiteSpace(val))
  484. {
  485. // Not valid, ignore these
  486. return null;
  487. }
  488. break;
  489. }
  490. case "FirstAired":
  491. {
  492. var val = reader.ReadElementContentAsString();
  493. if (!string.IsNullOrWhiteSpace(val))
  494. {
  495. DateTime date;
  496. if (DateTime.TryParse(val, out date))
  497. {
  498. airDate = date.ToUniversalTime();
  499. }
  500. }
  501. break;
  502. }
  503. default:
  504. reader.Skip();
  505. break;
  506. }
  507. }
  508. }
  509. }
  510. }
  511. return airDate;
  512. }
  513. }
  514. }