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"; if (args.length != 2) { writefln("Usage: %s ", args[0]); return -1; } auto path = thisExePath(); path = path.replaceFirst(regex("qobuz-get(\\.exe)?$"), "magic.json"); // HACK string json; try { json = readText(path); } catch (Exception e) { writeln("Could not open magic.json!"); } auto magic = parseJSON(json); // strip url part if we have it string id; auto urlPart = regex("^https?://play.qobuz.com/album/"); if (args[1].matchFirst(urlPart)) { id = args[1].replaceFirst(urlPart, ""); } else { id = args[1]; } writeln("Looking up album..."); auto album = getAlbum(magic, id); string title, artist, genre, year; JSONValue[] tracks; try { title = album["title"].str; artist = album["artist"]["name"].str; genre = album["genre"]["name"].str; auto releaseTime = SysTime.fromUnixTime(album["released_at"].integer, UTC()); year = releaseTime.year.text; writefln("[ %s - %s (%s, %s) ]", artist, title, genre, year); tracks = album["tracks"]["items"].array(); } catch (Exception e) { writeln("Could not parse album data!"); return -4; } string dirName = artist~" - "~title~" ("~year~") [WEB FLAC]"; dirName = dirName.replaceAll(regex("[\\?<>:\"/\\\\|\\*]"), ""); try { mkdir(dirName); } catch (Exception e) { writeln("Could not create directory: `"~dirName~"`. Does it exist already?"); return -9; } auto discs = tracks[tracks.length - 1]["media_number"].integer; foreach (track; tracks) { string url, num, discNum, trackName, trackArtist; 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); stdout.flush; url = getDownloadUrl(magic, track["id"].integer.text); } catch (Exception e) { writeln("Failed to parse track data!"); 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; } 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"); } writeln("Done!"); string choice; while (choice != "n" && choice != "y") { write("Generate spectrals? [y/n] "); stdout.flush; choice = readln().chomp; } 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", "-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", "-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"); writeln("SpecFull.png and SpecZoom.png written."); } catch (Exception e) { writeln("Generating spectrals failed! Is sox configured properly?"); } } choice = null; while (choice != "n" && choice != "y") { write("Create .torrent file? [y/n] "); stdout.flush; choice = readln().chomp; } if (choice == "y") { write("Announce URL: "); stdout.flush; string announce = readln().chomp; try { auto t = execute([magic["mktorrent"].str, "-l", "20", "-a", announce, dirName]); if (t.status != 0) throw new Exception("mktorrent failed"); writeln("'"~dirName~".torrent' created."); } catch (Exception e) { writeln("Creating .torrent file failed! Is mktorrent configured properly?"); } } writeln("All done, exiting."); return 0; } // ex: set tabstop=2 expandtab: