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

View file

@ -16,31 +16,42 @@ public class Spotify
_clientSecret = clientSecret; _clientSecret = clientSecret;
} }
public async Task Authorize() public async Task Authorize(bool login = false, bool needModify = false)
{ {
var config = SpotifyClientConfig.CreateDefault(); if (!login)
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)
{ {
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) private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
@ -103,8 +114,16 @@ public class Spotify
return res; 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 playlistId = GetPlaylistIdFromUrl(url);
var p = await _client.Playlists.Get(playlistId); var p = await _client.Playlists.Get(playlistId);
@ -122,8 +141,10 @@ public class Spotify
string artist = artists[0]; string artist = artists[0];
string name = (string)track.Track.ReadProperty("name"); string name = (string)track.Track.ReadProperty("name");
string album = (string)track.Track.ReadProperty("album").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"); 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) if (tracks.Items.Count < limit || res.Count >= max)
@ -133,7 +154,7 @@ public class Spotify
limit = Math.Min(max - res.Count, 100); limit = Math.Min(max - res.Count, 100);
} }
return (p.Name, res); return (p.Name, p.Id, res);
} }
private string GetPlaylistIdFromUrl(string url) 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, ""); (string title, string uploader, int length, string desc) info = ("", "", -1, "");
var track = new Track(); var track = new Track();
track.YtID = id; track.URI = id;
title = title.Replace("", "-"); title = title.Replace("", "-");

View file

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