1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00

Add --remove-from-playlist

This commit is contained in:
fiso64 2023-06-23 21:44:51 +02:00
parent 1cc392e4b2
commit 579e94d87e
5 changed files with 116 additions and 90 deletions

View file

@ -6,7 +6,7 @@ A batch downloader for Soulseek using Soulseek.NET. Accepts CSV files, Spotify &
```
slsk-batchdl --csv test.csv --artist-col "Artist Name(s)" --track-col "Track Name" --length-col "Duration (ms)" --time-unit ms
```
You can omit the column names provided they are named predictably (like in this example). Use `--print-tracks` before downloading to check if everything has been parsed correctly.
You can omit the column names if they are named predictably (like in this example). Use `--print-tracks` before downloading to check if everything has been parsed correctly.
- Download spotify likes while skipping existing songs, and create an m3u file:
```
@ -59,6 +59,7 @@ Options:
-n --number <maxtracks> Download at most n tracks of a playlist
-o --offset <offset> Skip a specified number of tracks
--reverse Download tracks in reverse order
--remove-from-playlist Remove downloaded tracks from playlist (spotify only)
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
--m3u Create an m3u8 playlist file
@ -131,3 +132,4 @@ Supports .conf files: Create a file named `slsk-batchdl.conf` in the same direct
### Notes:
- The CSV file must be saved with `,` as field delimiter and `"` as string delimiter, encoded with UTF8
- `--display single` and especially `double` can cause the printed lines to be duplicated or overwritten on some configurations. Use `simple` if that's an issue. In my testing on Windows, the terminal app seems to be affected by this (unlike the old command prompt).
- Why didn't I just use Python?

View file

@ -1,10 +1,13 @@
using Konsole;
using AngleSharp.Dom;
using Konsole;
using Soulseek;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using YoutubeExplode.Playlists;
static class Program
{
@ -56,6 +59,7 @@ static class Program
static bool createM3u = false;
static bool m3uOnly = false;
static bool useTagsCheckExisting = false;
static bool removeTracksFromSource = false;
static int maxTracks = int.MaxValue;
static int offset = 0;
@ -84,6 +88,8 @@ static class Program
static string confPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "slsk-batchdl.conf");
static string playlistUri = "";
static Spotify? spotifyClient = null;
static string ytdlpFormat = "bestaudio/best";
static int downloadMaxStaleTime = 50000;
static int updateDelay = 100;
@ -140,6 +146,7 @@ static class Program
"\n -n --number <maxtracks> Download at most n tracks of a playlist" +
"\n -o --offset <offset> Skip a specified number of tracks" +
"\n --reverse Download tracks in reverse order" +
"\n --remove-from-playlist Remove downloaded tracks from playlist (spotify only)" +
"\n --name-format <format> Name format for downloaded tracks, e.g \"{artist} - {title}\"" +
"\n --m3u Create an m3u8 playlist file" +
"\n" +
@ -210,7 +217,7 @@ static class Program
try
{
if (Console.BufferHeight <= 50)
WriteLine("You may be using the windows terminal app. Recommended to use command prompt to avoid printing issues.", ConsoleColor.DarkYellow);
WriteLine("Recommended to use the command prompt instead of terminal app to avoid printing issues.", ConsoleColor.DarkYellow);
}
catch { }
#endif
@ -346,6 +353,9 @@ static class Program
case "--skip-not-found":
skipNotFound = true;
break;
case "--remove-from-playlist":
removeTracksFromSource = true;
break;
case "--remove-ft":
removeFt = true;
break;
@ -486,53 +496,67 @@ static class Program
int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset;
if (spotifyUrl != "")
{
string? playlistName;
bool usedDefaultId = false;
bool login = spotifyUrl == "likes" || removeTracksFromSource;
void readSpotifyCreds()
{
Console.Write("Spotify client ID:");
spotifyId = Console.ReadLine();
Console.Write("Spotify client secret:");
spotifySecret = Console.ReadLine();
Console.WriteLine();
}
if (spotifyId == "" || spotifySecret == "")
{
spotifyId = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifyId));
spotifySecret = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifySecret));
usedDefaultId = true;
if (login)
readSpotifyCreds();
else
{
spotifyId = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifyId));
spotifySecret = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifySecret));
usedDefaultId = true;
}
}
string? playlistName;
spotifyClient = new Spotify(spotifyId, spotifySecret);
await spotifyClient.Authorize(login, removeTracksFromSource);
if (spotifyUrl == "likes")
{
Console.WriteLine("Loading Spotify likes");
tracks = await spotifyClient.GetLikes(max, off);
playlistName = "Spotify Likes";
if (usedDefaultId)
{
Console.Write("Spotify client ID:");
spotifyId = Console.ReadLine();
Console.Write("Spotify client secret:");
spotifySecret = Console.ReadLine();
Console.WriteLine();
}
tracks = await GetSpotifyLikes(spotifyId, spotifySecret, max, off);
}
else
{
try
{
(playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, false, max, off);
Console.WriteLine("Loading Spotify tracks");
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(spotifyUrl, max, off);
}
catch (SpotifyAPI.Web.APIException)
{
Console.WriteLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
string answer = Console.ReadLine();
if (answer.ToLower() == "y")
if (!login)
{
if (usedDefaultId)
Console.WriteLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
string answer = Console.ReadLine();
if (answer.ToLower() == "y")
{
Console.Write("Spotify client ID:");
spotifyId = Console.ReadLine();
Console.Write("Spotify client secret:");
spotifySecret = Console.ReadLine();
Console.WriteLine();
if (usedDefaultId)
readSpotifyCreds();
await spotifyClient.Authorize(true);
Console.WriteLine("Loading Spotify tracks");
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(spotifyUrl, max, off);
}
(playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, true, max, off);
else return;
}
else
return;
else throw;
}
}
if (folderName == "")
@ -561,7 +585,7 @@ static class Program
else if (tracksCsv != "")
{
if (!System.IO.File.Exists(tracksCsv))
throw new Exception("csv file not found");
throw new Exception("CSV file not found");
tracks = await ParseCsvIntoTrackInfo(tracksCsv, artistCol, trackCol, lengthCol, albumCol, descCol, ytIdCol, timeUnit, ytParse);
tracks = tracks.Skip(off).Take(max).ToList();
@ -761,8 +785,14 @@ static class Program
if (savedFilePath != "")
{
Interlocked.Increment(ref successCount);
m3uLines[tracksStart.IndexOf(track)] = Path.GetFileName(savedFilePath);
if (removeTracksFromSource)
{
if (!string.IsNullOrEmpty(spotifyUrl))
spotifyClient.RemoveTrackFromPlaylist(playlistUri, track.URI);
}
m3uLines[tracksStart.IndexOf(track)] = Path.GetFileName(savedFilePath);
if (createM3u)
{
using (var fileStream = new FileStream(m3uFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
@ -1259,11 +1289,11 @@ static class Program
static async Task<string> YtdlpSearchAndDownload(Track track, ProgressBar progress)
{
if (track.YtID != "")
if (track.URI != "")
{
string videoTitle = (await YouTube.GetVideoInfo(track.YtID)).title;
string videoTitle = (await YouTube.GetVideoInfo(track.URI)).title;
string saveFilePathNoExt = GetSavePathNoExt(videoTitle, track);
await YtdlpDownload(track.YtID, saveFilePathNoExt, progress);
await YtdlpDownload(track.URI, saveFilePathNoExt, progress);
return saveFilePathNoExt;
}
@ -1625,33 +1655,6 @@ static class Program
}
}
static async Task<(string?, List<Track>)> GetSpotifyPlaylist(string url, string id, string secret, bool login, int max=int.MaxValue, int offset=0)
{
var spotify = new Spotify(id, secret);
if (login)
{
await spotify.AuthorizeLogin();
await spotify.IsClientReady();
}
else
await spotify.Authorize();
Console.WriteLine("Loading Spotify tracks");
(string? name, var res) = await spotify.GetPlaylist(url, max, offset);
return (name, res);
}
static async Task<List<Track>> GetSpotifyLikes(string id, string secret, int max = int.MaxValue, int offset = 0)
{
var spotify = new Spotify(id, secret);
await spotify.AuthorizeLogin();
await spotify.IsClientReady();
Console.WriteLine("Loading Spotify tracks");
var res = await spotify.GetLikes(max, offset);
return res;
}
static async Task<List<Track>> ParseCsvIntoTrackInfo(string path, string? artistCol = "", string? trackCol = "",
string? lengthCol = "", string? albumCol = "", string? descCol = "", string? ytIdCol = "", string timeUnit = "", bool ytParse = false)
{
@ -1693,7 +1696,7 @@ static class Program
}
if (!string.IsNullOrEmpty(usingColumns))
Console.WriteLine($"Using columns: {usingColumns.TrimEnd(' ', ',')}.");
Console.WriteLine($"Using inferred columns: {usingColumns.TrimEnd(' ', ',')}.");
if (cols[0] == "")
WriteLine($"Warning: No artist column specified, results may be imprecise", ConsoleColor.DarkYellow);
@ -2218,7 +2221,7 @@ public struct Track
public string TrackTitle = "";
public string ArtistName = "";
public string Album = "";
public string YtID = "";
public string URI = "";
public int Length = -1;
public bool ArtistMaybeWrong = false;

View file

@ -16,31 +16,42 @@ public class Spotify
_clientSecret = clientSecret;
}
public async Task Authorize()
public async Task Authorize(bool login = false, bool needModify = false)
{
var config = SpotifyClientConfig.CreateDefault();
var request = new ClientCredentialsRequest(_clientId, _clientSecret);
var response = await new OAuthClient(config).RequestToken(request);
_client = new SpotifyClient(config.WithToken(response.AccessToken));
}
public async Task AuthorizeLogin()
{
Swan.Logging.Logger.NoLogging();
_server = new EmbedIOAuthServer(new Uri("http://localhost:48721/callback"), 48721);
await _server.Start();
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
_server.ErrorReceived += OnErrorReceived;
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code)
if (!login)
{
Scope = new List<string> { Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate }
};
var config = SpotifyClientConfig.CreateDefault();
BrowserUtil.Open(request.ToUri());
var request = new ClientCredentialsRequest(_clientId, _clientSecret);
var response = await new OAuthClient(config).RequestToken(request);
_client = new SpotifyClient(config.WithToken(response.AccessToken));
}
else
{
Swan.Logging.Logger.NoLogging();
_server = new EmbedIOAuthServer(new Uri("http://localhost:48721/callback"), 48721);
await _server.Start();
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
_server.ErrorReceived += OnErrorReceived;
var scope = new List<string> {
Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative
};
if (needModify)
{
scope.Add(Scopes.PlaylistModifyPublic);
scope.Add(Scopes.PlaylistModifyPrivate);
}
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code) { Scope = scope };
BrowserUtil.Open(request.ToUri());
await IsClientReady();
}
}
private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
@ -103,8 +114,16 @@ public class Spotify
return res;
}
public async Task RemoveTrackFromPlaylist(string playlistId, string trackUri)
{
var item = new PlaylistRemoveItemsRequest.Item { Uri = trackUri };
var pr = new PlaylistRemoveItemsRequest();
pr.Tracks = new List<PlaylistRemoveItemsRequest.Item>() { item };
try { await _client.Playlists.RemoveItems(playlistId, pr); }
catch { }
}
public async Task<(string?, List<Track>)> GetPlaylist(string url, int max = int.MaxValue, int offset = 0)
public async Task<(string?, string?, List<Track>)> GetPlaylist(string url, int max = int.MaxValue, int offset = 0)
{
var playlistId = GetPlaylistIdFromUrl(url);
var p = await _client.Playlists.Get(playlistId);
@ -122,8 +141,10 @@ public class Spotify
string artist = artists[0];
string name = (string)track.Track.ReadProperty("name");
string album = (string)track.Track.ReadProperty("album").ReadProperty("name");
string uri = (string)track.Track.ReadProperty("uri");
int duration = (int)track.Track.ReadProperty("durationMs");
res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 });
res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000, URI = uri });
}
if (tracks.Items.Count < limit || res.Count >= max)
@ -133,7 +154,7 @@ public class Spotify
limit = Math.Min(max - res.Count, 100);
}
return (p.Name, res);
return (p.Name, p.Id, res);
}
private string GetPlaylistIdFromUrl(string url)

View file

@ -89,7 +89,7 @@ public static class YouTube
{
(string title, string uploader, int length, string desc) info = ("", "", -1, "");
var track = new Track();
track.YtID = id;
track.URI = id;
title = title.Replace("", "-");

View file

@ -12,7 +12,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants)TRACE;</DefineConstants>
<DefineConstants>$(DefineConstants);TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup>