mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 22:42:41 +00:00
commit
This commit is contained in:
parent
1e7564dd7e
commit
93fb18f221
4 changed files with 100 additions and 38 deletions
|
@ -122,6 +122,8 @@ Options:
|
||||||
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
|
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
|
||||||
--fast-search Begin downloading as soon as a file satisfying the preferred
|
--fast-search Begin downloading as soon as a file satisfying the preferred
|
||||||
conditions is found. Higher chance to download wrong files.
|
conditions is found. Higher chance to download wrong files.
|
||||||
|
--remove-from-source Remove downloaded tracks from source playlist or CSV file
|
||||||
|
(spotify and CSV only)
|
||||||
--m3u <option> Create an m3u8 playlist file
|
--m3u <option> Create an m3u8 playlist file
|
||||||
'none': Do not create a playlist file
|
'none': Do not create a playlist file
|
||||||
'fails' (default): Write only failed downloads to the m3u
|
'fails' (default): Write only failed downloads to the m3u
|
||||||
|
@ -129,7 +131,6 @@ Options:
|
||||||
|
|
||||||
--spotify-id <id> spotify client ID
|
--spotify-id <id> spotify client ID
|
||||||
--spotify-secret <secret> spotify client secret
|
--spotify-secret <secret> spotify client secret
|
||||||
--remove-from-playlist Remove downloaded tracks from playlist (spotify only)
|
|
||||||
|
|
||||||
--youtube-key <key> Youtube data API key
|
--youtube-key <key> Youtube data API key
|
||||||
--get-deleted Attempt to retrieve titles of deleted videos from wayback
|
--get-deleted Attempt to retrieve titles of deleted videos from wayback
|
||||||
|
|
|
@ -70,6 +70,7 @@ static class Program
|
||||||
static string spotifySecret = "";
|
static string spotifySecret = "";
|
||||||
static string ytKey = "";
|
static string ytKey = "";
|
||||||
static string csvPath = "";
|
static string csvPath = "";
|
||||||
|
static int csvColumnCount = -1;
|
||||||
static string username = "";
|
static string username = "";
|
||||||
static string password = "";
|
static string password = "";
|
||||||
static string artistCol = "";
|
static string artistCol = "";
|
||||||
|
@ -138,6 +139,7 @@ static class Program
|
||||||
static int listenPort = 50000;
|
static int listenPort = 50000;
|
||||||
|
|
||||||
static object consoleLock = new object();
|
static object consoleLock = new object();
|
||||||
|
static object csvLock = new object();
|
||||||
|
|
||||||
static bool skipUpdate = false;
|
static bool skipUpdate = false;
|
||||||
static bool debugDisableDownload = false;
|
static bool debugDisableDownload = false;
|
||||||
|
@ -154,7 +156,17 @@ static class Program
|
||||||
private static M3UEditor? m3uEditor;
|
private static M3UEditor? m3uEditor;
|
||||||
private static CancellationTokenSource? mainLoopCts;
|
private static CancellationTokenSource? mainLoopCts;
|
||||||
|
|
||||||
static string inputType = "";
|
static InputType inputType = InputType.None;
|
||||||
|
|
||||||
|
public enum InputType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Spotify,
|
||||||
|
YouTube,
|
||||||
|
Bandcamp,
|
||||||
|
String,
|
||||||
|
CSV
|
||||||
|
};
|
||||||
|
|
||||||
static void PrintHelp()
|
static void PrintHelp()
|
||||||
{
|
{
|
||||||
|
@ -193,6 +205,8 @@ static class Program
|
||||||
"\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 --fast-search Begin downloading as soon as a file satisfying the preferred" +
|
"\n --fast-search Begin downloading as soon as a file satisfying the preferred" +
|
||||||
"\n conditions is found. Higher chance to download wrong files." +
|
"\n conditions is found. Higher chance to download wrong files." +
|
||||||
|
"\n --remove-from-source Remove downloaded tracks from source playlist or CSV file " +
|
||||||
|
"\n (spotify and CSV only)" +
|
||||||
"\n --m3u <option> Create an m3u8 playlist file" +
|
"\n --m3u <option> Create an m3u8 playlist file" +
|
||||||
"\n 'none': Do not create a playlist file" +
|
"\n 'none': Do not create a playlist file" +
|
||||||
"\n 'fails' (default): Write only failed downloads to the m3u" +
|
"\n 'fails' (default): Write only failed downloads to the m3u" +
|
||||||
|
@ -200,7 +214,6 @@ static class Program
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n --spotify-id <id> spotify client ID" +
|
"\n --spotify-id <id> spotify client ID" +
|
||||||
"\n --spotify-secret <secret> spotify client secret" +
|
"\n --spotify-secret <secret> spotify client secret" +
|
||||||
"\n --remove-from-playlist Remove downloaded tracks from playlist (spotify only)" +
|
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n --youtube-key <key> Youtube data API key" +
|
"\n --youtube-key <key> Youtube data API key" +
|
||||||
"\n --get-deleted Attempt to retrieve titles of deleted videos from wayback" +
|
"\n --get-deleted Attempt to retrieve titles of deleted videos from wayback" +
|
||||||
|
@ -399,7 +412,16 @@ static class Program
|
||||||
break;
|
break;
|
||||||
case "--it":
|
case "--it":
|
||||||
case "--input-type":
|
case "--input-type":
|
||||||
inputType = args[++i];
|
inputType = args[++i].ToLower().Trim() switch
|
||||||
|
{
|
||||||
|
"none" => InputType.None,
|
||||||
|
"csv" => InputType.CSV,
|
||||||
|
"youtube" => InputType.YouTube,
|
||||||
|
"spotify" => InputType.Spotify,
|
||||||
|
"bandcamp" => InputType.Bandcamp,
|
||||||
|
"string" => InputType.String,
|
||||||
|
_ => throw new ArgumentException($"Invalid input type '{args[i]}'"),
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case "-p":
|
case "-p":
|
||||||
case "--path":
|
case "--path":
|
||||||
|
@ -563,6 +585,8 @@ static class Program
|
||||||
skipNotFound = true;
|
skipNotFound = true;
|
||||||
break;
|
break;
|
||||||
case "--rfp":
|
case "--rfp":
|
||||||
|
case "--rfs":
|
||||||
|
case "--remove-from-source":
|
||||||
case "--remove-from-playlist":
|
case "--remove-from-playlist":
|
||||||
removeTracksFromSource = true;
|
removeTracksFromSource = true;
|
||||||
break;
|
break;
|
||||||
|
@ -925,7 +949,7 @@ static class Program
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (input == "")
|
if (input == "")
|
||||||
input = args[i];
|
input = args[i].Trim();
|
||||||
else
|
else
|
||||||
throw new ArgumentException($"Invalid argument \'{args[i]}\'. Input is already set to \'{input}\'");
|
throw new ArgumentException($"Invalid argument \'{args[i]}\'. Input is already set to \'{input}\'");
|
||||||
}
|
}
|
||||||
|
@ -933,8 +957,6 @@ static class Program
|
||||||
|
|
||||||
if (input == "")
|
if (input == "")
|
||||||
throw new ArgumentException($"No input provided");
|
throw new ArgumentException($"No input provided");
|
||||||
if (!(new string[] { "", "youtube", "spotify", "csv", "string", "bandcamp" }).Contains(inputType))
|
|
||||||
throw new ArgumentException($"Invalid input type '{inputType}'");
|
|
||||||
|
|
||||||
if (ytKey != "")
|
if (ytKey != "")
|
||||||
YouTube.apiKey = ytKey;
|
YouTube.apiKey = ytKey;
|
||||||
|
@ -944,22 +966,22 @@ static class Program
|
||||||
|
|
||||||
ignoreOn = Math.Min(ignoreOn, deprioritizeOn);
|
ignoreOn = Math.Min(ignoreOn, deprioritizeOn);
|
||||||
|
|
||||||
if (inputType == "youtube" || (inputType == "" && input.StartsWith("http") && input.Contains("youtu")))
|
if (inputType == InputType.YouTube || (inputType == InputType.None && input.StartsWith("http") && input.Contains("youtu")))
|
||||||
{
|
{
|
||||||
WriteLine("Youtube download", debugOnly: true);
|
WriteLine("Youtube download", debugOnly: true);
|
||||||
await YoutubeInput();
|
await YoutubeInput();
|
||||||
}
|
}
|
||||||
else if (inputType == "spotify" || (inputType == "" && (input.StartsWith("http") && input.Contains("spotify")) || input == "spotify-likes"))
|
else if (inputType == InputType.Spotify || (inputType == InputType.None && (input.StartsWith("http") && input.Contains("spotify")) || input == "spotify-likes"))
|
||||||
{
|
{
|
||||||
WriteLine("Spotify download", debugOnly: true);
|
WriteLine("Spotify download", debugOnly: true);
|
||||||
await SpotifyInput();
|
await SpotifyInput();
|
||||||
}
|
}
|
||||||
else if (inputType == "bandcamp" || (inputType == "" && input.StartsWith("http") && input.Contains("bandcamp")))
|
else if (inputType == InputType.Bandcamp || (inputType == InputType.None && input.StartsWith("http") && input.Contains("bandcamp")))
|
||||||
{
|
{
|
||||||
WriteLine("Bandcamp download", debugOnly: true);
|
WriteLine("Bandcamp download", debugOnly: true);
|
||||||
await BandcampInput();
|
await BandcampInput();
|
||||||
}
|
}
|
||||||
else if (inputType == "csv" || (inputType == "" && Path.GetExtension(input).Equals(".csv", StringComparison.OrdinalIgnoreCase)))
|
else if (inputType == InputType.CSV || (inputType == InputType.None && input.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
WriteLine("CSV download", debugOnly: true);
|
WriteLine("CSV download", debugOnly: true);
|
||||||
await CsvInput();
|
await CsvInput();
|
||||||
|
@ -1021,7 +1043,7 @@ 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;
|
||||||
ytUrl = input;
|
ytUrl = input;
|
||||||
inputType = "youtube";
|
inputType = InputType.YouTube;
|
||||||
|
|
||||||
string name;
|
string name;
|
||||||
List<Track>? deleted = null;
|
List<Track>? deleted = null;
|
||||||
|
@ -1071,10 +1093,9 @@ static class Program
|
||||||
int off = reverse ? 0 : offset;
|
int off = reverse ? 0 : offset;
|
||||||
|
|
||||||
spotifyUrl = input;
|
spotifyUrl = input;
|
||||||
inputType = "spotify";
|
inputType = InputType.Spotify;
|
||||||
|
|
||||||
string? playlistName;
|
string? playlistName;
|
||||||
bool usedDefaultId = false;
|
|
||||||
bool needLogin = spotifyUrl == "spotify-likes" || removeTracksFromSource;
|
bool needLogin = spotifyUrl == "spotify-likes" || removeTracksFromSource;
|
||||||
List<Track> tracks;
|
List<Track> tracks;
|
||||||
|
|
||||||
|
@ -1129,16 +1150,20 @@ static class Program
|
||||||
}
|
}
|
||||||
catch (SpotifyAPI.Web.APIException)
|
catch (SpotifyAPI.Web.APIException)
|
||||||
{
|
{
|
||||||
if (!needLogin)
|
if (!needLogin && !spotifyClient.UsedDefaultCredentials)
|
||||||
|
{
|
||||||
|
await spotifyClient.Authorize(true, removeTracksFromSource);
|
||||||
|
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(spotifyUrl, max, off);
|
||||||
|
}
|
||||||
|
else if (!needLogin)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
|
Console.WriteLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
|
||||||
string answer = Console.ReadLine();
|
if (Console.ReadLine()?.ToLower().Trim() == "y")
|
||||||
if (answer.ToLower() == "y")
|
|
||||||
{
|
{
|
||||||
if (usedDefaultId)
|
|
||||||
readSpotifyCreds();
|
readSpotifyCreds();
|
||||||
await spotifyClient.Authorize(true);
|
spotifyClient = new Spotify(spotifyId, spotifySecret);
|
||||||
Console.WriteLine("Loading Spotify tracks");
|
await spotifyClient.Authorize(true, removeTracksFromSource);
|
||||||
|
Console.WriteLine("Loading Spotify playlist");
|
||||||
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(spotifyUrl, max, off);
|
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(spotifyUrl, max, off);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1162,7 +1187,7 @@ static class Program
|
||||||
|
|
||||||
static async Task BandcampInput()
|
static async Task BandcampInput()
|
||||||
{
|
{
|
||||||
inputType = "bandcamp";
|
inputType = InputType.Bandcamp;
|
||||||
bool isAlbum = !input.Contains("/track/");
|
bool isAlbum = !input.Contains("/track/");
|
||||||
|
|
||||||
var web = new HtmlWeb();
|
var web = new HtmlWeb();
|
||||||
|
@ -1212,7 +1237,7 @@ static class Program
|
||||||
int off = reverse ? 0 : offset;
|
int off = reverse ? 0 : offset;
|
||||||
|
|
||||||
csvPath = input;
|
csvPath = input;
|
||||||
inputType = "csv";
|
inputType = InputType.CSV;
|
||||||
|
|
||||||
if (!File.Exists(csvPath))
|
if (!File.Exists(csvPath))
|
||||||
throw new FileNotFoundException("CSV file not found");
|
throw new FileNotFoundException("CSV file not found");
|
||||||
|
@ -1227,7 +1252,7 @@ static class Program
|
||||||
static async Task StringInput()
|
static async Task StringInput()
|
||||||
{
|
{
|
||||||
searchStr = input;
|
searchStr = input;
|
||||||
inputType = "string";
|
inputType = InputType.String;
|
||||||
var music = ParseTrackArg(searchStr, album);
|
var music = ParseTrackArg(searchStr, album);
|
||||||
bool isAlbum = false;
|
bool isAlbum = false;
|
||||||
|
|
||||||
|
@ -1557,19 +1582,21 @@ static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedFilePath != "")
|
if (savedFilePath != "")
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
lock (trackLists) { tracks[index] = new Track(track) { TrackState = Track.State.Downloaded, DownloadPath = savedFilePath }; }
|
lock (trackLists) { tracks[index] = new Track(track) { TrackState = Track.State.Downloaded, DownloadPath = savedFilePath }; }
|
||||||
|
|
||||||
if (removeTracksFromSource && !string.IsNullOrEmpty(spotifyUrl))
|
if (removeTracksFromSource)
|
||||||
spotifyClient.RemoveTrackFromPlaylist(playlistUri, track.URI);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RemoveTrackFromSource(track);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m3uEditor.Update();
|
m3uEditor.Update();
|
||||||
|
|
||||||
|
@ -1911,6 +1938,28 @@ static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static async Task RemoveTrackFromSource(Track track)
|
||||||
|
{
|
||||||
|
if (inputType == InputType.Spotify && track.URI != "")
|
||||||
|
{
|
||||||
|
await spotifyClient.RemoveTrackFromPlaylist(playlistUri, track.URI);
|
||||||
|
}
|
||||||
|
else if (inputType == InputType.CSV && track.CsvRow != -1)
|
||||||
|
{
|
||||||
|
lock (csvLock)
|
||||||
|
{
|
||||||
|
string[] lines = File.ReadAllLines(csvPath, System.Text.Encoding.UTF8);
|
||||||
|
|
||||||
|
if (lines.Length > track.CsvRow)
|
||||||
|
{
|
||||||
|
lines[track.CsvRow] = new string(',', Math.Max(0, csvColumnCount - 1));
|
||||||
|
File.WriteAllLines(csvPath, lines, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static async Task Login(bool random = false, int tries = 3)
|
static async Task Login(bool random = false, int tries = 3)
|
||||||
{
|
{
|
||||||
string user = username, pass = password;
|
string user = username, pass = password;
|
||||||
|
@ -3435,9 +3484,13 @@ static class Program
|
||||||
using var sr = new StreamReader(path, System.Text.Encoding.UTF8);
|
using var sr = new StreamReader(path, System.Text.Encoding.UTF8);
|
||||||
var parser = new SmallestCSV.SmallestCSVParser(sr);
|
var parser = new SmallestCSV.SmallestCSVParser(sr);
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
var header = parser.ReadNextRow();
|
var header = parser.ReadNextRow();
|
||||||
while (header == null || header.Count == 0 || !header.Any(t => t.Trim() != ""))
|
while (header == null || header.Count == 0 || !header.Any(t => t.Trim() != ""))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
header = parser.ReadNextRow();
|
header = parser.ReadNextRow();
|
||||||
|
}
|
||||||
|
|
||||||
string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol, trackCountCol };
|
string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol, trackCountCol };
|
||||||
string[][] aliases = {
|
string[][] aliases = {
|
||||||
|
@ -3482,6 +3535,7 @@ static class Program
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
index++;
|
||||||
var values = parser.ReadNextRow();
|
var values = parser.ReadNextRow();
|
||||||
if (values == null)
|
if (values == null)
|
||||||
break;
|
break;
|
||||||
|
@ -3490,9 +3544,12 @@ static class Program
|
||||||
while (values.Count < foundCount)
|
while (values.Count < foundCount)
|
||||||
values.Add("");
|
values.Add("");
|
||||||
|
|
||||||
var desc = "";
|
if (csvColumnCount == -1)
|
||||||
|
csvColumnCount = values.Count;
|
||||||
|
|
||||||
|
var desc = "";
|
||||||
|
var track = new Track() { CsvRow = index };
|
||||||
|
|
||||||
var track = new Track();
|
|
||||||
if (artistIndex >= 0) track.Artist = values[artistIndex];
|
if (artistIndex >= 0) track.Artist = values[artistIndex];
|
||||||
if (trackIndex >= 0) track.Title = values[trackIndex];
|
if (trackIndex >= 0) track.Title = values[trackIndex];
|
||||||
if (albumIndex >= 0) track.Album = values[albumIndex];
|
if (albumIndex >= 0) track.Album = values[albumIndex];
|
||||||
|
@ -4572,6 +4629,7 @@ public struct Track
|
||||||
public string FailureReason = "";
|
public string FailureReason = "";
|
||||||
public string DownloadPath = "";
|
public string DownloadPath = "";
|
||||||
public string Other = "";
|
public string Other = "";
|
||||||
|
public int CsvRow = -1;
|
||||||
public State TrackState = State.Initial;
|
public State TrackState = State.Initial;
|
||||||
|
|
||||||
public SlDictionary? Downloads = null;
|
public SlDictionary? Downloads = null;
|
||||||
|
@ -4604,6 +4662,7 @@ public struct Track
|
||||||
Other = other.Other;
|
Other = other.Other;
|
||||||
MinAlbumTrackCount = other.MinAlbumTrackCount;
|
MinAlbumTrackCount = other.MinAlbumTrackCount;
|
||||||
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
|
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
|
||||||
|
CsvRow = other.CsvRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override readonly string ToString()
|
public override readonly string ToString()
|
||||||
|
|
|
@ -7,7 +7,7 @@ public class Spotify
|
||||||
private EmbedIOAuthServer _server;
|
private EmbedIOAuthServer _server;
|
||||||
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;
|
private bool loggedIn = false;
|
||||||
|
|
||||||
// default spotify credentials (base64-encoded to keep the bots away)
|
// default spotify credentials (base64-encoded to keep the bots away)
|
||||||
|
@ -15,7 +15,7 @@ public class Spotify
|
||||||
public const string encodedSpotifySecret = "Y2JlM2QxYTE5MzJkNDQ2MmFiOGUy3shTuf4Y2JhY2M3ZDdjYWU=";
|
public const string encodedSpotifySecret = "Y2JlM2QxYTE5MzJkNDQ2MmFiOGUy3shTuf4Y2JhY2M3ZDdjYWU=";
|
||||||
public bool UsedDefaultCredentials { get; private set; }
|
public bool UsedDefaultCredentials { get; private set; }
|
||||||
|
|
||||||
public Spotify(string clientId, string clientSecret)
|
public Spotify(string clientId="", string clientSecret="")
|
||||||
{
|
{
|
||||||
_clientId = clientId;
|
_clientId = clientId;
|
||||||
_clientSecret = clientSecret;
|
_clientSecret = clientSecret;
|
||||||
|
@ -30,6 +30,8 @@ public class Spotify
|
||||||
|
|
||||||
public async Task Authorize(bool login = false, bool needModify = false)
|
public async Task Authorize(bool login = false, bool needModify = false)
|
||||||
{
|
{
|
||||||
|
_client = null;
|
||||||
|
|
||||||
if (!login)
|
if (!login)
|
||||||
{
|
{
|
||||||
var config = SpotifyClientConfig.CreateDefault();
|
var config = SpotifyClientConfig.CreateDefault();
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
||||||
<PackageReference Include="SmallestCSVParser" Version="1.1.1" />
|
<PackageReference Include="SmallestCSVParser" Version="1.1.1" />
|
||||||
<PackageReference Include="Soulseek" Version="6.4.1" />
|
<PackageReference Include="Soulseek" Version="6.4.1" />
|
||||||
<PackageReference Include="SpotifyAPI.Web" Version="7.0.2" />
|
<PackageReference Include="SpotifyAPI.Web" Version="7.1.1" />
|
||||||
<PackageReference Include="SpotifyAPI.Web.Auth" Version="7.0.2" />
|
<PackageReference Include="SpotifyAPI.Web.Auth" Version="7.1.1" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageReference Include="YoutubeExplode" Version="6.3.16" />
|
<PackageReference Include="YoutubeExplode" Version="6.3.16" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
Loading…
Reference in a new issue