PageRenderTime 75ms CodeModel.GetById 3ms app.highlight 63ms RepoModel.GetById 1ms app.codeStats 1ms

/Chavah/Controllers/SongsController.cs

http://chavah.codeplex.com
C# | 882 lines | 773 code | 87 blank | 22 comment | 85 complexity | eff1dedf3db34bd9f712e5059cdd7e18 MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.Linq;
  4using System.Runtime.Serialization;
  5using System.ServiceModel;
  6using System.ServiceModel.Web;
  7using System.Text;
  8using System.IO;
  9using System.Timers;
 10using System.Collections.Concurrent;
 11using System.Reactive.Linq;
 12using System.Threading.Tasks;
 13using System.ServiceModel.Syndication;
 14using Chavah.Common;
 15using System.Web.Mvc;
 16using Chavah.Data;
 17using Chavah.Services;
 18using System.Web;
 19using System.Globalization;
 20using Raven.Client;
 21using Raven.Client.Linq;
 22using Chavah.Models;
 23using SongLike = Chavah.Models.Like;
 24
 25namespace Chavah.Controllers
 26{
 27    public class SongsController : Controller
 28    {
 29        private static readonly Random random = new Random();
 30        private static readonly Lazy<List<SongInfo>> cachedSongs = new Lazy<List<SongInfo>>(() => GetCachedSongsWithLogging(), isThreadSafe: true);
 31        private static readonly ConcurrentDictionary<string, DateTime> visits = new ConcurrentDictionary<string, DateTime>();
 32        private readonly IDocumentStore raven = RavenStore.Db;
 33
 34        public SongsController()
 35        {
 36
 37        }
 38
 39        static SongsController()
 40        {
 41            Task.Factory
 42                .StartNew(UpdateSongPurchaseInfo)
 43                .ContinueWith(_ => ClearOutOldLogs());
 44        }
 45
 46        public JsonResult GetPeopleOnline(int minutes)
 47        {
 48            return Json(new
 49            {
 50                TotalSinceStart = visits.Count,
 51                PeopleOnline = "In the last " + minutes.ToString() + " minutes, " + visits.Values.Count(v => v >= DateTime.Now.Subtract(TimeSpan.FromMinutes(minutes))) + " people have used Chavah."
 52            }, JsonRequestBehavior.AllowGet);
 53        }
 54
 55        public ActionResult ActivityFeed()
 56        {
 57            using (var session = raven.OpenSession())
 58            {
 59                var recentActivities = session
 60                    .Query<Activity>()
 61                    .OrderByDescending(a => a.DateTime)
 62                    .Take(30);
 63                var feedItems = from activity in recentActivities.ToArray()
 64                                select new SyndicationItem(
 65                                    title: activity.Title,
 66                                    content: activity.Description,
 67                                    itemAlternateLink: activity.MoreInfoUri);
 68
 69                var feed = new SyndicationFeed("Chavah Messianic Radio", "The latest activity over at Chavah Messianic Radio", new Uri("http://messianicradio.com"), feedItems) { Language = "en-US" };
 70                return new RssActionResult { Feed = feed };
 71            }
 72        }
 73
 74        private static string GetArtistTwitterHandle(string artist)
 75        {
 76            return Match.Value(artist)
 77                .With("Asharyahuw", "@Asharyahuw")
 78                .With("Aviad Cohen", "@aviadcohen")
 79                .With("Barry & Batya Segal", "@VisionForIsrael") 
 80                .With("Carlos Perdomo", "@7ElCantante7")
 81                .With("Deborah Kline-Iantorno", "@deborahkline")
 82                .With("Devora Clark", "@devoraclark")
 83                .With("Downpour", "@downpourband1")
 84                .With("Frederique Vervoitte", "@fredouvervoitte")
 85                .With("Ephraim Ben Yoseph", "@EphraimbYoseph")
 86                .With("Giselle", "@GiselleTka")
 87                .With("Greg Silverman", "@GSilverPraise")
 88                .With("Hillel Ben Yochanan", "@Hillel_Yochanan")
 89                .With("Jonathan Lane", "@doyouknowyeshua")
 90                .With("Jonathan Settel", "@JSettel") 
 91                .With("Joshua Rosen", "@joshuajrosen")
 92                .With("Lev Shelo", "@corrybell")
 93                .With("Liberated Wailing Wall", "@jfjlww")
 94                .With("Lynne McDowell", "@Lynne_McDowell")
 95                .With("Maurice Sklar", "@mauricesklar")
 96                .With("Maurice Sklar & Hugh Sung", "@mauricesklar")
 97                .With("Magen David", "@MagenDavidMG")
 98                .With("Micha'el Eliyahu BenDavid", "@MalachHaBrit")
 99                .With("Philip Stanley Klein", "@minorkey1")
100                .With("Ross", "@ROSS_AandS")
101                .With("Sharon Wilbur", "@sharonwilbur")
102                .With("The Hebraism Music Project", "@Hebraism")
103                .With("The Lumbrosos", "@TheLumbrosos")
104                .With("Will Spires", "@wpspires")
105                .With("Yerubilee", "@yerubilee")
106                .With("Zemer Levav", "@zemerlevav")
107                .DefaultTo(null);
108        }
109
110        public ActionResult GetAllSongs()
111        {
112            var result = cachedSongs
113                .Value
114                .OrderBy(s => s.Artist)
115                .ThenBy(s => s.Album)
116                .ThenBy(s => s.Number)
117                .Select(s => s.ToDto(SongLikeStatus.None));
118
119            return Json(result, JsonRequestBehavior.AllowGet);
120        }
121        
122        public ActionResult GetAlbumArt(long songId)
123        {
124            var cachedResult = GetCachedContentResultOrNull(TimeSpan.FromDays(30));
125            if (cachedResult != null)
126            {
127                return cachedResult;
128            }
129            else
130            {
131                var song = cachedSongs.Value.FirstOrDefault(s => s.Id == songId);
132                if (song == null)
133                {
134                    throw new HttpException(404, "Couldn't find the song with ID " + songId.ToString());
135                }
136
137                var albumArtFilePath = song.GetAlbumArtFilePath();
138                var contentType = Match.Value(albumArtFilePath).With(".png", "image/png").DefaultTo("image/jpeg");
139                return File(albumArtFilePath, contentType);
140            }
141        }
142
143        private ContentResult GetCachedContentResultOrNull(TimeSpan cacheTime)
144        {
145            var modifiedSinceHeader = Request.Headers["If-Modified-Since"];
146            if (modifiedSinceHeader.Exists())
147            {
148                var lastMod = DateTime.ParseExact(modifiedSinceHeader, "r", CultureInfo.InvariantCulture);
149                var expireTime = lastMod.Add(cacheTime);
150                if (expireTime > DateTime.Now)
151                {
152                    Response.StatusCode = 304;
153                    Response.StatusDescription = "Not Modified";
154                    return Content(String.Empty);
155                }
156            }
157
158            Response.Cache.SetCacheability(HttpCacheability.Public);
159            Response.Cache.SetLastModified(DateTime.Now);
160            return null;
161        }
162
163        public ActionResult GetTotalPlays()
164        {
165            using (var session = raven.OpenSession())
166            {
167                //var result = session.Query<Models.User>().Sum(u => u.TotalPlays);
168                return Json(0, JsonRequestBehavior.AllowGet);
169            }
170        }
171
172        public ActionResult GetTrendingSongs(int count)
173        {
174            using (var session = raven.OpenSession())
175            {
176                var trendingSongs = session
177                    .Query<SongLike>()
178                    .Where(l => l.LikeStatus == true)
179                    .OrderByDescending(l => l.Date)
180                    .Take(count * 2) // Count * 2, so that we can .Distinct on the in-memory stuff and still get back the requested number of elements.
181                    .Select(l => l.SongId)
182                    .AsEnumerable()
183                    .Distinct()
184                    .Take(count)
185                    .Select(id => cachedSongs.Value.First(s => s.Id == id).ToDto(SongLikeStatus.None));
186
187                return Json(trendingSongs.ToArray(), JsonRequestBehavior.AllowGet);
188            }
189        }
190
191        [NoCaching]
192        public ActionResult GetTopSongs(int count)
193        {
194            var topSongCount = 25;
195            var maxPlace = topSongCount - Math.Min(count, topSongCount);
196            var randomPlace = random.Next(0, maxPlace);
197
198            var result = cachedSongs.Value
199                .OrderByDescending(s => s.CommunityRank)
200                .Skip(randomPlace)
201                .Take(count)
202                .Select(s => s.ToDto(SongLikeStatus.None));
203
204            return Json(result, JsonRequestBehavior.AllowGet);
205        }
206
207        public ActionResult GetSongMatches(string searchText)
208        {
209            var stringMatches = new Func<string, string, bool>((s1, s2) => string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase));
210            var isHeavenlySeventy = new Func<string, bool>(s => stringMatches(s, "heavenly seventy") || stringMatches(s, "heavenly 70"));
211            var isBowelyBottom = new Func<string, bool>(s => stringMatches(s, "bowely bottom"));
212            var isLucasLovelyList = new Func<string, bool>(s => stringMatches(s, "lucas lovely list") || stringMatches(s, "lucas' lovely list"));
213            var songMatches = Match.Value(searchText)
214                .With(isHeavenlySeventy, _ => GetHeavenlySeventy())
215                .With(isBowelyBottom, _ => GetBowelyBottom())
216                .With(isLucasLovelyList, _ => GetLucasLovelyList())
217                .With(_ => true, s => GetSongMatchingText(s));            
218
219            return Json(songMatches.Evaluate(), JsonRequestBehavior.AllowGet);
220        }
221
222        private IEnumerable<Song> GetLucasLovelyList()
223        {
224            var lucasLovelyListIds = new long[] { 1179, 359, 573, 1176, 2283, 518, 577, 350, 572, 2513, 667, 2068, 536, 1181, 382, 648, 2279, 324, 2209, 1826, 1913, 2053, 649, 568, 505, 357, 2169, 495, 1824, 335, 483, 97, 723, 1185, 2300, 1282, 1079, 149, 273, 1863, 401, 363, 1969, 241, 162, 513, 1159, 410, 330, 570, 168, 524, 1515, 1945, 633, 2350, 774, 82, 1972, 721, 134, 1283, 1350, 1970, 618, 343, 2341, 444, 2170, 728, 76, 527, 1178, 535, 74, 445, 2302, 553, 1, 277, 1952, 364, 1765, 1976, 346, 1946, 2204, 249, 236, 621, 2100, 1977, 422, 154, 411, 458, 459, 2510 };
225            return lucasLovelyListIds
226                .Join(cachedSongs.Value, l => l, s => s.Id, (i, s) => s)
227                .WhereNotNull()
228                .Select(s => s.ToDto(SongLikeStatus.None));
229        }
230
231        public IEnumerable<Song> GetSongMatchingText(string searchText)
232        {
233            if (searchText.Length <= 2)
234            {
235                return Enumerable.Empty<Song>();
236            }
237
238            var allSongs = cachedSongs.Value;
239            var matchingSongNames = allSongs.Where(s => s.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase));
240            var matchingArtists = allSongs.Where(s => s.Artist.Contains(searchText, StringComparison.OrdinalIgnoreCase));
241            var matchingAlbums = allSongs.Where(s => s.Album.Contains(searchText, StringComparison.OrdinalIgnoreCase));
242            return matchingSongNames
243                .Concat(matchingArtists)
244                .Concat(matchingAlbums)
245                .Take(25)
246                .Select(s => s.ToDto(SongLikeStatus.None));
247        }
248
249        private IEnumerable<Song> GetBowelyBottom()
250        {
251            return cachedSongs.Value
252                .OrderBy(s => s.CommunityRank)
253                .Take(70)
254                .AsEnumerable()
255                .Select(s => s.ToDto(SongLikeStatus.None));
256        }
257
258        public IEnumerable<Song> GetHeavenlySeventy()
259        {
260            return cachedSongs.Value
261                .OrderByDescending(s => s.CommunityRank)
262                .Take(70)
263                .AsEnumerable()
264                .Select(s => s.ToDto(SongLikeStatus.None));
265        }
266
267        public ActionResult GetRandomLikedSongs(int count)
268        {
269            var likedSongs = from like in Dependency.Get<LikesCache>()
270                                 .ForClient(GetUserIdFromRequest())
271                                 .Where(l => l.LikeStatus == true)
272                                 .ToList()
273                                 .RandomOrder()
274                             let song = cachedSongs.Value.FirstOrDefault(s => s.Id == like.SongId)
275                             where song != null
276                             select song.ToDto(SongLikeStatus.Like);
277
278            return Json(likedSongs.Take(count), JsonRequestBehavior.AllowGet);
279        }
280
281        [NoCaching]
282        public ActionResult GetSongForClient()
283        {
284            try
285            {
286                var userId = GetUserIdFromRequest();
287                OnSongPlayedForClient(userId);
288                var song = GetSongForClientWithLikeWeights(userId);
289
290                return Json(song, JsonRequestBehavior.AllowGet);
291            }
292            catch (Exception error)
293            {
294                ChavahEntities.LogInNewContext(error.ToString());
295                return Json(Song.GetErrorSong(error.ToString()), JsonRequestBehavior.AllowGet);
296            }
297        }
298        
299        public ActionResult GetSongById(long songId) 
300        {
301            var userId = GetUserIdFromRequest();
302            OnSongPlayedForClient(userId);
303            using (var entities = new ChavahEntities())
304            {
305                var song = entities.SongInfoes.FirstOrDefault(s => s.Id == songId);
306                if (song != null)
307                {
308                    var like = Dependency.Get<LikesCache>()
309                        .ForClient(userId)
310                        .FirstOrDefault(l => l.SongId == songId);
311                    var result = song.ToDto(like.ToSongLikeEnum());
312
313                    return Json(result, JsonRequestBehavior.AllowGet);
314                }
315
316                // This should never happen: a client requets a song ID that doesn't exist.
317                var errorMessage = "Unable to find song with ID = " + songId.ToString();
318                entities.Log(errorMessage);
319                throw new Exception(errorMessage);
320            }
321
322        }
323
324        public ActionResult GetRequestedSongId()
325        {
326            using (var session = raven.OpenSession())
327            {
328                var userId = GetUserIdFromRequest();
329                var user = session.Load<User>(userId);
330                if (user != null)
331                {
332                    var userDislikes = Dependency.Get<LikesCache>().ForClient(userId).Where(l => l.LikeStatus == false);
333                    var halfHourAgo = DateTime.Now.Subtract(TimeSpan.FromMinutes(30));
334                    var songRequest = session
335                        .Query<SongRequest>()
336                        .OrderByDescending(s => s.DateTime)
337                        .Where(s => s.DateTime >= halfHourAgo && s.UserWhoMadeRequestId != user.Id)
338                        .Take(30)
339                        .ToArray()
340                        .Where(s => !s.PlayedForClientIds.Contains(user.Id, StringComparer.OrdinalIgnoreCase))
341                        .Where(s => userDislikes.All(d => d.SongId != s.SongId))
342                        .FirstOrDefault();
343                    if (songRequest != null)
344                    {
345                        songRequest.PlayedForClientIds.Add(user.Id);
346                        session.SaveChanges();
347
348                        return Json(songRequest.SongId, JsonRequestBehavior.AllowGet);
349                    }
350                }
351                return Json(null, JsonRequestBehavior.AllowGet);
352            }
353        }
354
355        public ActionResult GetIndividualSongRanks(int songId)
356        {
357            using (var session = raven.OpenSession())
358            {
359                var upVoteCount = session.Query<SongLike>().Count(l => l.SongId == songId && l.LikeStatus == true);
360                var downVoteCount = session.Query<SongLike>().Count(l => l.SongId == songId && l.LikeStatus == false);
361                var result = new
362                {
363                    UpVotes = upVoteCount,
364                    DownVotes = downVoteCount,
365                    SongId = songId
366                };
367
368                return Json(result, JsonRequestBehavior.AllowGet);
369            }
370        }
371        
372        public ActionResult RequestSong(long songId)
373        {
374            using (var session = raven.OpenSession())
375            {
376                var user = session.Load<User>(this.GetUserIdFromRequest());
377                var song = cachedSongs.Value.FirstOrDefault(s => s.Id == songId);
378                if (song != null && user != null)
379                {
380                    var requestExpiration = DateTime.UtcNow.AddDays(14);
381                    if (!HasRecentPendingSongRequest(songId, session) && !HasManyPendingSongRequestForArtist(song.Artist, session))
382                    {
383                        var songRequest = new SongRequest
384                        {
385                            DateTime = DateTime.Now,
386                            PlayedForClientIds = new List<string> { user.Id },
387                            SongId = songId,
388                            Artist = song.Artist,
389                            UserWhoMadeRequestId = user.Id
390                        };
391                        session.Store(songRequest);
392                        session.AddRavenExpiration(songRequest, requestExpiration);
393                    }
394
395                    var songArtist = Match.Value(GetArtistTwitterHandle(song.Artist))
396                        .IfNotNull(h => h)
397                        .DefaultTo(song.Artist)
398                        .Evaluate();
399                    var activity = new Activity
400                    {
401                        DateTime = DateTime.Now,
402                        Title = string.Format("{0} - {1} was requested by one of our listeners", song.Artist, song.Name),
403                        Description = string.Format("\"{0}\" by {1} was requested by one of our listeners on Chavah Messianic Radio.", song.Name, songArtist),
404                        MoreInfoUri = song.GetAbsoluteSongUri()
405                    };
406                    session.Store(activity);
407                    session.AddRavenExpiration(activity, requestExpiration);
408
409                    session.SaveChanges();
410                }
411            }
412
413            return GetSongById(songId);
414        }
415
416        private bool HasRecentPendingSongRequest(long songId, IDocumentSession session)
417        {
418            var recent = DateTime.Now.Subtract(TimeSpan.FromMinutes(30));
419            return session
420                .Query<SongRequest>()
421                .Any(s => s.SongId == songId && s.DateTime >= recent);                
422        }
423
424        private bool HasManyPendingSongRequestForArtist(string artist, IDocumentSession session)
425        {
426            var recent = DateTime.Now.Subtract(TimeSpan.FromMinutes(60));
427            var many = 2;
428            return session
429                .Query<SongRequest>()
430                .Count(s => s.Artist == artist && s.DateTime >= recent) >= many;
431        }
432
433        private void OnSongPlayedForClient(string userId)
434        {
435            Task.Factory.StartNew(() => RecordSongPlayedForUser(userId));
436        }
437
438        private Song GetSongForClientWithLikeWeights(string userId)
439        {
440            // Song weights algorithm described here:
441            // http://stackoverflow.com/questions/3345788/algorithm-for-picking-thumbed-up-items/3345838#3345838
442
443            var allSongs = cachedSongs.Value;
444            var likeDislikeSongs = Dependency.Get<LikesCache>().ForClient(userId);
445            var songsWithWeight =
446                (
447                    from song in allSongs
448                    let likeStatus = GetLikeStatusForSong(song, likeDislikeSongs)
449                    select new
450                    {
451                        Weight = GetSongWeight(song, likeStatus),
452                        Like = likeStatus,
453                        Info = song
454                    }
455                ).ToArray();
456            var totalWeights = songsWithWeight.Sum(s => s.Weight);
457            var randomWeight = RandomDoubleWithMaxValue(totalWeights);
458            var runningWeight = 0.0;
459            foreach (var song in songsWithWeight)
460            {
461                var newWeight = runningWeight + song.Weight;
462                if (randomWeight >= runningWeight && randomWeight <= newWeight)
463                {
464                    return song.Info.ToDto(song.Like);
465                }
466                runningWeight = newWeight;
467            }
468
469            var errorMessage = "Unable to find random song. This should never happen. Random weight chosen was " + randomWeight.ToString() + ", max weight was " + totalWeights.ToString();
470            ChavahEntities.LogInNewContext(errorMessage);
471            throw new Exception(errorMessage);
472        }
473
474        private string GetUserIdFromRequest()
475        {
476            var userId = Match.Value(Request.Cookies["userIdValue"])
477                .IfNotNull(c => c.Value)
478                .Evaluate();
479            if (string.IsNullOrEmpty(userId))
480            {
481                throw new InvalidOperationException("Doesn't have a user Id");
482            }
483            return userId;
484        }
485
486        private static double GetSongWeight(SongInfo song, SongLikeStatus likeStatus)
487        {
488            var likeWeightMultiplier = GetSongLikeWeightMultiplier(likeStatus);
489            var shabbatWeight = GetShabbatWeight(song, likeStatus);
490            var popularityWeight = GetPopularityWeight(song);
491
492            var proposedFinalWeight = (shabbatWeight + popularityWeight) * likeWeightMultiplier;
493            return proposedFinalWeight.MinMax(.001, 10);
494        }
495
496        private static double GetSongLikeWeightMultiplier(SongLikeStatus likeStatus)
497        {
498            const double normalMultiplier = 1;
499            const double likeMultiplier = 1.5;
500            const double dislikeMultiplier = 0.01;
501
502            return Match.Value(likeStatus)
503                .With(SongLikeStatus.Like, likeMultiplier)
504                .With(SongLikeStatus.Dislike, dislikeMultiplier)
505                .DefaultTo(normalMultiplier);
506        }
507
508        private static double GetPopularityWeight(SongInfo song)
509        {
510            const double veryUnpopularWeight = 0.01;
511            const double unpopularWeight = 0.07;
512            const double normalWeight = 1;
513            const double likedWeight = 1.45;
514            const double popularWeight = 1.65;
515            const double veryPopularWeight = 2.05;
516            const double extremelyPopularWeight = 2.2;
517
518            return Match.Value(song.CommunityRank)
519                .With(v => v < -5, veryUnpopularWeight)
520                .With((-4).Through(-1), unpopularWeight)
521                .With(0.Through(9), normalWeight)
522                .With(10.Through(29), likedWeight)
523                .With(30.Through(49), popularWeight)
524                .With(50.Through(100), veryPopularWeight)
525                .With(v => v >= 101, extremelyPopularWeight)
526                .DefaultTo(normalWeight);
527        }
528
529        private static double GetShabbatWeight(SongInfo song, SongLikeStatus likeStatus)
530        {
531            const int shabbatBoost = 7;
532            return Match.Value(song)
533                .With(_ => likeStatus == SongLikeStatus.Dislike || song.CommunityRank < -3, 0)
534                .With(s => DateTime.Now.IsShabbat() && s.IsShabbatSong, shabbatBoost);
535        }
536
537        private static double RandomDoubleWithMaxValue(double maxValueInclusive)
538        {
539            var randomValue = random.NextDouble();
540            var desiredValue = randomValue * maxValueInclusive;
541            var desiredValueTrimmed = Math.Min(maxValueInclusive, desiredValue);
542            return desiredValueTrimmed;
543        }
544
545        private SongLikeStatus GetLikeStatusForSong(SongInfo song, SongLike[] userSongPreferences)
546        {
547            var likeDislikeForThisSong = userSongPreferences.FirstOrDefault(l => l.SongId == song.Id);
548            return likeDislikeForThisSong.ToSongLikeEnum();
549        }
550
551        private void RecordSongPlayedForUser(string userId)
552        {
553            visits.AddOrUpdate(userId, DateTime.Now, (_, __) => DateTime.Now);
554            using (var session = raven.OpenSession())
555            {
556                var user = session.Load<User>(userId);
557                if (user != null)
558                {
559                    user.TotalPlays += 1;
560                    user.LastVisit = DateTime.Now.Date;
561                }
562                session.SaveChanges();
563            }       
564        }
565
566        [HttpPost]
567        public ActionResult LikeById(long songId)
568        {
569            UpdateLikeStatus(this.GetUserIdFromRequest(), songId, SongLikeStatus.Like);
570            StoreLikeActivity(songId);
571            return Json(true);
572        }
573
574        [HttpPost]
575        public ActionResult DislikeById(long songId)
576        {
577            UpdateLikeStatus(this.GetUserIdFromRequest(), songId, SongLikeStatus.Dislike);
578            return Json(true);
579        }
580
581        public ActionResult GetSongByAlbum(string album, string artist)
582        {
583            var albumSongs = cachedSongs.Value.Where(s => string.Equals(s.Album, album, StringComparison.OrdinalIgnoreCase));
584            var song = albumSongs.RandomOrder().FirstOrDefault();
585            if (song != null)
586            {
587                return GetSongById(song.Id);
588            }
589            else
590            {
591                ChavahEntities.LogInNewContext("Unable to find an album matching name " + album);
592                return GetSongForClient();
593            }
594        }
595
596        public ActionResult GetSongByArtist(string artist)
597        {
598            var artistSongs = cachedSongs.Value.Where(s => string.Equals(s.Artist, artist, StringComparison.OrdinalIgnoreCase));
599            var song = artistSongs.RandomOrder().FirstOrDefault();
600            if (song != null)
601            {
602                return GetSongById(song.Id);
603            }
604            else
605            {
606                ChavahEntities.LogInNewContext("Unable to find a song name starting with " + artist);
607                return GetSongForClient();
608            }
609        }
610
611        private static List<SongInfo> GetCachedSongsWithLogging()
612        {
613            try
614            {
615                return GetCachedSongs();
616            }
617            catch (Exception error)
618            {
619                using (var entities = new ChavahEntities())
620                {
621                    entities.Logs.AddObject(new Chavah.Data.Log() { Message = error.ToString(), TimeStamp = DateTime.Now });
622                    entities.SaveChanges();
623                }
624                throw; 
625            }
626        }
627
628        static void UpdateSongPurchaseInfo()
629        {
630            using (var entities = new ChavahEntities())
631            {
632                var knownPurchaseLinks = new Dictionary<string, string>()
633                { 
634                    { "Aviad Cohen", "http://aviadcohen.com/shop.cfm" },
635                    { "Alicia Smith", "http://www.cduniverse.com/productinfo.asp?pid=7345816" },
636                    { "Alyssa Kennedy", "http://www.youtube.com/watch?v=lSSXeuXvYWU" },
637                    { "Avner & Rachel Boskey", "http://www.davidstent.org" },
638                    { "Baruch HaShem Worship Team", "http://baruchhashemsynagogue.org/" },
639                    { "Barry & Batya Segal", "http://www.visionforisrael.com" },
640                    { "Bruce & Lynne Patterson", "http://www.ldpatterson.com" },
641                    { "Bruce Cohen", "http://www.bethelnyc.org/meet_the_rabbi/r'bruce_products.htm" },
642                    { "Carlos Perdomo", "http://www.carlosperdomoministries.com/" },
643                    { "Carolyn Hyde", "http://www.heartofg-d.org" },
644                    { "Christene Jackman", "http://christenejackman.com" },
645                    { "Christopher Mann", "http://www.kadoshmann.com" },
646                    { "Deborah Kline-Iantorno", "http://deborahkline-iantorno.com" },
647                    { "Devora Clark", "http://devoraclark.bandcamp.com/" },
648                    { "Deanne Glenn", "http://www.deannemusic.com/" },
649                    { "Deanne Shallenberger", "http://www.deannemusic.com/" },
650                    { "Downpour", "http://www.thedownpourband.com/" },
651                    { "Elisheva Shomron", "http://www.last.fm/music/Elisheva+Shomron" },
652                    { "Ephraim Ben Yoseph", "http://www.reverbnation.com/ephraimbenyoseph" },
653                    { "Giselle", "http://www.cdbaby.com/cd/giselle33" },
654                    { "Greg Silverman", "http://www.gregsilverman.com/" },
655                    { "Hananyah Naftali", "https://itunes.apple.com/il/album/your-presence/id680795419?i=680795748" },
656                    { "Helen Shapiro", "http://www.mannamusic.co.uk/materialspage/materialspg.htm" },
657                    { "Hillel Ben Yochanan", "http://www.facebook.com/hillel70#!/pages/Hillel/214029558644294?sk=app_178091127385" },
658                    { "Israel's Hope", "http://www.fvgifts.com/music.html" },
659                    { "Joel Chernoff", "http://www.lambmessianicmusic.com/lamb_05_joelchernoff_mn.html" },
660                    { "Jonathan Kegans", "http://www.jonathankegans.com/index.html" },
661                    { "Jonathan Settel", "http://www.settel.org" },
662                    { "Joshua Aaron", "http://worshipinisrael.com/" },
663                    { "Joshua Rosen", "http://joshuarosen.bandcamp.com" },
664                    { "Justin Black", "http://www.facebook.com/profile.php?id=100000597792846#!/profile.php?id=100002453000204" },
665                    { "Karen Davis", "http://www.messianicweb.com/Music/GOTN/Davis/" },
666                    { "Kathy Shooster", "http://kathyshoostermusic.com/?page_id=10" }, 
667                    { "Kehilat Ha Ma'ayan Congregation", "http://kehilat-hamaayan.org.il/" },
668                    { "Lamb", "http://www.lambmessianicmusic.com/lamb_04_lamb_mn.html" },
669                    { "Lee Rothman", "http://www.purevolume.com/hisway" },
670                    { "Lenny & Varda Harris", "http://www.lennyandvarda.com/" },
671                    { "Leslie Ann", "http://www.songsofleslieann.com/" },
672                    { "Lev Shelo", "http://levshelo.com/store.cfm" },
673                    { "Lynne McDowell", "http://lynnemcdowell.com/" },
674                    { "Magen David", "http://www.cdbaby.com/cd/magendavid" },
675                    { "Martin Sarvis", "http://www.cdbaby.com/cd/martinsarvis" },
676                    { "Marty Goetz", "http://www.martygoetz.com/products/products.php" },
677                    { "Micha'el Eliyahu BenDavid", "http://emetzionmusic.com/index.php?option=com_maianmedia&view=music&Itemid=84" },
678                    { "Michael Nissim", "http://www.fvgifts.com/music.html" },
679                    { "Misha Goetz", "http://mishagoetz.com/" },
680                    { "Mishkanim", "http://mishkanim.com/" },
681                    { "Meha Shamayim", "http://www.galileeofthenations.com/" },
682                    { "Mijael Hayom", "http://www.librerialosolivos.com/index.php?cPath=615_70_267" },
683                    { "Natalie Isaacs", "http://natalieisaacs.com" },
684                    { "Natasha Kraus-Reynolds", "http://www.facebook.com/pages/Natasha-Kraus-Reynolds/199220550117812" },
685                    { "New Wine", "http://messianic.beithassedel.org/music.htm" },
686                    { "Paul Wilbur", "https://wilburministries.com/" },
687                    { "Philip Stanley Klein", "http://www.yeshuasongs.com" },
688                    { "Roeh Israel Worship Team", "http://www.storesonline.com/site/634305/page/916905" },
689                    { "Roman and Alaina", "http://romanandalaina.com/" },
690                    { "Ross", "http://www.andrewandsarahross.com" },
691                    { "Sally Klein O'Connor", "http://www.sallykleinoconnor.com/" },
692                    { "Sha'rei HaShamayim", "http://www.hebraic.info/Hebraic/Music.html" },
693                    { "Sharon Wilbur", "http://sharonwilbur.com" },
694                    { "Sons of Korah", "http://www.sonsofkorah.com" },
695                    { "Will Spires", "http://www.myspace.com/WiLLiamSpires/" },
696                    { "Steve McConnell", "http://www.amazon.com/s/ref=pd_lpo_k2_dp_sr_sq_top?ie=UTF8&keywords=steve%20mcconnell%20messianic%20music&index=blended&pf_rd_p=486539851&pf_rd_s=lpo-top-stripe-1&pf_rd_t=201&pf_rd_i=B000CAG4US&pf_rd_m=ATVPDKIKX0DER&pf_rd_r=1F61C18YZ8YW8XH4D8RH" },
697                    { "Ted Pearce", "http://www.tedpearce.com/music/" },
698                    { "Tents of Mercy", "http://www.fvgifts.com/music.html" },
699                    { "The Hebraism Music Project", "http://www.hebraism.org/Hebraism/Home.html" },
700                    { "The Lumbrosos", "http://thelumbrosos.com/" },
701                    { "Troy Mitchell", "http://ffoz.com/troy-mitchell-yoke-of-the-king-music-audio-cd.html" }
702                };
703
704                entities.SongInfoes
705                    .AsEnumerable()
706                    .Where(s => knownPurchaseLinks.ContainsKey(s.Artist))
707                    .Where(s => s.PurchaseUrl != knownPurchaseLinks[s.Artist])
708                    .ForEach(s => s.PurchaseUrl = knownPurchaseLinks[s.Artist]);
709
710                entities.SaveChanges();
711            }
712        }
713
714        private static void ClearOutOldLogs()
715        {
716            using (var entities = new ChavahEntities())
717            {
718                var monthAgo = DateTime.Now.Subtract(TimeSpan.FromDays(30));
719                entities
720                    .Logs
721                    .Where(l => l.TimeStamp < monthAgo)
722                    .ForEach(entities.Logs.DeleteObject);
723            }
724        }
725
726        private static List<SongInfo> GetCachedSongs()
727        {
728            var songsOnDisk = Directory.EnumerateFiles(Constants.MessianicMusicPath, "*.mp3").Select(Path.GetFileName).Memoize();
729            var cachedSongsList = new List<SongInfo>(1800);
730
731            using (var entities = new ChavahEntities())
732            {
733                // Ensure they're all in the database.
734                var songsInDatabase = entities.SongInfoes.ToArray();
735                songsOnDisk
736                    .Where(fileName => !songsInDatabase.Any(s => string.Equals(s.FileName, fileName, StringComparison.InvariantCultureIgnoreCase)))
737                    .Select(fileName => CreateSongFromDisk(fileName))
738                    .Do(s => entities.Log("Adding song to DB: " + s.FileName))
739                    .Do(entities.SongInfoes.AddObject)
740                    .Concat(songsInDatabase)
741                    .ForEach(cachedSongsList.Add);
742
743                // Remove from the database any that are missing on disk.
744                if (songsOnDisk.Any())
745                {
746                    var songsRemovedFromDisk =
747                        (
748                            from dbSong in songsInDatabase
749                            where !songsOnDisk.Any(s => dbSong.FileName == s)
750                            select dbSong
751                        ).ToArray();
752
753                    // If we're missing a bunch of songs, something is wrong, don't delete.
754                    if (songsRemovedFromDisk.Any() && songsRemovedFromDisk.Length < 100)
755                    {
756                        songsRemovedFromDisk
757                            .Do(entities.SongInfoes.DeleteObject)
758                            .Do(s => cachedSongsList.Remove(s))
759                            .ForEach(s => entities.Log("Removing song from DB: " + s.FileName));
760                    }
761                }
762
763                entities.SaveChanges();
764                cachedSongsList.ForEach(entities.SongInfoes.Detach);
765            }
766
767            return cachedSongsList;
768        }
769
770        private static SongInfo CreateSongFromDisk(string fileName)
771        {
772            var song = new SongInfo { FileName = fileName };
773            song.FillSongDetailsFromFileName(fileName);
774            return song;
775        }
776
777        private void StoreLikeActivity(long songId)
778        {
779            using (var session = raven.OpenSession())
780            {
781                var song = cachedSongs.Value.FirstOrDefault(s => s.Id == songId);
782                if (song != null)
783                {
784                    var songRankString = Match
785                        .Value(song.CommunityRank)
786                        .With(i => i > 0, "+")
787                        .DefaultTo("")
788                        .Evaluate() + song.CommunityRank.ToString();
789                    var songArtist = Match.Value(GetArtistTwitterHandle(song.Artist))
790                        .IfNotNull(h => h)
791                        .DefaultTo(song.Artist)
792                        .Evaluate();
793                    var activity = new Activity
794                    {
795                        DateTime = DateTime.Now,
796                        Title = string.Format("{0} - {1} was thumbed up", song.Artist, song.Name),
797                        Description = string.Format("\"{0}\" by {1} was thumbed up ({2}) on Chavah Messianic Radio.", song.Name, songArtist, songRankString),
798                        MoreInfoUri = song.GetAbsoluteSongUri()
799                    };
800
801                    session.Store(activity);
802                    session.AddRavenExpiration(activity, DateTime.UtcNow.AddDays(30));
803                    session.SaveChanges();
804                }
805            }
806        }
807
808        private static void UpdateLikeStatus(string userId, long songId, SongLikeStatus likeStatus)
809        {
810            var hasReversedLikeStatus = false;
811
812            using (var session = RavenStore.Db.OpenSession())
813            {
814                var existingLike = session.Query<SongLike>().FirstOrDefault(l => l.SongId == songId && l.UserId == userId);
815                if (existingLike != null)
816                {
817                    if (existingLike.LikeStatus == likeStatus.ToBool())
818                    {
819                        // You already like/dislike this song. There's nothing to update.
820                        return;
821                    } 
822
823                    hasReversedLikeStatus = true;
824                    existingLike.LikeStatus = likeStatus.ToBool();
825                }
826                else
827                {
828                    var newLikeStatus = new SongLike
829                    {
830                        LikeStatus = likeStatus.ToBool(),
831                        SongId = (int)songId,
832                        UserId = userId,
833                        Date = DateTime.Now
834                    };
835                    session.Store(newLikeStatus);
836                }
837
838                session.SaveChanges();
839            }
840
841            // Update the community rank.
842            using (var entities = new ChavahEntities())
843            {
844                var song = entities.SongInfoes.FirstOrDefault(s => s.Id == songId);
845                if (song != null)
846                {
847                    var adjustmentAmount = hasReversedLikeStatus ? 2 : 1;
848                    song.CommunityRank += likeStatus == SongLikeStatus.Like ? adjustmentAmount : (-1 * adjustmentAmount);
849                }
850
851                entities.SaveChanges();
852            }
853
854            Dependency.Get<LikesCache>().OnLikesChanged(userId);
855
856            // Update the in-memory item.
857            var adjustementAmount = Match.Value(likeStatus)
858                .With(s => s == SongLikeStatus.Like && !hasReversedLikeStatus, 1)
859                .With(s => s == SongLikeStatus.Like && hasReversedLikeStatus, 2)
860                .With(s => s == SongLikeStatus.Dislike && !hasReversedLikeStatus, -1)
861                .With(s => s == SongLikeStatus.Dislike && hasReversedLikeStatus, -2)
862                .Evaluate();
863            cachedSongs.Value
864                .Where(s => s.Id == songId)
865                .Take(1)
866                .ForEach(s => s.CommunityRank += adjustementAmount);
867        }
868        
869        //private static void UpdateLikeStatus(Guid clientId, Uri songUri, SongLike likeStatus)
870        //{
871        //    var songName = songUri.Segments.Last().Replace("%20", " ");
872        //    using (var entities = new ChavahEntities())
873        //    {
874        //        var song = entities.SongInfoes.FirstOrDefault(s => s.FileName == songName);
875        //        if (song != null)
876        //        {
877        //            UpdateLikeStatus(clientId, song.Id, likeStatus);
878        //        }
879        //    }
880        //}
881    }
882}