// 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; constexpr const char MAGICK_INVOKE[] = "magick"; #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(const string& path) { // Do a resize by default bool result = true; ifstream file; string filename; int marker = 0; int symbol; filename = path; filename += FOLDER_DELIM; filename += "Cover"; file.open(filename); // 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(const string& path) { ifstream file; string filename; uint8_t buf[4]; uint32_t width; uint32_t height; filename = path; filename += FOLDER_DELIM; filename += "Cover"; file.open(filename); // Seek file.seekg(16); // 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]; } file.close(); 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 resize_cover(const string& path, bool is_jpg) { string cmd; bool should_resize; // Should we actually resize if (is_jpg == IS_JPG) { should_resize = should_resize_JPG(path); } else { should_resize = should_resize_PNG(path); } cmd = MAGICK_INVOKE; cmd += "\""; cmd += path; cmd += FOLDER_DELIM; cmd += "Cover"; cmd += "\" "; if (should_resize) { cmd += "-resize "; cmd += to_string(COVER_SIZE); cmd += "x"; cmd += to_string(COVER_SIZE); cmd += " "; } cmd += "\""; cmd += path; cmd += FOLDER_DELIM; cmd += "Cover_small.jpg\""; return system_command(cmd); } 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 tag_MP3(const string& path, const string& filename, bool is_single_dir, const string& title, const string& album, const string& artist, const string& tracknum) { string cmd; cmd = "id3edit --set-name \""; cmd += title; cmd += "\" --set-album \""; cmd += album; cmd += "\" --set-artist \""; cmd += artist; cmd += "\" --set-track "; cmd += tracknum; cmd += " --set-artwork \""; cmd += path; cmd += FOLDER_DELIM; cmd += "Cover_small.jpg\" \""; cmd += path; cmd += FOLDER_DELIM; if (!is_single_dir) { cmd += "MP3"; cmd += FOLDER_DELIM; } cmd += filename; cmd += ".mp3\""; return system_command(cmd); } bool tag_FLAC(const string& path, const string& filename, bool is_single_dir, const string& title, const string& album, const string& artist, const string& tracknum) { string cmd; bool ok; // First command - remove metadata cmd = "metaflac --preserve-modtime --no-utf8-convert "; cmd += "--remove --block-type=PICTURE "; cmd += "--remove-tag=TITLE "; cmd += "--remove-tag=ARTIST "; cmd += "--remove-tag=ALBUM "; cmd += "--remove-tag=TRACKNUMBER \""; cmd += path; cmd += FOLDER_DELIM; if (!is_single_dir) { cmd += "FLAC"; cmd += FOLDER_DELIM; } cmd += filename; cmd += ".flac\""; ok = system_command(cmd); if (!ok) { return false; } // Second command - set metadata cmd = "metaflac --preserve-modtime --no-utf8-convert --dont-use-padding "; cmd += "\"--import-picture-from=3|image/jpeg|||"; cmd += path; cmd += FOLDER_DELIM; cmd += "Cover_small.jpg\" \"--set-tag=TITLE="; cmd += title; cmd += "\" \"--set-tag=ARTIST="; cmd += artist; cmd += "\" \"--set-tag=ALBUM="; cmd += album; cmd += "\" --set-tag=TRACKNUMBER="; cmd += tracknum; cmd += " \""; cmd += path; cmd += FOLDER_DELIM; if (!is_single_dir) { cmd += "FLAC"; cmd += FOLDER_DELIM; } cmd += filename; cmd += ".flac\""; return system_command(cmd); } 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; } // Make 750x750 Cover JPEG Image ok = resize_cover(release_dir, cover_is_jpg); if (!ok) { return false; } // 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; } // Tag MP3 ok = tag_MP3(release_dir, track_filename, is_single_dir, tags[i].title, release_artist_title.second, tags[i].artist, tags[i].number); if (!ok) { return false; } // 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; } // Tag FLAC ok = tag_FLAC(release_dir, track_filename, is_single_dir, tags[i].title, release_artist_title.second, tags[i].artist, tags[i].number); if (!ok) { return false; } } return true; } int main(int argc, char **argv) { // TODO: Implement return 0; }