private List <DTO.MalListEntry> CreateDtoAnimeList(IDictionary <int, RecEngine.MAL.MalListEntry> animeList)
        {
            List <DTO.MalListEntry> dtoAnimeList = new List <DTO.MalListEntry>();

            foreach (int animeId in animeList.Keys)
            {
                RecEngine.MAL.MalListEntry engineEntry = animeList[animeId];
                DTO.MalListEntry           dtoEntry    = new DTO.MalListEntry(animeId, engineEntry.Rating, engineEntry.Status, engineEntry.NumEpisodesWatched);
                dtoAnimeList.Add(dtoEntry);
            }

            return(dtoAnimeList);
        }
        public async Task <GetMalRecsResponse> GetMalRecsAsync(GetMalRecsRequest request, CancellationToken cancellationToken)
        {
            request.AssertArgumentNotNull("request");
            request.RecSourceName.AssertArgumentNotNull("request.RecSourceName");
            request.AnimeList.AssertArgumentNotNull("request.AnimeList");
            request.AnimeList.Entries.AssertArgumentNotNull("request.AnimeList.Entries");

            if (request.TargetScore == null && request.TargetFraction == null)
            {
                Error error = new Error(ErrorCodes.InvalidArgument, "Payload.TargetScore or Payload.TargetFraction must be set.");
                throw new RecServiceErrorException(error);
            }

            string targetScoreString;

            if (request.TargetFraction != null)
            {
                targetScoreString = request.TargetFraction.Value.ToString("P2");
            }
            else
            {
                targetScoreString = request.TargetScore.Value.ToString();
            }

            Logging.Log.InfoFormat("Request for {0} MAL recs using rec source {1}. User has {2} anime list entries. Target score is {3}.",
                                   request.NumRecsDesired, request.RecSourceName, request.AnimeList.Entries.Count, targetScoreString);

            // Acquire read lock on rec sources
            const int lockTimeoutInSeconds = 3;

            using (CancellationTokenSource lockTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(lockTimeoutInSeconds)))
                using (CancellationTokenSource lockCancel = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, lockTimeout.Token))
                {
                    bool gotLock = false;
                    try
                    {
                        using (var recSourcesLock = await m_recSourcesLockAsync.EnterReadLockAsync(lockCancel.Token).ConfigureAwait(false))
                        {
                            gotLock = true;
                            // Get rec source by name
                            if (!m_recSources.ContainsKey(request.RecSourceName))
                            {
                                Error error = new Error(errorCode: ErrorCodes.NoSuchRecSource,
                                                        message: string.Format("No rec source called \"{0}\" is loaded.", request.RecSourceName));
                                throw new RecServiceErrorException(error);
                            }
                            ITrainableJsonRecSource recSource = m_recSources[request.RecSourceName];

                            // Convert DTO anime list to RecEngine anime list

                            Dictionary <int, AnimeRecs.RecEngine.MAL.MalListEntry> entries = new Dictionary <int, RecEngine.MAL.MalListEntry>();
                            foreach (AnimeRecs.RecService.DTO.MalListEntry dtoEntry in request.AnimeList.Entries)
                            {
                                AnimeRecs.RecEngine.MAL.MalListEntry recEngineEntry = new RecEngine.MAL.MalListEntry(dtoEntry.Rating, dtoEntry.Status, dtoEntry.NumEpisodesWatched);
                                entries[dtoEntry.MalAnimeId] = recEngineEntry;
                            }
                            MalUserListEntries animeList = new MalUserListEntries(ratings: entries, animes: m_animes,
                                                                                  malUsername: null, prerequisites: m_prereqs);

                            Stopwatch          timer    = Stopwatch.StartNew();
                            GetMalRecsResponse response = recSource.GetRecommendations(animeList, request, cancellationToken);
                            timer.Stop();

                            Logging.Log.InfoFormat("Got recommendations from rec source {0}. Took {1}.", request.RecSourceName, timer.Elapsed);
                            return(response);
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        // If we couldn't get a read lock within 3 seconds, a reload/retrain is probably going on
                        if (!gotLock)
                        {
                            Error error = new Error(errorCode: ErrorCodes.Maintenance,
                                                    message: "The rec service is currently undergoing maintenance and cannot respond to rec requests.");
                            throw new RecServiceErrorException(error);
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
        }
        public GetMalRecsResponse GetMalRecs(GetMalRecsRequest request)
        {
            request.AssertArgumentNotNull("Payload");
            request.RecSourceName.AssertArgumentNotNull("Payload.RecSourceName");
            request.AnimeList.AssertArgumentNotNull("Payload.AnimeList");
            request.AnimeList.Entries.AssertArgumentNotNull("Payload.AnimeList.Entries");

            if (request.TargetScore == null && request.TargetFraction == null)
            {
                Error error = new Error(ErrorCodes.InvalidArgument, "Payload.TargetScore or Payload.TargetFraction must be set.");
                throw new RecServiceErrorException(error);
            }

            string targetScoreString;
            if (request.TargetFraction != null)
            {
                targetScoreString = request.TargetFraction.Value.ToString("P2");
            }
            else
            {
                targetScoreString = request.TargetScore.Value.ToString();
            }

            Logging.Log.InfoFormat("Request for {0} MAL recs using rec source {1}. User has {2} anime list entries. Target score is {3}.",
                request.NumRecsDesired, request.RecSourceName, request.AnimeList.Entries.Count, targetScoreString);

            // Acquire read lock on rec sources
            bool enteredLock;
            const int lockTimeoutInMs = 3000;
            using (var trainingDataReadLock = m_trainingDataLock.ScopedReadLock(lockTimeoutInMs, out enteredLock))
            {
                // If we couldn't get a read lock within 3 seconds, a reload/retrain is probably going on
                if (!enteredLock)
                {
                    Error error = new Error(errorCode: ErrorCodes.Maintenance,
                        message: "The rec service is currently undergoing maintenance and cannot respond to rec requests.");
                    throw new RecServiceErrorException(error);
                }

                using (var recSourcesReadLock = m_recSourcesLock.ScopedReadLock())
                {
                    // Get rec source by name
                    if (!m_recSources.ContainsKey(request.RecSourceName))
                    {
                        Error error = new Error(errorCode: ErrorCodes.NoSuchRecSource,
                            message: string.Format("No rec source called \"{0}\" is loaded.", request.RecSourceName));
                        throw new RecServiceErrorException(error);
                    }
                    ITrainableJsonRecSource recSource = m_recSources[request.RecSourceName];

                    // Convert DTO anime list to RecEngine anime list

                    Dictionary<int, AnimeRecs.RecEngine.MAL.MalListEntry> entries = new Dictionary<int, RecEngine.MAL.MalListEntry>();
                    foreach (AnimeRecs.RecService.DTO.MalListEntry dtoEntry in request.AnimeList.Entries)
                    {
                        AnimeRecs.RecEngine.MAL.MalListEntry recEngineEntry = new RecEngine.MAL.MalListEntry(dtoEntry.Rating, dtoEntry.Status, dtoEntry.NumEpisodesWatched);
                        entries[dtoEntry.MalAnimeId] = recEngineEntry;
                    }
                    MalUserListEntries animeList = new MalUserListEntries(ratings: entries, animes: m_animes,
                        malUsername: null, prerequisites: m_prereqs);

                    Stopwatch timer = Stopwatch.StartNew();
                    GetMalRecsResponse response = recSource.GetRecommendations(animeList, request);
                    timer.Stop();

                    Logging.Log.InfoFormat("Got recommendations from rec source {0}. Took {1}.", request.RecSourceName, timer.Elapsed);
                    return response;
                }
            }
        }