PageRenderTime 54ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/Espera.Core/Management/Playlist.cs

http://github.com/flagbug/Espera
C# | 333 lines | 221 code | 63 blank | 49 comment | 37 complexity | ddfb9777f6eb0560f7ea25c3baec9bf5 MD5 | raw file
Possible License(s): BSD-3-Clause, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.Specialized;
  5. using System.Linq;
  6. using System.Reactive;
  7. using System.Reactive.Disposables;
  8. using System.Reactive.Linq;
  9. using Rareform.Validation;
  10. using ReactiveMarrow;
  11. using ReactiveUI;
  12. namespace Espera.Core.Management
  13. {
  14. /// <summary>
  15. /// Represents a playlist where songs are stored with an associated index.
  16. /// </summary>
  17. public sealed class Playlist : ReactiveObject, IEnumerable<PlaylistEntry>, INotifyCollectionChanged
  18. {
  19. private readonly ObservableAsPropertyHelper<bool> canPlayNextSong;
  20. private readonly ObservableAsPropertyHelper<bool> canPlayPreviousSong;
  21. private readonly ReactiveList<PlaylistEntry> playlist;
  22. private int? currentSongIndex;
  23. private string name;
  24. internal Playlist(string name, bool isTemporary = false)
  25. {
  26. this.Name = name;
  27. this.IsTemporary = isTemporary;
  28. this.playlist = new ReactiveList<PlaylistEntry>();
  29. this.WhenAnyValue(x => x.CurrentSongIndex).Where(x => x != null).Subscribe(x =>
  30. {
  31. this.ResetVotesBeforeIndex(x.Value);
  32. });
  33. this.canPlayNextSong = this.WhenAnyValue(x => x.CurrentSongIndex)
  34. .CombineLatest(this.playlist.Changed.ToUnit().StartWith(Unit.Default), (i, _) => i.HasValue && this.ContainsIndex(i.Value + 1))
  35. .ToProperty(this, x => x.CanPlayNextSong);
  36. this.canPlayPreviousSong = this.WhenAnyValue(x => x.CurrentSongIndex)
  37. .CombineLatest(this.playlist.Changed.ToUnit().StartWith(Unit.Default), (i, _) => i.HasValue && this.ContainsIndex(i.Value - 1))
  38. .ToProperty(this, x => x.CanPlayPreviousSong);
  39. }
  40. public event NotifyCollectionChangedEventHandler CollectionChanged;
  41. /// <summary>
  42. /// Gets a value indicating whether the next song in the playlist can be played.
  43. /// </summary>
  44. /// <value>true if the next song in the playlist can be played; otherwise, false.</value>
  45. public bool CanPlayNextSong
  46. {
  47. get { return this.canPlayNextSong.Value; }
  48. }
  49. /// <summary>
  50. /// Gets a value indicating whether the previous song in the playlist can be played.
  51. /// </summary>
  52. /// <value>true if the previous song in the playlist can be played; otherwise, false.</value>
  53. public bool CanPlayPreviousSong
  54. {
  55. get { return this.canPlayPreviousSong.Value; }
  56. }
  57. /// <summary>
  58. /// Gets or sets the index of the currently played song in the playlist.
  59. /// </summary>
  60. /// <value>
  61. /// The index of the currently played song in the playlist. <c>null</c> , if no song is
  62. /// currently played.
  63. /// </value>
  64. /// <exception cref="ArgumentOutOfRangeException">
  65. /// The value is not in the range of the playlist's indexes.
  66. /// </exception>
  67. public int? CurrentSongIndex
  68. {
  69. get { return this.currentSongIndex; }
  70. set
  71. {
  72. if (value != null && !this.ContainsIndex(value.Value))
  73. throw new ArgumentOutOfRangeException("value");
  74. this.RaiseAndSetIfChanged(ref this.currentSongIndex, value);
  75. }
  76. }
  77. /// <summary>
  78. /// Gets a value indicating whether this playlist is temporary and used for instant-playing.
  79. /// This means that this playlist isn't saved to the harddrive when closing the application.
  80. /// </summary>
  81. public bool IsTemporary { get; private set; }
  82. public string Name
  83. {
  84. get { return this.name; }
  85. set
  86. {
  87. if (this.IsTemporary)
  88. throw new InvalidOperationException("Cannot change the name of a temporary playlist.");
  89. this.name = value;
  90. }
  91. }
  92. public PlaylistEntry this[int index]
  93. {
  94. get
  95. {
  96. if (index < 0)
  97. Throw.ArgumentOutOfRangeException(() => index, 0);
  98. int maxIndex = this.playlist.Count - 1;
  99. if (index > maxIndex)
  100. Throw.ArgumentOutOfRangeException(() => index, maxIndex);
  101. return this.playlist[index];
  102. }
  103. }
  104. /// <summary>
  105. /// Gets a value indicating whether there exists a song at the specified index.
  106. /// </summary>
  107. /// <param name="songIndex">The index to look for.</param>
  108. /// <returns>True, if there exists a song at the specified index; otherwise, false.</returns>
  109. public bool ContainsIndex(int songIndex)
  110. {
  111. return this.playlist.Any(entry => entry.Index == songIndex);
  112. }
  113. public IEnumerator<PlaylistEntry> GetEnumerator()
  114. {
  115. return this.playlist.GetEnumerator();
  116. }
  117. /// <summary>
  118. /// Gets all indexes of the specified songs.
  119. /// </summary>
  120. public IEnumerable<int> GetIndexes(IEnumerable<Song> songs)
  121. {
  122. return this.playlist
  123. .Where(entry => songs.Contains(entry.Song))
  124. .Select(entry => entry.Index)
  125. .ToList();
  126. }
  127. IEnumerator IEnumerable.GetEnumerator()
  128. {
  129. return GetEnumerator();
  130. }
  131. internal PlaylistEntry AddShadowVotedSong(Song song)
  132. {
  133. this.AddSongs(new[] { song });
  134. PlaylistEntry entry = this.Last();
  135. entry.ShadowVote();
  136. return entry;
  137. }
  138. /// <summary>
  139. /// Adds the specified songs to end of the playlist.
  140. /// </summary>
  141. /// <param name="songList">The songs to add to the end of the playlist.</param>
  142. internal void AddSongs(IEnumerable<Song> songList)
  143. {
  144. if (songList == null)
  145. Throw.ArgumentNullException(() => songList);
  146. int index = this.playlist.Count;
  147. var itemsToAdd = songList.Select(song => new PlaylistEntry(index++, song)).ToList();
  148. // We don't use the change notification directly, but record them for later. This allows
  149. // us to have the correct semantics, even if we rebuild the indexes after the changes to
  150. // the list were made
  151. using (this.WithIndexRebuild())
  152. {
  153. this.playlist.AddRange(itemsToAdd);
  154. }
  155. }
  156. internal void MoveSong(int fromIndex, int toIndex)
  157. {
  158. if (fromIndex >= this.playlist.Count)
  159. Throw.ArgumentOutOfRangeException(() => fromIndex);
  160. if (fromIndex < 0)
  161. Throw.ArgumentOutOfRangeException(() => fromIndex);
  162. if (toIndex >= this.playlist.Count)
  163. Throw.ArgumentOutOfRangeException(() => fromIndex);
  164. if (toIndex < 0)
  165. Throw.ArgumentOutOfRangeException(() => fromIndex);
  166. using (this.WithIndexRebuild())
  167. {
  168. this.playlist.Move(fromIndex, toIndex);
  169. }
  170. }
  171. /// <summary>
  172. /// Removes the songs with the specified indexes from the <see cref="Playlist" /> .
  173. /// </summary>
  174. /// <param name="indexes">The indexes of the songs to remove.</param>
  175. internal void RemoveSongs(IEnumerable<int> indexes)
  176. {
  177. if (indexes == null)
  178. Throw.ArgumentNullException(() => indexes);
  179. // Use a HashSet for better lookup performance
  180. var indexList = new HashSet<int>(indexes);
  181. if (this.CurrentSongIndex.HasValue && indexList.Contains(this.CurrentSongIndex.Value))
  182. {
  183. this.CurrentSongIndex = null;
  184. }
  185. var itemsToRemove = this.playlist.Where(x => indexList.Contains(x.Index)).ToList();
  186. using (this.WithIndexRebuild())
  187. {
  188. this.playlist.RemoveAll(itemsToRemove);
  189. }
  190. }
  191. internal void Shuffle()
  192. {
  193. var newList = new List<PlaylistEntry>(this.playlist.Count);
  194. newList.AddRange(this.playlist.OrderBy(x => Guid.NewGuid()));
  195. this.playlist.Clear();
  196. this.playlist.AddRange(newList);
  197. this.RebuildIndexes();
  198. this.OnCollectionChanged(Observable.Return(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)));
  199. }
  200. internal PlaylistEntry VoteFor(int index)
  201. {
  202. if (index < 0)
  203. Throw.ArgumentOutOfRangeException(() => index, 0);
  204. if (index > this.playlist.Count)
  205. Throw.ArgumentOutOfRangeException(() => index, this.playlist.Count);
  206. if (this.CurrentSongIndex.HasValue && index <= this.CurrentSongIndex.Value)
  207. throw new InvalidOperationException("Index can't be less or equal the current song index");
  208. PlaylistEntry entry = this[index];
  209. entry.Vote();
  210. if (this.playlist.Count == 1 || (this.CurrentSongIndex.HasValue && index == this.CurrentSongIndex.Value + 1))
  211. return entry;
  212. var targetEntry = this.Skip(this.CurrentSongIndex.HasValue ?
  213. this.CurrentSongIndex.Value + 1 : 0)
  214. .SkipWhile(x => x.Votes >= this[index].Votes && x != this[index])
  215. .First();
  216. using (this.WithIndexRebuild())
  217. {
  218. this.playlist.Move(index, targetEntry.Index);
  219. }
  220. return entry;
  221. }
  222. private void OnCollectionChanged(IObservable<NotifyCollectionChangedEventArgs> args)
  223. {
  224. if (this.CollectionChanged == null)
  225. return;
  226. args.Subscribe(x => this.CollectionChanged(this, x));
  227. }
  228. private void RebuildIndexes()
  229. {
  230. int index = 0;
  231. int? migrateIndex = null;
  232. foreach (var entry in this.playlist)
  233. {
  234. if (this.CurrentSongIndex == entry.Index)
  235. {
  236. migrateIndex = index;
  237. }
  238. entry.Index = index;
  239. index++;
  240. }
  241. if (migrateIndex.HasValue)
  242. {
  243. this.CurrentSongIndex = migrateIndex;
  244. }
  245. }
  246. private void ResetVotesBeforeIndex(int index)
  247. {
  248. for (int i = 0; i < index; i++)
  249. {
  250. this[i].ResetVotes();
  251. }
  252. }
  253. /// <summary>
  254. /// Records the collection changes that are made unitil the method is disposed, rebuilds the
  255. /// playlist indexes and then sends the change notification to the subscribers.
  256. /// </summary>
  257. private IDisposable WithIndexRebuild()
  258. {
  259. var recordedChanges = this.playlist.Changed.Replay();
  260. IDisposable record = recordedChanges.Connect();
  261. var rebuildAndNotify = Disposable.Create(() =>
  262. {
  263. this.RebuildIndexes();
  264. record.Dispose();
  265. this.OnCollectionChanged(recordedChanges);
  266. });
  267. return rebuildAndNotify;
  268. }
  269. }
  270. }