/// <summary> /// Creates a new <see cref="AutoAnimeGroupCalculator"/> using relationships stored in the database. /// </summary> /// <param name="session">The NHibernate session.</param> /// <returns>The created <see cref="AutoAnimeGroupCalculator"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="session"/> is <c>null</c>.</exception> public static AutoAnimeGroupCalculator CreateFromServerSettings(ISessionWrapper session) { string exclusionsSetting = ServerSettings.Instance.AutoGroupSeriesRelationExclusions; AutoGroupExclude exclusions = AutoGroupExclude.None; AnimeRelationType relationsToFuzzyTitleTest = AnimeRelationType.None; MainAnimeSelectionStrategy mainAnimeSelectionStrategy = ServerSettings.Instance.AutoGroupSeriesUseScoreAlgorithm ? MainAnimeSelectionStrategy.Weighted : MainAnimeSelectionStrategy.MinAirDate; if (!string.IsNullOrEmpty(exclusionsSetting)) { var exclusionTokens = exclusionsSetting .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => s.Length > 0) .ToList(); exclusions = exclusionTokens .Select(s => { s = s.Replace(" ", string.Empty); Enum.TryParse(s, true, out AutoGroupExclude exclude); return(exclude); }) .Aggregate(AutoGroupExclude.None, (exclude, allExcludes) => allExcludes | exclude); if (exclusionTokens.Contains("AllowDissimilarTitleExclusion", StringComparer.OrdinalIgnoreCase)) { relationsToFuzzyTitleTest = AnimeRelationType.SecondaryRelations; } } return(Create(session, exclusions, relationsToFuzzyTitleTest, mainAnimeSelectionStrategy)); }
/// <summary> /// Initializes a new <see cref="AutoAnimeGroupCalculator"/> instance. /// </summary> /// <param name="relationMap">A <see cref="ILookup{TKey,TElement}"/> that maps anime IDs to their relations.</param> /// <param name="exclusions">The relation/anime types to ignore when building relation graphs.</param> /// <exception cref="ArgumentNullException"><paramref name="relationMap"/> is <c>null</c>.</exception> public AutoAnimeGroupCalculator(ILookup <int, AnimeRelation> relationMap, AutoGroupExclude exclusions = AutoGroupExclude.SameSetting | AutoGroupExclude.Character, AnimeRelationType relationsToFuzzyTitleTest = AnimeRelationType.SecondaryRelations, MainAnimeSelectionStrategy mainAnimeSelectionStrategy = MainAnimeSelectionStrategy.MinAirDate) { if (relationMap == null) { throw new ArgumentNullException(nameof(relationMap)); } _relationMap = relationMap; _exclusions = exclusions; _relationsToFuzzyTitleTest = relationsToFuzzyTitleTest; switch (mainAnimeSelectionStrategy) { case MainAnimeSelectionStrategy.MinAirDate: _mainAnimeSelector = FindSuitableAnimeByMinAirDate; break; case MainAnimeSelectionStrategy.Weighted: _mainAnimeSelector = FindSuitableAnimeByWeighting; break; } }
/// <summary> /// Creates a new <see cref="AutoAnimeGroupCalculator"/> using relationships stored in the database. /// </summary> /// <param name="session">The NHibernate session.</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.None; string exclusionsSetting = ServerSettings.AutoGroupSeriesRelationExclusions; if (!String.IsNullOrEmpty(exclusionsSetting)) { exclusions = exclusionsSetting.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => { AutoGroupExclude exclude; s = s.Replace(" ", String.Empty); Enum.TryParse(s, true, out exclude); return(exclude); }) .Aggregate(AutoGroupExclude.None, (exclude, allExcludes) => allExcludes | exclude); } return(Create(session, exclusions)); }
/// <summary> /// Initializes a new <see cref="AutoAnimeGroupCalculator"/> instance. /// </summary> /// <param name="relationMap">A <see cref="ILookup{TKey,TElement}"/> that maps anime IDs to their relations.</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> /// <exception cref="ArgumentNullException"><paramref name="relationMap"/> is <c>null</c>.</exception> public AutoAnimeGroupCalculator(Dictionary <int, List <AnimeRelation> > relationMap, AutoGroupExclude exclusions, AnimeRelationType relationsToFuzzyTitleTest, MainAnimeSelectionStrategy mainAnimeSelectionStrategy) { if (relationMap == null) { throw new ArgumentNullException(nameof(relationMap)); } _relationMap = relationMap; _exclusions = exclusions; _relationsToFuzzyTitleTest = relationsToFuzzyTitleTest; switch (mainAnimeSelectionStrategy) { case MainAnimeSelectionStrategy.MinAirDate: _mainAnimeSelector = FindSuitableAnimeByMinAirDate; break; case MainAnimeSelectionStrategy.Weighted: _mainAnimeSelector = FindSuitableAnimeByWeighting; break; } }
/// <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)); }
public static void Main(string[] args) { using (IDbConnection con = OpenDatabaseConnection()) { var relations = GetRelations(con).ToList(); var knownAnime = GetAnime(con).ToDictionary(a => a.AnimeId); var relationsPerAnime = relations.Select(r => new { From = r.AnimeId, To = r.RelatedAnimeId }) .Union(relations.Select(r => new { From = r.RelatedAnimeId, To = r.AnimeId })) .ToLookup(r => r.From, r => r.To); var animeGroupCalculator = AutoAnimeGroupCalculator.CreateFromServerSettings(con); AutoGroupExclude relExclude = ServerSettings.GetAutoGroupSeriesRelationExclusions(); List <HashSet <int> > subGraphs = BuildSubGraphs(relationsPerAnime); WriteLine("digraph Shoko_Anime_Relations {"); WriteLine("\tnode [fontsize=11, fillcolor=powderblue, style=filled];"); WriteLine("\tgraph [nodesep=1.5, ranksep=6, overlap=false, splines=spline, style=dashed, smoothing=power_dist];"); int sgCount = 0; // Render each subgraph foreach (var subgraph in subGraphs) { Write("\tsubgraph cluster_"); Write(sgCount); WriteLine(" {"); foreach (int animeId in subgraph) { Write("\t\t\""); Write(animeId); Write("\" [fillcolor="); if (knownAnime.TryGetValue(animeId, out var anime)) { switch (anime.AnimeType) { case AnimeType.Movie: Write("palegreen"); break; case AnimeType.OVA: Write("tan"); break; case AnimeType.TVSeries: Write("lightblue"); break; case AnimeType.TVSpecial: Write("cornsilk"); break; case AnimeType.Other: Write("lightpink"); break; case AnimeType.Web: Write("pink"); break; } if (animeGroupCalculator.GetGroupAnimeId(animeId) == animeId) { Write(", penwidth=2, shape=doubleoctagon"); } string title = anime.MainTitle; if (title.Length > MaxTitleLength) { title = title.Substring(0, MaxTitleLength - 3) + "..."; } title = WordWrap(title, WordWrapLength); Write(", label=<"); Write(anime.AnimeId); if (anime.AirDate != null) { Write(" <i>("); Write(anime.AirDate.Value.ToString("MMM yyyy")); Write(")</i>"); } Write("<br/><b>"); Write(HttpUtility.HtmlEncode(title).Replace("\n", "<br/>")); Write("</b>>"); } else // Write colour for missing series { Write("tomato, shape=box"); } Write(", target=_blank, href=\"https://anidb.net/a"); Write(animeId); WriteLine("\"];"); } WriteLine("\t}"); sgCount++; } // Render all edges WriteLine(); WriteLine("\t#Edges"); foreach (var edge in relations) { Write("\t\""); Write(edge.AnimeId); Write("\" -> \""); Write(edge.RelatedAnimeId); Write("\" [label=\""); Write(edge.RelationType); Write("\""); // If the relation is meant to be exlcuded when calculating groups, then we'll colour it differently if (Enum.TryParse(edge.RelationType.Replace(" ", string.Empty), true, out AutoGroupExclude relType) && (relType & relExclude) > 0) { Write(", color=grey, style=dashed"); } WriteLine("];"); } WriteLine("}"); } }
/// <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); } } }