private Season AddVirtualSeason(int season, Series series) { string seasonName; if (season == 0) { seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName; } else { seasonName = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameSeasonNumber"), season.ToString(CultureInfo.InvariantCulture)); } _logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var newSeason = new Season { Name = seasonName, IndexNumber = season, Id = _libraryManager.GetNewItemId( series.Id + season.ToString(CultureInfo.InvariantCulture) + seasonName, typeof(Season)), IsVirtualItem = true, SeriesId = series.Id, SeriesName = series.Name, SeriesPresentationUniqueKey = series.GetPresentationUniqueKey() }; series.AddChild(newSeason, CancellationToken.None); return(newSeason); }
public static void CleanupGenres(Series series) { PluginConfiguration config = PluginConfiguration.Instance(); if (config.TidyGenreList) { series.Genres = RemoveRedundantGenres(series.Genres) .Where(g => !"Animation".Equals(g) && !"Anime".Equals(g)) .Distinct() .ToList(); TidyGenres(series); } if (config.MaxGenres > 0) { if (config.MoveExcessGenresToTags) { foreach (string genre in series.Genres.Skip(config.MaxGenres - 1)) { if (!series.Tags.Contains(genre)) series.Tags.Add(genre); } } series.Genres = series.Genres.Take(config.MaxGenres - 1).ToList(); } if (!series.Genres.Contains("Anime")) series.Genres.Add("Anime"); }
private IEnumerable <Season> AddMissingSeasons(Series series, List <Season> existingSeasons, IReadOnlyList <int> allSeasons) { var missingSeasons = allSeasons.Except(existingSeasons.Select(s => s.IndexNumber !.Value)).ToList(); for (var i = 0; i < missingSeasons.Count; i++) { var season = missingSeasons[i]; yield return(AddVirtualSeason(season, series)); } }
public static Series Convert(MazeSeries mazeSeries) { var series = new Series(); SetProviderIds(series, mazeSeries.externals, mazeSeries.id); series.Name = mazeSeries.name; series.Genres = mazeSeries.genres.ToList(); // TODO: Do we have a Series property for original language? //series = mazeSeries.language; if (mazeSeries.network != null && !string.IsNullOrWhiteSpace(mazeSeries.network.name)) { var networkName = mazeSeries.network.name; if (mazeSeries.network.country != null && !string.IsNullOrWhiteSpace(mazeSeries.network.country.code)) { networkName = string.Format("{0} ({1})", mazeSeries.network.name, mazeSeries.network.country.code); } series.Studios.Add(networkName); } if (mazeSeries.premiered.HasValue) { series.PremiereDate = mazeSeries.premiered.Value; series.ProductionYear = mazeSeries.premiered.Value.Year; } if (mazeSeries.rating != null && mazeSeries.rating.average.HasValue) { series.CommunityRating = (float)mazeSeries.rating.average.Value; } if (mazeSeries.runtime.HasValue) { series.RunTimeTicks = TimeSpan.FromMinutes(mazeSeries.runtime.Value).Ticks; } switch (mazeSeries.status.ToLower()) { case "running": series.Status = SeriesStatus.Continuing; break; case "ended": series.Status = SeriesStatus.Ended; break; } series.Overview = StripHtml(mazeSeries.summary); series.HomePageUrl = mazeSeries.url.ToString(); return series; }
private async Task HandleSeries(Series series) { if (!series.TryGetProviderId(MetadataProvider.Tvdb.ToString(), out var tvdbIdTxt)) { return; } var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture); var children = series.GetRecursiveChildren(); var existingSeasons = new List <Season>(); var existingEpisodes = new Dictionary <int, List <Episode> >(); for (var i = 0; i < children.Count; i++) { switch (children[i]) { case Season season: if (season.IndexNumber.HasValue) { existingSeasons.Add(season); } break; case Episode episode: var seasonNumber = episode.ParentIndexNumber ?? 1; if (!existingEpisodes.ContainsKey(seasonNumber)) { existingEpisodes[seasonNumber] = new List <Episode>(); } existingEpisodes[seasonNumber].Add(episode); break; } } var allEpisodes = await GetAllEpisodes(tvdbId, series.GetPreferredMetadataLanguage()).ConfigureAwait(false); var allSeasons = allEpisodes .Where(ep => ep.AiredSeason.HasValue) .Select(ep => ep.AiredSeason !.Value) .Distinct() .ToList(); // Add missing seasons var newSeasons = AddMissingSeasons(series, existingSeasons, allSeasons); AddMissingEpisodes(existingEpisodes, allEpisodes, existingSeasons.Concat(newSeasons).ToList()); }
private RemoteImageInfo GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken) { var tvdbPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.GetProviderId(MetadataProviders.Tvdb)); var actorXmlPath = Path.Combine(tvdbPath, "actors.xml"); try { return GetImageInfo(actorXmlPath, personName, cancellationToken); } catch (FileNotFoundException) { return null; } }
public async Task Run(Series series, CancellationToken cancellationToken) { await RemoveObsoleteSeasons(series).ConfigureAwait(false); var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false); if (hasNewSeasons) { var directoryService = new DirectoryService(_fileSystem); //await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false); //await series.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(directoryService)) // .ConfigureAwait(false); } }
public static void CleanupGenres(Series series) { PluginConfiguration config = PluginConfiguration.Instance(); if (config.TidyGenreList) { series.Genres = RemoveRedundantGenres(series.Genres) .Distinct() .ToList(); TidyGenres(series); } var max = config.MaxGenres; if (config.AddAnimeGenre) { series.Genres.Remove("Animation"); series.Genres.Remove("Anime"); max = Math.Max(max - 1, 0); } if (config.MaxGenres > 0) { if (config.MoveExcessGenresToTags) { foreach (string genre in series.Genres.Skip(max)) { if (!series.Tags.Contains(genre)) series.Tags.Add(genre); } } series.Genres = series.Genres.Take(max).ToList(); } if (!series.Genres.Contains("Anime") && config.AddAnimeGenre) { if (series.Genres.Contains("Animation")) series.Genres.Remove("Animation"); series.AddGenre("Anime"); } series.Genres.Sort(); }
private bool IsEnabledForLibrary(BaseItem item) { Series series = item switch { Episode episode => episode.Series, Season season => season.Series, _ => item as Series }; if (series == null) { return(false); } var libraryOptions = _libraryManager.GetLibraryOptions(series); return(series.IsMetadataFetcherEnabled(libraryOptions, Plugin.MetadataProviderName)); }
private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken) { var episodesInSeriesFolder = series.GetRecursiveChildren() .OfType<Episode>() .Where(i => !i.IsInSeasonFolder) .ToList(); var hasChanges = false; // Loop through the unique season numbers foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1) .Where(i => i >= 0) .Distinct() .ToList()) { var hasSeason = series.Children.OfType<Season>() .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); if (!hasSeason) { await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false); hasChanges = true; } } // Unknown season - create a dummy season to put these under if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue)) { var hasSeason = series.Children.OfType<Season>() .Any(i => !i.IndexNumber.HasValue); if (!hasSeason) { await AddSeason(series, null, cancellationToken).ConfigureAwait(false); hasChanges = true; } } return hasChanges; }
public async Task TestScrapePage() { var data = File.ReadAllText("TestData/anilist/9756.html", Encoding.UTF8); var series = new Series(); AniListSeriesProvider.ParseTitle(series, data, "en"); AniListSeriesProvider.ParseSummary(series, data); AniListSeriesProvider.ParseStudio(series, data); AniListSeriesProvider.ParseRating(series, data); AniListSeriesProvider.ParseGenres(series, data); AniListSeriesProvider.ParseDuration(series, data); AniListSeriesProvider.ParseAirDates(series, data); Assert.That(series.Name, Is.EqualTo("Mahou Shoujo Madoka★Magica")); Assert.That(series.Genres, Contains.Item("Drama")); Assert.That(series.Genres, Contains.Item("Fantasy")); Assert.That(series.Genres, Contains.Item("Psychological Thriller")); Assert.That(series.Genres, Contains.Item("Thriller")); Assert.That(series.PremiereDate, Is.EqualTo(new DateTime(2011, 1, 7))); Assert.That(series.EndDate, Is.EqualTo(new DateTime(2011, 4, 22))); }
/// <summary> /// Downloads the image from series. /// </summary> /// <param name="item">The item.</param> /// <param name="series">The series.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> private async Task DownloadImageFromSeries(BaseItem item, Series series, CancellationToken cancellationToken) { var tvdbPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, series.GetProviderId(MetadataProviders.Tvdb)); var actorXmlPath = Path.Combine(tvdbPath, "actors.xml"); var xmlDoc = new XmlDocument(); xmlDoc.Load(actorXmlPath); var actorNodes = xmlDoc.SelectNodes("//Actor"); if (actorNodes == null) { return; } foreach (var actorNode in actorNodes.OfType<XmlNode>()) { var name = actorNode.SafeGetString("Name"); if (string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)) { var image = actorNode.SafeGetString("Image"); if (!string.IsNullOrEmpty(image)) { var url = TVUtils.BannerUrl + image; await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); } break; } } }
/// <summary> /// Fetches the actors. /// </summary> /// <param name="series">The series.</param> /// <param name="actorsXmlPath">The actors XML path.</param> /// <param name="cancellationToken">The cancellation token.</param> private void FetchActors(Series series, string actorsXmlPath, CancellationToken cancellationToken) { var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, ValidationType = ValidationType.None }; using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8)) { // Use XmlReader for best performance using (var reader = XmlReader.Create(streamReader, settings)) { reader.MoveToContent(); // Loop through each element while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "Actor": { using (var subtree = reader.ReadSubtree()) { FetchDataFromActorNode(series, subtree); } break; } default: reader.Skip(); break; } } } } } }
private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken) { reader.MoveToContent(); // Loop through each element while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "SeriesName": { if (!item.LockedFields.Contains(MetadataFields.Name)) { item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); } break; } case "Overview": { if (!item.LockedFields.Contains(MetadataFields.Overview)) { item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); } break; } case "Airs_DayOfWeek": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { item.AirDays = TVUtils.GetAirDays(val); } break; } case "Airs_Time": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { item.AirTime = val; } break; } case "ContentRating": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { if (!item.LockedFields.Contains(MetadataFields.OfficialRating)) { item.OfficialRating = val; } } break; } case "Rating": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { // Only fill this if it doesn't already have a value, since we get it from imdb which has better data if (!item.CommunityRating.HasValue || string.IsNullOrWhiteSpace(item.GetProviderId(MetadataProviders.Imdb))) { float rval; // float.TryParse is local aware, so it can be probamatic, force us culture if (float.TryParse(val, NumberStyles.AllowDecimalPoint, UsCulture, out rval)) { item.CommunityRating = rval; } } } break; } case "RatingCount": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { int rval; // int.TryParse is local aware, so it can be probamatic, force us culture if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) { item.VoteCount = rval; } } break; } case "IMDB_ID": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { item.SetProviderId(MetadataProviders.Imdb, val); } break; } case "zap2it_id": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { item.SetProviderId(MetadataProviders.Zap2It, val); } break; } case "Status": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { SeriesStatus seriesStatus; if (Enum.TryParse(val, true, out seriesStatus)) item.Status = seriesStatus; } break; } case "FirstAired": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { DateTime date; if (DateTime.TryParse(val, out date)) { date = date.ToUniversalTime(); item.PremiereDate = date; item.ProductionYear = date.Year; } } break; } case "Runtime": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val) && !item.LockedFields.Contains(MetadataFields.Runtime)) { int rval; // int.TryParse is local aware, so it can be probamatic, force us culture if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) { item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks; } } break; } case "Genre": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { // Only fill this in if there's no existing genres, because Imdb data from Omdb is preferred if (!item.LockedFields.Contains(MetadataFields.Genres) && (item.Genres.Count == 0 || !string.Equals(item.GetPreferredMetadataLanguage(), "en", StringComparison.OrdinalIgnoreCase))) { var vals = val .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .Where(i => !string.IsNullOrWhiteSpace(i)) .ToList(); if (vals.Count > 0) { item.Genres.Clear(); foreach (var genre in vals) { item.AddGenre(genre); } } } } break; } case "Network": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { if (!item.LockedFields.Contains(MetadataFields.Studios)) { var vals = val .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .Where(i => !string.IsNullOrWhiteSpace(i)) .ToList(); if (vals.Count > 0) { item.Studios.Clear(); foreach (var genre in vals) { item.AddStudio(genre); } } } } break; } default: reader.Skip(); break; } } } }
private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken) { var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, ValidationType = ValidationType.None }; var episiodeAirDates = new List<DateTime>(); using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8)) { // Use XmlReader for best performance using (var reader = XmlReader.Create(streamReader, settings)) { reader.MoveToContent(); // Loop through each element while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "Series": { using (var subtree = reader.ReadSubtree()) { FetchDataFromSeriesNode(item, subtree, cancellationToken); } break; } case "Episode": { using (var subtree = reader.ReadSubtree()) { var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken); if (date.HasValue) { episiodeAirDates.Add(date.Value); } } break; } default: reader.Skip(); break; } } } } } if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0) { item.EndDate = episiodeAirDates.Max(); } }
/// <summary> /// Fetches the series data. /// </summary> /// <param name="series">The series.</param> /// <param name="seriesId">The series id.</param> /// <param name="seriesDataPath">The series data path.</param> /// <param name="isForcedRefresh">if set to <c>true</c> [is forced refresh].</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> private async Task FetchSeriesData(Series series, string seriesId, string seriesDataPath, bool isForcedRefresh, CancellationToken cancellationToken) { Directory.CreateDirectory(seriesDataPath); var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly) .Select(Path.GetFileName) .ToList(); var seriesXmlFilename = series.GetPreferredMetadataLanguage().ToLower() + ".xml"; // Only download if not already there // The prescan task will take care of updates so we don't need to re-download here if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase)) { await DownloadSeriesZip(seriesId, seriesDataPath, null, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); } // Have to check this here since we prevent the normal enforcement through ProviderManager if (!series.DontFetchMeta) { // Examine if there's no local metadata, or save local is on (to get updates) if (isForcedRefresh || ConfigurationManager.Configuration.EnableTvDbUpdates || !HasLocalMeta(series)) { series.SetProviderId(MetadataProviders.Tvdb, seriesId); var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename); var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); FetchSeriesInfo(series, seriesXmlPath, cancellationToken); if (!series.LockedFields.Contains(MetadataFields.Cast)) { series.People.Clear(); FetchActors(series, actorsXmlPath, cancellationToken); } } } }
public IEnumerable <Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable <Episode> allSeriesEpisodes) { return(series.GetEpisodes(user, this, includeMissingEpisodes, includeVirtualUnairedEpisodes, allSeriesEpisodes)); }
/// <summary> /// Add or remove a Show(Series) to/from the users trakt.tv library /// </summary> /// <param name="show">The show to remove</param> /// <param name="traktUser">The user who's library is being updated</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="eventType"></param> /// <returns>Task{TraktResponseDataContract}.</returns> public async Task<TraktSyncResponse> SendLibraryUpdateAsync(Series show, TraktUser traktUser, CancellationToken cancellationToken, EventType eventType) { if (show == null) throw new ArgumentNullException("show"); if (traktUser == null) throw new ArgumentNullException("traktUser"); if (eventType == EventType.Update) return null; var showPayload = new List<TraktShowCollected> { new TraktShowCollected { Title = show.Name, Year = show.ProductionYear, Ids = new TraktShowId { Tvdb = show.GetProviderId(MetadataProviders.Tvdb).ConvertToInt(), Imdb = show.GetProviderId(MetadataProviders.Imdb), TvRage = show.GetProviderId(MetadataProviders.TvRage).ConvertToInt() }, } }; var data = new TraktSyncCollected { Shows = showPayload.ToList() }; var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; var response = await PostToTrakt(url, data, cancellationToken, traktUser); return _jsonSerializer.DeserializeFromStream<TraktSyncResponse>(response); }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private async Task<string> GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var episodeInfo = new EpisodeInfo { IndexNumber = episodeNumber, IndexNumberEnd = endingEpisodeNumber, MetadataCountryCode = series.GetPreferredMetadataCountryCode(), MetadataLanguage = series.GetPreferredMetadataLanguage(), ParentIndexNumber = seasonNumber, SeriesProviderIds = series.ProviderIds }; var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo> { SearchInfo = episodeInfo }, cancellationToken).ConfigureAwait(false); var episode = searchResults.FirstOrDefault(); if (episode == null) { return null; } var newPath = GetSeasonFolderPath(series, seasonNumber, options); var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options); newPath = Path.Combine(newPath, episodeFileName); return newPath; }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="premiereDate">The premiere date.</param> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> private async Task<string> GetNewPath(string sourcePath, Series series, int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, DateTime? premiereDate, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var episodeInfo = new EpisodeInfo { IndexNumber = episodeNumber, IndexNumberEnd = endingEpisodeNumber, MetadataCountryCode = series.GetPreferredMetadataCountryCode(), MetadataLanguage = series.GetPreferredMetadataLanguage(), ParentIndexNumber = seasonNumber, SeriesProviderIds = series.ProviderIds, PremiereDate = premiereDate }; var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo> { SearchInfo = episodeInfo }, cancellationToken).ConfigureAwait(false); var episode = searchResults.FirstOrDefault(); if (episode == null) { var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); _logger.Warn(msg); return null; } var episodeName = episode.Name; //if (string.IsNullOrWhiteSpace(episodeName)) //{ // var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); // _logger.Warn(msg); // return null; //} seasonNumber = seasonNumber ?? episode.ParentIndexNumber; episodeNumber = episodeNumber ?? episode.IndexNumber; var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options); // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256 // Usually newPath would include the drive component, but use 256 to be sure var maxFilenameLength = 256 - newPath.Length; if (!newPath.EndsWith(@"\")) { // Remove 1 for missing backslash combining path and filename maxFilenameLength--; } // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt) maxFilenameLength -= 4; var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength); if (string.IsNullOrEmpty(episodeFileName)) { // cause failure return string.Empty; } newPath = Path.Combine(newPath, episodeFileName); return newPath; }
private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options) { if (string.IsNullOrEmpty(matchString) || matchString.Length < 3) { return; } SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase)); if (info == null) { info = new SmartMatchInfo(); info.ItemName = series.Name; info.OrganizerType = FileOrganizerType.Episode; info.DisplayName = series.Name; var list = options.SmartMatchInfos.ToList(); list.Add(info); options.SmartMatchInfos = list.ToArray(); } if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase)) { var list = info.MatchStrings.ToList(); list.Add(matchString); info.MatchStrings = list.ToArray(); _config.SaveAutoOrganizeOptions(options); } }
public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken) { var result = _organizationService.GetResult(request.ResultId); Series series = null; if (request.NewSeriesProviderIds.Count > 0) { // We're having a new series here SeriesInfo seriesRequest = new SeriesInfo(); seriesRequest.ProviderIds = request.NewSeriesProviderIds; var refreshOptions = new MetadataRefreshOptions(_fileSystem); series = new Series(); series.Id = Guid.NewGuid(); series.Name = request.NewSeriesName; int year; if (int.TryParse(request.NewSeriesYear, out year)) { series.ProductionYear = year; } var seriesFolderName = series.Name; if (series.ProductionYear.HasValue) { seriesFolderName = string.Format("{0} ({1})", seriesFolderName, series.ProductionYear); } series.Path = Path.Combine(request.TargetFolder, seriesFolderName); series.ProviderIds = request.NewSeriesProviderIds; await series.RefreshMetadata(refreshOptions, cancellationToken); } if (series == null) { // Existing Series series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); } await OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, null, options, true, request.RememberCorrection, result, cancellationToken).ConfigureAwait(false); await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return result; }
public List <BaseItem> GetEpisodes() { return(Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true))); }
public IEnumerable <Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes) { return(GetEpisodes(series, user, includeMissingEpisodes, includeVirtualUnairedEpisodes, null)); }
private async Task OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); // Proceed to sort the file var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(newPath)) { var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; _logger.Warn(msg); return; } _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; var fileExists = File.Exists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting) { if (fileExists || otherDuplicatePaths.Count > 0) { result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; result.DuplicatePaths = otherDuplicatePaths; return; } if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; return; } } PerformFileSorting(options, result); if (overwriteExisting) { foreach (var path in otherDuplicatePaths) { _logger.Debug("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); try { File.Delete(path); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } }
private List<string> GetOtherDuplicatePaths(string targetPath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber) { var episodePaths = series.RecursiveChildren .OfType<Episode>() .Where(i => { var locationType = i.LocationType; // Must be file system based and match exactly if (locationType != LocationType.Remote && locationType != LocationType.Virtual && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == seasonNumber && i.IndexNumber.HasValue && i.IndexNumber.Value == episodeNumber) { if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue) { return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue && endingEpisodeNumber.Value == i.IndexNumberEnd.Value; } return true; } return false; }) .Select(i => i.Path) .ToList(); var folder = Path.GetDirectoryName(targetPath); var targetFileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath); try { var filesOfOtherExtensions = Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly) .Where(i => EntityResolutionHelper.IsVideoFile(i) && string.Equals(Path.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); episodePaths.AddRange(filesOfOtherExtensions); } catch (DirectoryNotFoundException) { // No big deal. Maybe the season folder doesn't already exist. } return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); }
/// <summary> /// Adds the episode. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) { var season = series.Children.OfType<Season>() .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); if (season == null) { season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false); } var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture)); var episode = new Episode { Name = name, IndexNumber = episodeNumber, ParentIndexNumber = seasonNumber, Parent = season, DisplayMediaType = typeof(Episode).Name, Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Episode)) }; await season.AddChild(episode, cancellationToken).ConfigureAwait(false); await episode.RefreshMetadata(new MetadataRefreshOptions { }, cancellationToken).ConfigureAwait(false); }
private int AdjustForSeriesOffset(Series series, int seasonNumber) { var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds); if (offset != null) return (seasonNumber + offset.Value); return seasonNumber; }
public List <BaseItem> GetEpisodes(Series series, User user, IEnumerable <Episode> allSeriesEpisodes, DtoOptions options) { return(series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options)); }
/// <summary> /// Adds the episode. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) { var season = series.Children.OfType<Season>() .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); if (season == null) { var provider = new DummySeasonProvider(_config, _logger, _localization, _libraryManager, _fileSystem); season = await provider.AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false); } var name = string.Format("Episode {0}", episodeNumber.ToString(_usCulture)); var episode = new Episode { Name = name, IndexNumber = episodeNumber, ParentIndexNumber = seasonNumber, Id = _libraryManager.GetNewItemId((series.Id + seasonNumber.ToString(_usCulture) + name), typeof(Episode)) }; episode.SetParent(season); await season.AddChild(episode, cancellationToken).ConfigureAwait(false); await episode.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { }, cancellationToken).ConfigureAwait(false); }
/// <summary> /// Fetches the data from actor node. /// </summary> /// <param name="series">The series.</param> /// <param name="reader">The reader.</param> private void FetchDataFromActorNode(Series series, XmlReader reader) { reader.MoveToContent(); var personInfo = new PersonInfo(); while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "Name": { personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); break; } case "Role": { personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); break; } case "SortOrder": { var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val)) { int rval; // int.TryParse is local aware, so it can be probamatic, force us culture if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) { personInfo.SortOrder = rval; } } break; } default: reader.Skip(); break; } } } personInfo.Type = PersonType.Actor; if (!string.IsNullOrEmpty(personInfo.Name)) { series.AddPerson(personInfo); } }
private async Task OrganizeEpisode(string sourcePath, Series series, int? seasonNumber, int? episodeNumber, int? endingEpiosdeNumber, DateTime? premiereDate, AutoOrganizeOptions options, bool overwriteExisting, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); var originalExtractedSeriesString = result.ExtractedName; // Proceed to sort the file var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(newPath)) { var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; _logger.Warn(msg); return; } _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; var fileExists = _fileSystem.FileExists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting) { if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { var msg = string.Format("File '{0}' already copied to new path '{1}', stopping organization", sourcePath, newPath); _logger.Info(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; return; } if (fileExists) { var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, newPath); _logger.Info(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; result.TargetPath = newPath; return; } if (otherDuplicatePaths.Count > 0) { var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths)); _logger.Info(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; result.DuplicatePaths = otherDuplicatePaths; return; } } PerformFileSorting(options.TvOptions, result); if (overwriteExisting) { var hasRenamedFiles = false; foreach (var path in otherDuplicatePaths) { _logger.Debug("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); var renameRelatedFiles = !hasRenamedFiles && string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); if (renameRelatedFiles) { hasRenamedFiles = true; } try { DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } if (rememberCorrection) { SaveSmartMatchString(originalExtractedSeriesString, series, options); } }
public static void RemoveDuplicateTags(Series series) { for (int i = series.Tags.Count - 1; i >= 0; i--) { if (series.Genres.Contains(series.Tags[i])) series.Tags.RemoveAt(i); } }
private List<string> GetOtherDuplicatePaths(string targetPath, Series series, int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber) { // TODO: Support date-naming? if (!seasonNumber.HasValue || !episodeNumber.HasValue) { return new List<string>(); } var episodePaths = series.GetRecursiveChildren() .OfType<Episode>() .Where(i => { var locationType = i.LocationType; // Must be file system based and match exactly if (locationType != LocationType.Remote && locationType != LocationType.Virtual && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == seasonNumber && i.IndexNumber.HasValue && i.IndexNumber.Value == episodeNumber) { if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue) { return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue && endingEpisodeNumber.Value == i.IndexNumberEnd.Value; } return true; } return false; }) .Select(i => i.Path) .ToList(); var folder = Path.GetDirectoryName(targetPath); var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath); try { var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder) .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); episodePaths.AddRange(filesOfOtherExtensions); } catch (DirectoryNotFoundException) { // No big deal. Maybe the season folder doesn't already exist. } return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); }
public static void TidyGenres(Series series) { var config = PluginConfiguration.Instance != null ? PluginConfiguration.Instance() : new PluginConfiguration(); var genres = new HashSet<string>(); var tags = new HashSet<string>(series.Tags); foreach (string genre in series.Genres) { string mapped; if (GenreMappings.TryGetValue(genre, out mapped)) genres.Add(mapped); else { if (config.MoveExcessGenresToTags) tags.Add(genre); else genres.Add(genre); } if (GenresAsTags.Contains(genre)) { if (config.MoveExcessGenresToTags) tags.Add(genre); else if (!genres.Contains(genre)) genres.Add(genre); } } series.Genres = genres.ToList(); series.Tags = tags.ToList(); }
/// <summary> /// Gets the season folder path. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) { // If there's already a season folder, use that var season = series .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber) .FirstOrDefault(); if (season != null) { return season.Path; } var path = series.Path; if (series.ContainsEpisodesWithoutSeasonFolders) { return path; } if (seasonNumber == 0) { return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName)); } var seasonFolderName = options.SeasonFolderPattern .Replace("%s", seasonNumber.ToString(_usCulture)) .Replace("%0s", seasonNumber.ToString("00", _usCulture)) .Replace("%00s", seasonNumber.ToString("000", _usCulture)); return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); }
private void AttachSoundtrackIds(BaseItemDto dto, Series item, User user) { var tvdb = item.GetProviderId(MetadataProviders.Tvdb); if (string.IsNullOrEmpty(tvdb)) { return; } var recursiveChildren = user == null ? _libraryManager.RootFolder.RecursiveChildren : user.RootFolder.GetRecursiveChildren(user); dto.SoundtrackIds = recursiveChildren .Where(i => { if (!string.IsNullOrEmpty(tvdb) && string.Equals(tvdb, i.GetProviderId(MetadataProviders.Tvdb), StringComparison.OrdinalIgnoreCase) && i is MusicAlbum) { return true; } return false; }) .Select(GetClientItemId) .ToArray(); }
/// <summary> /// Adds the season. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{Season}.</returns> private async Task<Season> AddSeason(Series series, int seasonNumber, CancellationToken cancellationToken) { _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name); var name = seasonNumber == 0 ? _config.Configuration.SeasonZeroDisplayName : string.Format("Season {0}", seasonNumber.ToString(UsCulture)); var season = new Season { Name = name, IndexNumber = seasonNumber, Parent = series, DisplayMediaType = typeof(Season).Name, Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Season)) }; await series.AddChild(season, cancellationToken).ConfigureAwait(false); await season.RefreshMetadata(new MetadataRefreshOptions(), cancellationToken).ConfigureAwait(false); return season; }
public List <BaseItem> GetEpisodes(Series series, User user, DtoOptions options) { return(GetEpisodes(series, user, null, options)); }