mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
commit
This commit is contained in:
parent
e4505a53c7
commit
4a3e8ddadb
3 changed files with 145 additions and 45 deletions
25
README.md
25
README.md
|
@ -7,26 +7,41 @@ A batch downloader for Soulseek built with Soulseek.NET. Accepts CSV files or Sp
|
||||||
Download tracks from a csv file:
|
Download tracks from a csv file:
|
||||||
```
|
```
|
||||||
slsk-batchdl test.csv
|
slsk-batchdl test.csv
|
||||||
```
|
```
|
||||||
|
<details>
|
||||||
|
<summary>CSV details</summary>
|
||||||
|
|
||||||
The names of the columns in the csv should be: `Artist`, `Title`, `Album`, `Length`. Some alternatives are also accepted. You can use `--print tracks` before downloading to check if everything has been parsed correctly. Only the title or album column is required, but additional info may improve search results.
|
The names of the columns in the csv should be: `Artist`, `Title`, `Album`, `Length`. Some alternatives are also accepted. You can use `--print tracks` before downloading to check if everything has been parsed correctly. Only the title or album column is required, but additional info may improve search results.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Download spotify likes while skipping songs that already exist in the output folder:
|
Download spotify likes while skipping songs that already exist in the output folder:
|
||||||
```
|
```
|
||||||
slsk-batchdl spotify-likes --skip-existing
|
slsk-batchdl spotify-likes --skip-existing
|
||||||
```
|
```
|
||||||
|
<details>
|
||||||
|
<summary>Spotify details</summary>
|
||||||
|
|
||||||
To download private playlists or liked songs you will need to provide a client id and secret, which you can get here https://developer.spotify.com/dashboard/applications. Create an app and add `http://localhost:48721/callback` as a redirect url in its settings.
|
To download private playlists or liked songs you will need to provide a client id and secret, which you can get here https://developer.spotify.com/dashboard/applications. Create an app and add `http://localhost:48721/callback` as a redirect url in its settings.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Download from a youtube playlist with fallback to yt-dlp in case it is not found on soulseek, and retrieve deleted video titles from wayback machine:
|
Download from a youtube playlist with fallback to yt-dlp in case it is not found on soulseek, and retrieve deleted video titles from wayback machine:
|
||||||
```
|
```
|
||||||
slsk-batchdl --get-deleted --yt-dlp "https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX"
|
slsk-batchdl --get-deleted --yt-dlp "https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX"
|
||||||
```
|
```
|
||||||
|
<details>
|
||||||
|
<summary>YouTube details</summary>
|
||||||
|
|
||||||
Playlists are retrieved using the YoutubeExplode library which unfortunately doesn't always return all videos. You can use the official API by providing a key with `--youtube-key`. Get it here https://console.cloud.google.com. Create a new project, click "Enable Api" and search for "youtube data", then follow the prompts.
|
Playlists are retrieved using the YoutubeExplode library which unfortunately doesn't always return all videos. You can use the official API by providing a key with `--youtube-key`. Get it here https://console.cloud.google.com. Create a new project, click "Enable Api" and search for "youtube data", then follow the prompts.
|
||||||
Also note that due the high number of music videos in the above example playlist, it may be better to remove all text in parentheses and disable song duration checking: `--regex "[\[\(].*?[\]\)]" --length-tol -1 --pref-length-tol -1`.
|
Also note that due the high number of music videos in the above example playlist, it may be better to remove all text in parentheses and disable song duration checking: `--regex "[\[\(].*?[\]\)]" --length-tol -1 --pref-length-tol -1`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Search & download a specific song, preferring lossless:
|
Search & download a specific song, preferring lossless:
|
||||||
|
@ -54,7 +69,7 @@ Depending on the provided input, the download behaviour changes:
|
||||||
|
|
||||||
- Normal download: When the song title is set (in the CSV row, or in the string input), the program will download a single file for every entry.
|
- Normal download: When the song title is set (in the CSV row, or in the string input), the program will download a single file for every entry.
|
||||||
- Album download: When the album name is set and the song title is NOT set, the program will search for the album and download the entire folder.
|
- Album download: When the album name is set and the song title is NOT set, the program will search for the album and download the entire folder.
|
||||||
- Aggregate download: With `--aggregate`, the program will first perform an ordinary search for the input, then attempt to group the results into distinct songs and download one of each kind. This can be used to download an artist's entire discography (or simply printing it, like in the example above).
|
- Aggregate download: With `--aggregate`, the program will first perform an ordinary search for the input, then attempt to group the results into distinct songs and download one of each kind. This can be used to download an artist's entire discography (or simply printing it, like in the example above), finding remixes of a song, etc. Note that it is not 100% reliable, which is why `--min-users-aggregate` is set to 2 by default, i.e. any song that is shared by only one person will be ignored. Enabling `--relax` will give even more results.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
```
|
```
|
||||||
|
@ -90,8 +105,8 @@ Options:
|
||||||
-n --number <maxtracks> Download the first n tracks of a playlist
|
-n --number <maxtracks> Download the first n tracks of a playlist
|
||||||
-o --offset <offset> Skip a specified number of tracks
|
-o --offset <offset> Skip a specified number of tracks
|
||||||
-r --reverse Download tracks in reverse order
|
-r --reverse Download tracks in reverse order
|
||||||
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
|
--nf --name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
|
||||||
--fast-search Begin downloading as soon as a file satisfying the preferred
|
--fs --fast-search Begin downloading as soon as a file satisfying the preferred
|
||||||
conditions is found. Increases chance to download bad files.
|
conditions is found. Increases chance to download bad files.
|
||||||
--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
|
||||||
|
@ -161,6 +176,7 @@ Options:
|
||||||
'default': Download from the same folder as the music
|
'default': Download from the same folder as the music
|
||||||
'largest': Download from the folder with the largest image
|
'largest': Download from the folder with the largest image
|
||||||
'most': Download from the folder containing the most images
|
'most': Download from the folder containing the most images
|
||||||
|
--album-art-only Only download album art for the provided album
|
||||||
|
|
||||||
-s --skip-existing Skip if a track matching file conditions is found in the
|
-s --skip-existing Skip if a track matching file conditions is found in the
|
||||||
output folder or your music library (if provided)
|
output folder or your music library (if provided)
|
||||||
|
@ -212,6 +228,7 @@ Options:
|
||||||
### File conditions
|
### File conditions
|
||||||
Files not satisfying the conditions will not be downloaded. For example, `--length-tol` is set to 3 by default, meaning that files whose duration differs from the supplied duration by more than 3 seconds will not be downloaded (can be disabled by setting it to -1).
|
Files not satisfying the conditions will not be downloaded. For example, `--length-tol` is set to 3 by default, meaning that files whose duration differs from the supplied duration by more than 3 seconds will not be downloaded (can be disabled by setting it to -1).
|
||||||
Files satisfying `pref-` conditions will be preferred; setting `--pref-format "flac,wav"` will make it download high quality files if they exist, and only download low quality files if there's nothing else. Conditions can also be supplied as a semicolon-delimited string to `--cond` and `--pref`, e.g `--cond "br>=320;f=mp3,ogg;sr<96000"`.\
|
Files satisfying `pref-` conditions will be preferred; setting `--pref-format "flac,wav"` will make it download high quality files if they exist, and only download low quality files if there's nothing else. Conditions can also be supplied as a semicolon-delimited string to `--cond` and `--pref`, e.g `--cond "br>=320;f=mp3,ogg;sr<96000"`.\
|
||||||
|
|
||||||
**Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict`. (As a consequence, if `--strict` and `--min-bitrate` is set then any files shared by users with the default client will be ignored)
|
**Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict`. (As a consequence, if `--strict` and `--min-bitrate` is set then any files shared by users with the default client will be ignored)
|
||||||
|
|
||||||
### Name format
|
### Name format
|
||||||
|
|
|
@ -116,7 +116,9 @@ static class Program
|
||||||
static string descCol = "";
|
static string descCol = "";
|
||||||
static string lengthCol = "";
|
static string lengthCol = "";
|
||||||
static bool aggregate = false;
|
static bool aggregate = false;
|
||||||
|
static bool album = false;
|
||||||
static string albumArtOption = "";
|
static string albumArtOption = "";
|
||||||
|
static bool albumArtOnly = false;
|
||||||
static bool interactiveMode = false;
|
static bool interactiveMode = false;
|
||||||
static bool albumIgnoreFails = false;
|
static bool albumIgnoreFails = false;
|
||||||
static int albumTrackCount = -1;
|
static int albumTrackCount = -1;
|
||||||
|
@ -187,7 +189,7 @@ static class Program
|
||||||
// undocumented options:
|
// undocumented options:
|
||||||
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col
|
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col
|
||||||
// --remove-brackets, --spotify, --csv, --string, --youtube, --random-login
|
// --remove-brackets, --spotify, --csv, --string, --youtube, --random-login
|
||||||
// --danger-words, --pref-danger-words, --no-modify-share-count, --yt-dlp-argument
|
// --danger-words, --pref-danger-words, --no-modify-share-count, --yt-dlp-argument, --album, --album-art-only
|
||||||
Console.WriteLine("Usage: slsk-batchdl <input> [OPTIONS]" +
|
Console.WriteLine("Usage: slsk-batchdl <input> [OPTIONS]" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n <input> <input> is one of the following:" +
|
"\n <input> <input> is one of the following:" +
|
||||||
|
@ -220,8 +222,8 @@ static class Program
|
||||||
"\n -n --number <maxtracks> Download the first n tracks of a playlist" +
|
"\n -n --number <maxtracks> Download the first n tracks of a playlist" +
|
||||||
"\n -o --offset <offset> Skip a specified number of tracks" +
|
"\n -o --offset <offset> Skip a specified number of tracks" +
|
||||||
"\n -r --reverse Download tracks in reverse order" +
|
"\n -r --reverse Download tracks in reverse order" +
|
||||||
"\n --name-format <format> Name format for downloaded tracks, e.g \"{artist} - {title}\"" +
|
"\n --nf --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 --fs --fast-search Begin downloading as soon as a file satisfying the preferred" +
|
||||||
"\n conditions is found. Increases chance to download bad files." +
|
"\n conditions is found. Increases chance to download bad files." +
|
||||||
"\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" +
|
||||||
|
@ -291,6 +293,7 @@ static class Program
|
||||||
"\n 'default': Download from the same folder as the music" +
|
"\n 'default': Download from the same folder as the music" +
|
||||||
"\n 'largest': Download from the folder with the largest image" +
|
"\n 'largest': Download from the folder with the largest image" +
|
||||||
"\n 'most': Download from the folder containing the most images" +
|
"\n 'most': Download from the folder containing the most images" +
|
||||||
|
"\n --album-art-only Only download album art for the provided album" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n -s --skip-existing Skip if a track matching file conditions is found in the" +
|
"\n -s --skip-existing Skip if a track matching file conditions is found in the" +
|
||||||
"\n output folder or your music library (if provided)" +
|
"\n output folder or your music library (if provided)" +
|
||||||
|
@ -422,6 +425,7 @@ static class Program
|
||||||
case "--folder":
|
case "--folder":
|
||||||
folderName = args[++i];
|
folderName = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--md":
|
||||||
case "--music-dir":
|
case "--music-dir":
|
||||||
musicDir = args[++i];
|
musicDir = args[++i];
|
||||||
break;
|
break;
|
||||||
|
@ -429,6 +433,7 @@ static class Program
|
||||||
case "--aggregate":
|
case "--aggregate":
|
||||||
aggregate = true;
|
aggregate = true;
|
||||||
break;
|
break;
|
||||||
|
case "--mua":
|
||||||
case "--min-users-aggregate":
|
case "--min-users-aggregate":
|
||||||
minUsersAggregate = int.Parse(args[++i]);
|
minUsersAggregate = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
@ -452,6 +457,7 @@ static class Program
|
||||||
case "--password":
|
case "--password":
|
||||||
password = args[++i];
|
password = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--rl":
|
||||||
case "--random-login":
|
case "--random-login":
|
||||||
useRandomLogin = true;
|
useRandomLogin = true;
|
||||||
break;
|
break;
|
||||||
|
@ -478,6 +484,7 @@ static class Program
|
||||||
case "--offset":
|
case "--offset":
|
||||||
offset = int.Parse(args[++i]);
|
offset = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--nf":
|
||||||
case "--name-format":
|
case "--name-format":
|
||||||
nameFormat = args[++i];
|
nameFormat = args[++i];
|
||||||
break;
|
break;
|
||||||
|
@ -517,21 +524,27 @@ static class Program
|
||||||
useYtdlp = true;
|
useYtdlp = true;
|
||||||
break;
|
break;
|
||||||
case "-s":
|
case "-s":
|
||||||
|
case "--se":
|
||||||
case "--skip-existing":
|
case "--skip-existing":
|
||||||
skipExisting = true;
|
skipExisting = true;
|
||||||
break;
|
break;
|
||||||
|
case "--snf":
|
||||||
case "--skip-not-found":
|
case "--skip-not-found":
|
||||||
skipNotFound = true;
|
skipNotFound = true;
|
||||||
break;
|
break;
|
||||||
|
case "--rfp":
|
||||||
case "--remove-from-playlist":
|
case "--remove-from-playlist":
|
||||||
removeTracksFromSource = true;
|
removeTracksFromSource = true;
|
||||||
break;
|
break;
|
||||||
|
case "--rft":
|
||||||
case "--remove-ft":
|
case "--remove-ft":
|
||||||
removeFt = true;
|
removeFt = true;
|
||||||
break;
|
break;
|
||||||
|
case "--rb":
|
||||||
case "--remove-brackets":
|
case "--remove-brackets":
|
||||||
removeBrackets = true;
|
removeBrackets = true;
|
||||||
break;
|
break;
|
||||||
|
case "--gd":
|
||||||
case "--get-deleted":
|
case "--get-deleted":
|
||||||
getDeleted = true;
|
getDeleted = true;
|
||||||
break;
|
break;
|
||||||
|
@ -549,31 +562,43 @@ static class Program
|
||||||
reverse = true;
|
reverse = true;
|
||||||
break;
|
break;
|
||||||
case "--m3u":
|
case "--m3u":
|
||||||
|
case "--m3u8":
|
||||||
m3uOption = args[++i];
|
m3uOption = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--port":
|
||||||
case "--listen-port":
|
case "--listen-port":
|
||||||
listenPort = int.Parse(args[++i]);
|
listenPort = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--st":
|
||||||
|
case "--timeout":
|
||||||
case "--search-timeout":
|
case "--search-timeout":
|
||||||
searchTimeout = int.Parse(args[++i]);
|
searchTimeout = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--mst":
|
||||||
|
case "--stale-time":
|
||||||
case "--max-stale-time":
|
case "--max-stale-time":
|
||||||
downloadMaxStaleTime = int.Parse(args[++i]);
|
downloadMaxStaleTime = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--cp":
|
||||||
case "--processes":
|
case "--processes":
|
||||||
case "--concurrent-processes":
|
case "--concurrent-processes":
|
||||||
case "--concurrent-downloads":
|
case "--concurrent-downloads":
|
||||||
maxConcurrentProcesses = int.Parse(args[++i]);
|
maxConcurrentProcesses = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--spt":
|
||||||
case "--searches-per-time":
|
case "--searches-per-time":
|
||||||
searchesPerTime = int.Parse(args[++i]);
|
searchesPerTime = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--srt":
|
||||||
case "--searches-renew-time":
|
case "--searches-renew-time":
|
||||||
searchResetTime = int.Parse(args[++i]);
|
searchResetTime = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--mr":
|
||||||
|
case "--retries":
|
||||||
case "--max-retries":
|
case "--max-retries":
|
||||||
maxRetriesPerTrack = int.Parse(args[++i]);
|
maxRetriesPerTrack = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--atc":
|
||||||
case "--album-track-count":
|
case "--album-track-count":
|
||||||
string a = args[++i];
|
string a = args[++i];
|
||||||
if (a.Last() == '+' || a.Last() == '-')
|
if (a.Last() == '+' || a.Last() == '-')
|
||||||
|
@ -583,6 +608,7 @@ static class Program
|
||||||
}
|
}
|
||||||
albumTrackCount = int.Parse(a);
|
albumTrackCount = int.Parse(a);
|
||||||
break;
|
break;
|
||||||
|
case "--aa":
|
||||||
case "--album-art":
|
case "--album-art":
|
||||||
switch (args[++i])
|
switch (args[++i])
|
||||||
{
|
{
|
||||||
|
@ -597,15 +623,31 @@ static class Program
|
||||||
throw new ArgumentException($"Invalid album art download mode \'{args[i]}\'");
|
throw new ArgumentException($"Invalid album art download mode \'{args[i]}\'");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "--aao":
|
||||||
|
case "--aa-only":
|
||||||
|
case "--album-art-only":
|
||||||
|
albumArtOnly = true;
|
||||||
|
if (albumArtOption == "")
|
||||||
|
{
|
||||||
|
albumArtOption = "largest";
|
||||||
|
}
|
||||||
|
preferredCond = new FileConditions();
|
||||||
|
necessaryCond = new FileConditions();
|
||||||
|
break;
|
||||||
|
case "--aif":
|
||||||
case "--album-ignore-fails":
|
case "--album-ignore-fails":
|
||||||
albumIgnoreFails = true;
|
albumIgnoreFails = true;
|
||||||
break;
|
break;
|
||||||
|
case "--int":
|
||||||
case "--interactive":
|
case "--interactive":
|
||||||
interactiveMode = true;
|
interactiveMode = true;
|
||||||
break;
|
break;
|
||||||
|
case "--pref-f":
|
||||||
|
case "--pref-af":
|
||||||
case "--pref-format":
|
case "--pref-format":
|
||||||
preferredCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
preferredCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
break;
|
break;
|
||||||
|
case "--pref-t":
|
||||||
case "--pref-tol":
|
case "--pref-tol":
|
||||||
case "--pref-length-tol":
|
case "--pref-length-tol":
|
||||||
preferredCond.LengthTolerance = int.Parse(args[++i]);
|
preferredCond.LengthTolerance = int.Parse(args[++i]);
|
||||||
|
@ -647,6 +689,7 @@ static class Program
|
||||||
case "--pref-max-bitdepth":
|
case "--pref-max-bitdepth":
|
||||||
preferredCond.MaxBitDepth = int.Parse(args[++i]);
|
preferredCond.MaxBitDepth = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--af":
|
||||||
case "--format":
|
case "--format":
|
||||||
necessaryCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
necessaryCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
break;
|
break;
|
||||||
|
@ -700,9 +743,11 @@ static class Program
|
||||||
case "--preferred":
|
case "--preferred":
|
||||||
ParseConditions(preferredCond, args[++i]);
|
ParseConditions(preferredCond, args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--nmsc":
|
||||||
case "--no-modify-share-count":
|
case "--no-modify-share-count":
|
||||||
noModifyShareCount = true;
|
noModifyShareCount = true;
|
||||||
break;
|
break;
|
||||||
|
case "--seut":
|
||||||
case "--skip-existing-use-tags":
|
case "--skip-existing-use-tags":
|
||||||
skipExisting = true;
|
skipExisting = true;
|
||||||
useTagsCheckExisting = true;
|
useTagsCheckExisting = true;
|
||||||
|
@ -723,6 +768,7 @@ static class Program
|
||||||
throw new ArgumentException($"Invalid display style \"{args[i]}\"");
|
throw new ArgumentException($"Invalid display style \"{args[i]}\"");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "--sm":
|
||||||
case "--skip-mode":
|
case "--skip-mode":
|
||||||
switch (args[++i])
|
switch (args[++i])
|
||||||
{
|
{
|
||||||
|
@ -737,12 +783,15 @@ static class Program
|
||||||
throw new ArgumentException($"Invalid skip mode \'{args[i]}\'");
|
throw new ArgumentException($"Invalid skip mode \'{args[i]}\'");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "--nrsc":
|
||||||
case "--no-remove-special-chars":
|
case "--no-remove-special-chars":
|
||||||
noRemoveSpecialChars = true;
|
noRemoveSpecialChars = true;
|
||||||
break;
|
break;
|
||||||
|
case "--amw":
|
||||||
case "--artist-maybe-wrong":
|
case "--artist-maybe-wrong":
|
||||||
artistMaybeWrong = true;
|
artistMaybeWrong = true;
|
||||||
break;
|
break;
|
||||||
|
case "--fs":
|
||||||
case "--fast-search":
|
case "--fast-search":
|
||||||
fastSearch = true;
|
fastSearch = true;
|
||||||
break;
|
break;
|
||||||
|
@ -753,9 +802,14 @@ static class Program
|
||||||
preferredCond.AcceptMissingProps = false;
|
preferredCond.AcceptMissingProps = false;
|
||||||
necessaryCond.AcceptMissingProps = false;
|
necessaryCond.AcceptMissingProps = false;
|
||||||
break;
|
break;
|
||||||
|
case "--yda":
|
||||||
case "--yt-dlp-argument":
|
case "--yt-dlp-argument":
|
||||||
ytdlpArgument = args[++i];
|
ytdlpArgument = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--al":
|
||||||
|
case "--album":
|
||||||
|
album = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException($"Unknown argument: {args[i]}");
|
throw new ArgumentException($"Unknown argument: {args[i]}");
|
||||||
}
|
}
|
||||||
|
@ -804,7 +858,7 @@ static class Program
|
||||||
if (reverse)
|
if (reverse)
|
||||||
{
|
{
|
||||||
trackLists.Reverse();
|
trackLists.Reverse();
|
||||||
trackLists = TrackLists.FromFlatList(trackLists.Flattened().Skip(offset).Take(maxTracks).ToList(), aggregate);
|
trackLists = TrackLists.FromFlatList(trackLists.Flattened().Skip(offset).Take(maxTracks).ToList(), aggregate, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
PreprocessTrackList(trackLists);
|
PreprocessTrackList(trackLists);
|
||||||
|
@ -877,10 +931,13 @@ static class Program
|
||||||
tracks.InsertRange(0, deleted);
|
tracks.InsertRange(0, deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackLists.AddEntry(tracks);
|
|
||||||
defaultFolderName = ReplaceInvalidChars(name, " ");
|
|
||||||
|
|
||||||
YouTube.StopService();
|
YouTube.StopService();
|
||||||
|
trackLists.AddEntry(tracks);
|
||||||
|
|
||||||
|
if (album || aggregate)
|
||||||
|
trackLists = TrackLists.FromFlatList(trackLists.Flattened().ToList(), aggregate, album);
|
||||||
|
|
||||||
|
defaultFolderName = ReplaceInvalidChars(name, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -955,6 +1012,9 @@ static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
trackLists.AddEntry(tracks);
|
trackLists.AddEntry(tracks);
|
||||||
|
if (album || aggregate)
|
||||||
|
trackLists = TrackLists.FromFlatList(trackLists.Flattened().ToList(), aggregate, album);
|
||||||
|
|
||||||
defaultFolderName = ReplaceInvalidChars(playlistName, " ");
|
defaultFolderName = ReplaceInvalidChars(playlistName, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -972,7 +1032,7 @@ static class Program
|
||||||
|
|
||||||
var tracks = await ParseCsvIntoTrackInfo(csvPath, artistCol, trackCol, lengthCol, albumCol, descCol, ytIdCol, timeUnit, ytParse);
|
var tracks = await ParseCsvIntoTrackInfo(csvPath, artistCol, trackCol, lengthCol, albumCol, descCol, ytIdCol, timeUnit, ytParse);
|
||||||
tracks = tracks.Skip(off).Take(max).ToList();
|
tracks = tracks.Skip(off).Take(max).ToList();
|
||||||
trackLists = TrackLists.FromFlatList(tracks, aggregate);
|
trackLists = TrackLists.FromFlatList(tracks, aggregate, album);
|
||||||
defaultFolderName = Path.GetFileNameWithoutExtension(csvPath);
|
defaultFolderName = Path.GetFileNameWithoutExtension(csvPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -984,7 +1044,11 @@ static class Program
|
||||||
var music = ParseTrackArg(searchStr);
|
var music = ParseTrackArg(searchStr);
|
||||||
bool isAlbum = false;
|
bool isAlbum = false;
|
||||||
|
|
||||||
if (!aggregate && music.TrackTitle != "")
|
if (album)
|
||||||
|
{
|
||||||
|
trackLists.AddEntry(TrackLists.ListType.Album, new Track(music) { TrackIsAlbum = true });
|
||||||
|
}
|
||||||
|
else if (!aggregate && music.TrackTitle != "")
|
||||||
{
|
{
|
||||||
trackLists.AddEntry(music);
|
trackLists.AddEntry(music);
|
||||||
}
|
}
|
||||||
|
@ -1003,7 +1067,7 @@ static class Program
|
||||||
throw new ArgumentException("Need track title or album");
|
throw new ArgumentException("Need track title or album");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aggregate || isAlbum)
|
if (aggregate || isAlbum || album)
|
||||||
defaultFolderName = ReplaceInvalidChars(music.ToString(true), " ").Trim();
|
defaultFolderName = ReplaceInvalidChars(music.ToString(true), " ").Trim();
|
||||||
else
|
else
|
||||||
defaultFolderName = ".";
|
defaultFolderName = ".";
|
||||||
|
@ -1028,9 +1092,9 @@ static class Program
|
||||||
|
|
||||||
if (trackLists.lists.Count > 1 || type != TrackLists.ListType.Normal)
|
if (trackLists.lists.Count > 1 || type != TrackLists.ListType.Normal)
|
||||||
{
|
{
|
||||||
string sourceStr = type == TrackLists.ListType.Normal ? "" : $" {source.ToString(noInfo: type == TrackLists.ListType.Album)}";
|
string sourceStr = type == TrackLists.ListType.Normal ? "" : $": {source.ToString(noInfo: type == TrackLists.ListType.Album)}";
|
||||||
bool needSearchStr = type == TrackLists.ListType.Normal || skipNotFound && source.TrackState == Track.State.NotFoundLastTime;
|
bool needSearchStr = type == TrackLists.ListType.Normal || skipNotFound && source.TrackState == Track.State.NotFoundLastTime;
|
||||||
string searchStr = needSearchStr ? "" : $", searching...";
|
string searchStr = needSearchStr ? "" : $", searching..";
|
||||||
Console.WriteLine($"{Enum.GetName(typeof(TrackLists.ListType), type)} download{sourceStr}{searchStr}");
|
Console.WriteLine($"{Enum.GetName(typeof(TrackLists.ListType), type)} download{sourceStr}{searchStr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1068,7 +1132,11 @@ static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
m3uEditor.Update();
|
m3uEditor.Update();
|
||||||
PrintTracksTbd(list[0].Where(t => t.TrackState == Track.State.Initial).ToList(), existing, notFound, type);
|
|
||||||
|
if (!interactiveMode)
|
||||||
|
{
|
||||||
|
PrintTracksTbd(list[0].Where(t => t.TrackState == Track.State.Initial).ToList(), existing, notFound, type);
|
||||||
|
}
|
||||||
|
|
||||||
if (debugPrintTracks || list.Count == 0 || list[0].Count == 0)
|
if (debugPrintTracks || list.Count == 0 || list[0].Count == 0)
|
||||||
{
|
{
|
||||||
|
@ -1082,7 +1150,7 @@ static class Program
|
||||||
}
|
}
|
||||||
else if (type == TrackLists.ListType.Album)
|
else if (type == TrackLists.ListType.Album)
|
||||||
{
|
{
|
||||||
await TracksDownloadAlbum(list);
|
await TracksDownloadAlbum(list, albumArtOnly);
|
||||||
}
|
}
|
||||||
else if (type == TrackLists.ListType.Aggregate)
|
else if (type == TrackLists.ListType.Aggregate)
|
||||||
{
|
{
|
||||||
|
@ -1285,7 +1353,7 @@ static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static async Task TracksDownloadAlbum(List<List<Track>> list) // bad
|
static async Task TracksDownloadAlbum(List<List<Track>> list, bool imagesOnly) // bad
|
||||||
{
|
{
|
||||||
var dlFiles = new ConcurrentDictionary<string, char>();
|
var dlFiles = new ConcurrentDictionary<string, char>();
|
||||||
var dlAdditionalImages = new ConcurrentDictionary<string, char>();
|
var dlAdditionalImages = new ConcurrentDictionary<string, char>();
|
||||||
|
@ -1294,6 +1362,33 @@ static class Program
|
||||||
bool albumDlFailed = false;
|
bool albumDlFailed = false;
|
||||||
var listRef = list;
|
var listRef = list;
|
||||||
|
|
||||||
|
void prepareImageDownload()
|
||||||
|
{
|
||||||
|
var albumArtList = list.Select(tracks => tracks.Where(t => Utils.IsImageFile(t.Downloads.First().Value.Item2.Filename))).Where(tracks => tracks.Any());
|
||||||
|
if (albumArtOption == "largest")
|
||||||
|
{
|
||||||
|
list = albumArtList // shitty shortcut
|
||||||
|
.OrderByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Max() / 1024 / 100)
|
||||||
|
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
||||||
|
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
||||||
|
.Select(x => x.ToList()).ToList();
|
||||||
|
}
|
||||||
|
else if (albumArtOption == "most")
|
||||||
|
{
|
||||||
|
list = albumArtList // shitty shortcut
|
||||||
|
.OrderByDescending(tracks => tracks.Count())
|
||||||
|
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
||||||
|
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
||||||
|
.Select(x => x.ToList()).ToList();
|
||||||
|
}
|
||||||
|
downloadingImages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesOnly)
|
||||||
|
{
|
||||||
|
prepareImageDownload();
|
||||||
|
}
|
||||||
|
|
||||||
while (list.Count > 0)
|
while (list.Count > 0)
|
||||||
{
|
{
|
||||||
albumDlFailed = false;
|
albumDlFailed = false;
|
||||||
|
@ -1386,24 +1481,7 @@ static class Program
|
||||||
|
|
||||||
if (!downloadingImages && !albumDlFailed && albumArtOption != "")
|
if (!downloadingImages && !albumDlFailed && albumArtOption != "")
|
||||||
{
|
{
|
||||||
var albumArtList = list.Select(tracks => tracks.Where(t => Utils.IsImageFile(t.Downloads.First().Value.Item2.Filename))).Where(tracks => tracks.Any());
|
prepareImageDownload();
|
||||||
if (albumArtOption == "largest")
|
|
||||||
{
|
|
||||||
list = albumArtList // shitty shortcut
|
|
||||||
.OrderByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Max() / 1024 / 100)
|
|
||||||
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
|
||||||
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
|
||||||
.Select(x => x.ToList()).ToList();
|
|
||||||
}
|
|
||||||
else if (albumArtOption == "most")
|
|
||||||
{
|
|
||||||
list = albumArtList // shitty shortcut
|
|
||||||
.OrderByDescending(tracks => tracks.Count())
|
|
||||||
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
|
||||||
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
|
||||||
.Select(x => x.ToList()).ToList();
|
|
||||||
}
|
|
||||||
downloadingImages = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1764,7 +1842,7 @@ static class Program
|
||||||
|
|
||||||
string fullPath((SearchResponse r, Soulseek.File f) x) { return x.r.Username + "\\" + x.f.Filename; }
|
string fullPath((SearchResponse r, Soulseek.File f) x) { return x.r.Username + "\\" + x.f.Filename; }
|
||||||
|
|
||||||
var groupedLists = OrderedResults(results, track, albumMode: false)
|
var groupedLists = OrderedResults(results, track, albumMode: true)
|
||||||
.GroupBy(x => fullPath(x).Substring(0, fullPath(x).LastIndexOf('\\')));
|
.GroupBy(x => fullPath(x).Substring(0, fullPath(x).LastIndexOf('\\')));
|
||||||
|
|
||||||
var musicFolders = groupedLists
|
var musicFolders = groupedLists
|
||||||
|
@ -1982,6 +2060,8 @@ static class Program
|
||||||
useBracketCheck = false;
|
useBracketCheck = false;
|
||||||
useLevenshtein = false;
|
useLevenshtein = false;
|
||||||
useInfer = false;
|
useInfer = false;
|
||||||
|
preferredCond.StrictTitle = false; // bad!
|
||||||
|
necessaryCond.StrictTitle = false; // bad!
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<string, (Track, int)>? result = null;
|
Dictionary<string, (Track, int)>? result = null;
|
||||||
|
@ -3689,7 +3769,7 @@ public class TrackLists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TrackLists FromFlatList(List<Track> flatList, bool aggregate)
|
public static TrackLists FromFlatList(List<Track> flatList, bool aggregate, bool album)
|
||||||
{
|
{
|
||||||
var res = new TrackLists();
|
var res = new TrackLists();
|
||||||
for (int i = 0; i < flatList.Count; i++)
|
for (int i = 0; i < flatList.Count; i++)
|
||||||
|
@ -3698,9 +3778,9 @@ public class TrackLists
|
||||||
{
|
{
|
||||||
res.AddEntry(ListType.Aggregate, flatList[i]);
|
res.AddEntry(ListType.Aggregate, flatList[i]);
|
||||||
}
|
}
|
||||||
else if (flatList[i].Album != "" && flatList[i].TrackTitle == "")
|
else if (album || (flatList[i].Album != "" && flatList[i].TrackTitle == ""))
|
||||||
{
|
{
|
||||||
res.AddEntry(ListType.Album, new Track(flatList[i]) { TrackIsAlbum=true });
|
res.AddEntry(ListType.Album, new Track(flatList[i]) { TrackIsAlbum = true });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -3955,7 +4035,7 @@ public class M3UEditor
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
this.option = option;
|
this.option = option;
|
||||||
path = Path.GetFullPath(m3uPath);
|
path = Path.GetFullPath(m3uPath);
|
||||||
m3uListLabels = trackLists.lists.Any(x => x.type != TrackLists.ListType.Normal);
|
m3uListLabels = false;/*trackLists.lists.Any(x => x.type != TrackLists.ListType.Normal);*/
|
||||||
fails = ReadAllLines()
|
fails = ReadAllLines()
|
||||||
.Where(x => x.StartsWith("# Failed: "))
|
.Where(x => x.StartsWith("# Failed: "))
|
||||||
.Select(line =>
|
.Select(line =>
|
||||||
|
|
|
@ -480,14 +480,17 @@ public static class YouTube
|
||||||
ytdlpArgument = "\"{id}\" -f bestaudio/best -ci -o \"{savepath-noext}.%(ext)s\" -x";
|
ytdlpArgument = "\"{id}\" -f bestaudio/best -ci -o \"{savepath-noext}.%(ext)s\" -x";
|
||||||
|
|
||||||
startInfo.FileName = "yt-dlp";
|
startInfo.FileName = "yt-dlp";
|
||||||
startInfo.Arguments = ytdlpArgument.Replace("{id}", id).Replace("{savepath-noext}", savePathNoExt);
|
startInfo.Arguments = ytdlpArgument
|
||||||
|
.Replace("{id}", id)
|
||||||
|
.Replace("{savepath-noext}", savePathNoExt)
|
||||||
|
.Replace("{savedir}", Path.GetDirectoryName(savePathNoExt));
|
||||||
|
|
||||||
startInfo.RedirectStandardOutput = true;
|
startInfo.RedirectStandardOutput = true;
|
||||||
startInfo.RedirectStandardError = true;
|
startInfo.RedirectStandardError = true;
|
||||||
startInfo.UseShellExecute = false;
|
startInfo.UseShellExecute = false;
|
||||||
process.StartInfo = startInfo;
|
process.StartInfo = startInfo;
|
||||||
process.OutputDataReceived += (sender, e) => { Console.WriteLine(e.Data); };
|
//process.OutputDataReceived += (sender, e) => { Console.WriteLine(e.Data); };
|
||||||
process.ErrorDataReceived += (sender, e) => { Console.WriteLine(e.Data); };
|
//process.ErrorDataReceived += (sender, e) => { Console.WriteLine(e.Data); };
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
|
|
Loading…
Reference in a new issue