From c6362020c1dad563ef0b63c8c902601f892e1efe Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 16 May 2017 00:09:54 +0100 Subject: [PATCH] initial commit --- .gitignore | 9 +++++ dub.json | 12 +++++++ magic.json.default | 7 ++++ source/app.d | 87 ++++++++++++++++++++++++++++++++++++++++++++++ source/qobuz/api.d | 82 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 .gitignore create mode 100644 dub.json create mode 100644 magic.json.default create mode 100644 source/app.d create mode 100644 source/qobuz/api.d diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..955424f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.dub +docs.json +__dummy.html +*.o +*.obj +__test__*__ +qobuz-get +*.swp +magic.json diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..a2f5aea --- /dev/null +++ b/dub.json @@ -0,0 +1,12 @@ +{ + "name": "qobuz-get", + "authors": [ + "Al Beano" + ], + "libs": [ + "curl" + ], + "description": "Tool to download albums from Qobuz.", + "copyright": "Copyright (C) 2017, Al Beano", + "license": "GPLv2" +} diff --git a/magic.json.default b/magic.json.default new file mode 100644 index 0000000..2a18a21 --- /dev/null +++ b/magic.json.default @@ -0,0 +1,7 @@ +{ + "ffmpeg" : "/usr/bin/ffmpeg", + + "app_secret" : "your_secret", + "app_id" : "your_id", + "user_auth_token" : "your_token" +} diff --git a/source/app.d b/source/app.d new file mode 100644 index 0000000..a6087ad --- /dev/null +++ b/source/app.d @@ -0,0 +1,87 @@ +import std.stdio, std.regex, std.json, std.file, std.datetime, std.conv, std.process, std.net.curl; +import qobuz.api; + +int main(string[] args) +{ + if (args.length != 2) { + writefln("Usage: %s ", args[0]); + return -1; + } + + auto path = thisExePath(); + path = path.replaceFirst(regex("qobuz-get$"), "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["genres_list"][0].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]"; + mkdir(dirName); + + foreach (i, track; tracks) { + auto num = (i+1).text; + string url, trackName; + try { + trackName = track["title"].str; + if (num.length < 2) + num = "0"~num; + writef(" [%s] %s... ", num, trackName); + stdout.flush; + url = getDownloadUrl(magic, track["id"].integer.text); + } catch (Exception e) { + writeln("Failed to parse track data!"); + return -7; + } + + try { + auto pipes = pipeProcess([magic["ffmpeg"].str, "-i", "-", "-metadata", "title="~trackName, "-metadata", "author="~artist, + "-metadata", "album="~title, "-metadata", "year="~year, "-metadata", "track="~num, "-metadata", "genre="~genre, + dirName~"/"~num~" "~trackName~".flac"], Redirect.stdin | Redirect.stderr | Redirect.stdout); + foreach (chunk; byChunkAsync(url, 1024)) { + pipes.stdin.rawWrite(chunk); + pipes.stdin.flush; + } + pipes.stdin.close; + wait(pipes.pid); + } catch (Exception e) { + writeln("Failed to download track! Check that ffmpeg is properly configured."); + return -8; + } + writeln("Done!"); + } + + return 0; +} diff --git a/source/qobuz/api.d b/source/qobuz/api.d new file mode 100644 index 0000000..e4d0867 --- /dev/null +++ b/source/qobuz/api.d @@ -0,0 +1,82 @@ +module qobuz.api; +import core.stdc.stdlib, std.digest.md, std.conv, std.uni, std.json, std.net.curl, std.stdio, std.datetime; +import std.algorithm : sort; + +string createSignature(string obj, string method, string[string] params, string tstamp, string secret) { + string str = obj; + str ~= method; + foreach (k; sort(params.keys)) { + str ~= k ~ params[k]; + } + str ~= tstamp; + str ~= secret; + + auto md5 = new MD5Digest(); + return md5.digest(str).toHexString.toLower; +} + +string apiRequest(JSONValue magic, string request) { + auto curl = HTTP(); + curl.addRequestHeader("x-app-id", magic["app_id"].str); + curl.addRequestHeader("x-user-auth-token", magic["user_auth_token"].str); +// curl.proxy = "localhost:8080"; + + string jsonResponse; + try { + jsonResponse = get("http://qobuz.com/api.json/0.2/"~request, curl).text(); + } catch (Exception e) { + writeln("Request to qobuz failed!"); + exit(-2); + } + + return jsonResponse; +} + +JSONValue getAlbum(JSONValue magic, string id) { + try { + return apiRequest(magic, "album/get?offset=0&limit=500&album_id="~id).parseJSON; + } catch (Exception e) { + writeln("Invalid JSON data!"); + exit(-3); + } + + assert(0); +} + +string buildGETRequest(string[string] params) { + string req; + foreach (i, k; params.keys) { + if (i == 0) + req ~= "?"; + else + req ~= "&"; + req ~= k~"="~params[k]; + } + return req; +} + +string getDownloadUrl(JSONValue magic, string id) { + string[string] params; + params["track_id"] = id; + params["format_id"] = "6"; // TODO: support for multiple formats + params["intent"] = "stream"; + + auto tstamp = Clock.currTime.toUnixTime.text; + auto sig = createSignature("track", "getFileUrl", params, tstamp, magic["app_secret"].str); + + JSONValue response; + try { + response = apiRequest(magic, "track/getFileUrl"~buildGETRequest(params)~"&request_ts="~tstamp~"&request_sig="~sig).parseJSON; + } catch (Exception e) { + writeln("Invalid JSON data!"); + exit(-5); + } + try { + return response["url"].str; + } catch (Exception e) { + writeln("No download URI given!"); + exit(-6); + } + + assert(0); +}