|
|
|
@ -1,10 +1,9 @@
|
|
|
|
|
import std.stdio, std.regex, std.json, std.file, std.datetime, std.conv, std.process, std.net.curl, std.string;
|
|
|
|
|
import qobuz.api;
|
|
|
|
|
import etc.c.curl;
|
|
|
|
|
|
|
|
|
|
int main(string[] args)
|
|
|
|
|
{
|
|
|
|
|
string VERSION = "1.4";
|
|
|
|
|
string VERSION = "1.1";
|
|
|
|
|
|
|
|
|
|
if (args.length != 2) {
|
|
|
|
|
writefln("Usage: %s <album id or url>", args[0]);
|
|
|
|
@ -39,7 +38,7 @@ int main(string[] args)
|
|
|
|
|
try {
|
|
|
|
|
title = album["title"].str;
|
|
|
|
|
artist = album["artist"]["name"].str;
|
|
|
|
|
genre = album["genre"]["name"].str;
|
|
|
|
|
genre = album["genres_list"][0].str;
|
|
|
|
|
auto releaseTime = SysTime.fromUnixTime(album["released_at"].integer, UTC());
|
|
|
|
|
year = releaseTime.year.text;
|
|
|
|
|
|
|
|
|
@ -52,8 +51,6 @@ int main(string[] args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string dirName = artist~" - "~title~" ("~year~") [WEB FLAC]";
|
|
|
|
|
dirName = dirName.replaceAll(regex("[\\?<>:\"/\\\\|\\*]"), "");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
mkdir(dirName);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
@ -61,24 +58,14 @@ int main(string[] args)
|
|
|
|
|
return -9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto discs = tracks[tracks.length - 1]["media_number"].integer;
|
|
|
|
|
|
|
|
|
|
foreach (track; tracks) {
|
|
|
|
|
string url, num, discNum, trackName, trackArtist;
|
|
|
|
|
foreach (i, track; tracks) {
|
|
|
|
|
auto num = (i+1).text;
|
|
|
|
|
string url, trackName;
|
|
|
|
|
try {
|
|
|
|
|
num = track["track_number"].integer.text;
|
|
|
|
|
discNum = track["media_number"].integer.text;
|
|
|
|
|
trackName = track["title"].str;
|
|
|
|
|
try {
|
|
|
|
|
trackArtist = track["performer"]["name"].str;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// Qobuz doesn't return a "performer" for all albums, and I'm not sure about
|
|
|
|
|
// the best way to deal with this. Leaving blank for now.A
|
|
|
|
|
trackArtist = "";
|
|
|
|
|
}
|
|
|
|
|
if (num.length < 2)
|
|
|
|
|
num = "0"~num;
|
|
|
|
|
writef(" [%s/%s] %s... ", discNum, num, trackName);
|
|
|
|
|
writef(" [%s] %s... ", num, trackName);
|
|
|
|
|
stdout.flush;
|
|
|
|
|
url = getDownloadUrl(magic, track["id"].integer.text);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
@ -86,89 +73,27 @@ int main(string[] args)
|
|
|
|
|
return -7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string discDir;
|
|
|
|
|
if (discs > 1)
|
|
|
|
|
discDir = dirName~"/Disc "~discNum;
|
|
|
|
|
else
|
|
|
|
|
discDir = dirName;
|
|
|
|
|
|
|
|
|
|
if (!discDir.exists || !discDir.isDir) {
|
|
|
|
|
try {
|
|
|
|
|
mkdir(discDir);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeln("Failed to create directory `"~discDir~"`.");
|
|
|
|
|
return -11;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
auto fileName = trackName;
|
|
|
|
|
fileName = fileName.replaceAll(regex("[\\?<>:\"/\\\\|\\*]"), "");
|
|
|
|
|
auto relPath = discDir~"/"~num~" - "~fileName~".flac";
|
|
|
|
|
|
|
|
|
|
version (Windows) {
|
|
|
|
|
// making up for NTFS/Windows inadequacy
|
|
|
|
|
// can't really do much better than truncating, sorry.
|
|
|
|
|
auto totalPath = getcwd() ~ relPath;
|
|
|
|
|
if (totalPath.length > 255) {
|
|
|
|
|
totalPath = totalPath[0..(totalPath.length - 4)];
|
|
|
|
|
relPath = relPath[0..(relPath.length - 4)];
|
|
|
|
|
while (totalPath.length > 250) {
|
|
|
|
|
totalPath = totalPath[0..(totalPath.length - 1)];
|
|
|
|
|
relPath = relPath[0..(relPath.length - 1)];
|
|
|
|
|
}
|
|
|
|
|
totalPath ~= ".flac";
|
|
|
|
|
relPath ~= ".flac";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pipes = pipeProcess([magic["ffmpeg"].str, "-i", "-", "-metadata", "title="~trackName, "-metadata", "artist="~trackArtist,
|
|
|
|
|
"-metadata", "album="~title, "-metadata", "date="~year, "-metadata", "track="~num, "-metadata", "genre="~genre,
|
|
|
|
|
"-metadata", "albumartist="~artist, "-metadata", "discnumber="~discNum, "-metadata", "tracktotal="~tracks.length.text,
|
|
|
|
|
"-metadata", "disctotal="~discs.text, relPath],
|
|
|
|
|
Redirect.stdin | Redirect.stderr | Redirect.stdout);
|
|
|
|
|
|
|
|
|
|
extern(C) static size_t writefunc(const ubyte* data, size_t size, size_t nmemb, void* p) {
|
|
|
|
|
auto pp = *(cast(ProcessPipes*) p);
|
|
|
|
|
pp.stdin.rawWrite(data[0..size*nmemb]);
|
|
|
|
|
pp.stdin.flush();
|
|
|
|
|
return size*nmemb;
|
|
|
|
|
auto pipes = pipeProcess([magic["ffmpeg"].str, "-i", "-", "-metadata", "title="~trackName, "-metadata", "artist="~artist,
|
|
|
|
|
"-metadata", "album="~title, "-metadata", "year="~year, "-metadata", "track="~num, "-metadata", "genre="~genre,
|
|
|
|
|
"-metadata", "comment=qobuz-get "~VERSION, dirName~"/"~num~" "~trackName~".flac"], Redirect.stdin | Redirect.stderr | Redirect.stdout);
|
|
|
|
|
foreach (chunk; byChunkAsync(url, 1024)) {
|
|
|
|
|
pipes.stdin.rawWrite(chunk);
|
|
|
|
|
pipes.stdin.flush;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CURL* curl;
|
|
|
|
|
CURLcode res;
|
|
|
|
|
curl = curl_easy_init();
|
|
|
|
|
curl_easy_setopt(curl, CurlOption.url, toStringz(url));
|
|
|
|
|
curl_easy_setopt(curl, CurlOption.followlocation, 1L);
|
|
|
|
|
curl_easy_setopt(curl, CurlOption.writefunction, cast(void*) &writefunc);
|
|
|
|
|
curl_easy_setopt(curl, CurlOption.writedata, cast(void*) &pipes);
|
|
|
|
|
res = curl_easy_perform(curl);
|
|
|
|
|
assert(res == CurlError.ok);
|
|
|
|
|
curl_easy_cleanup(curl);
|
|
|
|
|
|
|
|
|
|
pipes.stdin.close;
|
|
|
|
|
wait(pipes.pid);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeln("Failed to download track! Check that ffmpeg is properly configured.");
|
|
|
|
|
writeln(e.msg);
|
|
|
|
|
return -8;
|
|
|
|
|
}
|
|
|
|
|
writeln("Done!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string firstDisc;
|
|
|
|
|
if (discs > 1)
|
|
|
|
|
firstDisc = dirName~"/Disc 1";
|
|
|
|
|
else
|
|
|
|
|
firstDisc = dirName;
|
|
|
|
|
|
|
|
|
|
// Get album art
|
|
|
|
|
write("Getting album art... ");
|
|
|
|
|
stdout.flush;
|
|
|
|
|
download(id.getArtUrl, firstDisc~"/cover.jpg");
|
|
|
|
|
for (int i = 2; i <= discs; i++) {
|
|
|
|
|
copy(firstDisc~"/cover.jpg", dirName~"/Disc "~i.text~"/cover.jpg");
|
|
|
|
|
}
|
|
|
|
|
download(id.getArtUrl, dirName~"/cover.jpg");
|
|
|
|
|
writeln("Done!");
|
|
|
|
|
|
|
|
|
|
string choice;
|
|
|
|
@ -179,15 +104,12 @@ int main(string[] args)
|
|
|
|
|
}
|
|
|
|
|
if (choice == "y") {
|
|
|
|
|
try {
|
|
|
|
|
auto trackName = tracks[0]["title"].str;
|
|
|
|
|
trackName = trackName.replaceAll(regex("[\\?<>:\"/\\\\|\\*]"), "");
|
|
|
|
|
|
|
|
|
|
auto full = execute([magic["sox"].str, firstDisc~"/01 - "~trackName~".flac", "-n", "remix", "1", "spectrogram",
|
|
|
|
|
auto full = execute([magic["sox"].str, dirName~"/01 "~tracks[0]["title"].str~".flac", "-n", "remix", "1", "spectrogram",
|
|
|
|
|
"-x", "3000", "-y", "513", "-z", "120", "-w", "Kaiser", "-o", "SpecFull.png"]);
|
|
|
|
|
auto zoom = execute([magic["sox"].str, firstDisc~"/01 - "~trackName~".flac", "-n", "remix", "1", "spectrogram",
|
|
|
|
|
auto zoom = execute([magic["sox"].str, dirName~"/01 "~tracks[0]["title"].str~".flac", "-n", "remix", "1", "spectrogram",
|
|
|
|
|
"-X", "500", "-y", "1025", "-z", "120", "-w", "Kaiser", "-S", "0:30", "-d", "0:04", "-o", "SpecZoom.png"]);
|
|
|
|
|
if (full.status != 0 || zoom.status != 0)
|
|
|
|
|
throw new Exception("sox failed");
|
|
|
|
|
throw new Exception("mktorrent failed");
|
|
|
|
|
writeln("SpecFull.png and SpecZoom.png written.");
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeln("Generating spectrals failed! Is sox configured properly?");
|
|
|
|
@ -219,5 +141,3 @@ int main(string[] args)
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ex: set tabstop=2 expandtab:
|
|
|
|
|