Example #1
0
        public void ParseWithXElementMangaTest()
        {
            XDocument            doc     = XDocument.Parse(Helpers.GetResourceText("test_manga_clean.xml"));
            MalUserLookupResults results = MalAppInfoXml.ParseResults(doc);

            DoMangaAsserts(results);
        }
Example #2
0
        static void Main(string[] args)
        {
            // No logging support for MalApi in .NET Core yet.
            // At this time there is no Common.Logging adapter for NLog that supports .NET core.
            using (MyAnimeListApi api = new MyAnimeListApi())
            {
                api.UserAgent   = "MalApiExample";
                api.TimeoutInMs = 15000;

                MalUserLookupResults userLookup = api.GetAnimeListForUser("LordHighCaptain");
                foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
                {
                    Console.WriteLine("Rating for {0}: {1}", listEntry.AnimeInfo.Title, listEntry.Score);
                }

                Console.WriteLine();
                Console.WriteLine();

                RecentUsersResults recentUsersResults = api.GetRecentOnlineUsers();
                foreach (string user in recentUsersResults.RecentUsers)
                {
                    Console.WriteLine("Recent user: {0}", user);
                }

                Console.WriteLine();
                Console.WriteLine();

                int eurekaSevenID = 237;
                AnimeDetailsResults eurekaSeven = api.GetAnimeDetails(eurekaSevenID);
                Console.WriteLine("Eureka Seven genres: {0}", string.Join(", ", eurekaSeven.Genres));
            }
        }
Example #3
0
        static void Main(string[] args)
        {
            // MalApi uses the Common.Logging logging abstraction.
            // You can hook it up to any logging library that has a Common.Logging adapter.
            // See App.config for an example of hooking up MalApi to NLog.
            // Note that you will also need the appropriate NLog and Common.Logging.NLogXX packages installed.
            // Hooking up logging is not necessary but can be useful.
            // With the configuration in this example and with this example program, you will see lines like:

            // Logged from MalApi: Getting anime list for MAL user LordHighCaptain using URI https://myanimelist.net/malappinfo.php?status=all&type=anime&u=LordHighCaptain
            // Logged from MalApi: Successfully retrieved anime list for user LordHighCaptain

            using (MyAnimeListApi api = new MyAnimeListApi())
            {
                api.UserAgent   = "MalApiExample";
                api.TimeoutInMs = 15000;

                var animeUpdateInfo = new AnimeUpdate()
                {
                    Episode = 26,
                    Status  = AnimeCompletionStatus.Completed,
                    Score   = 9,
                };
                string userUpdateAnime = api.UpdateAnimeForUser(1, animeUpdateInfo, "user", "password");

                var mangaUpdateInfo = new MangaUpdate()
                {
                    Chapter = 20,
                    Volume  = 3,
                    Score   = 8,
                    Status  = MangaCompletionStatus.Completed
                };
                string userUpdateManga = api.UpdateMangaForUser(952, mangaUpdateInfo, "user", "password");



                MalUserLookupResults userLookup = api.GetAnimeListForUser("user");
                foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
                {
                    Console.WriteLine("Rating for {0}: {1}", listEntry.AnimeInfo.Title, listEntry.Score);
                }

                Console.WriteLine();
                Console.WriteLine();

                RecentUsersResults recentUsersResults = api.GetRecentOnlineUsers();
                foreach (string user in recentUsersResults.RecentUsers)
                {
                    Console.WriteLine("Recent user: {0}", user);
                }

                Console.WriteLine();
                Console.WriteLine();

                int eurekaSevenID = 237;
                AnimeDetailsResults eurekaSeven = api.GetAnimeDetails(eurekaSevenID);
                Console.WriteLine("Eureka Seven genres: {0}", string.Join(", ", eurekaSeven.Genres));
            }
        }
Example #4
0
 public void ParseWithTextReaderMangaTest()
 {
     using (TextReader reader = Helpers.GetResourceStream("test_manga.xml"))
     {
         MalUserLookupResults results = MalAppInfoXml.Parse(reader);
         DoMangaAsserts(results);
     }
 }
Example #5
0
        private void DoAnimeAsserts(MalUserLookupResults results)
        {
            Assert.Equal("LordHighCaptain", results.CanonicalUserName);
            Assert.Equal(158667, results.UserId);
            Assert.Equal(163, results.AnimeList.Count);

            MyAnimeListEntry entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 853).First();

            Assert.Equal("Ouran Koukou Host Club", entry.AnimeInfo.Title);
            Assert.Equal(MalAnimeType.Tv, entry.AnimeInfo.Type);
            entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List <string>()
            {
                "Ohran Koko Host Club", "Ouran Koukou Hosutobu", "Ouran High School Host Club"
            });

            Assert.Equal(7, entry.NumEpisodesWatched);
            Assert.Equal(7, entry.Score);
            Assert.Equal(AnimeCompletionStatus.Watching, entry.Status);

            // Test tags with Equal, not equivalent, because order in tags matters
            Assert.Equal(new List <string>()
            {
                "duck", "goose"
            }, entry.Tags);

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 7311).First();
            Assert.Equal("Suzumiya Haruhi no Shoushitsu", entry.AnimeInfo.Title);
            Assert.Equal(MalAnimeType.Movie, entry.AnimeInfo.Type);
            entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List <string>()
            {
                "The Vanishment of Haruhi Suzumiya", "Suzumiya Haruhi no Syoshitsu", "Haruhi movie", "The Disappearance of Haruhi Suzumiya"
            });
            Assert.Equal((decimal?)null, entry.Score);
            Assert.Equal(0, entry.NumEpisodesWatched);
            Assert.Equal(AnimeCompletionStatus.PlanToWatch, entry.Status);
            Assert.Equal(new List <string>(), entry.Tags);

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 889).First();
            Assert.Equal("Black Lagoon", entry.AnimeInfo.Title);

            // Make sure synonyms that are the same as the real name get filtered out
            entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List <string>());

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.Title == "Test").First();
            // Make sure that <series_synonyms/> is the same as <series_synonyms></series_synonyms>
            entry.AnimeInfo.Synonyms.Should().BeEquivalentTo(new List <string>());
            Assert.Equal(new UncertainDate(2010, 2, 6), entry.AnimeInfo.StartDate);
            Assert.Equal(UncertainDate.Unknown, entry.AnimeInfo.EndDate);
            Assert.Equal("https://cdn.myanimelist.net/images/anime/9/24646.jpg", entry.AnimeInfo.ImageUrl);
            Assert.Equal(new UncertainDate(year: null, month: 2, day: null), entry.MyStartDate);
            Assert.Equal(UncertainDate.Unknown, entry.MyFinishDate);
            Assert.Equal(new DateTime(year: 2011, month: 4, day: 2, hour: 22, minute: 50, second: 58, kind: DateTimeKind.Utc), entry.MyLastUpdate);
            Assert.Equal(new List <string>()
            {
                "test&test", "< less than", "> greater than", "apos '", "quote \"", "hex ö", "dec !", "control character"
            }, entry.Tags);
        }
Example #6
0
        private void DoMangaAsserts(MalUserLookupResults results)
        {
            Assert.Equal("naps250", results.CanonicalUserName);
            Assert.Equal(5544903, results.UserId);
            Assert.Equal(6, results.MangaList.Count);

            MyMangaListEntry entry = results.MangaList.Where(manga => manga.MangaInfo.MangaId == 2).First();

            Assert.Equal("Berserk", entry.MangaInfo.Title);
            Assert.Equal(MalMangaType.Manga, entry.MangaInfo.Type);
            entry.MangaInfo.Synonyms.Should().BeEquivalentTo(new List <string>()
            {
                "Berserk: The Prototype"
            });

            Assert.Equal(352, entry.NumChaptersRead);
            Assert.Equal(10, entry.Score);
            Assert.Equal(MangaCompletionStatus.Reading, entry.Status);

            // Test tags with Equal, not equivalent, because order in tags matters
            Assert.Equal(new List <string>()
            {
                "CLANG", "Miura pls"
            }, entry.Tags);

            entry = results.MangaList.Where(manga => manga.MangaInfo.MangaId == 9115).First();
            Assert.Equal("Ookami to Koushinryou", entry.MangaInfo.Title);
            Assert.Equal(MalMangaType.Novel, entry.MangaInfo.Type);
            entry.MangaInfo.Synonyms.Should().BeEquivalentTo(new List <string>()
            {
                "Okami to Koshinryo", "Spice and Wolf", "Spice & Wolf"
            });
            Assert.Equal((decimal?)null, entry.Score);
            Assert.Equal(0, entry.NumChaptersRead);
            Assert.Equal(MangaCompletionStatus.Completed, entry.Status);
            Assert.Equal(new List <string>(), entry.Tags);

            entry = results.MangaList.Where(manga => manga.MangaInfo.MangaId == 1).First();
            Assert.Equal("Monster", entry.MangaInfo.Title);

            // Make sure synonyms that are the same as the real name get filtered out
            entry.MangaInfo.Synonyms.Should().BeEquivalentTo(new List <string>());

            entry = results.MangaList.Where(manga => manga.MangaInfo.Title == "Test").First();
            // Make sure that <series_synonyms/> is the same as <series_synonyms></series_synonyms>
            entry.MangaInfo.Synonyms.Should().BeEquivalentTo(new List <string>());
            Assert.Equal(new UncertainDate(2010, 2, 6), entry.MangaInfo.StartDate);
            Assert.Equal(UncertainDate.Unknown, entry.MangaInfo.EndDate);
            Assert.Equal("https://myanimelist.cdn-dena.com/images/manga/2/159423.jpg", entry.MangaInfo.ImageUrl);
            Assert.Equal(new UncertainDate(year: null, month: 2, day: null), entry.MyStartDate);
            Assert.Equal(UncertainDate.Unknown, entry.MyFinishDate);
            Assert.Equal(new DateTime(year: 2011, month: 4, day: 2, hour: 22, minute: 50, second: 58, kind: DateTimeKind.Utc), entry.MyLastUpdate);
            Assert.Equal(new List <string>()
            {
                "test&test", "< less than", "> greater than", "apos '", "quote \"", "hex ö", "dec !", "control character"
            }, entry.Tags);
        }
Example #7
0
        public void GetMangaListForUser()
        {
            string username = "******";

            using (MyAnimeListApi api = new MyAnimeListApi())
            {
                MalUserLookupResults userLookup = api.GetMangaListForUser(username);

                Assert.NotEmpty(userLookup.MangaList);
            }
        }
Example #8
0
        public async Task <IActionResult> GetRecs([FromBody] AnimeRecsInputJson input,
                                                  [FromServices] IOptionsSnapshot <Config.RecommendationsConfig> recConfig,
                                                  [FromServices] IMyAnimeListApiFactory malApiFactory, [FromServices] IAnimeRecsClientFactory recClientFactory,
                                                  [FromServices] IAnimeRecsDbConnectionFactory dbConnFactory, [FromServices] IRazorViewEngine viewEngine,
                                                  [FromServices] ITempDataProvider tempProvider)
        {
            if (!ModelState.IsValid)
            {
                AjaxError error = new AjaxError(ModelState);
                _logger.LogDebug("Invalid input received for GetRecs: {0}", error.Message);
                return(BadRequest(error));
            }

            if (input.RecSourceName == null)
            {
                input.RecSourceName = recConfig.Value.DefaultRecSource;
            }

            try
            {
                MalUserLookupResults userLookup = await GetUserLookupAsync(input, malApiFactory).ConfigureAwait(false);

                Dictionary <int, MalListEntry> animeList = new Dictionary <int, MalListEntry>();
                foreach (MyAnimeListEntry listEntry in userLookup.AnimeList)
                {
                    animeList[listEntry.AnimeInfo.AnimeId] = new AnimeRecs.RecEngine.MAL.MalListEntry((byte?)listEntry.Score, listEntry.Status, (short)listEntry.NumEpisodesWatched);
                }

                Dictionary <int, MalListEntry> animeWithheld = WithholdAnime(input, animeList);

                MalRecResults <IEnumerable <IRecommendation> > recResults = await GetRecommendationsAsync(input, recConfig.Value, animeList, animeWithheld, recClientFactory).ConfigureAwait(false);

                GetRecsViewModel viewModel = new GetRecsViewModel(
                    results: recResults,
                    userId: userLookup.UserId,
                    userName: userLookup.CanonicalUserName,
                    userLookup: userLookup,
                    userAnimeList: animeList,
                    maximumRecommendationsToReturn: recConfig.Value.MaximumRecommendationsToReturn,
                    maximumRecommendersToReturn: recConfig.Value.MaximumRecommendersToReturn,
                    animeWithheld: animeWithheld,
                    dbConnectionFactory: dbConnFactory
                    );

                RecResultsAsHtmlJson resultsJson = await GetResultHtmlAsync(viewModel, input, viewEngine, tempProvider).ConfigureAwait(false);

                return(Ok(resultsJson));
            }
            catch (ShortCircuitException ex)
            {
                return(ex.Result);
            }
        }
Example #9
0
        public void GetAnimeListForUser()
        {
            string username = "******";

            using (MyAnimeListApi api = new MyAnimeListApi())
            {
                MalUserLookupResults userLookup = api.GetAnimeListForUser(username);

                // Just a smoke test that checks that getting an anime list returns something
                Assert.NotEmpty(userLookup.AnimeList);
            }
        }
Example #10
0
 public GetRecsViewModel(MalRecResults <IEnumerable <IRecommendation> > results, int userId, string userName,
                         MalUserLookupResults userLookup, IDictionary <int, MalListEntry> userAnimeList,
                         int maximumRecommendationsToReturn, int maximumRecommendersToReturn, IDictionary <int, MalListEntry> animeWithheld, IAnimeRecsDbConnectionFactory dbConnectionFactory)
 {
     Results       = results;
     UserId        = userId;
     UserName      = userName;
     UserLookup    = userLookup;
     UserAnimeList = userAnimeList;
     MaximumRecommendationsToReturn = maximumRecommendationsToReturn;
     MaximumRecommendersToReturn    = maximumRecommendersToReturn;
     DbConnectionFactory            = dbConnectionFactory;
     StreamsByAnime = new Dictionary <int, ICollection <streaming_service_anime_map> >();
     AnimeWithheld  = animeWithheld;
 }
Example #11
0
        static bool UserMeetsCriteria(MalUserLookupResults userLookup, NpgsqlConnection conn, NpgsqlTransaction transaction)
        {
            // completed, rated >= X, and user is not in DB
            int completedRated = userLookup.AnimeList.Count(anime => anime.Score.HasValue && anime.Status == CompletionStatus.Completed);

            if (completedRated < config.MinimumAnimesCompletedAndRated)
            {
                return(false);
            }

            Logging.Log.DebugFormat("Really checking if {0} is in the database by user id.", userLookup.CanonicalUserName);
            bool isInDb = mal_user.UserIsInDb(userLookup.UserId, conn, transaction);

            Logging.Log.DebugFormat("{0} really in database = {1}", userLookup.CanonicalUserName, isInDb);
            return(!isInDb);
        }
Example #12
0
        private void DoAsserts(MalUserLookupResults results)
        {
            Assert.That(results.CanonicalUserName, Is.EqualTo("LordHighCaptain"));
            Assert.That(results.UserId, Is.EqualTo(158667));
            Assert.That(results.AnimeList.Count, Is.EqualTo(163));

            MyAnimeListEntry entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 853).First();
            Assert.That(entry.AnimeInfo.Title, Is.EqualTo("Ouran Koukou Host Club"));
            Assert.That(entry.AnimeInfo.Type, Is.EqualTo(MalAnimeType.Tv));
            Assert.That(entry.AnimeInfo.Synonyms, Is.EquivalentTo(new List<string>() { "Ohran Koko Host Club", "Ouran Koukou Hosutobu", "Ouran High School Host Club" }));
            Assert.That(entry.NumEpisodesWatched, Is.EqualTo(7));
            Assert.That(entry.Score, Is.EqualTo(7));
            Assert.That(entry.Status, Is.EqualTo(CompletionStatus.Watching));
            Assert.That(entry.Tags, Is.EqualTo(new List<string>() { "duck", "goose" }));

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 7311).First();
            Assert.That(entry.AnimeInfo.Title, Is.EqualTo("Suzumiya Haruhi no Shoushitsu"));
            Assert.That(entry.AnimeInfo.Type, Is.EqualTo(MalAnimeType.Movie));
            Assert.That(entry.AnimeInfo.Synonyms, Is.EquivalentTo(new List<string>() { "The Vanishment of Haruhi Suzumiya", "Suzumiya Haruhi no Syoshitsu", "Haruhi movie", "The Disappearance of Haruhi Suzumiya" }));
            Assert.That(entry.Score, Is.EqualTo((decimal?)null));
            Assert.That(entry.NumEpisodesWatched, Is.EqualTo(0));
            Assert.That(entry.Status, Is.EqualTo(CompletionStatus.PlanToWatch));
            Assert.That(entry.Tags, Is.EqualTo(new List<string>()));

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.AnimeId == 889).First();
            Assert.That(entry.AnimeInfo.Title, Is.EqualTo("Black Lagoon"));

            // Make sure synonyms that are the same as the real name get filtered out
            Assert.That(entry.AnimeInfo.Synonyms, Is.EquivalentTo(new List<string>()));

            entry = results.AnimeList.Where(anime => anime.AnimeInfo.Title == "Test").First();
            // Make sure that <series_synonyms/> is the same as <series_synonyms></series_synonyms>
            Assert.That(entry.AnimeInfo.Synonyms, Is.EquivalentTo(new List<string>()));
            Assert.That(entry.AnimeInfo.StartDate, Is.EqualTo(new UncertainDate(2010, 2, 6)));
            Assert.That(entry.AnimeInfo.EndDate, Is.EqualTo(UncertainDate.Unknown));
            Assert.That(entry.AnimeInfo.ImageUrl, Is.EqualTo("http://cdn.myanimelist.net/images/anime/9/24646.jpg"));
            Assert.That(entry.MyStartDate, Is.EqualTo(new UncertainDate(year: null, month: 2, day: null)));
            Assert.That(entry.MyFinishDate, Is.EqualTo(UncertainDate.Unknown));
            Assert.That(entry.MyLastUpdate, Is.EqualTo(new DateTime(year: 2011, month: 4, day: 2, hour: 22, minute: 50, second: 58, kind: DateTimeKind.Utc)));
            Assert.That(entry.Tags, Is.EqualTo(new List<string>() { "test&test", "< less than", "> greater than", "apos '", "quote \"", "hex ö", "dec !", "control character" }));
        }
Example #13
0
        static int Main(string[] args)
        {
            CommandLineArgs commandLine;

            try
            {
                commandLine = new CommandLineArgs(args);
                if (commandLine.ShowHelp)
                {
                    commandLine.DisplayHelp(Console.Out);
                    return((int)ExitCode.Success);
                }

                IConfigurationBuilder configBuilder = new ConfigurationBuilder()
                                                      .AddXmlFile(commandLine.ConfigFile);

                IConfigurationRoot rawConfig = configBuilder.Build();
                config = rawConfig.Get <Config>();

                if (config.LoggingConfigPath != null)
                {
                    Logging.SetUpLogging(config.LoggingConfigPath);
                }
                else
                {
                    Console.Error.WriteLine("No logging configuration file set. Logging to console.");
                    Logging.SetUpConsoleLogging();
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine("Fatal error: {0}", ex, ex.Message);
                return((int)ExitCode.Failure);
            }

            try
            {
                Logging.Log.Debug($"Command line args parsed. ConfigFile={commandLine.ConfigFile}");

                using (IMyAnimeListApi basicApi = new MyAnimeListApi()
                {
                    TimeoutInMs = config.MalTimeoutInMs, UserAgent = config.MalApiUserAgentString
                })
                    using (IMyAnimeListApi rateLimitingApi = new RateLimitingMyAnimeListApi(basicApi, TimeSpan.FromMilliseconds(config.DelayBetweenRequestsInMs)))
                        using (IMyAnimeListApi malApi = new RetryOnFailureMyAnimeListApi(rateLimitingApi, config.NumMalRequestFailuresBeforeGivingUp, config.DelayAfterMalRequestFailureInMs))
                            using (NpgsqlConnection conn = new NpgsqlConnection(config.ConnectionStrings.AnimeRecs))
                            {
                                conn.Open();
                                int usersAddedSoFar = 0;
                                while (usersAddedSoFar < config.UsersPerRun)
                                {
                                    RecentUsersResults recentMalUsers = malApi.GetRecentOnlineUsers();

                                    foreach (string user in recentMalUsers.RecentUsers)
                                    {
                                        using (var transaction = conn.BeginTransaction(System.Data.IsolationLevel.RepeatableRead))
                                        {
                                            if (!UserIsInDatabase(user, conn, transaction))
                                            {
                                                MalUserLookupResults userLookup = malApi.GetAnimeListForUser(user);
                                                if (UserMeetsCriteria(userLookup, conn, transaction))
                                                {
                                                    InsertUserAndRatingsInDatabase(userLookup, conn, transaction);
                                                    usersAddedSoFar++;
                                                    Logging.Log.Debug("Committing transaction.");
                                                    transaction.Commit();
                                                    Logging.Log.Debug("Transaction committed.");

                                                    if (usersAddedSoFar == config.UsersPerRun)
                                                    {
                                                        break;
                                                    }
                                                }
                                                else
                                                {
                                                    Logging.Log.InfoFormat("{0} does not meet criteria for inclusion, skipping", user);
                                                }
                                            }
                                            else
                                            {
                                                Logging.Log.InfoFormat("{0} is already in the database, skipping.", user);
                                            }
                                        }
                                    }
                                }

                                using (var transaction = conn.BeginTransaction(System.Data.IsolationLevel.RepeatableRead))
                                {
                                    TrimDatabaseToMaxUsers(config.MaxUsersInDatabase, conn, transaction);
                                    transaction.Commit();
                                }
                            }
            }
            catch (Exception ex)
            {
                Logging.Log.FatalFormat("Fatal error: {0}", ex, ex.Message);
                return((int)ExitCode.Failure);
            }

            return((int)ExitCode.Success);
        }
Example #14
0
        static void InsertUserAndRatingsInDatabase(MalUserLookupResults userLookup, NpgsqlConnection conn, NpgsqlTransaction transaction)
        {
            Logging.Log.InfoFormat("Inserting anime and list entries for {0} ({1} entries).", userLookup.CanonicalUserName, userLookup.AnimeList.Count);

            List <mal_anime> animesToUpsert = new List <mal_anime>();
            Dictionary <int, List <mal_anime_synonym> > synonymsToUpsert = new Dictionary <int, List <mal_anime_synonym> >();
            List <mal_list_entry>     entriesToInsert = new List <mal_list_entry>();
            List <mal_list_entry_tag> tagsToInsert    = new List <mal_list_entry_tag>();

            // Buffer animes, anime synonyms, list entries, and tags.
            // For animes not upserted this session, upsert animes all at once, clear synonyms, insert synonyms
            // insert user
            // insert list entries all at once
            // insert tags all at once

            foreach (MyAnimeListEntry anime in userLookup.AnimeList)
            {
                if (!AnimesUpserted.ContainsKey(anime.AnimeInfo.AnimeId))
                {
                    mal_anime animeRow = new mal_anime(
                        _mal_anime_id: anime.AnimeInfo.AnimeId,
                        _title: anime.AnimeInfo.Title,
                        _mal_anime_type_id: (int)anime.AnimeInfo.Type,
                        _num_episodes: anime.AnimeInfo.NumEpisodes,
                        _mal_anime_status_id: (int)anime.AnimeInfo.Status,
                        _start_year: (short?)anime.AnimeInfo.StartDate.Year,
                        _start_month: (short?)anime.AnimeInfo.StartDate.Month,
                        _start_day: (short?)anime.AnimeInfo.StartDate.Day,
                        _end_year: (short?)anime.AnimeInfo.EndDate.Year,
                        _end_month: (short?)anime.AnimeInfo.EndDate.Month,
                        _end_day: (short?)anime.AnimeInfo.EndDate.Day,
                        _image_url: anime.AnimeInfo.ImageUrl,
                        _last_updated: DateTime.UtcNow
                        );

                    animesToUpsert.Add(animeRow);

                    List <mal_anime_synonym> synonymRowsForThisAnime = new List <mal_anime_synonym>();
                    foreach (string synonym in anime.AnimeInfo.Synonyms)
                    {
                        mal_anime_synonym synonymRow = new mal_anime_synonym(
                            _mal_anime_id: anime.AnimeInfo.AnimeId,
                            _synonym: synonym
                            );
                        synonymRowsForThisAnime.Add(synonymRow);
                    }

                    synonymsToUpsert[anime.AnimeInfo.AnimeId] = synonymRowsForThisAnime;
                }

                mal_list_entry dbListEntry = new mal_list_entry(
                    _mal_user_id: userLookup.UserId,
                    _mal_anime_id: anime.AnimeInfo.AnimeId,
                    _rating: (short?)anime.Score,
                    _mal_list_entry_status_id: (short)anime.Status,
                    _num_episodes_watched: (short)anime.NumEpisodesWatched,
                    _started_watching_year: (short?)anime.MyStartDate.Year,
                    _started_watching_month: (short?)anime.MyStartDate.Month,
                    _started_watching_day: (short?)anime.MyStartDate.Day,
                    _finished_watching_year: (short?)anime.MyFinishDate.Year,
                    _finished_watching_month: (short?)anime.MyFinishDate.Month,
                    _finished_watching_day: (short?)anime.MyFinishDate.Day,
                    _last_mal_update: anime.MyLastUpdate
                    );

                entriesToInsert.Add(dbListEntry);

                foreach (string tag in anime.Tags)
                {
                    mal_list_entry_tag dbTag = new mal_list_entry_tag(
                        _mal_user_id: userLookup.UserId,
                        _mal_anime_id: anime.AnimeInfo.AnimeId,
                        _tag: tag
                        );
                    tagsToInsert.Add(dbTag);
                }
            }

            // For animes not upserted this session, upsert animes, clear synonyms all at once, insert synonyms all at once
            Logging.Log.DebugFormat("Upserting {0} animes.", animesToUpsert.Count);
            foreach (mal_anime animeToUpsert in animesToUpsert)
            {
                Logging.Log.TraceFormat("Checking if anime \"{0}\" is in the database.", animeToUpsert.title);
                bool animeIsInDb = mal_anime.IsInDatabase(animeToUpsert.mal_anime_id, conn, transaction);
                if (!animeIsInDb)
                {
                    // Not worth optimizing this by batching inserts because once there are a couple hundred users in the database,
                    // inserts will be relatively few in number.
                    Logging.Log.Trace("Not in database. Inserting it.");
                    animeToUpsert.Insert(conn, transaction);
                    Logging.Log.TraceFormat("Inserted anime \"{0}\" in database.", animeToUpsert.title);
                    AnimesUpserted[animeToUpsert.mal_anime_id] = animeToUpsert;
                }
                else
                {
                    Logging.Log.TraceFormat("Already in database. Updating it.");
                    animeToUpsert.Update(conn, transaction);
                    Logging.Log.TraceFormat("Updated anime \"{0}\".", animeToUpsert.title);
                    AnimesUpserted[animeToUpsert.mal_anime_id] = animeToUpsert;
                }
            }
            Logging.Log.DebugFormat("Upserted {0} animes.", animesToUpsert.Count);

            if (synonymsToUpsert.Count > 0)
            {
                List <mal_anime_synonym> flattenedSynonyms = synonymsToUpsert.Values.SelectMany(synonyms => synonyms).ToList();

                // clear synonyms for all these animes
                Logging.Log.DebugFormat("Clearing {0} synonyms for this batch.", flattenedSynonyms.Count);
                mal_anime_synonym.Delete(synonymsToUpsert.Keys, conn, transaction);
                Logging.Log.DebugFormat("Cleared {0} synonyms for this batch.", flattenedSynonyms.Count);

                // insert synonyms for all these animes
                Logging.Log.DebugFormat("Inserting {0} synonyms for this batch.", flattenedSynonyms.Count);
                mal_anime_synonym.Insert(flattenedSynonyms, conn, transaction);
                Logging.Log.DebugFormat("Inserted {0} synonyms for this batch.", flattenedSynonyms.Count);
            }
            else
            {
                Logging.Log.Debug("No synonyms in this batch.");
            }

            // Insert user
            mal_user user = new mal_user(
                _mal_user_id: userLookup.UserId,
                _mal_name: userLookup.CanonicalUserName,
                _time_added: DateTime.UtcNow
                );

            Logging.Log.DebugFormat("Inserting {0} into DB.", userLookup.CanonicalUserName);
            user.Insert(conn, transaction);
            Logging.Log.DebugFormat("Inserted {0} into DB.", userLookup.CanonicalUserName);

            // insert list entries all at once
            if (entriesToInsert.Count > 0)
            {
                Logging.Log.DebugFormat("Inserting {0} list entries for user \"{1}\".", entriesToInsert.Count, userLookup.CanonicalUserName);
                mal_list_entry.Insert(entriesToInsert, conn, transaction);
                Logging.Log.DebugFormat("Inserted {0} list entries for user \"{1}\".", entriesToInsert.Count, userLookup.CanonicalUserName);
            }

            // insert tags all at once
            if (tagsToInsert.Count > 0)
            {
                Logging.Log.DebugFormat("Inserting {0} tags by user \"{1}\".", tagsToInsert.Count, userLookup.CanonicalUserName);
                mal_list_entry_tag.Insert(tagsToInsert, conn, transaction);
                Logging.Log.DebugFormat("Inserted {0} tags by user \"{1}\".", tagsToInsert.Count, userLookup.CanonicalUserName);
            }

            Logging.Log.InfoFormat("Done inserting anime and list entries for {0}.", userLookup.CanonicalUserName);
        }