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;
 }
Exemple #2
0
        public bool GetListForUser(string user, out MalUserLookupResults animeList)
        {
            m_cacheLock.EnterReadLock();

            try
            {
                if (m_expiration == null)
                {
                    if (m_animeListCache.TryGetValue(user, out animeList))
                    {
                        return true;
                    }
                    else
                    {
                        animeList = null;
                        return false;
                    }
                }

                LinkedListNode<Tuple<string, DateTime>> userAndTimeInsertedNode;

                // Check if this user is in the cache and if the cache entry is not stale
                if (m_cachePutTimesByName.TryGetValue(user, out userAndTimeInsertedNode))
                {
                    DateTime expirationTime = userAndTimeInsertedNode.Value.Item2 + m_expiration.Value;
                    if (DateTime.UtcNow < expirationTime)
                    {
                        animeList = m_animeListCache[user];
                        return true;
                    }
                    else
                    {
                        animeList = null;
                        return false;
                    }
                }
                else
                {
                    animeList = null;
                    return false;
                }
            }
            finally
            {
                m_cacheLock.ExitReadLock();
            }
        }
Exemple #3
0
        public bool GetListForUser(string user, out MalUserLookupResults animeList)
        {
            m_cacheLock.EnterReadLock();

            try
            {
                if (m_expiration == null)
                {
                    if (m_animeListCache.TryGetValue(user, out animeList))
                    {
                        return(true);
                    }
                    else
                    {
                        animeList = null;
                        return(false);
                    }
                }

                // Check if this user is in the cache and if the cache entry is not stale
                if (m_cachePutTimesByName.TryGetValue(user, out LinkedListNode <Tuple <string, DateTime> > userAndTimeInsertedNode))
                {
                    DateTime expirationTime = userAndTimeInsertedNode.Value.Item2 + m_expiration.Value;
                    if (DateTime.UtcNow < expirationTime)
                    {
                        animeList = m_animeListCache[user];
                        return(true);
                    }
                    else
                    {
                        animeList = null;
                        return(false);
                    }
                }
                else
                {
                    animeList = null;
                    return(false);
                }
            }
            finally
            {
                m_cacheLock.ExitReadLock();
            }
        }
Exemple #4
0
        /// <summary>
        /// Gets a user's manga list. This method requires a MAL API key.
        /// </summary>
        /// <param name="user"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="MalApi.MalUserNotFoundException"></exception>
        /// <exception cref="MalApi.MalApiException"></exception>
        public async Task <MalUserLookupResults> GetMangaListForUserAsync(string user,
                                                                          CancellationToken cancellationToken)
        {
            const string malAppMangaInfoUriFormatString = "https://myanimelist.net/malappinfo.php?status=all&type=manga&u={0}";

            string userInfoUri = string.Format(malAppMangaInfoUriFormatString, Uri.EscapeDataString(user));

            Logging.Log.InfoFormat("Getting manga list for MAL user {0} using URI {1}", user, userInfoUri);

            Func <string, MalUserLookupResults> responseProcessingFunc = (xml) =>
            {
                using (TextReader xmlTextReader = new StringReader(xml))
                {
                    try
                    {
                        return(MalAppInfoXml.Parse(xmlTextReader));
                    }
                    catch (MalUserNotFoundException ex)
                    {
                        throw new MalUserNotFoundException(string.Format("No MAL list exists for {0}.", user), ex);
                    }
                }
            };

            try
            {
                HttpRequestMessage   request    = InitNewRequest(userInfoUri, HttpMethod.Get);
                MalUserLookupResults parsedList = await ProcessRequestAsync(request, responseProcessingFunc,
                                                                            cancellationToken : cancellationToken,
                                                                            baseErrorMessage : string.Format("Failed getting manga list for user {0} using url {1}", user,
                                                                                                             userInfoUri)).ConfigureAwait(continueOnCapturedContext: false);

                Logging.Log.InfoFormat("Successfully retrieved manga list for user {0}", user);
                return(parsedList);
            }
            catch (OperationCanceledException)
            {
                Logging.Log.InfoFormat("Canceled getting manga list for MAL user {0}", user);
                throw;
            }
        }
Exemple #5
0
        public void PutListForUser(string user, MalUserLookupResults animeList)
        {
            m_cacheLock.EnterWriteLock();

            try
            {
                if (m_expiration == null)
                {
                    m_animeListCache[user] = animeList;
                    return;
                }

                if (m_cachePutTimesByName.TryGetValue(user, out LinkedListNode <Tuple <string, DateTime> > nodeForLastInsert))
                {
                    m_cachePutTimesSortedByTime.Remove(nodeForLastInsert);
                }

                DateTime nowUtc          = DateTime.UtcNow;
                DateTime deleteOlderThan = nowUtc - m_expiration.Value;

                var newNode = m_cachePutTimesSortedByTime.AddFirst(new Tuple <string, DateTime>(user, nowUtc));

                m_cachePutTimesByName[user] = newNode;

                m_animeListCache[user] = animeList;

                // Check for old entries and remove them

                while (m_cachePutTimesSortedByTime.Count > 0 && m_cachePutTimesSortedByTime.Last.Value.Item2 < deleteOlderThan)
                {
                    string oldUser = m_cachePutTimesSortedByTime.Last.Value.Item1;
                    m_animeListCache.Remove(oldUser);
                    m_cachePutTimesByName.Remove(oldUser);
                    m_cachePutTimesSortedByTime.RemoveLast();
                }
            }
            finally
            {
                m_cacheLock.ExitWriteLock();
            }
        }
        /// <summary>
        /// Gets a user's anime list.
        /// </summary>
        /// <param name="user"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <MalUserLookupResults> GetAnimeListForUserAsync(string user, CancellationToken cancellationToken)
        {
            Logging.Log.InfoFormat("Checking cache for user {0}.", user);

            if (m_cache.GetListForUser(user, out MalUserLookupResults cachedAnimeList))
            {
                if (cachedAnimeList != null)
                {
                    Logging.Log.InfoFormat("Got anime list for {0} from cache.", user);
                    return(cachedAnimeList);
                }
                else
                {
                    // User does not have an anime list/no such user exists
                    Logging.Log.InfoFormat("Cache indicates that user {0} does not have an anime list.", user);
                    throw new MalUserNotFoundException(string.Format("No MAL list exists for {0}.", user));
                }
            }
            else
            {
                Logging.Log.InfoFormat("Cache did not contain anime list for {0}.", user);

                try
                {
                    MalUserLookupResults animeList = await m_underlyingApi.GetAnimeListForUserAsync(user, cancellationToken)
                                                     .ConfigureAwait(continueOnCapturedContext: false);

                    m_cache.PutListForUser(user, animeList);
                    return(animeList);
                }
                catch (MalUserNotFoundException)
                {
                    // Cache the fact that the user does not have an anime list
                    m_cache.PutListForUser(user, null);
                    throw;
                }
            }
        }
Exemple #7
0
        /// <summary>
        /// Parses XML obtained from malappinfo.php.
        /// </summary>
        /// <param name="doc"></param>
        /// <returns></returns>
        public static MalUserLookupResults Parse(XDocument doc)
        {
            Logging.Log.Trace("Parsing XML.");

            XElement error = doc.Root.Element("error");

            if (error != null && (string)error == "Invalid username")
            {
                throw new MalUserNotFoundException("No MAL list exists for this user.");
            }
            else if (error != null)
            {
                throw new MalApiException((string)error);
            }

            if (!doc.Root.HasElements)
            {
                throw new MalUserNotFoundException("No MAL list exists for this user.");
            }

            XElement myinfo            = GetExpectedElement(doc.Root, "myinfo");
            int      userId            = GetElementValueInt(myinfo, "user_id");
            string   canonicalUserName = GetElementValueString(myinfo, "user_name");

            List <MyAnimeListEntry> entries = new List <MyAnimeListEntry>();

            IEnumerable <XElement> animes = doc.Root.Elements("anime");

            foreach (XElement anime in animes)
            {
                int    animeId = GetElementValueInt(anime, "series_animedb_id");
                string title   = GetElementValueString(anime, "series_title");

                string   synonymList = GetElementValueString(anime, "series_synonyms");
                string[] rawSynonyms = synonymList.Split(SynonymSeparator, StringSplitOptions.RemoveEmptyEntries);

                // filter out synonyms that are the same as the main title
                HashSet <string> synonyms = new HashSet <string>(rawSynonyms.Where(synonym => !synonym.Equals(title, StringComparison.Ordinal)));

                int          seriesTypeInt = GetElementValueInt(anime, "series_type");
                MalAnimeType seriesType    = (MalAnimeType)seriesTypeInt;

                int numEpisodes = GetElementValueInt(anime, "series_episodes");

                int             seriesStatusInt = GetElementValueInt(anime, "series_status");
                MalSeriesStatus seriesStatus    = (MalSeriesStatus)seriesStatusInt;

                string        seriesStartString = GetElementValueString(anime, "series_start");
                UncertainDate seriesStart       = UncertainDate.FromMalDateString(seriesStartString);

                string        seriesEndString = GetElementValueString(anime, "series_end");
                UncertainDate seriesEnd       = UncertainDate.FromMalDateString(seriesEndString);

                string seriesImage = GetElementValueString(anime, "series_image");

                MalAnimeInfoFromUserLookup animeInfo = new MalAnimeInfoFromUserLookup(animeId: animeId, title: title,
                                                                                      type: seriesType, synonyms: synonyms, status: seriesStatus, numEpisodes: numEpisodes, startDate: seriesStart,
                                                                                      endDate: seriesEnd, imageUrl: seriesImage);


                int numEpisodesWatched = GetElementValueInt(anime, "my_watched_episodes");

                string        myStartDateString = GetElementValueString(anime, "my_start_date");
                UncertainDate myStartDate       = UncertainDate.FromMalDateString(myStartDateString);

                string        myFinishDateString = GetElementValueString(anime, "my_finish_date");
                UncertainDate myFinishDate       = UncertainDate.FromMalDateString(myFinishDateString);

                decimal rawScore = GetElementValueDecimal(anime, "my_score");
                decimal?myScore  = rawScore == 0 ? (decimal?)null : rawScore;

                int completionStatusInt           = GetElementValueInt(anime, "my_status");
                CompletionStatus completionStatus = (CompletionStatus)completionStatusInt;

                long     lastUpdatedUnixTimestamp = GetElementValueLong(anime, "my_last_updated");
                DateTime lastUpdated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromSeconds(lastUpdatedUnixTimestamp);

                string        rawTagsString = GetElementValueString(anime, "my_tags");
                string[]      untrimmedTags = rawTagsString.Split(TagSeparator, StringSplitOptions.RemoveEmptyEntries);
                List <string> tags          = new List <string>(untrimmedTags.Select(tag => tag.Trim()));

                MyAnimeListEntry entry = new MyAnimeListEntry(score: myScore, status: completionStatus, numEpisodesWatched: numEpisodesWatched,
                                                              myStartDate: myStartDate, myFinishDate: myFinishDate, myLastUpdate: lastUpdated, animeInfo: animeInfo, tags: tags);

                entries.Add(entry);
            }

            MalUserLookupResults results = new MalUserLookupResults(userId: userId, canonicalUserName: canonicalUserName, animeList: entries);

            Logging.Log.Trace("Parsed XML.");
            return(results);
        }
Exemple #8
0
        public void PutListForUser(string user, MalUserLookupResults animeList)
        {
            m_cacheLock.EnterWriteLock();

            try
            {
                if (m_expiration == null)
                {
                    m_animeListCache[user] = animeList;
                    return;
                }

                LinkedListNode<Tuple<string, DateTime>> nodeForLastInsert;
                if (m_cachePutTimesByName.TryGetValue(user, out nodeForLastInsert))
                {
                    m_cachePutTimesSortedByTime.Remove(nodeForLastInsert);
                }

                DateTime nowUtc = DateTime.UtcNow;
                DateTime deleteOlderThan = nowUtc - m_expiration.Value;

                var newNode = m_cachePutTimesSortedByTime.AddFirst(new Tuple<string, DateTime>(user, nowUtc));

                m_cachePutTimesByName[user] = newNode;

                m_animeListCache[user] = animeList;

                // Check for old entries and remove them

                while (m_cachePutTimesSortedByTime.Count > 0 && m_cachePutTimesSortedByTime.Last.Value.Item2 < deleteOlderThan)
                {
                    string oldUser = m_cachePutTimesSortedByTime.Last.Value.Item1;
                    m_animeListCache.Remove(oldUser);
                    m_cachePutTimesByName.Remove(oldUser);
                    m_cachePutTimesSortedByTime.RemoveLast();
                }
            }
            finally
            {
                m_cacheLock.ExitWriteLock();
            }
        }
Exemple #9
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);
        }
Exemple #10
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;
        }