mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
search with album name, yt-dlp fallback
This commit is contained in:
parent
7245c4179b
commit
70c618e069
4 changed files with 374 additions and 155 deletions
14
README.md
14
README.md
|
@ -11,15 +11,16 @@ Options:
|
||||||
--password <password> Soulseek password
|
--password <password> Soulseek password
|
||||||
|
|
||||||
--spotify <url> Download a spotify playlist
|
--spotify <url> Download a spotify playlist
|
||||||
--spotify-id <id> Your spotify client id (in case the default one failed)
|
--spotify-id <id> Your spotify client id (use if the default fails or if playlist private)
|
||||||
--spotify-secret <sec> Your spotify client secret (in case the default one failed)
|
--spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private)
|
||||||
|
|
||||||
--csv <path> Use a csv file containing track info to download
|
--csv <path> Use a csv file containing track info to download
|
||||||
--artist-col <column> Specify if the csv file contains an artist name column
|
--artist-col <column> Specify if the csv file contains an artist name column
|
||||||
--track-col <column> Specify if if the csv file contains an track name column
|
--track-col <column> Specify if if the csv file contains an track name column
|
||||||
|
--album-col <unit> CSV album column name. Optional, may improve searching
|
||||||
--full-title-col <column> Specify only if there are no separate artist and track name columns in the csv
|
--full-title-col <column> Specify only if there are no separate artist and track name columns in the csv
|
||||||
--uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)
|
--uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)
|
||||||
--length-col <column> Specify the name of the track duration column, if exists
|
--length-col <column> CSV duration column name. Recommended, will improve accuracy
|
||||||
--time-unit <unit> Time unit for the track duration column, ms or s (default: s)
|
--time-unit <unit> Time unit for the track duration column, ms or s (default: s)
|
||||||
|
|
||||||
--pref-format <format> Preferred file format (default: mp3)
|
--pref-format <format> Preferred file format (default: mp3)
|
||||||
|
@ -39,6 +40,8 @@ Options:
|
||||||
--create-m3u Create an m3u playlist file
|
--create-m3u Create an m3u playlist file
|
||||||
--m3u-only Only create an m3u playlist file with existing tracks and exit
|
--m3u-only Only create an m3u playlist file with existing tracks and exit
|
||||||
--m3u <path> Where to place created m3u files (--parent by default)
|
--m3u <path> Where to place created m3u files (--parent by default)
|
||||||
|
--yt-dlp Use yt-dlp to download tracks that weren't found on Soulseek. yt-dlp must be availble from the command line.
|
||||||
|
--yt-dlp-f <format> yt-dlp audio format (default: "bestaudio/best")
|
||||||
|
|
||||||
--search-timeout <timeout> Maximal search time (default: 15000)
|
--search-timeout <timeout> Maximal search time (default: 15000)
|
||||||
--download-max-stale-time <time> Maximal download time with no progress (default: 80000)
|
--download-max-stale-time <time> Maximal download time with no progress (default: 80000)
|
||||||
|
@ -52,10 +55,11 @@ Download tracks from a csv file and create m3u:
|
||||||
```
|
```
|
||||||
slsk-batchdl.exe -p "C:\Users\fiso64\Music\Playlists" --csv "C:\Users\fiso64\Downloads\test.csv" --username "fakename" --password "fakepass" --artist-col "Artist Name(s)" --track-col "Track Name" --length-col "Duration (ms)" --time-unit "ms" --skip-existing --create-m3u --pref-format "flac"
|
slsk-batchdl.exe -p "C:\Users\fiso64\Music\Playlists" --csv "C:\Users\fiso64\Downloads\test.csv" --username "fakename" --password "fakepass" --artist-col "Artist Name(s)" --track-col "Track Name" --length-col "Duration (ms)" --time-unit "ms" --skip-existing --create-m3u --pref-format "flac"
|
||||||
```
|
```
|
||||||
Download spotify playlist and create m3u:
|
Download spotify playlist with fallback to yt-dlp and create a m3u:
|
||||||
```
|
```
|
||||||
slsk-batchdl.exe --spotify <url> -p "C:\Users\fiso64\Music\Playlists" --m3u "C:\Users\fiso64\Documents\MusicBee\Playlists" --music-dir "C:\Users\fiso64\Music" --username "fakename" --password "fakepass" --skip-existing --pref-format "flac"
|
slsk-batchdl.exe --spotify <url> -p "C:\Users\fiso64\Music\Playlists" --m3u "C:\Users\fiso64\Documents\MusicBee\Playlists" --music-dir "C:\Users\fiso64\Music" --username "fakename" --password "fakepass" --skip-existing --pref-format "flac" --yt-dlp
|
||||||
```
|
```
|
||||||
|
You might need to provide an id and secret when using spotify (which you can get here https://developer.spotify.com/dashboard/applications, under "Create an app").
|
||||||
|
|
||||||
## Notes:
|
## Notes:
|
||||||
- The console output tends to break after a while
|
- The console output tends to break after a while
|
||||||
|
|
|
@ -7,6 +7,7 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -27,6 +28,7 @@ class Program
|
||||||
static string failsFilePath = "";
|
static string failsFilePath = "";
|
||||||
static string m3uFilePath = "";
|
static string m3uFilePath = "";
|
||||||
static string musicDir = "";
|
static string musicDir = "";
|
||||||
|
static string ytdlpFormat = "";
|
||||||
static int downloadMaxStaleTime = 0;
|
static int downloadMaxStaleTime = 0;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
static int displayUpdateDelay = 1000;
|
static int displayUpdateDelay = 1000;
|
||||||
|
@ -44,15 +46,16 @@ class Program
|
||||||
Console.WriteLine(" --password <password> Soulseek password");
|
Console.WriteLine(" --password <password> Soulseek password");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --spotify <url> Download a spotify playlist");
|
Console.WriteLine(" --spotify <url> Download a spotify playlist");
|
||||||
Console.WriteLine(" --spotify-id <id> Your spotify client id (in case the default one failed)");
|
Console.WriteLine(" --spotify-id <id> Your spotify client id (use if the default fails or if playlist private)");
|
||||||
Console.WriteLine(" --spotify-secret <sec> Your spotify client secret (in case the default one failed)");
|
Console.WriteLine(" --spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private)");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --csv <path> Use a csv file containing track info to download");
|
Console.WriteLine(" --csv <path> Use a csv file containing track info to download");
|
||||||
Console.WriteLine(" --artist-col <column> Specify if the csv file contains an artist name column");
|
Console.WriteLine(" --artist-col <column> Specify if the csv file contains an artist name column");
|
||||||
Console.WriteLine(" --track-col <column> Specify if if the csv file contains an track name column");
|
Console.WriteLine(" --track-col <column> Specify if if the csv file contains an track name column");
|
||||||
|
Console.WriteLine(" --album-col <unit> CSV album column name. Optional, may improve searching");
|
||||||
Console.WriteLine(" --full-title-col <column> Specify only if there are no separate artist and track name columns in the csv");
|
Console.WriteLine(" --full-title-col <column> Specify only if there are no separate artist and track name columns in the csv");
|
||||||
Console.WriteLine(" --uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)");
|
Console.WriteLine(" --uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)");
|
||||||
Console.WriteLine(" --length-col <column> Specify the name of the track duration column, if exists");
|
Console.WriteLine(" --length-col <column> CSV duration column name. Recommended, will improve accuracy");
|
||||||
Console.WriteLine(" --time-unit <unit> Time unit for the track duration column, ms or s (default: s)");
|
Console.WriteLine(" --time-unit <unit> Time unit for the track duration column, ms or s (default: s)");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --pref-format <format> Preferred file format (default: mp3)");
|
Console.WriteLine(" --pref-format <format> Preferred file format (default: mp3)");
|
||||||
|
@ -72,6 +75,8 @@ class Program
|
||||||
Console.WriteLine(" --create-m3u Create an m3u playlist file");
|
Console.WriteLine(" --create-m3u Create an m3u playlist file");
|
||||||
Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit");
|
Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit");
|
||||||
Console.WriteLine(" --m3u <path> Where to place created m3u files (--parent by default)");
|
Console.WriteLine(" --m3u <path> Where to place created m3u files (--parent by default)");
|
||||||
|
Console.WriteLine(" --yt-dlp Use yt-dlp to download tracks that weren't found on Soulseek. yt-dlp must be availble from the command line.");
|
||||||
|
Console.WriteLine(" --yt-dlp-f <format> yt-dlp audio format (default: \"bestaudio/best\")");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --search-timeout <timeout> Maximal search time (default: 15000)");
|
Console.WriteLine(" --search-timeout <timeout> Maximal search time (default: 15000)");
|
||||||
Console.WriteLine(" --download-max-stale-time <time> Maximal download time with no progress (default: 80000)");
|
Console.WriteLine(" --download-max-stale-time <time> Maximal download time with no progress (default: 80000)");
|
||||||
|
@ -87,28 +92,32 @@ class Program
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
lastLine = Console.CursorTop;
|
lastLine = Console.CursorTop;
|
||||||
if (args.Contains("--help"))
|
if (args.Contains("--help") || args.Length == 0)
|
||||||
{
|
{
|
||||||
PrintHelp();
|
PrintHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
musicDir = "";
|
musicDir = "";
|
||||||
string parentFolder = "";
|
string parentFolder = "";
|
||||||
string folderName = "";
|
string folderName = "";
|
||||||
string spotifyUrl = "";
|
string spotifyUrl = "";
|
||||||
string spotifyId = "1bf4691bbb1a4f41bced9b2c1cfdbbd2";
|
string spotifyId = "";
|
||||||
string spotifySecret = "e79992e56f4642169acef68c742303f1";
|
string spotifySecret = "";
|
||||||
|
string encodedSpotifyId = "MWJmNDY5MWJiYjFhNGY0MWJjZWQ5YjJjMWNmZGJiZDI="; // base64 encoded client id and secret to avoid git guardian detection (annoying)
|
||||||
|
string encodedSpotifySecret = "ZmQ3NjYyNmM0ZjcxNGJkYzg4Y2I4ZTQ1ZTU1MDBlNzE=";
|
||||||
string tracksCsv = "";
|
string tracksCsv = "";
|
||||||
string username = "";
|
string username = "";
|
||||||
string password = "";
|
string password = "";
|
||||||
string artistCol = "";
|
string artistCol = "";
|
||||||
|
string albumCol = "";
|
||||||
string trackCol = "";
|
string trackCol = "";
|
||||||
string fullTitleCol = "";
|
string fullTitleCol = "";
|
||||||
string uploaderCol = "";
|
string uploaderCol = "";
|
||||||
string lengthCol = "";
|
string lengthCol = "";
|
||||||
string timeUnit = "s";
|
string timeUnit = "s";
|
||||||
|
ytdlpFormat = "bestaudio/best";
|
||||||
|
bool useYtdlp = false;
|
||||||
bool skipExisting = false;
|
bool skipExisting = false;
|
||||||
bool skipIfPrefFailed = false;
|
bool skipIfPrefFailed = false;
|
||||||
bool createM3u = false;
|
bool createM3u = false;
|
||||||
|
@ -134,7 +143,6 @@ class Program
|
||||||
MaxSampleRate = -1,
|
MaxSampleRate = -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
switch (args[i])
|
switch (args[i])
|
||||||
|
@ -174,6 +182,9 @@ class Program
|
||||||
case "--track-col":
|
case "--track-col":
|
||||||
trackCol = args[++i];
|
trackCol = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--album-col":
|
||||||
|
albumCol = args[++i];
|
||||||
|
break;
|
||||||
case "--full-title-col":
|
case "--full-title-col":
|
||||||
fullTitleCol = args[++i];
|
fullTitleCol = args[++i];
|
||||||
break;
|
break;
|
||||||
|
@ -186,6 +197,12 @@ class Program
|
||||||
case "--time-unit":
|
case "--time-unit":
|
||||||
timeUnit = args[++i];
|
timeUnit = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--yt-dlp":
|
||||||
|
useYtdlp = true;
|
||||||
|
break;
|
||||||
|
case "--yt-dlp-f":
|
||||||
|
ytdlpFormat = args[++i];
|
||||||
|
break;
|
||||||
case "--skip-existing":
|
case "--skip-existing":
|
||||||
skipExisting = true;
|
skipExisting = true;
|
||||||
break;
|
break;
|
||||||
|
@ -251,22 +268,46 @@ class Program
|
||||||
|
|
||||||
if (spotifyUrl != "")
|
if (spotifyUrl != "")
|
||||||
{
|
{
|
||||||
string? playlistName;
|
bool usedDefaultId = false;
|
||||||
try
|
if (spotifyId == "" || spotifySecret == "")
|
||||||
{
|
{
|
||||||
(playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, false);
|
spotifyId = Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifyId));
|
||||||
|
spotifySecret = Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifySecret));
|
||||||
|
usedDefaultId = true;
|
||||||
}
|
}
|
||||||
catch (SpotifyAPI.Web.APIException)
|
string? playlistName;
|
||||||
|
if (spotifyUrl == "likes")
|
||||||
{
|
{
|
||||||
WriteLastLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
|
playlistName = "Spotify Likes";
|
||||||
string answer = Console.ReadLine();
|
tracks = await GetSpotifyLikes(spotifyId, spotifySecret);
|
||||||
if (answer.ToLower() == "y")
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try { (playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, true); }
|
(playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, false);
|
||||||
catch (SpotifyAPI.Web.APIException) { throw; }
|
}
|
||||||
|
catch (SpotifyAPI.Web.APIException)
|
||||||
|
{
|
||||||
|
WriteLastLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
|
||||||
|
string answer = Console.ReadLine();
|
||||||
|
if (answer.ToLower() == "y")
|
||||||
|
{
|
||||||
|
if (usedDefaultId)
|
||||||
|
{
|
||||||
|
WriteLastLine("");
|
||||||
|
Console.Write("Spotify client ID:");
|
||||||
|
spotifyId = Console.ReadLine();
|
||||||
|
WriteLastLine("");
|
||||||
|
Console.Write("Spotify client secret:");
|
||||||
|
spotifySecret = Console.ReadLine();
|
||||||
|
}
|
||||||
|
try { (playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, true); }
|
||||||
|
catch (SpotifyAPI.Web.APIException) { throw; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (folderName == "")
|
if (folderName == "")
|
||||||
folderName = playlistName;
|
folderName = playlistName;
|
||||||
|
@ -279,7 +320,8 @@ class Program
|
||||||
throw new Exception("Use one of: full title column, (artist column AND track name)");
|
throw new Exception("Use one of: full title column, (artist column AND track name)");
|
||||||
if (lengthCol == "")
|
if (lengthCol == "")
|
||||||
WriteLastLine($"Warning: No length column specified, results may be imprecise.");
|
WriteLastLine($"Warning: No length column specified, results may be imprecise.");
|
||||||
tracks = ParseCsvIntoTrackInfo(tracksCsv, artistCol, trackCol, lengthCol, fullTitleCol, uploaderCol, timeUnit: timeUnit);
|
|
||||||
|
tracks = ParseCsvIntoTrackInfo(tracksCsv, artistCol, trackCol, lengthCol, fullTitleCol, uploaderCol, albumCol, timeUnit: timeUnit);
|
||||||
|
|
||||||
if (folderName == "")
|
if (folderName == "")
|
||||||
folderName = Path.GetFileNameWithoutExtension(tracksCsv);
|
folderName = Path.GetFileNameWithoutExtension(tracksCsv);
|
||||||
|
@ -399,7 +441,7 @@ class Program
|
||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout);
|
var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout, albumSearchTimeout: 5000, useYtdlp);
|
||||||
if (savedFilePath != "")
|
if (savedFilePath != "")
|
||||||
{
|
{
|
||||||
tracksRemaining--;
|
tracksRemaining--;
|
||||||
|
@ -429,7 +471,7 @@ class Program
|
||||||
WriteLastLine($"Failed to download:\n{System.IO.File.ReadAllText(failsFilePath)}");
|
WriteLastLine($"Failed to download:\n{System.IO.File.ReadAllText(failsFilePath)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task<string> SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout)
|
static async Task<string> SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout, int albumSearchTimeout, bool useYtdlp)
|
||||||
{
|
{
|
||||||
var title = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}";
|
var title = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}";
|
||||||
if (track.TrackTitle == "")
|
if (track.TrackTitle == "")
|
||||||
|
@ -442,15 +484,13 @@ class Program
|
||||||
}
|
}
|
||||||
var saveFilePath = "";
|
var saveFilePath = "";
|
||||||
|
|
||||||
WriteLastLine($"Searching for {title}");
|
|
||||||
|
|
||||||
var searchQuery = SearchQuery.FromText($"{title}");
|
var searchQuery = SearchQuery.FromText($"{title}");
|
||||||
var searchOptions = new SearchOptions
|
var searchOptions = new SearchOptions
|
||||||
(
|
(
|
||||||
minimumPeerUploadSpeed: 1, searchTimeout: searchTimeout,
|
minimumPeerUploadSpeed: 1, searchTimeout: searchTimeout,
|
||||||
responseFilter: (response) =>
|
responseFilter: (response) =>
|
||||||
{
|
{
|
||||||
return response.UploadSpeed > 0 && response.HasFreeUploadSlot;
|
return response.UploadSpeed > 0;
|
||||||
},
|
},
|
||||||
fileFilter: (file) =>
|
fileFilter: (file) =>
|
||||||
{
|
{
|
||||||
|
@ -464,55 +504,83 @@ class Program
|
||||||
var responses = new List<SearchResponse>();
|
var responses = new List<SearchResponse>();
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Action<SearchResponse> responseHandler = (r) =>
|
||||||
|
{
|
||||||
|
if (r.Files.Count > 0)
|
||||||
|
{
|
||||||
|
responses.Add(r);
|
||||||
|
if (!downloading)
|
||||||
|
{
|
||||||
|
var f = r.Files.First();
|
||||||
|
if (preferredCond.FileSatisfies(f, track.Length) && r.HasFreeUploadSlot && r.UploadSpeed / 1000000 >= 1)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Early download");
|
||||||
|
downloading = true;
|
||||||
|
saveFilePath = GetSavePath(f, track);
|
||||||
|
attemptedDownloadPref = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadTask = DownloadFile(r, f, saveFilePath, cts);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
saveFilePath = "";
|
||||||
|
downloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
lock (searches) {
|
lock (searches) {
|
||||||
searches[track] = new SearchInfo(searchQuery, responses, searchOptions);
|
searches[track] = new SearchInfo(searchQuery, responses, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WriteLastLine($"Searching for {title}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var search = await client.SearchAsync(searchQuery, options: searchOptions, cancellationToken: cts.Token, responseHandler: (r) =>
|
var search = await client.SearchAsync(searchQuery, options: searchOptions, cancellationToken: cts.Token, responseHandler: responseHandler);
|
||||||
{
|
|
||||||
if (r.Files.Count > 0)
|
|
||||||
{
|
|
||||||
responses.Add(r);
|
|
||||||
if (!downloading)
|
|
||||||
{
|
|
||||||
var f = r.Files.First();
|
|
||||||
if (preferredCond.FileSatisfies(f, track.Length) && r.HasFreeUploadSlot && r.UploadSpeed / 1000000 >= 1)
|
|
||||||
{
|
|
||||||
Debug.WriteLine("Early download");
|
|
||||||
downloading = true;
|
|
||||||
saveFilePath = GetSavePath(f, track);
|
|
||||||
attemptedDownloadPref = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
downloadTask = DownloadFile(r, f, saveFilePath, cts);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
saveFilePath = "";
|
|
||||||
downloading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (OperationCanceledException ex) { }
|
||||||
|
|
||||||
|
if (responses.Count == 0 && track.Album != "" && track.TrackTitle != "")
|
||||||
{
|
{
|
||||||
if (responses.Count == 0 && !downloading)
|
Debug.WriteLine("\"Artist - Track\" not found, trying \"Album Track\"");
|
||||||
|
string searchText = $"{track.Album} {track.TrackTitle}";
|
||||||
|
searchOptions = new SearchOptions
|
||||||
|
(
|
||||||
|
minimumPeerUploadSpeed: 1, searchTimeout: albumSearchTimeout,
|
||||||
|
responseFilter: (response) =>
|
||||||
|
{
|
||||||
|
return response.UploadSpeed > 0;
|
||||||
|
},
|
||||||
|
fileFilter: (file) =>
|
||||||
|
{
|
||||||
|
var seps = new string[] { " ", "_" };
|
||||||
|
return IsMusicFile(file.Filename) && necessaryCond.FileSatisfies(file, track.Length)
|
||||||
|
&& file.Filename.Replace(seps, "").Contains(track.ArtistName.Replace(seps, ""), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
WriteLastLine($"Searching with album name: {searchText}");
|
||||||
|
try
|
||||||
{
|
{
|
||||||
lock (searches) { searches.Remove(track); }
|
var search = await client.SearchAsync(SearchQuery.FromText(searchText), options: searchOptions, cancellationToken: cts.Token, responseHandler: responseHandler);
|
||||||
WriteLastLine($"Search {title} failed, skipping: {e.Message}", ConsoleColor.Red);
|
|
||||||
cts.Dispose();
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException ex) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (searches) { searches.Remove(track); }
|
lock (searches) { searches.Remove(track); }
|
||||||
|
cts.Dispose();
|
||||||
|
|
||||||
Debug.WriteLine($"Found {responses.Count} responses");
|
Debug.WriteLine($"Found {responses.Count} responses");
|
||||||
|
|
||||||
if (downloading)
|
bool notFound = false;
|
||||||
|
if (!downloading && responses.Count == 0 && !useYtdlp)
|
||||||
|
{
|
||||||
|
notFound = true;
|
||||||
|
}
|
||||||
|
else if (downloading)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -525,7 +593,7 @@ class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloading)
|
if (!downloading && responses.Count > 0)
|
||||||
{
|
{
|
||||||
var fileResponses = responses
|
var fileResponses = responses
|
||||||
.SelectMany(response => response.Files.Select(file => (response, file)))
|
.SelectMany(response => response.Files.Select(file => (response, file)))
|
||||||
|
@ -536,17 +604,6 @@ class Program
|
||||||
.ThenByDescending(x => x.response.UploadSpeed)
|
.ThenByDescending(x => x.response.UploadSpeed)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (fileResponses.Count == 0)
|
|
||||||
{
|
|
||||||
WriteLastLine($"Failed to find: {title}, skipping", ConsoleColor.Red);
|
|
||||||
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
|
||||||
var failedDownloadInfo = $"{title} {length}[Reason: No file found with matching criteria]";
|
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
|
||||||
cts.Dispose();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
int downloadRetries = maxRetriesPerFile;
|
|
||||||
foreach (var x in fileResponses)
|
foreach (var x in fileResponses)
|
||||||
{
|
{
|
||||||
bool pref = preferredCond.FileSatisfies(x.file, track.Length);
|
bool pref = preferredCond.FileSatisfies(x.file, track.Length);
|
||||||
|
@ -556,7 +613,6 @@ class Program
|
||||||
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
var failedDownloadInfo = $"{title} {length}[Preferred version of the file exists, but couldn't be downloaded]";
|
var failedDownloadInfo = $"{title} {length}[Preferred version of the file exists, but couldn't be downloaded]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,31 +629,110 @@ class Program
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
downloading = false;
|
downloading = false;
|
||||||
if (--downloadRetries <= 0)
|
if (--maxRetriesPerFile <= 0)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Failed to download: {title}, skipping", ConsoleColor.Red);
|
WriteLastLine($"Failed to download: {title}, skipping", ConsoleColor.Red);
|
||||||
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
var failedDownloadInfo = $"{title} {length}[Reason: Out of download retries]";
|
var failedDownloadInfo = $"{title} {length}[Reason: Out of download retries]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!downloading)
|
if (!downloading && useYtdlp)
|
||||||
|
{
|
||||||
|
notFound = false;
|
||||||
|
try {
|
||||||
|
downloading = true;
|
||||||
|
string fname = GetSaveName(track);
|
||||||
|
YtdlpDownload(track, necessaryCond, Path.Combine(outputFolder, fname));
|
||||||
|
string[] files = System.IO.Directory.GetFiles(outputFolder, fname + ".*");
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
if (IsMusicFile(file))
|
||||||
|
return saveFilePath = file;
|
||||||
|
}
|
||||||
|
if (saveFilePath == "")
|
||||||
|
throw new Exception("yt-dlp download failed");
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
WriteLastLine(e.Message, ConsoleColor.Red);
|
||||||
|
saveFilePath = "";
|
||||||
|
downloading = false;
|
||||||
|
if (e.Message.Contains("No matching files found"))
|
||||||
|
notFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!downloading)
|
||||||
|
{
|
||||||
|
if (notFound)
|
||||||
|
{
|
||||||
|
WriteLastLine($"Failed to find: {title}", ConsoleColor.Red);
|
||||||
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
|
var failedDownloadInfo = $"{title} {length}[Reason: No file found with matching criteria]";
|
||||||
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
WriteLastLine($"Failed to download: {title}", ConsoleColor.Red);
|
WriteLastLine($"Failed to download: {title}", ConsoleColor.Red);
|
||||||
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
var failedDownloadInfo = $"{title} {length}[Reason: All downloads failed]";
|
var failedDownloadInfo = $"{title} {length}[Reason: All downloads failed]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
}
|
||||||
return "";
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, CancellationTokenSource? searchCts = null)
|
||||||
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||||
|
|
||||||
|
bool transferSet = false;
|
||||||
|
var transferOptions = new TransferOptions(
|
||||||
|
stateChanged: (state) =>
|
||||||
|
{
|
||||||
|
if (downloads.ContainsKey(file.Filename) && !transferSet)
|
||||||
|
downloads[file.Filename].transfer = state.Transfer;
|
||||||
|
},
|
||||||
|
progressUpdated: (progress) =>
|
||||||
|
{
|
||||||
|
if (downloads.ContainsKey(file.Filename))
|
||||||
|
downloads[file.Filename].bytesTransferred = progress.PreviousBytesTransferred;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
using (var cts = new CancellationTokenSource())
|
||||||
|
using (var outputStream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
lock (downloads) { downloads[file.Filename] = new DownloadInfo(filePath, response, file, cts); }
|
||||||
|
WriteLastLine(downloads[file.Filename].displayText);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.DownloadAsync(response.Username, file.Filename, () => Task.FromResult((Stream)outputStream), file.Size, options: transferOptions, cancellationToken: cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
downloads[file.Filename].UpdateText();
|
||||||
|
lock (downloads) { downloads.Remove(file.Filename); }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
System.IO.File.Delete(filePath);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cts.Dispose();
|
searchCts?.Cancel();
|
||||||
return saveFilePath;
|
downloads[file.Filename].success = true;
|
||||||
|
downloads[file.Filename].UpdateText();
|
||||||
|
lock (downloads) { downloads.Remove(file.Filename); }
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task Update()
|
static async Task Update()
|
||||||
|
@ -648,67 +783,89 @@ class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, CancellationTokenSource? searchCts = null)
|
static string GetSavePath(Soulseek.File file, Track track)
|
||||||
{
|
{
|
||||||
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
return $"{GetSavePathNoExt(track)}{Path.GetExtension(file.Filename)}";
|
||||||
|
}
|
||||||
|
|
||||||
bool transferSet = false;
|
static string GetSavePathNoExt(Track track)
|
||||||
var transferOptions = new TransferOptions(
|
{
|
||||||
stateChanged: (state) =>
|
return Path.Combine(outputFolder, $"{GetSaveName(track)}");
|
||||||
{
|
}
|
||||||
if (downloads.ContainsKey(file.Filename) && !transferSet)
|
|
||||||
downloads[file.Filename].transfer = state.Transfer;
|
static string GetSaveName(Track track)
|
||||||
},
|
{
|
||||||
progressUpdated: (progress) =>
|
string name = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}";
|
||||||
{
|
return RemoveInvalidChars(name, " ");
|
||||||
if (downloads.ContainsKey(file.Filename))
|
}
|
||||||
downloads[file.Filename].bytesTransferred = progress.PreviousBytesTransferred;
|
|
||||||
}
|
static void YtdlpDownload(Track track, FileConditions conditions, string savePath)
|
||||||
);
|
{
|
||||||
using (var cts = new CancellationTokenSource())
|
Process process = new Process();
|
||||||
using (var outputStream = new FileStream(filePath, FileMode.Create))
|
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||||||
|
|
||||||
|
startInfo.FileName = "yt-dlp";
|
||||||
|
string search = track.TrackTitle == "" ? track.UnparsedTitle : $"{track.ArtistName} - {track.TrackTitle}";
|
||||||
|
startInfo.Arguments = $"\"ytsearch3:{search}\" --print \"%(duration>%H:%M:%S)s - %(id)s - %(title)s\"";
|
||||||
|
|
||||||
|
startInfo.RedirectStandardOutput = true;
|
||||||
|
startInfo.RedirectStandardError = true;
|
||||||
|
startInfo.UseShellExecute = false;
|
||||||
|
process.StartInfo = startInfo;
|
||||||
|
process.OutputDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||||
|
process.ErrorDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||||
|
|
||||||
|
WriteLastLine($"[yt-dlp] Searching: {search}");
|
||||||
|
process.Start();
|
||||||
|
//process.BeginOutputReadLine();
|
||||||
|
//process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
List<(int, string, string)> results = new List<(int, string, string)>();
|
||||||
|
string output;
|
||||||
|
Regex regex = new Regex(@"^(\d+):(\d+):(\d+) - ([\w-]+) - (.+)$"); // I LOVE CHATGPT !!!!
|
||||||
|
while ((output = process.StandardOutput.ReadLine()) != null)
|
||||||
{
|
{
|
||||||
lock (downloads) { downloads[file.Filename] = new DownloadInfo(filePath, response, file, cts); }
|
Match match = regex.Match(output);
|
||||||
WriteLastLine(downloads[file.Filename].displayText);
|
if (match.Success)
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await client.DownloadAsync(response.Username, file.Filename, () => Task.FromResult((Stream)outputStream), file.Size, options: transferOptions, cancellationToken: cts.Token);
|
int hours = int.Parse(match.Groups[1].Value);
|
||||||
}
|
int minutes = int.Parse(match.Groups[2].Value);
|
||||||
catch (Exception e)
|
int seconds = int.Parse(match.Groups[3].Value);
|
||||||
{
|
int totalSeconds = (hours * 60 * 60) + (minutes * 60) + seconds;
|
||||||
downloads[file.Filename].UpdateText();
|
string id = match.Groups[4].Value;
|
||||||
lock (downloads) { downloads.Remove(file.Filename); }
|
string title = match.Groups[5].Value;
|
||||||
try
|
results.Add((totalSeconds, id, title));
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(filePath))
|
|
||||||
System.IO.File.Delete(filePath);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchCts?.Cancel();
|
process.WaitForExit();
|
||||||
downloads[file.Filename].success = true;
|
|
||||||
downloads[file.Filename].UpdateText();
|
|
||||||
lock (downloads) { downloads.Remove(file.Filename); }
|
|
||||||
}
|
|
||||||
|
|
||||||
static string GetSavePath(Soulseek.File file, Track track)
|
foreach (var res in results)
|
||||||
{
|
{
|
||||||
string name = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}";
|
if (conditions.LengthToleranceSatisfies(track, res.Item1))
|
||||||
return Path.Combine(outputFolder, $"{RemoveInvalidChars(name, " ")}{Path.GetExtension(file.Filename)}");
|
{
|
||||||
}
|
WriteLastLine($"[yt-dlp] Downloading: {res.Item3} ({res.Item1}s)");
|
||||||
|
process = new Process();
|
||||||
|
startInfo = new ProcessStartInfo();
|
||||||
|
|
||||||
struct Track
|
startInfo.FileName = "yt-dlp";
|
||||||
{
|
startInfo.Arguments = $"{res.Item2} -f {ytdlpFormat} -ci -o \"{savePath}.%(ext)s\" --extract-audio";
|
||||||
public string UnparsedTitle = "";
|
WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}");
|
||||||
public string Uploader = "";
|
|
||||||
public string TrackTitle = "";
|
startInfo.RedirectStandardOutput = true;
|
||||||
public string ArtistName = "";
|
startInfo.RedirectStandardError = true;
|
||||||
public int Length = -1;
|
startInfo.UseShellExecute = false;
|
||||||
public Track() { }
|
process.StartInfo = startInfo;
|
||||||
|
process.OutputDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||||
|
process.ErrorDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.WaitForExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"[yt-dlp] No matching files found");
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadInfo
|
class DownloadInfo
|
||||||
|
@ -827,12 +984,23 @@ class Program
|
||||||
|
|
||||||
public bool LengthToleranceSatisfies(TagLib.File file, int actualLength)
|
public bool LengthToleranceSatisfies(TagLib.File file, int actualLength)
|
||||||
{
|
{
|
||||||
|
if (LengthTolerance < 0 || actualLength < 0)
|
||||||
|
return true;
|
||||||
int fileLength = (int)file.Properties.Duration.TotalSeconds;
|
int fileLength = (int)file.Properties.Duration.TotalSeconds;
|
||||||
if (Math.Abs(fileLength - actualLength) <= LengthTolerance)
|
if (Math.Abs(fileLength - actualLength) <= LengthTolerance)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool LengthToleranceSatisfies(Track track, int actualLength)
|
||||||
|
{
|
||||||
|
if (LengthTolerance < 0 || actualLength < 0 || track.Length < 0)
|
||||||
|
return true;
|
||||||
|
if (Math.Abs(track.Length - actualLength) <= LengthTolerance)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public bool BitrateSatisfies(Soulseek.File file)
|
public bool BitrateSatisfies(Soulseek.File file)
|
||||||
{
|
{
|
||||||
if ((MinBitrate < 0 && MaxBitrate < 0) || file.BitRate == null)
|
if ((MinBitrate < 0 && MaxBitrate < 0) || file.BitRate == null)
|
||||||
|
@ -880,18 +1048,21 @@ class Program
|
||||||
await spotify.Authorize();
|
await spotify.Authorize();
|
||||||
|
|
||||||
(string? name, var res) = await spotify.GetPlaylist(url);
|
(string? name, var res) = await spotify.GetPlaylist(url);
|
||||||
|
return (name, res);
|
||||||
List<Track> trackList = res.Select(t =>
|
|
||||||
new Track
|
|
||||||
{
|
|
||||||
TrackTitle = t.Item2,
|
|
||||||
ArtistName = t.Item1,
|
|
||||||
Length = t.Item3
|
|
||||||
}).ToList();
|
|
||||||
return (name, trackList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Track> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "", string lengthCol = "", string titleCol = "", string uploaderCol = "", string timeUnit = "s")
|
static async Task<List<Track>> GetSpotifyLikes(string id, string secret)
|
||||||
|
{
|
||||||
|
var spotify = new Client(id, secret);
|
||||||
|
await spotify.AuthorizeLogin();
|
||||||
|
await spotify.IsClientReady();
|
||||||
|
|
||||||
|
var res = await spotify.GetLikes();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Track> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "",
|
||||||
|
string lengthCol = "", string titleCol = "", string uploaderCol = "", string albumCol = "", string timeUnit = "s")
|
||||||
{
|
{
|
||||||
var tracks = new List<Track>();
|
var tracks = new List<Track>();
|
||||||
|
|
||||||
|
@ -901,6 +1072,7 @@ class Program
|
||||||
var header = reader.ReadLine();
|
var header = reader.ReadLine();
|
||||||
|
|
||||||
var artistIndex = string.IsNullOrEmpty(artistCol) ? -1 : Array.IndexOf(header.Split(','), artistCol);
|
var artistIndex = string.IsNullOrEmpty(artistCol) ? -1 : Array.IndexOf(header.Split(','), artistCol);
|
||||||
|
var albumIndex = string.IsNullOrEmpty(albumCol) ? -1 : Array.IndexOf(header.Split(','), albumCol);
|
||||||
var trackIndex = string.IsNullOrEmpty(trackCol) ? -1 : Array.IndexOf(header.Split(','), trackCol);
|
var trackIndex = string.IsNullOrEmpty(trackCol) ? -1 : Array.IndexOf(header.Split(','), trackCol);
|
||||||
var titleIndex = string.IsNullOrEmpty(titleCol) ? -1 : Array.IndexOf(header.Split(','), titleCol);
|
var titleIndex = string.IsNullOrEmpty(titleCol) ? -1 : Array.IndexOf(header.Split(','), titleCol);
|
||||||
var uploaderIndex = string.IsNullOrEmpty(uploaderCol) ? -1 : Array.IndexOf(header.Split(','), uploaderCol);
|
var uploaderIndex = string.IsNullOrEmpty(uploaderCol) ? -1 : Array.IndexOf(header.Split(','), uploaderCol);
|
||||||
|
@ -916,6 +1088,7 @@ class Program
|
||||||
var track = new Track();
|
var track = new Track();
|
||||||
if (artistIndex >= 0) track.ArtistName = values[artistIndex].Trim('"').Split(',').First().Trim(' ');
|
if (artistIndex >= 0) track.ArtistName = values[artistIndex].Trim('"').Split(',').First().Trim(' ');
|
||||||
if (trackIndex >= 0) track.TrackTitle = values[trackIndex].Trim('"');
|
if (trackIndex >= 0) track.TrackTitle = values[trackIndex].Trim('"');
|
||||||
|
if (albumIndex >= 0) track.Album = values[albumIndex].Trim('"');
|
||||||
if (titleIndex >= 0) track.UnparsedTitle = values[titleIndex].Trim('"');
|
if (titleIndex >= 0) track.UnparsedTitle = values[titleIndex].Trim('"');
|
||||||
if (uploaderIndex >= 0) track.Uploader = values[uploaderIndex].Trim('"');
|
if (uploaderIndex >= 0) track.Uploader = values[uploaderIndex].Trim('"');
|
||||||
if (lengthIndex >= 0 && int.TryParse(values[lengthIndex], out int result) && result > 0)
|
if (lengthIndex >= 0 && int.TryParse(values[lengthIndex], out int result) && result > 0)
|
||||||
|
@ -927,8 +1100,6 @@ class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.UnparsedTitle != "" || track.TrackTitle != "") tracks.Add(track);
|
if (track.UnparsedTitle != "" || track.TrackTitle != "") tracks.Add(track);
|
||||||
else
|
|
||||||
Debug.WriteLine("bad csv line");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1040,6 +1211,17 @@ class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Track
|
||||||
|
{
|
||||||
|
public string UnparsedTitle = "";
|
||||||
|
public string Uploader = "";
|
||||||
|
public string TrackTitle = "";
|
||||||
|
public string ArtistName = "";
|
||||||
|
public string Album = "";
|
||||||
|
public int Length = -1;
|
||||||
|
public Track() { }
|
||||||
|
}
|
||||||
|
|
||||||
public static class ExtensionMethods
|
public static class ExtensionMethods
|
||||||
{
|
{
|
||||||
public static string Replace(this string s, string[] separators, string newVal)
|
public static string Replace(this string s, string[] separators, string newVal)
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"slsk-batchdl": {
|
"slsk-batchdl": {
|
||||||
"commandName": "Project"
|
"commandName": "Project"
|
||||||
|
},
|
||||||
|
"Profile 1": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "-p \"C:\\Users\\fiso64\\Music\\Playlists\" --csv \"C:\\Users\\fiso64\\Downloads\\test.csv\" --username \"fakename99123\" --password \"fakepass123123\" --artist-col \"Artist Name(s)\" --album-col \"Album Name\" --track-col \"Track Name\" --length-col \"Duration (ms)\" --time-unit \"ms\" --pref-format \"flac\" --yt-dlp"
|
||||||
|
},
|
||||||
|
"Profile 2": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "--spotify \"\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --pref-format \"flac\" --yt-dlp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using SpotifyAPI.Web.Auth;
|
using SpotifyAPI.Web.Auth;
|
||||||
using Swan;
|
using Swan;
|
||||||
|
using TagLib.IFD.Tags;
|
||||||
|
|
||||||
namespace Spotify
|
namespace Spotify
|
||||||
{
|
{
|
||||||
|
@ -13,6 +14,7 @@ namespace Spotify
|
||||||
private readonly string _clientId;
|
private readonly string _clientId;
|
||||||
private readonly string _clientSecret;
|
private readonly string _clientSecret;
|
||||||
private SpotifyClient _client;
|
private SpotifyClient _client;
|
||||||
|
private bool loggedIn = false;
|
||||||
|
|
||||||
public Client(string clientId, string clientSecret)
|
public Client(string clientId, string clientSecret)
|
||||||
{
|
{
|
||||||
|
@ -41,9 +43,10 @@ namespace Spotify
|
||||||
|
|
||||||
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code)
|
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code)
|
||||||
{
|
{
|
||||||
Scope = new List<string> { Scopes.UserReadEmail }
|
Scope = new List<string> { Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate }
|
||||||
};
|
};
|
||||||
BrowserUtil.Open(request.ToUri());
|
BrowserUtil.Open(request.ToUri());
|
||||||
|
loggedIn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
|
private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
|
||||||
|
@ -72,20 +75,42 @@ namespace Spotify
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(string?, List<(string, string, int)> )> GetPlaylist(string url)
|
public async Task<List<Track>> GetLikes()
|
||||||
{
|
{
|
||||||
var playlistId = GetPlaylistIdFromUrl(url);
|
if (!loggedIn)
|
||||||
var p = await _client.Playlists.Get(playlistId);
|
throw new Exception("Can't get liked music, not logged in");
|
||||||
var tracks = await _client.Playlists.GetItems(playlistId);
|
|
||||||
List<(string, string, int)> res = new List<(string, string, int)>();
|
var tracks = await _client.Library.GetTracks();
|
||||||
|
List<Track> res = new List<Track>();
|
||||||
|
|
||||||
foreach (var track in tracks.Items)
|
foreach (var track in tracks.Items)
|
||||||
{
|
{
|
||||||
string[] artists = ((IEnumerable<object>)track.Track.ReadProperty("artists")).Select(a => (string)a.ReadProperty("name")).ToArray();
|
string[] artists = ((IEnumerable<object>)track.Track.ReadProperty("artists")).Select(a => (string)a.ReadProperty("name")).ToArray();
|
||||||
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");
|
||||||
int duration = (int)track.Track.ReadProperty("durationMs");
|
int duration = (int)track.Track.ReadProperty("durationMs");
|
||||||
res.Add((artist, name, duration / 1000));
|
res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string?, List<Track>)> GetPlaylist(string url)
|
||||||
|
{
|
||||||
|
var playlistId = GetPlaylistIdFromUrl(url);
|
||||||
|
var p = await _client.Playlists.Get(playlistId);
|
||||||
|
var tracks = await _client.Playlists.GetItems(playlistId);
|
||||||
|
List<Track> res = new List<Track>();
|
||||||
|
|
||||||
|
foreach (var track in tracks.Items)
|
||||||
|
{
|
||||||
|
string[] artists = ((IEnumerable<object>)track.Track.ReadProperty("artists")).Select(a => (string)a.ReadProperty("name")).ToArray();
|
||||||
|
string artist = artists[0];
|
||||||
|
string name = (string)track.Track.ReadProperty("name");
|
||||||
|
string album = (string)track.Track.ReadProperty("album").ReadProperty("name");
|
||||||
|
int duration = (int)track.Track.ReadProperty("durationMs");
|
||||||
|
res.Add(new Track { Album=album, ArtistName=artist, TrackTitle=name, Length=duration / 1000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (p.Name, res);
|
return (p.Name, res);
|
||||||
|
|
Loading…
Reference in a new issue