// C++ Libs #include #include #include #include #include #include // C Libs #include #include // File Libs #include "json.hpp" struct tag { std::string artist; std::string title; std::string number; }; constexpr const char RELEASE_JSON[] = "release.json"; constexpr bool IS_MP3 = true; constexpr bool IS_FLAC = false; constexpr bool IS_JPG = true; constexpr bool IS_PNG = false; constexpr uint32_t COVER_SIZE = 750; #ifdef _WIN32 constexpr const char FOLDER_DELIM = '\\'; #else constexpr const char FOLDER_DELIM = '/'; #endif using namespace std; using nlohmann::json; // 1. Login // 2. For each release // 2.1 Download Release JSON // 2.2 Parse JSON // 2.3 Make Release Folder // (Catalog Number %*d - Artist - Title) // 2.4 Make MP3 & FLAC Folders ??? (If Tracks > 1) // 2.5 Download Cover // 2.6 Make 750x750 Cover JPEG Image // 2.7 Rename Cover Image (Proper Extension) // 2.8 Download Track (MP3) // (Track Num - Artist - Title) // 2.9 Tag MP3 // 2.10 Download Track (FLAC) // (Track Num - Artist - Title) // 2.11 Tag FLAC // 3. Logout void usage(const string& name) { cout << "Usage:" << endl; cout << name << " [path]" << endl; cout << " -1 " << endl; cout << " Single release" << endl; cout << " -j " << endl; cout << " JSON file denoting release range" << endl; cout << " path - root path to download to" << endl; cout << " Default: ." << endl; } bool system_command(const string& cmd) { int result = system(cmd.c_str()); if (0 == result) { return true; } else { return false; } } bool login() { string cmd; cmd = "curl -f -L -c cookies.txt -b cookies.txt -X POST"; cmd += " https://player.monstercat.app/api/sign-in"; cmd += " -H \"Content-Type: application/json\" --data-binary \"@login.json\""; return system_command(cmd); } bool logout() { string cmd; cmd = "curl -f -L -c cookies.txt -b cookies.txt -X POST"; cmd += " https://player.monstercat.app/api/sign-out"; return system_command(cmd); } bool download_release_json(const string& catalog_release) { string cmd; cmd = "curl -f -L -c cookies.txt -b cookies.txt"; cmd += " https://player.monstercat.app/api/catalog/release/"; cmd += catalog_release; cmd += " -o "; cmd += RELEASE_JSON; return system_command(cmd); } bool download_cover(const string& release_id, const string& path) { string cmd; cmd = "curl -f -L -c cookies.txt -b cookies.txt"; cmd += " \"https://www.monstercat.com/release/"; cmd += release_id; cmd += "/cover\" -o \""; cmd += path; cmd += FOLDER_DELIM; cmd += "Cover\""; return system_command(cmd); } json parse_json_file(const string& filename) { json j; ifstream file; string str; file.open(filename); str.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); file.close(); j = json::parse(str); return j; } pair get_artist_feat(const string& str) { pair result; int pos; pos = str.find("feat."); if (string::npos == pos) { result.first = str; result.second = ""; } else { result.first = str.substr(0, pos - 1); result.second = str.substr(pos); } return result; } pair get_artist_title(const json& obj) { pair result; pair artist_feat; string str; string feat; bool contains; int pos; artist_feat = get_artist_feat(obj["Release"]["ArtistsTitle"]); result.first = artist_feat.first; contains = false; str = obj["Release"]["Title"]; if (!artist_feat.second.empty()) { str += " ("; str += artist_feat.second; str += ')'; contains = true; } if (!(obj["Release"]["Version"].empty())) { if (contains) { str += " ["; } else { str += " ("; } str += obj["Release"]["Version"]; str += contains ? ']' : ')'; } result.second = str; return result; } tag get_single_tag(const json& track) { tag result; pair artist_title; result.number = to_string(track["TrackNumber"]); artist_title = get_artist_title(track); result.artist = artist_title.first; result.title = artist_title.second; return result; } vector get_tags_from_release(const json& info) { vector result; for (int i = 0; i < info["Tracks"].size(); ++i) { result.push_back(get_single_tag(info["Tracks"][i])); } return result; } bool make_dir(const string& path) { string cmd; cmd = "mkdir \""; cmd += path; cmd += '\"'; return system_command(cmd); } bool download_track(const string& release_id, const string& track_id, const string& path, const string& filename, bool single_folder, bool is_mp3) { string format_str; string format_ext; string format_folder; string cmd; if (is_mp3) { format_str = "mp3_320"; format_ext = "mp3"; format_folder = FOLDER_DELIM; format_folder += "MP3"; } else { format_str = "flac"; format_ext = "flac"; format_folder = FOLDER_DELIM; format_folder += "FLAC"; } cmd = "curl -f -L -c cookies.txt -b cookies.txt"; cmd += " \"https://player.monstercat.app/api/release/"; cmd += release_id; cmd += "/track-download/"; cmd += track_id; cmd += "?format="; cmd += format_str; cmd += "\" -o \""; cmd += path; if (!single_folder) { cmd += format_folder; } cmd += FOLDER_DELIM; cmd += filename; cmd += "."; cmd += format_ext; cmd += "\""; return system_command(cmd); } string num_to_str(int num, int precision) { string result; result = to_string(num); while (result.size() < precision) { result.insert(result.begin(), '0'); } return result; } string get_release_dir_name(const string& main_path, int release_num, const string& artist, const string& title, int precision) { string path; path = main_path; path += FOLDER_DELIM; path += num_to_str(release_num, precision); path += " - "; if ((artist != "Monstercat") && (artist != "Various Artists")) { path += artist; path += " - "; } path += title; return path; } string get_track_filename(int track_num, const string& artist, const string& title, const string& album_artist) { string filename; if (0 != track_num) { filename = num_to_str(track_num, 2); filename += " - "; } if ((artist != "Monstercat") && (artist != "Various Artists") && (artist != album_artist)) { filename += artist; filename += " - "; } filename += title; return filename; } bool should_resize_JPG(ifstream& file) { // Do a resize by default bool result = true; int marker = 0; int symbol; // Check Header 1 symbol = file.get(); if (symbol != 0xFF) { return result; } // Check Header 2 symbol = file.get(); if (symbol != 0xD8) { return result; } while(1) { int discarded_bytes = 0; if (file.eof()) { return result; } marker = file.get(); // Seek to 0xFF while (marker != 0xFF) { discarded_bytes++; if (file.eof()) { return result; } marker = file.get(); } // Find end of 0xFF do { if (file.eof()) { return result; } marker = file.get(); } while (marker == 0xFF); // ??? if (0 == discarded_bytes) { return result; } switch(marker) { // Read width & height case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC5: case 0xC6: case 0xC7: case 0xC9: case 0xCA: case 0xCB: case 0xCD: case 0xCE: case 0xCF: int16_t width; int16_t height; // Skip 3 bytes file.get(); file.get(); file.get(); file.read(reinterpret_cast(&height), 2); file.read(reinterpret_cast(&width), 2); if ((width > COVER_SIZE) || (height > COVER_SIZE)) { return true; } else { return false; } break; // ??? case 0xDA: case 0xD9: return result; break; // Skip length-2 bytes default: int16_t length; file.read(reinterpret_cast(&length), 2); if (length < 2) { return 0; } length -= 2; for (int i = 0; i < length; ++i) { file.get(); } if (file.eof()) { return result; } break; } } } bool should_resize_PNG(ifstream& file) { uint8_t buf[4]; uint32_t width; uint32_t height; // Seek file.read(reinterpret_cast(&buf), 4); file.read(reinterpret_cast(&buf), 4); file.read(reinterpret_cast(&buf), 4); file.read(reinterpret_cast(&buf), 4); // Read width width = 0; file.read(reinterpret_cast(&buf), 4); for (int i = 0; i < 4; ++i) { width <<= 8; width += buf[i]; } // Read height height = 0; file.read(reinterpret_cast(&buf), 4); for (int i = 0; i < 4; ++i) { height <<= 8; height += buf[i]; } if ((width > COVER_SIZE) || (height > COVER_SIZE)) { return true; } else { return false; } } bool get_cover_type(const string& path) { string filename; ifstream file; int header; filename = path; filename += FOLDER_DELIM; filename += "Cover"; file.open(filename); header = file.peek(); switch (header) { // JPG case 0xFF: return IS_JPG; break; case 0x89: return IS_PNG; break; } throw invalid_argument(filename); } bool rename_cover(const string& path, bool is_jpg) { string filename; string filename_new; int error; filename = path; filename += FOLDER_DELIM; filename += "Cover"; filename_new = filename; if (is_jpg == IS_JPG) { filename_new += ".jpg"; } else { filename_new += ".png"; } error = rename(filename.c_str(), filename_new.c_str()); if (0 != error) { return false; } return true; } bool full_donwload(const string& path, const string& release_prefix, int release_num, const string& release_suffix) { bool ok; string catalog_release_str; json info; string release_dir; string track_filename; pair release_artist_title; int release_precision; bool is_single_dir; bool cover_is_jpg; vector tags; catalog_release_str = release_prefix; catalog_release_str += num_to_str(release_num, 3); catalog_release_str == release_suffix; release_precision = 3; if (release_prefix == "MCS") { release_precision = 4; } // Download Release JSON ok = download_release_json(catalog_release_str); if (!ok) { return false; } // Parse JSON info = parse_json_file(RELEASE_JSON); release_artist_title = get_artist_title(info["Release"]); release_dir = get_release_dir_name(path, release_num, release_artist_title.first, release_artist_title.second, release_precision); // Make Release Folder ok = make_dir(release_dir); if (!ok) { return false; } is_single_dir = (info["Tracks"].size() == 1); // Make MP3 & FLAC Folders (If more than 1 Track) if (!is_single_dir) { string tmp; tmp = release_dir; tmp += FOLDER_DELIM; tmp += "MP3"; ok = make_dir(tmp); if (!ok) { return false; } tmp = release_dir; tmp += FOLDER_DELIM; tmp += "FLAC"; ok = make_dir(tmp); if (!ok) { return false; } } // Download Cover ok = download_cover(info["Release"]["Id"], release_dir); if (!ok) { return false; } // Try to find out Cover type try { cover_is_jpg = get_cover_type(release_dir); } catch (const std::exception& e) { return false; } // TODO: Make 750x750 Cover JPEG Image // Rename Cover Image (Proper Extension) ok = rename_cover(release_dir, cover_is_jpg); if (!ok) { return false; } tags = get_tags_from_release(info); for (int i = 0; i < info["Tracks"].size(); ++i) { track_filename = get_track_filename(info["Tracks"][i]["TrackNumber"], tags[i].artist, tags[i].title, release_artist_title.first); // Download Track (MP3) ok = download_track(info["Release"]["Id"], info["Tracks"][i]["Id"], release_dir, track_filename, is_single_dir, IS_MP3); if (!ok) { return false; } // TODO: Tag MP3 // Download Track (FLAC) ok = download_track(info["Release"]["Id"], info["Tracks"][i]["Id"], release_dir, track_filename, is_single_dir, IS_FLAC); if (!ok) { return false; } // TODO: Tag FLAC } return true; } int main(int argc, char **argv) { // TODO: Implement return 0; }