Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #2 from play/library-search

Basic library search
  • Loading branch information...
commit ac8c93a3566178d0e13c8d0d23877498f377bac6 2 parents a6dbdf2 + cd000c3
Paul Betts authored
View
25 Play.Tests/Models/Fakes.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Play.Models;
+
+namespace Play.Tests
+{
+ public static class Fakes
+ {
+ const string albumJson = "{\"songs\":[{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Get Innocuous!\",\"id\":\"4B52884E1E293890\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Time To Get Away\",\"id\":\"044198D2344B2CAE\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"North American Scum\",\"id\":\"2659D99BCA5BF132\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Someone Great\",\"id\":\"EDC2A0C6F45A31C3\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"All My Friends\",\"id\":\"F85FDE2A63393803\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Us V Them\",\"id\":\"BA41D8967BBDD1B6\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Watch The Tapes\",\"id\":\"4DBE327B3EAE10AA\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"Sound Of Silver\",\"id\":\"ADFE3123DC34FCD9\",\"artist\":\"LCD Soundsystem\"},{\"starred\":false,\"album\":\"Sound Of Silver\",\"queued\":false,\"name\":\"New York, I Love You But You're Bringing Me Down\",\"id\":\"9EA31D331E9ED436\",\"artist\":\"LCD Soundsystem\"}]}";
+
+ public static List<Song> GetAlbum()
+ {
+ return JsonConvert.DeserializeObject<SongQueue>(albumJson).songs;
+ }
+
+ public static Song GetSong()
+ {
+ return GetAlbum().First();
+ }
+ }
+}
View
6 Play.Tests/Play.Tests.csproj
@@ -46,6 +46,10 @@
<HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
<Private>True</Private>
</Reference>
+ <Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Newtonsoft.Json.4.5.1\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
<Reference Include="Ninject">
<HintPath>..\packages\Ninject.3.0.0.15\lib\net45-full\Ninject.dll</HintPath>
</Reference>
@@ -113,10 +117,12 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Compile Include="Models\Fakes.cs" />
<Compile Include="IntegrationTestUrl.cs" />
<Compile Include="Models\PlayApiTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewModels\PlayViewModel.cs" />
+ <Compile Include="ViewModels\SearchViewModel.cs" />
<Compile Include="ViewModels\WelcomeViewModel.cs" />
</ItemGroup>
<ItemGroup>
View
117 Play.Tests/ViewModels/SearchViewModel.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+using Akavache;
+using FluentAssertions;
+using Moq;
+using Ninject;
+using Ninject.MockingKernel.Moq;
+using Play.Models;
+using Play.ViewModels;
+using ReactiveUI;
+using ReactiveUI.Routing;
+using ReactiveUI.Xaml;
+using Xunit;
+
+namespace Play.Tests.ViewModels
+{
+ public class SearchViewModelTests
+ {
+ [Fact]
+ public void PlayApiShouldBeCalledOnPerformSearch()
+ {
+ var kernel = new MoqMockingKernel();
+ var fixture = setupStandardFixture(kernel);
+ fixture.PerformSearch.CanExecute(null).Should().BeFalse();
+
+ fixture.SearchQuery = "Foo";
+ fixture.PerformSearch.CanExecute(null).Should().BeTrue();
+ fixture.PerformSearch.Execute(null);
+
+ kernel.GetMock<IPlayApi>().Verify(x => x.Search("Foo"), Times.Once());
+
+ fixture.SearchResults.Count.Should().Be(1);
+ fixture.SearchResults[0].Model.id.Should().Be("12345");
+ }
+
+ [Fact]
+ public void DontThrashPlayApiOnMultipleSearchCalls()
+ {
+ var kernel = new MoqMockingKernel();
+ var fixture = setupStandardFixture(kernel);
+
+ fixture.SearchQuery = "Foo";
+ fixture.PerformSearch.Execute(null);
+ fixture.PerformSearch.Execute(null);
+ fixture.PerformSearch.Execute(null);
+
+ kernel.GetMock<IPlayApi>().Verify(x => x.Search("Foo"), Times.Once());
+ }
+
+ [Fact]
+ public void EnsureWeFetchAnAlbumCover()
+ {
+ var kernel = new MoqMockingKernel();
+ var fixture = setupStandardFixture(kernel);
+
+ fixture.SearchQuery = "Foo";
+ fixture.PerformSearch.Execute(null);
+
+ kernel.GetMock<IPlayApi>().Verify(x => x.Search("Foo"), Times.Once());
+ kernel.GetMock<IPlayApi>().Verify(x => x.FetchImageForAlbum(It.IsAny<Song>()), Times.Once());
+ }
+
+ static ISearchViewModel setupStandardFixture(MoqMockingKernel kernel)
+ {
+ kernel.Bind<ISearchViewModel>().To<SearchViewModel>();
+ kernel.Bind<IBlobCache>().To<TestBlobCache>().Named("UserAccount");
+ kernel.Bind<IBlobCache>().To<TestBlobCache>().Named("LocalMachine");
+
+ kernel.GetMock<IPlayApi>().Setup(x => x.Search("Foo"))
+ .Returns(Observable.Return(new List<Song>() { Fakes.GetSong() }))
+ .Verifiable();
+
+ var img = new BitmapImage();
+ kernel.GetMock<IPlayApi>().Setup(x => x.FetchImageForAlbum(It.IsAny<Song>()))
+ .Returns(Observable.Return(img))
+ .Verifiable();
+
+ var fixture = kernel.Get<ISearchViewModel>();
+ return fixture;
+ }
+ }
+
+ public class SearchResultTileViewModelTests
+ {
+ [Fact]
+ public void QueuingASongShouldCallPlayApi()
+ {
+ var kernel = new MoqMockingKernel();
+ var song = Fakes.GetSong();
+ var fixture = setupStandardFixture(song, kernel);
+
+ fixture.QueueSong.Execute(null);
+ kernel.GetMock<IPlayApi>().Verify(x => x.QueueSong(It.IsAny<Song>()));
+ }
+
+ static ISearchResultTileViewModel setupStandardFixture(Song song, MoqMockingKernel kernel)
+ {
+ kernel.Bind<IBlobCache>().To<TestBlobCache>().Named("UserAccount");
+ kernel.Bind<IBlobCache>().To<TestBlobCache>().Named("LocalMachine");
+
+ kernel.GetMock<IPlayApi>().Setup(x => x.QueueSong(It.IsAny<Song>()))
+ .Returns(Observable.Return(Unit.Default))
+ .Verifiable();
+
+ kernel.GetMock<IPlayApi>().Setup(x => x.FetchImageForAlbum(It.IsAny<Song>()))
+ .Returns(Observable.Return(new BitmapImage()));
+
+ return new SearchResultTileViewModel(song, kernel.Get<IPlayApi>());
+ }
+ }
+}
View
9 Play/Models/PlayApi.cs
@@ -21,6 +21,7 @@ public interface IPlayApi
IObservable<BitmapImage> FetchImageForAlbum(Song song);
IObservable<string> ListenUrl();
IObservable<List<Song>> Queue();
+ IObservable<Unit> QueueSong(Song song);
IObservable<Unit> Star(Song song);
IObservable<Unit> Unstar(Song song);
IObservable<List<Song>> Search(string query);
@@ -54,6 +55,14 @@ public IObservable<List<Song>> Queue()
});
}
+ public IObservable<Unit> QueueSong(Song song)
+ {
+ var rq = new RestRequest("queue") {Method = Method.POST};
+ rq.AddParameter("id", song.id);
+
+ return client.RequestAsync(rq).Select(_ => Unit.Default);
+ }
+
public IObservable<Unit> Star(Song song)
{
var rq = new RestRequest("star") {Method = Method.POST};
View
7 Play/Models/Song.cs
@@ -12,7 +12,7 @@
namespace Play.Models
{
- public class Song
+ public class Song : IEquatable<Song>
{
// ReSharper disable InconsistentNaming
public string album { get; set; }
@@ -23,6 +23,11 @@ public class Song
public string id { get; set; }
// ReSharper restore InconsistentNaming
+ public bool Equals(Song other)
+ {
+ return this.id == other.id;
+ }
+
public override string ToString()
{
return JsonConvert.SerializeObject(this);
View
1  Play/Play.csproj
@@ -116,6 +116,7 @@
<Compile Include="RestSharpRxHelper.cs" />
<Compile Include="ViewModels\AppBootstrapper.cs" />
<Compile Include="ViewModels\PlayViewModel.cs" />
+ <Compile Include="ViewModels\SearchViewModel.cs" />
<Compile Include="ViewModels\WelcomeViewModel.cs" />
<Compile Include="Views\PlayView.xaml.cs">
<DependentUpon>PlayView.xaml</DependentUpon>
View
103 Play/ViewModels/SearchViewModel.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+using Akavache;
+using Ninject;
+using Play.Models;
+using ReactiveUI;
+using ReactiveUI.Routing;
+using ReactiveUI.Xaml;
+
+namespace Play.ViewModels
+{
+ public interface ISearchViewModel : IRoutableViewModel
+ {
+ string SearchQuery { get; set; }
+
+ ReactiveCollection<ISearchResultTileViewModel> SearchResults { get; }
+ ReactiveAsyncCommand PerformSearch { get; }
+ }
+
+ public interface ISearchResultTileViewModel : IReactiveNotifyPropertyChanged
+ {
+ Song Model { get; }
+ BitmapImage AlbumArt { get; }
+
+ ReactiveCommand QueueSong { get; }
+ ReactiveCommand QueueAlbum { get; }
+ ReactiveCommand ShowSongsFromArtist { get; }
+ ReactiveCommand ShowSongsFromAlbum { get; }
+ }
+
+ public class SearchViewModel : ReactiveObject, ISearchViewModel
+ {
+ public string UrlPathSegment {
+ get { return "/search"; }
+ }
+
+ public IScreen HostScreen { get; protected set; }
+
+ string _SearchQuery;
+ public string SearchQuery {
+ get { return _SearchQuery; }
+ set { this.RaiseAndSetIfChanged(x => x.SearchQuery, value); }
+ }
+
+ public ReactiveCollection<ISearchResultTileViewModel> SearchResults { get; protected set; }
+ public ReactiveAsyncCommand PerformSearch { get; protected set; }
+
+ [Inject]
+ public SearchViewModel(IScreen hostScreen, IPlayApi playApi, [Named("UserAccount")] IBlobCache userCache)
+ {
+ HostScreen = hostScreen;
+ SearchResults = new ReactiveCollection<ISearchResultTileViewModel>();
+
+ var canSearch = this.WhenAny(x => x.SearchQuery, x => !String.IsNullOrWhiteSpace(x.Value));
+ PerformSearch = new ReactiveAsyncCommand(canSearch);
+
+ var searchResults = PerformSearch.RegisterAsyncObservable(_ =>
+ userCache.GetOrFetchObject(
+ "search__" + SearchQuery,
+ () => playApi.Search(SearchQuery),
+ RxApp.TaskpoolScheduler.Now + TimeSpan.FromMinutes(1.0)));
+
+ SearchResults = searchResults
+ .Do(_ => SearchResults.Clear())
+ .SelectMany(list => list.ToObservable())
+ .CreateCollection(x => (ISearchResultTileViewModel) new SearchResultTileViewModel(x, playApi));
+ }
+ }
+
+ public class SearchResultTileViewModel : ReactiveObject, ISearchResultTileViewModel
+ {
+ public Song Model { get; protected set; }
+
+ ObservableAsPropertyHelper<BitmapImage> _AlbumArt;
+ public BitmapImage AlbumArt {
+ get { return _AlbumArt.Value; }
+ }
+
+ public ReactiveCommand QueueSong { get; protected set; }
+ public ReactiveCommand QueueAlbum { get; protected set; }
+ public ReactiveCommand ShowSongsFromArtist { get; protected set; }
+ public ReactiveCommand ShowSongsFromAlbum { get; protected set; }
+
+ public SearchResultTileViewModel(Song model, IPlayApi playApi)
+ {
+ Model = model;
+
+ playApi.FetchImageForAlbum(model).ToProperty(this, x => x.AlbumArt);
+
+ QueueSong = new ReactiveCommand();
+ QueueSong
+ .SelectMany(_ => playApi.QueueSong(Model))
+ .Subscribe(
+ x => this.Log().Info("Queued {0}", Model.name),
+ ex => this.Log().WarnException("Failed to queue: {0}", ex));
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.