diff --git a/README.md b/README.md index b5eee32..2c6e68e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # slsk-batchdl -A batch downloader for Soulseek using Soulseek.NET. Accepts csv files and spotify playlist urls. +A batch downloader for Soulseek using Soulseek.NET. Accepts CSV files, Spotify & YouTube urls. ``` Usage: slsk-batchdl.exe [OPTIONS] @@ -10,14 +10,16 @@ Options: --username Soulseek username --password Soulseek password - --spotify Download a spotify playlist + --spotify Download a spotify playlist. "likes" to download all your liked music. --spotify-id Your spotify client id (use if the default fails or if playlist private) --spotify-secret Your spotify client secret (use if the default fails or if playlist private) + --youtube Download YouTube playlist + --csv Use a csv file containing track info to download --artist-col Specify if the csv file contains an artist name column --track-col Specify if if the csv file contains an track name column - --album-col CSV album column name. Optional, may improve searching + --album-col CSV album column name. Optional, may improve searching, slower --full-title-col Specify only if there are no separate artist and track name columns in the csv --uploader-col 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 CSV duration column name. Recommended, will improve accuracy @@ -34,8 +36,10 @@ Options: --nec-max-bitrate Necessary maximum bitrate --nec-max-sample-rate Necessary maximum sample rate + --album-search Also search for "[Album name] [track name]". Occasionally helps to find more --skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided) --music-dir Specify to also skip downloading tracks which are in your library, use with --skip-existing + --reverse Download tracks in reverse order --skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal. --create-m3u Create an m3u playlist file --m3u-only Only create an m3u playlist file with existing tracks and exit @@ -62,5 +66,6 @@ slsk-batchdl.exe --spotify -p "C:\Users\fiso64\Music\Playlists" --m3u "C:\ 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: +- YouTube playlist downloading is bad: There is no way of reliably parsing video information into artist & track name. Also, many videos may be unavailable (or unavailable specifically through the api, for reasons that are beyond me), in which case they won't be downloaded. - The console output tends to break after a while - Much of the code was written by ChatGPT diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs index a3815b1..13ed081 100644 --- a/slsk-batchdl/Program.cs +++ b/slsk-batchdl/Program.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Soulseek; -using Spotify; using TagLib.Matroska; using static System.Formats.Asn1.AsnWriter; using static System.Net.WebRequestMethods; @@ -45,14 +44,16 @@ class Program Console.WriteLine(" --username Soulseek username"); Console.WriteLine(" --password Soulseek password"); Console.WriteLine(); - Console.WriteLine(" --spotify Download a spotify playlist"); + Console.WriteLine(" --spotify Download a spotify playlist. \"likes\" to download all your liked music."); Console.WriteLine(" --spotify-id Your spotify client id (use if the default fails or if playlist private)"); Console.WriteLine(" --spotify-secret Your spotify client secret (use if the default fails or if playlist private)"); Console.WriteLine(); + Console.WriteLine(" --youtube Download YouTube playlist"); + Console.WriteLine(); Console.WriteLine(" --csv Use a csv file containing track info to download"); Console.WriteLine(" --artist-col Specify if the csv file contains an artist name column"); Console.WriteLine(" --track-col Specify if if the csv file contains an track name column"); - Console.WriteLine(" --album-col CSV album column name. Optional, may improve searching"); + Console.WriteLine(" --album-col CSV album column name. Optional, may improve searching, slower"); Console.WriteLine(" --full-title-col Specify only if there are no separate artist and track name columns in the csv"); Console.WriteLine(" --uploader-col 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 CSV duration column name. Recommended, will improve accuracy"); @@ -69,8 +70,10 @@ class Program Console.WriteLine(" --nec-max-bitrate Necessary maximum bitrate"); Console.WriteLine(" --nec-max-sample-rate Necessary maximum sample rate"); Console.WriteLine(); + Console.WriteLine(" --album-search Also search for \"[Album name] [track name]\". Occasionally helps to find more"); Console.WriteLine(" --skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided)"); Console.WriteLine(" --music-dir Specify to also skip downloading tracks which are in your library, use with --skip-existing"); + Console.WriteLine(" --reverse Download tracks in reverse order"); Console.WriteLine(" --skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal."); Console.WriteLine(" --create-m3u Create an m3u playlist file"); Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit"); @@ -101,6 +104,7 @@ class Program musicDir = ""; string parentFolder = ""; string folderName = ""; + string ytUrl = ""; string spotifyUrl = ""; string spotifyId = ""; string spotifySecret = ""; @@ -117,9 +121,11 @@ class Program string lengthCol = ""; string timeUnit = "s"; ytdlpFormat = "bestaudio/best"; + bool reverse = false; bool useYtdlp = false; bool skipExisting = false; bool skipIfPrefFailed = false; + bool albumSearch = false; bool createM3u = false; bool m3uOnly = false; int searchTimeout = 15000; @@ -161,6 +167,9 @@ class Program case "--csv": tracksCsv = args[++i]; break; + case "--youtube": + ytUrl = args[++i]; + break; case "--spotify": spotifyUrl = args[++i]; break; @@ -185,6 +194,9 @@ class Program case "--album-col": albumCol = args[++i]; break; + case "--album-search": + albumSearch = true; + break; case "--full-title-col": fullTitleCol = args[++i]; break; @@ -206,6 +218,9 @@ class Program case "--skip-existing": skipExisting = true; break; + case "--reverse": + reverse = true; + break; case "--skip-if-pref-failed": skipIfPrefFailed = true; break; @@ -279,6 +294,15 @@ class Program if (spotifyUrl == "likes") { playlistName = "Spotify Likes"; + if (usedDefaultId) + { + WriteLastLine(""); + Console.Write("Spotify client ID:"); + spotifyId = Console.ReadLine(); + WriteLastLine(""); + Console.Write("Spotify client secret:"); + spotifySecret = Console.ReadLine(); + } tracks = await GetSpotifyLikes(spotifyId, spotifySecret); } else @@ -310,7 +334,15 @@ class Program } } if (folderName == "") - folderName = playlistName; + folderName = RemoveInvalidChars(playlistName, " "); + } + else if (ytUrl != "") + { + WriteLastLine("Loading youtube playlist..."); + (string name, tracks) = await YouTube.GetTracks(ytUrl); + + if (folderName == "") + folderName = RemoveInvalidChars(name, " "); } else if (tracksCsv != "") { @@ -327,7 +359,7 @@ class Program folderName = Path.GetFileNameWithoutExtension(tracksCsv); } else - throw new Exception("No csv or spotify url provided"); + throw new Exception("No csv, spotify or youtube url provided"); folderName = RemoveInvalidChars(folderName, " "); @@ -423,7 +455,10 @@ class Program } } + albumSearch |= albumCol != ""; int tracksRemaining = tracks.Count; + if (reverse) + tracks.Reverse(); //foreach (var track in tracks) // WriteLastLine($"{track.Title}, {track.ArtistName} - {track.TackTitle} ({track.Length}s)"); @@ -441,7 +476,7 @@ class Program await semaphore.WaitAsync(); try { - var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout, albumSearchTimeout: 5000, useYtdlp); + var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout, albumSearch, useYtdlp); if (savedFilePath != "") { tracksRemaining--; @@ -471,7 +506,7 @@ class Program WriteLastLine($"Failed to download:\n{System.IO.File.ReadAllText(failsFilePath)}"); } - static async Task SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout, int albumSearchTimeout, bool useYtdlp) + static async Task SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout, bool albumSearch, bool useYtdlp) { var title = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}"; if (track.TrackTitle == "") @@ -544,13 +579,13 @@ class Program } catch (OperationCanceledException ex) { } - if (responses.Count == 0 && track.Album != "" && track.TrackTitle != "") + if (albumSearch && responses.Count == 0 && track.Album != "" && track.TrackTitle != "") { Debug.WriteLine("\"Artist - Track\" not found, trying \"Album Track\""); string searchText = $"{track.Album} {track.TrackTitle}"; searchOptions = new SearchOptions ( - minimumPeerUploadSpeed: 1, searchTimeout: albumSearchTimeout, + minimumPeerUploadSpeed: 1, searchTimeout: 5000, responseFilter: (response) => { return response.UploadSpeed > 0; @@ -647,7 +682,7 @@ class Program try { downloading = true; string fname = GetSaveName(track); - YtdlpDownload(track, necessaryCond, Path.Combine(outputFolder, fname)); + YtdlpSearchAndDownload(track, necessaryCond, Path.Combine(outputFolder, fname)); string[] files = System.IO.Directory.GetFiles(outputFolder, fname + ".*"); foreach (string file in files) { @@ -799,14 +834,20 @@ class Program return RemoveInvalidChars(name, " "); } - static void YtdlpDownload(Track track, FileConditions conditions, string savePath) + static void YtdlpSearchAndDownload(Track track, FileConditions conditions, string savePathNoExt) { + if (track.YtID != "") + { + YtdlpDownload(track.YtID, savePathNoExt); + return; + } + Process process = new Process(); 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.Arguments = $"\"ytsearch3:{search}\" --print \"%(duration>%H:%M:%S)s ¦¦ %(id)s ¦¦ %(title)s\""; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; @@ -822,7 +863,7 @@ class Program List<(int, string, string)> results = new List<(int, string, string)>(); string output; - Regex regex = new Regex(@"^(\d+):(\d+):(\d+) - ([\w-]+) - (.+)$"); // I LOVE CHATGPT !!!! + Regex regex = new Regex(@"^(\d+):(\d+):(\d+) ¦¦ ([\w-]+) ¦¦ (.+)$"); // I LOVE CHATGPT !!!! while ((output = process.StandardOutput.ReadLine()) != null) { Match match = regex.Match(output); @@ -842,25 +883,11 @@ class Program foreach (var res in results) { + bool possibleMatch = false; if (conditions.LengthToleranceSatisfies(track, res.Item1)) { WriteLastLine($"[yt-dlp] Downloading: {res.Item3} ({res.Item1}s)"); - process = new Process(); - startInfo = new ProcessStartInfo(); - - startInfo.FileName = "yt-dlp"; - startInfo.Arguments = $"{res.Item2} -f {ytdlpFormat} -ci -o \"{savePath}.%(ext)s\" --extract-audio"; - WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}"); - - 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); }; - - process.Start(); - process.WaitForExit(); + YtdlpDownload(res.Item2, savePathNoExt); return; } } @@ -868,6 +895,26 @@ class Program throw new Exception($"[yt-dlp] No matching files found"); } + static void YtdlpDownload(string id, string savePathNoExt) + { + Process process = new Process(); + ProcessStartInfo startInfo = new ProcessStartInfo(); + + startInfo.FileName = "yt-dlp"; + startInfo.Arguments = $"{id} -f {ytdlpFormat} -ci -o \"{savePathNoExt}.%(ext)s\" --extract-audio"; + WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}"); + + 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); }; + + process.Start(); + process.WaitForExit(); + } + class DownloadInfo { public string savePath; @@ -1038,7 +1085,7 @@ class Program static async Task<(string?, List)> GetSpotifyPlaylist(string url, string id, string secret, bool login) { - var spotify = new Client(id, secret); + var spotify = new Spotify(id, secret); if (login) { await spotify.AuthorizeLogin(); @@ -1053,7 +1100,7 @@ class Program static async Task> GetSpotifyLikes(string id, string secret) { - var spotify = new Client(id, secret); + var spotify = new Spotify(id, secret); await spotify.AuthorizeLogin(); await spotify.IsClientReady(); @@ -1209,6 +1256,16 @@ class Program str = str.Replace(c.ToString(), replaceStr); return str; } + + static void PrintTracks(List tracks) + { + foreach (var track in tracks) + { + string title = track.TrackTitle == "" ? track.UnparsedTitle : $"{track.ArtistName} - {track.TrackTitle}"; + WriteLastLine($"{title} ({track.Length})"); + } + WriteLastLine(tracks.Count); + } } public struct Track @@ -1218,6 +1275,7 @@ public struct Track public string TrackTitle = ""; public string ArtistName = ""; public string Album = ""; + public string YtID = ""; public int Length = -1; public Track() { } } diff --git a/slsk-batchdl/Properties/launchSettings.json b/slsk-batchdl/Properties/launchSettings.json index 749eeaa..66567b7 100644 --- a/slsk-batchdl/Properties/launchSettings.json +++ b/slsk-batchdl/Properties/launchSettings.json @@ -7,9 +7,13 @@ "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": { + "Profile 3": { "commandName": "Project", - "commandLineArgs": "--spotify \"\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --pref-format \"flac\" --yt-dlp" + "commandLineArgs": "--spotify \"likes\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --yt-dlp" + }, + "Profile 4": { + "commandName": "Project", + "commandLineArgs": "--youtube \"https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --yt-dlp" } } } \ No newline at end of file diff --git a/slsk-batchdl/Spotify.cs b/slsk-batchdl/Spotify.cs index 3b95e3a..57fda6b 100644 --- a/slsk-batchdl/Spotify.cs +++ b/slsk-batchdl/Spotify.cs @@ -6,82 +6,85 @@ using SpotifyAPI.Web.Auth; using Swan; using TagLib.IFD.Tags; -namespace Spotify +public class Spotify { - public class Client + private EmbedIOAuthServer _server; + private readonly string _clientId; + private readonly string _clientSecret; + private SpotifyClient _client; + private bool loggedIn = false; + + public Spotify(string clientId, string clientSecret) { - private EmbedIOAuthServer _server; - private readonly string _clientId; - private readonly string _clientSecret; - private SpotifyClient _client; - private bool loggedIn = false; + _clientId = clientId; + _clientSecret = clientSecret; + } - public Client(string clientId, string clientSecret) + public async Task Authorize() + { + 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:5000/callback"), 5000); + await _server.Start(); + + _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; + _server.ErrorReceived += OnErrorReceived; + + var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code) { - _clientId = clientId; - _clientSecret = clientSecret; - } + Scope = new List { Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate } + }; + BrowserUtil.Open(request.ToUri()); + loggedIn = true; + } - public async Task Authorize() + private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response) + { + await _server.Stop(); + + var config = SpotifyClientConfig.CreateDefault(); + var tokenResponse = await new OAuthClient(config).RequestToken( + new AuthorizationCodeTokenRequest( + _clientId, _clientSecret, response.Code, new Uri("http://localhost:5000/callback") + ) + ); + _client = new SpotifyClient(tokenResponse.AccessToken); + } + + private async Task OnErrorReceived(object sender, string error, string state) + { + Console.WriteLine($"Aborting authorization, error received: {error}"); + await _server.Stop(); + } + + public async Task IsClientReady() + { + while (_client == null) + await Task.Delay(1000); + return true; + } + + public async Task> GetLikes() + { + if (!loggedIn) + throw new Exception("Can't get liked music, not logged in"); + + List res = new List(); + int offset = 0; + int limit = 50; + + while (true) { - 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:5000/callback"), 5000); - await _server.Start(); - - _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; - _server.ErrorReceived += OnErrorReceived; - - var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code) - { - Scope = new List { Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate } - }; - BrowserUtil.Open(request.ToUri()); - loggedIn = true; - } - - private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response) - { - await _server.Stop(); - - var config = SpotifyClientConfig.CreateDefault(); - var tokenResponse = await new OAuthClient(config).RequestToken( - new AuthorizationCodeTokenRequest( - _clientId, _clientSecret, response.Code, new Uri("http://localhost:5000/callback") - ) - ); - _client = new SpotifyClient(tokenResponse.AccessToken); - } - - private async Task OnErrorReceived(object sender, string error, string state) - { - Console.WriteLine($"Aborting authorization, error received: {error}"); - await _server.Stop(); - } - - public async Task IsClientReady() - { - while (_client == null) - await Task.Delay(1000); - return true; - } - - public async Task> GetLikes() - { - if (!loggedIn) - throw new Exception("Can't get liked music, not logged in"); - - var tracks = await _client.Library.GetTracks(); - List res = new List(); + var tracks = await _client.Library.GetTracks(new LibraryTracksRequest { Limit = limit, Offset = offset }); foreach (var track in tracks.Items) { @@ -93,15 +96,28 @@ namespace Spotify res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 }); } - return res; + if (tracks.Items.Count < limit) + break; + + offset += limit; } - public async Task<(string?, List)> GetPlaylist(string url) + return res; + } + + + public async Task<(string?, List)> GetPlaylist(string url) + { + var playlistId = GetPlaylistIdFromUrl(url); + var p = await _client.Playlists.Get(playlistId); + + List res = new List(); + int offset = 0; + int limit = 100; + + while (true) { - var playlistId = GetPlaylistIdFromUrl(url); - var p = await _client.Playlists.Get(playlistId); - var tracks = await _client.Playlists.GetItems(playlistId); - List res = new List(); + var tracks = await _client.Playlists.GetItems(playlistId, new PlaylistGetItemsRequest { Limit = limit, Offset = offset }); foreach (var track in tracks.Items) { @@ -110,17 +126,22 @@ namespace Spotify 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 }); + res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 }); } - return (p.Name, res); + if (tracks.Items.Count < limit) + break; + + offset += limit; } - private string GetPlaylistIdFromUrl(string url) - { - var uri = new Uri(url); - var segments = uri.Segments; - return segments[segments.Length - 1].TrimEnd('/'); - } + return (p.Name, res); + } + + private string GetPlaylistIdFromUrl(string url) + { + var uri = new Uri(url); + var segments = uri.Segments; + return segments[segments.Length - 1].TrimEnd('/'); } } diff --git a/slsk-batchdl/YouTube.cs b/slsk-batchdl/YouTube.cs new file mode 100644 index 0000000..9e27336 --- /dev/null +++ b/slsk-batchdl/YouTube.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using YoutubeExplode; + + +public static class YouTube +{ + public static async Task<(string, List)> GetTracks(string url) + { + var youtube = new YoutubeClient(); + var playlist = await youtube.Playlists.GetAsync(url); + + var playlistTitle = playlist.Title; + var tracks = new List(); + var videoTasks = new List<(ValueTask, int)>(); + + await foreach (var video in youtube.Playlists.GetVideosAsync(playlist.Id)) + { + var title = video.Title; + var uploader = video.Author.Title; + var ytId = video.Id.Value; + var length = (int)video.Duration.Value.TotalSeconds; + + title = title.Replace("–", "-"); + + var trackTitle = title.Trim(); + var artist = uploader.Trim(); + + if (artist.EndsWith("- Topic")) + { + artist = artist.Substring(0, artist.Length - 7).Trim(); + trackTitle = title; + + if (artist == "Various Artists") + { + //var vid = await youtube.Videos.GetAsync(video.Id); + videoTasks.Add((youtube.Videos.GetAsync(video.Id), tracks.Count)); + //Thread.Sleep(20); + } + } + else + { + int idx = title.IndexOf('-'); + var split = title.Split(new[] { '-' }, 2); + if (idx > 0 && idx < title.Length - 1 && (title[idx - 1] == ' ' || title[idx + 1] == ' ') && split[0].Trim() != "" && split[1].Trim() != "") + + { + artist = title.Split(new[] { '-' }, 2)[0].Trim(); + trackTitle = title.Split(new[] { '-' }, 2)[1].Trim(); + } + else + { + artist = uploader; + trackTitle = title; + } + } + + var track = new Track + { + UnparsedTitle = video.Title, + Uploader = uploader, + TrackTitle = trackTitle, + ArtistName = artist, + YtID = ytId, + Length = length + }; + + tracks.Add(track); + } + + foreach ((var vidTask, int idx) in videoTasks) + { + var vid = await vidTask; + + var lines = vid.Description.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); + var dotLine = lines.FirstOrDefault(line => line.Contains(" · ")); + + if (dotLine != null) + { + var t = tracks[idx]; + t.ArtistName = dotLine.Split(new[] { " · " }, StringSplitOptions.None)[1]; // can't be asked to do it properly + tracks[idx] = t; + } + } + + return (playlistTitle, tracks); + } +} diff --git a/slsk-batchdl/slsk-batchdl.csproj b/slsk-batchdl/slsk-batchdl.csproj index b84e9ad..017f8e6 100644 --- a/slsk-batchdl/slsk-batchdl.csproj +++ b/slsk-batchdl/slsk-batchdl.csproj @@ -12,6 +12,7 @@ +