/// <summary> /// Determines whether or not we should consider using the specified <see cref="AnimeRelation"/>. /// </summary> /// <remarks> /// Relationships such as Prequel/Sequel, Full Story/Summary, Parent Story/Side Story, are automatically /// considered (unless in exclusion list). However, if fuzzy title testing is enabled then the other relationship types /// are only considered if their main titles "fuzzily" match. /// </remarks> /// <param name="rel">The <see cref="AnimeRelation"/> to test.</param> /// <returns><c>true</c> if the specified <see cref="AnimeRelation"/> should be considered when building /// the group graph; otherwise, <c>false</c>.</returns> private bool ShouldConsiderAnimeRelation(AnimeRelation rel) { if (((int)rel.RelationType & (int)_exclusions) != 0) { return(false); // The relation is in the exclusion list, so ignore it } // Check if we are excluding Movies or OVAs if ((_exclusions & AutoGroupExclude.Movie) == AutoGroupExclude.Movie && (rel.FromType == AnimeType.Movie || rel.ToType == AnimeType.Movie) || (_exclusions & AutoGroupExclude.Ova) == AutoGroupExclude.Ova && (rel.FromType == AnimeType.OVA || rel.ToType == AnimeType.OVA)) { return(false); } // Are we configured to do a fuzzy title test for this particular relation type? If not, then the relation is immediately allowed if ((rel.RelationType & _relationsToFuzzyTitleTest) == 0) { return(true); } // Perform a very poor man's string metric test. // We split both titles up into tokens (basically words) and count the number of times each word // appears in both titles (we'll also count characters of matching words). // The more words/characters that match, the more likely we'll consider the relationship string[] fromTitleTokens = CreateTokensFromTitle(rel.FromMainTitle); string[] toTitleTokens = CreateTokensFromTitle(rel.ToMainTitle); int matchLen = 0; int matches = 0; foreach (string fromToken in fromTitleTokens) { foreach (string toToken in toTitleTokens) { if (fromToken.Equals(toToken, StringComparison.InvariantCultureIgnoreCase)) { matchLen += fromToken.Length; matches++; break; } } } if (matches == 0) { return(false); } int shortestTitleLen = Math.Min(fromTitleTokens.Sum(s => s.Length), toTitleTokens.Sum(s => s.Length)); int minTokenCount = Math.Min(fromTitleTokens.Length, toTitleTokens.Length); // Either approximately half the words must match, // or the total length of the matched words must equate to 40% or more of the shortest title return((matches >= minTokenCount / 2) || matchLen >= Math.Max(1, (int)(shortestTitleLen * 0.4))); }
/// <summary> /// Creates a new <see cref="AutoAnimeGroupCalculator"/> using relationships stored in the database. /// </summary> /// <param name="session">The NHibernate session.</param> /// <param name="exclusions">The relation/anime types to ignore when building relation graphs.</param> /// <param name="relationsToFuzzyTitleTest">The relationships for which we'll perform title similarity checks for /// (If the titles aren't similar enough then the anime will end up in different groups).</param> /// <param name="mainAnimeSelectionStrategy">The strategy to use for selecting the "main" anime that will be used /// for representing the group.</param> /// <returns>The created <see cref="AutoAnimeGroupCalculator"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="session"/> is <c>null</c>.</exception> public static AutoAnimeGroupCalculator Create(ISessionWrapper session, AutoGroupExclude exclusions = AutoGroupExclude.SameSetting | AutoGroupExclude.Character, AnimeRelationType relationsToFuzzyTitleTest = AnimeRelationType.SecondaryRelations, MainAnimeSelectionStrategy mainAnimeSelectionStrategy = MainAnimeSelectionStrategy.MinAirDate) { if (session == null) { throw new ArgumentNullException(nameof(session)); } var relationshipMap = session.CreateSQLQuery(@" SELECT fromAnime.AnimeID AS fromAnimeId , toAnime.AnimeID AS toAnimeId , fromAnime.AnimeType AS fromAnimeType , toAnime.AnimeType AS toAnimeType , fromAnime.MainTitle AS fromMainTitle , toAnime.MainTitle AS toMainTitle , fromAnime.AirDate AS fromAirDate , toAnime.AirDate AS toAirDate , rel.RelationType AS relationType FROM AniDB_Anime_Relation rel INNER JOIN AniDB_Anime fromAnime ON fromAnime.AnimeID = rel.AnimeID INNER JOIN AniDB_Anime toAnime ON toAnime.AnimeID = rel.RelatedAnimeID") .AddScalar("fromAnimeId", NHibernateUtil.Int32) .AddScalar("toAnimeId", NHibernateUtil.Int32) .AddScalar("fromAnimeType", NHibernateUtil.Int32) .AddScalar("toAnimeType", NHibernateUtil.Int32) .AddScalar("fromMainTitle", NHibernateUtil.String) .AddScalar("toMainTitle", NHibernateUtil.String) .AddScalar("fromAirDate", NHibernateUtil.DateTime) .AddScalar("toAirDate", NHibernateUtil.DateTime) .AddScalar("relationType", NHibernateUtil.String) .List <object[]>() .Select(r => { var relation = new AnimeRelation { FromId = (int)r[0], ToId = (int)r[1], FromType = (AnimeType)r[2], ToType = (AnimeType)r[3], FromMainTitle = (string)r[4], ToMainTitle = (string)r[5], FromAirDate = (DateTime?)r[6], ToAirDate = (DateTime?)r[7] }; switch (((string)r[8]).ToLowerInvariant()) { case "full story": relation.RelationType = AnimeRelationType.FullStory; break; case "summary": relation.RelationType = AnimeRelationType.Summary; break; case "parent story": relation.RelationType = AnimeRelationType.ParentStory; break; case "side story": relation.RelationType = AnimeRelationType.SideStory; break; case "prequel": relation.RelationType = AnimeRelationType.Prequel; break; case "sequel": relation.RelationType = AnimeRelationType.Sequel; break; case "alternative setting": relation.RelationType = AnimeRelationType.AlternateSetting; break; case "alternative version": relation.RelationType = AnimeRelationType.AlternateVersion; break; case "same setting": relation.RelationType = AnimeRelationType.SameSetting; break; case "character": relation.RelationType = AnimeRelationType.Character; break; default: relation.RelationType = AnimeRelationType.Other; break; } return(relation); }) .ToLookup(k => k.FromId); return(new AutoAnimeGroupCalculator(relationshipMap, exclusions, relationsToFuzzyTitleTest, mainAnimeSelectionStrategy)); }
/// <summary> /// Creates a new <see cref="AutoAnimeGroupCalculator"/> using relationships stored in the database. /// </summary> /// <param name="exclusions">The relation/anime types to ignore when building relation graphs.</param> /// <param name="relationsToFuzzyTitleTest">The relationships for which we'll perform title similarity checks for /// (If the titles aren't similar enough then the anime will end up in different groups).</param> /// <param name="mainAnimeSelectionStrategy">The strategy to use for selecting the "main" anime that will be used /// for representing the group.</param> /// <returns>The created <see cref="AutoAnimeGroupCalculator"/>.</returns> public static AutoAnimeGroupCalculator Create(AutoGroupExclude exclusions = AutoGroupExclude.SameSetting | AutoGroupExclude.Character, AnimeRelationType relationsToFuzzyTitleTest = AnimeRelationType.SecondaryRelations, MainAnimeSelectionStrategy mainAnimeSelectionStrategy = MainAnimeSelectionStrategy.MinAirDate) { Dictionary <int, (int type, string title, DateTime?airdate)> animes = Repo.Instance.AniDB_Anime.GetRelationInfo(); Dictionary <int, List <AnimeRelation> > relations = new Dictionary <int, List <AnimeRelation> >(); foreach (AniDB_Anime_Relation rel in Repo.Instance.AniDB_Anime_Relation.GetAll()) { (int type, string title, DateTime? airdate)from = animes.ContainsKey(rel.AnimeID) ? animes[rel.AnimeID] : (0, null, null); (int type, string title, DateTime? airdate)to = animes.ContainsKey(rel.RelatedAnimeID) ? animes[rel.RelatedAnimeID] : (0, null, null); if (from.title != null && to.title != null) { var relation = new AnimeRelation { FromId = rel.AnimeID, ToId = rel.RelatedAnimeID, FromType = (AnimeType)from.type, ToType = (AnimeType)to.type, FromMainTitle = from.title, ToMainTitle = to.title, FromAirDate = from.airdate, ToAirDate = to.airdate }; switch ((rel.RelationType).ToLowerInvariant()) { case "full story": relation.RelationType = AnimeRelationType.FullStory; break; case "summary": relation.RelationType = AnimeRelationType.Summary; break; case "parent story": relation.RelationType = AnimeRelationType.ParentStory; break; case "side story": relation.RelationType = AnimeRelationType.SideStory; break; case "prequel": relation.RelationType = AnimeRelationType.Prequel; break; case "sequel": relation.RelationType = AnimeRelationType.Sequel; break; case "alternative setting": relation.RelationType = AnimeRelationType.AlternateSetting; break; case "alternative version": relation.RelationType = AnimeRelationType.AlternateVersion; break; case "same setting": relation.RelationType = AnimeRelationType.SameSetting; break; case "character": relation.RelationType = AnimeRelationType.Character; break; default: relation.RelationType = AnimeRelationType.Other; break; } if (!relations.ContainsKey(rel.AnimeID)) { relations.Add(rel.AnimeID, new List <AnimeRelation>()); } relations[rel.AnimeID].Add(relation); } } return(new AutoAnimeGroupCalculator(relations, exclusions, relationsToFuzzyTitleTest, mainAnimeSelectionStrategy)); }
private static ILookup <int, AnimeRelation> LoadRelations(IDbConnection con) { using (IDbCommand cmd = con.CreateCommand()) { cmd.CommandText = @" SELECT fromAnime.AnimeID, toAnime.AnimeID, fromAnime.AnimeType, toAnime.AnimeType, fromAnime.MainTitle, toAnime.MainTitle, rel.RelationType, fromAnime.AirDate, toAnime.AirDate FROM AniDB_Anime_Relation rel INNER JOIN AniDB_Anime fromAnime ON fromAnime.AnimeID = rel.AnimeID INNER JOIN AniDB_Anime toAnime ON toAnime.AnimeID = rel.RelatedAnimeID"; using (IDataReader reader = cmd.ExecuteReader()) { var relationMap = reader.EnumerateRecords() .Select(r => { var relation = new AnimeRelation { FromId = r.GetInt32(0), ToId = r.GetInt32(1), FromType = (AnimeTypes)r.GetInt32(2), ToType = (AnimeTypes)r.GetInt32(3), FromMainTitle = r.GetString(4), ToMainTitle = r.GetString(5), FromAirDate = r.IsDBNull(7) ? (DateTime?)null : r.GetDateTime(7), ToAirDate = r.IsDBNull(8) ? (DateTime?)null : r.GetDateTime(8) }; switch (r.GetString(6).ToLowerInvariant()) { case "full story": relation.RelationType = AnimeRelationType.FullStory; break; case "summary": relation.RelationType = AnimeRelationType.Summary; break; case "parent story": relation.RelationType = AnimeRelationType.ParentStory; break; case "side story": relation.RelationType = AnimeRelationType.SideStory; break; case "prequel": relation.RelationType = AnimeRelationType.Prequel; break; case "sequel": relation.RelationType = AnimeRelationType.Sequel; break; case "alternative setting": relation.RelationType = AnimeRelationType.AlternativeSetting; break; case "alternative version": relation.RelationType = AnimeRelationType.AlternativeVersion; break; case "same setting": relation.RelationType = AnimeRelationType.SameSetting; break; case "character": relation.RelationType = AnimeRelationType.Character; break; default: relation.RelationType = AnimeRelationType.Other; break; } return(relation); }) .ToLookup(r => r.FromId); return(relationMap); } } }
/// <summary> /// Creates a new <see cref="AutoAnimeGroupCalculator"/> using relationships stored in the database. /// </summary> /// <param name="con">The open database connection.</param> /// <param name="exclusions">The relation/anime types to ignore when building relation graphs.</param> /// <param name="relationsToFuzzyTitleTest">The relationships for which we'll perform title similarity checks for /// (If the titles aren't similar enough then the anime will end up in different groups).</param> /// <param name="mainAnimeSelectionStrategy">The strategy to use for selecting the "main" anime that will be used /// for representing the group.</param> /// <returns>The created <see cref="AutoAnimeGroupCalculator"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="con"/> is <c>null</c>.</exception> public static AutoAnimeGroupCalculator Create(IDbConnection con, AutoGroupExclude exclusions = AutoGroupExclude.SameSetting | AutoGroupExclude.Character, AnimeRelationType relationsToFuzzyTitleTest = AnimeRelationType.SecondaryRelations, MainAnimeSelectionStrategy mainAnimeSelectionStrategy = MainAnimeSelectionStrategy.MinAirDate) { if (con == null) { throw new ArgumentNullException(nameof(con)); } using (IDbCommand cmd = con.CreateCommand()) { cmd.CommandText = @" SELECT fromAnime.AnimeID AS fromAnimeId , toAnime.AnimeID AS toAnimeId , fromAnime.AnimeType AS fromAnimeType , toAnime.AnimeType AS toAnimeType , fromAnime.MainTitle AS fromMainTitle , toAnime.MainTitle AS toMainTitle , fromAnime.AirDate AS fromAirDate , toAnime.AirDate AS toAirDate , rel.RelationType AS relationType FROM AniDB_Anime_Relation rel INNER JOIN AniDB_Anime fromAnime ON fromAnime.AnimeID = rel.AnimeID INNER JOIN AniDB_Anime toAnime ON toAnime.AnimeID = rel.RelatedAnimeID"; using (IDataReader reader = cmd.ExecuteReader()) { var relationshipMap = ReadAnimeRelations(reader).ToLookup(k => k.FromId); return(new AutoAnimeGroupCalculator(relationshipMap, exclusions, relationsToFuzzyTitleTest, mainAnimeSelectionStrategy)); } } IEnumerable <AnimeRelation> ReadAnimeRelations(IDataReader reader) { while (reader.Read()) { var relation = new AnimeRelation { FromId = reader.GetInt32(0), ToId = reader.GetInt32(1), FromType = (AnimeType)reader.GetInt32(2), ToType = (AnimeType)reader.GetInt32(3), FromMainTitle = reader.GetString(4), ToMainTitle = reader.GetString(5), FromAirDate = reader.IsDBNull(6) ? null : (DateTime?)reader.GetDateTime(6), ToAirDate = reader.IsDBNull(7) ? null : (DateTime?)reader.GetDateTime(7) }; switch ((reader.GetString(8)).ToLowerInvariant()) { case "full story": relation.RelationType = AnimeRelationType.FullStory; break; case "summary": relation.RelationType = AnimeRelationType.Summary; break; case "parent story": relation.RelationType = AnimeRelationType.ParentStory; break; case "side story": relation.RelationType = AnimeRelationType.SideStory; break; case "prequel": relation.RelationType = AnimeRelationType.Prequel; break; case "sequel": relation.RelationType = AnimeRelationType.Sequel; break; case "alternative setting": relation.RelationType = AnimeRelationType.AlternateSetting; break; case "alternative version": relation.RelationType = AnimeRelationType.AlternateVersion; break; case "same setting": relation.RelationType = AnimeRelationType.SameSetting; break; case "character": relation.RelationType = AnimeRelationType.Character; break; default: relation.RelationType = AnimeRelationType.Other; break; } yield return(relation); } } }