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:
parent
1cc392e4b2
commit
579e94d87e
5 changed files with 116 additions and 90 deletions
|
@ -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?
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("–", "-");
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants)TRACE;</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
Loading…
Reference in a new issue