diff --git a/SpotifyShuffle.sln b/SpotifyShuffle.sln
new file mode 100644
index 0000000..a9ba2b6
--- /dev/null
+++ b/SpotifyShuffle.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30907.101
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyShuffle", "SpotifyShuffle\SpotifyShuffle.csproj", "{440F6E9D-3D40-44BB-BC2C-580E16CE27B7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {440F6E9D-3D40-44BB-BC2C-580E16CE27B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {440F6E9D-3D40-44BB-BC2C-580E16CE27B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {440F6E9D-3D40-44BB-BC2C-580E16CE27B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {440F6E9D-3D40-44BB-BC2C-580E16CE27B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {27410299-8DA9-4330-A0D3-6AE979686FD5}
+ EndGlobalSection
+EndGlobal
diff --git a/SpotifyShuffle/Program.cs b/SpotifyShuffle/Program.cs
new file mode 100644
index 0000000..b632622
--- /dev/null
+++ b/SpotifyShuffle/Program.cs
@@ -0,0 +1,326 @@
+using SpotifyAPI.Web;
+using SpotifyAPI.Web.Auth;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using static SpotifyAPI.Web.PlaylistRemoveItemsRequest;
+
+namespace SpotifyShuffle
+{
+ ///
+ /// A program to shuffle an entire Spotify playlist instead of using shuffle play.
+ ///
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("# Spotify Shuffle\nBecause Spotify likes to repeat songs on shuffle play...\n\n");
+ new Program().AuthoriseAsync().GetAwaiter().GetResult();
+ }
+
+ private static readonly string CLIENT_ID = "9ffc2360c76f49cd8d7e0b2ac115a18f";
+ private static SpotifyClient client;
+ private static EmbedIOAuthServer server;
+
+ ///
+ /// Creates the server and handles the authorisation request
+ ///
+ private async Task AuthoriseAsync()
+ {
+ server = new EmbedIOAuthServer(new Uri("http://localhost:8888/callback"), 8888);
+ await server.Start();
+ server.ImplictGrantReceived += OnImplicitGrantReceivedAsync;
+
+ var request = new LoginRequest(server.BaseUri, CLIENT_ID, LoginRequest.ResponseType.Token)
+ {
+ Scope = new List { Scopes.PlaylistModifyPrivate, Scopes.PlaylistModifyPublic, Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative }
+ };
+ BrowserUtil.Open(request.ToUri());
+
+ await Task.Delay(-1); // prevents the program from closing
+ }
+
+ ///
+ /// Called when the user has been authorised
+ ///
+ private async Task OnImplicitGrantReceivedAsync(object sender, ImplictGrantResponse response)
+ {
+ await server.Stop();
+ client = new SpotifyClient(response.AccessToken);
+ string user = (await client.UserProfile.Current()).Id;
+
+ if (!String.IsNullOrEmpty(user) && !String.IsNullOrWhiteSpace(user))
+ {
+ Paging playlists = await client.Playlists.GetUsers(user);
+
+ if (playlists != null && playlists.Items != null)
+ {
+ ListPlaylists(user, playlists);
+
+ try
+ {
+ Console.WriteLine("\nEnter ID of playlist to shuffle: ");
+ int playlistId = Convert.ToInt32(Console.ReadLine());
+ Console.Clear();
+ if (playlistId >= 0 && playlistId < playlists.Items.Count)
+ {
+ Log(LogType.Info, "Shuffle", "Shuffling, this may take a moment...");
+
+ string playlistUri = playlists.Items[playlistId].Uri.Split(':')[2];
+ List> allTracks = new List>();
+ List songs = new List();
+ List songsToRemove = new List();
+ int loops = (int)playlists.Items[playlistId].Tracks.Total / 100;
+ int remainder = (int)playlists.Items[playlistId].Tracks.Total % 100;
+
+ await GetAllTracks(playlistUri, allTracks, loops);
+ int tracks = PopulateSongLists(allTracks, songs, songsToRemove);
+
+ loops = tracks / 100;
+ remainder = tracks % 100;
+ Log(LogType.Info, "Shuffle", $"Tracks: {tracks}, Loops: {loops}, Remainder: {remainder}");
+
+ Log(LogType.Info, "Shuffle", "Shuffling the list...");
+ List shuffled = Shuffle(songs);
+ if (shuffled.Count != songsToRemove.Count) throw new Exception($"For some reason there are not the same amount of songs in each list... Shuffled: {shuffled.Count}, Original: {songsToRemove.Count}");
+
+ await RemoveSongsFromPlaylist(playlistUri, songsToRemove, loops);
+ await Task.Delay(100);
+ await AddSongsToPlaylist(playlistUri, shuffled, loops);
+
+ Log(LogType.Info, "Shuffle", "Playlist shuffle complete.");
+ }
+ }
+ catch (APIException apiEx)
+ {
+ Log(LogType.Error, apiEx.Response.StatusCode.ToString(), apiEx.Message);
+ }
+ catch (Exception ex)
+ {
+ Log(LogType.Error, ex.Source, ex.Message);
+ }
+ }
+ else
+ {
+ Log(LogType.Error, "Playlist", "No playlists found");
+ }
+ }
+ else
+ {
+ Log(LogType.Error, "Playlist", "Invalid playlist ID");
+ }
+
+
+ Console.WriteLine("\n\nPress any key to exit...");
+ Console.ReadKey();
+ Environment.Exit(0);
+ }
+
+ ///
+ /// Gets the full list of tracks from a playlist
+ ///
+ /// Playlist to retreive the songs from
+ /// List to add the songs to
+ /// How many loops are needed to complete the task
+ private async Task GetAllTracks(string playlistUri, List> allTracks, int loops)
+ {
+ for (int i = 0; i <= loops; i++)
+ {
+ var toAdd = await client.Playlists.GetItems(playlistUri, new PlaylistGetItemsRequest() { Offset = i * 100 });
+ allTracks.AddRange(toAdd.Items);
+ }
+ }
+
+ ///
+ /// Lists the playlist to the user so they can select which playlist to shuffle
+ ///
+ /// The users id
+ /// The list of playlists
+ private void ListPlaylists(string user, Paging playlists)
+ {
+ Console.Clear();
+ Console.WriteLine($"# List of Playlists [{user}]\n");
+ for (int i = 0; i < playlists.Items.Count; i++)
+ {
+ if (playlists.Items[i].Tracks != null)
+ {
+ Console.WriteLine($"[ID: {i,3} ] {playlists.Items[i].Name} ({playlists.Items[i].Tracks.Total} tracks)");
+ }
+ else Console.WriteLine(playlists.Items[i].Name + " [INVALID]");
+ }
+ }
+
+ ///
+ /// Populate the songs and songsToRemove lists using allTracks
+ ///
+ /// The full list of tracks to add from
+ /// The playlists uri list
+ /// The list of songs to remove
+ ///
+ private int PopulateSongLists(List> allTracks, List- songs, List
- songsToRemove)
+ {
+ int tracks = 0;
+
+ for (int i = allTracks.Count - 1; i >= 0; i--)
+ {
+ PlaylistTrack track = allTracks[i];
+ if (track.Track != null)
+ {
+ bool local = false;
+ string uri = String.Empty;
+
+ switch (track.Track.Type)
+ {
+ case ItemType.Track:
+ FullTrack t = (track.Track as FullTrack);
+ if (t.Uri.ToLower().Contains("local")) local = true;
+ else uri = t.Uri;
+ break;
+ case ItemType.Episode:
+ FullEpisode e = (track.Track as FullEpisode);
+ if (e.Uri.ToLower().Contains("local")) local = true;
+ else uri = e.Uri;
+ break;
+ }
+ if (!local)
+ {
+ songs.Add(new PlaylistRemoveItemsRequest.Item() { Uri = uri });
+ songsToRemove.Add(new PlaylistRemoveItemsRequest.Item() { Uri = uri });
+ tracks++;
+ }
+ else
+ {
+ Log(LogType.Warning, "Shuffle", "Found a local song. Skipping...");
+ }
+ }
+ else Log(LogType.Warning, "Shuffle", "Found an unavailable song. Skipping...");
+ }
+
+ return tracks;
+ }
+
+ ///
+ /// Shuffle the list of songs into a list of Uris
+ ///
+ /// List of songs to shuffle
+ /// List of strings representing the songs Uris
+ private List Shuffle(List songs)
+ {
+ List shuffled = new List();
+
+ Random rnd = new Random();
+
+ while (songs.Count > 0)
+ {
+ int i = rnd.Next(0, songs.Count);
+ shuffled.Add(songs[i].Uri);
+ songs.RemoveAt(i);
+ }
+
+ return shuffled;
+ }
+
+ ///
+ /// Remove the songs from the playlist
+ ///
+ /// The playlist to remove from
+ /// The songs to remove
+ /// How many loops of 100 this will take
+ private async Task RemoveSongsFromPlaylist(string playlistUri, List
- songsToRemove, int loops)
+ {
+ Log(LogType.Info, "Shuffle", "Removing songs from playlist...");
+ for (int i = 0; i <= loops; i++)
+ {
+ if (i == loops)
+ {
+ if (songsToRemove.Count > 0)
+ {
+ var removeRequest = new PlaylistRemoveItemsRequest();
+ removeRequest.Tracks = songsToRemove;
+ await client.Playlists.RemoveItems(playlistUri, removeRequest);
+ Log(LogType.Info, "Shuffle", $"Removed {songsToRemove.Count} songs");
+ }
+ }
+ else
+ {
+ List songsToRemoveThisLoop = songsToRemove.GetRange(0, 100);
+ songsToRemove.RemoveRange(0, 100);
+
+ var removeRequest = new PlaylistRemoveItemsRequest();
+ removeRequest.Tracks = songsToRemoveThisLoop;
+ await client.Playlists.RemoveItems(playlistUri, removeRequest);
+ Log(LogType.Info, "Shuffle", "Removed 100 songs");
+ }
+ await Task.Delay(50);
+ }
+ }
+
+ ///
+ /// Add songs back to the playlist
+ ///
+ /// The playlist to add the songs to
+ /// The songs to add
+ /// How many loops of 100 this will take
+ private async Task AddSongsToPlaylist(string playlistUri, List songsToAdd, int loops)
+ {
+ Log(LogType.Info, "Shuffle", "Adding songs back in shuffled order...");
+ for (int i = 0; i <= loops; i++)
+ {
+ if (i == loops)
+ {
+ if (songsToAdd.Count > 0)
+ {
+ var addRequest = new PlaylistAddItemsRequest(songsToAdd);
+ await client.Playlists.AddItems(playlistUri, addRequest);
+ Log(LogType.Info, "Shuffle", $"Added back {songsToAdd.Count} songs");
+ }
+ }
+ else
+ {
+ List songsToAddThisLoop = songsToAdd.GetRange(0, 100);
+ songsToAdd.RemoveRange(0, 100);
+
+ var addRequest = new PlaylistAddItemsRequest(songsToAddThisLoop);
+ await client.Playlists.AddItems(playlistUri, addRequest);
+ Log(LogType.Info, "Shuffle", "Added back 100 songs");
+ }
+ await Task.Delay(50);
+ }
+ }
+
+ ///
+ /// Create a console log
+ ///
+ /// Type of console message
+ /// The source of the message
+ /// The message
+ private void Log(LogType logType, string source, string message)
+ {
+ DateTime time = DateTime.Now;
+
+ switch (logType)
+ {
+ case LogType.Error:
+ Console.ForegroundColor = ConsoleColor.Red;
+ break;
+ case LogType.Warning:
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ break;
+ case LogType.Info:
+ Console.ForegroundColor = ConsoleColor.Gray;
+ break;
+ }
+
+ Console.WriteLine($"{time} - [{logType.ToString(), 8}] {source, 15}: {message}");
+ Console.ResetColor();
+ }
+ }
+
+ ///
+ /// The different log types
+ ///
+ public enum LogType
+ {
+ Warning, Error, Info
+ }
+}
diff --git a/SpotifyShuffle/SpotifyShuffle.csproj b/SpotifyShuffle/SpotifyShuffle.csproj
new file mode 100644
index 0000000..928adf8
--- /dev/null
+++ b/SpotifyShuffle/SpotifyShuffle.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+