monstercat-downloader/main.cpp
2022-08-25 16:40:20 +03:00

1110 lines
19 KiB
C++

// C++ Libs
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <utility>
// C Libs
#include <stdint.h>
#include <stdlib.h>
// 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 ID3EDIT_INVOKE[] = "id3edit";
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 << " <options> [path]" << endl;
cout << " -1 <catalog release id>" << endl;
cout << " Single release" << endl;
cout << " -j <json file>" << endl;
cout << " JSON file denoting release range or releases array" << 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<char>(file), std::istreambuf_iterator<char>());
file.close();
j = json::parse(str);
return j;
}
pair<string, string> get_artist_feat(const string& str)
{
pair<string, string> 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<string, string> get_artist_title(const json& obj)
{
pair<string, string> result;
pair<string, string> artist_feat;
string str;
string feat;
string version;
bool contains;
int pos;
artist_feat = get_artist_feat(obj["ArtistsTitle"]);
result.first = artist_feat.first;
contains = false;
str = obj["Title"];
if (!artist_feat.second.empty())
{
str += " (";
str += artist_feat.second;
str += ')';
contains = true;
}
// Empty does not work on json string
version = obj["Version"];
if (!version.empty())
{
str += contains ? " [" : " (";
str += version;
str += contains ? ']' : ')';
}
result.second = str;
return result;
}
tag get_single_tag(const json& track)
{
tag result;
pair<string, string> 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<tag> get_tags_from_release(const json& info)
{
vector<tag> 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, const string& release_prefix,
int release_num, const string& release_suffix, const string& artist,
const string& title, int precision)
{
string path;
path = main_path;
path += FOLDER_DELIM;
path += release_prefix;
path += FOLDER_DELIM;
path += num_to_str(release_num, precision);
path += release_suffix;
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<char*>(&height), 2);
file.read(reinterpret_cast<char*>(&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<char*>(&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<char*>(&buf), 4);
for (int i = 0; i < 4; ++i)
{
width <<= 8;
width += buf[i];
}
// Read height
height = 0;
file.read(reinterpret_cast<char*>(&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_INVOKE;
cmd += " --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 tags
cmd = "metaflac --preserve-modtime --no-utf8-convert ";
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 - remove pictures
cmd = "metaflac --preserve-modtime --no-utf8-convert ";
cmd += "--remove --block-type=PICTURE \"";
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;
}
// Third 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);
}
void save_release_date(const string& path, const string& date)
{
ofstream file;
string filepath;
filepath = path;
filepath += FOLDER_DELIM;
filepath += "Date.txt";
file.open(filepath);
file << date << endl;
file.close();
}
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<string, string> release_artist_title;
int release_precision;
bool is_single_dir;
bool cover_is_jpg;
vector<tag> 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_prefix, release_num,
release_suffix, release_artist_title.first, release_artist_title.second,
release_precision);
// Make Release Folder
ok = make_dir(release_dir);
if (!ok)
{
return false;
}
// Save Release Date
save_release_date(release_dir, info["Release"]["ReleaseDate"]);
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(catalog_release_str, 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;
}
bool break_down_release_str(const string& release, string& prefix, int& num, string& suffix)
{
int pos;
int pos2;
string digits = "0123456789";
prefix = "";
num = 0;
suffix = "";
pos = release.find_first_of(digits);
if (string::npos == pos)
{
return false;
}
prefix = release.substr(0, pos);
pos2 = release.find_first_not_of(digits, pos);
if (string::npos == pos2)
{
try
{
num = stod(release.substr(pos));
}
catch(const std::exception& e)
{
return false;
}
}
else
{
try
{
num = stod(release.substr(pos, pos2 - pos));
}
catch(const std::exception& e)
{
return false;
}
suffix = release.substr(pos2);
}
return true;
}
int main(int argc, char **argv)
{
vector<string> args(&argv[1], &argv[argc]);
pair<bool, string> single_release = {false, ""};
pair<bool, string> json_option = {false, ""};
string path = ".";
if (argc < 3)
{
cout << "Too few arguments" << endl;
usage(argv[0]);
return -1;
}
// Ugly getopt
for (int i = 0; i < args.size(); ++i)
{
if ((args[i] == "-1") && (i + 1 < args.size()))
{
single_release.first = true;
single_release.second = args[i + 1];
args.erase(args.begin() + i);
args.erase(args.begin() + i);
--i;
continue;
}
if ((args[i] == "-j") && (i + 1 < args.size()))
{
json_option.first = true;
json_option.second = args[i + 1];
args.erase(args.begin() + i);
args.erase(args.begin() + i);
--i;
continue;
}
}
// Both selected
if (single_release.first && json_option.first)
{
cout << "Both single release and json file are selected." << endl;
usage(argv[0]);
return -1;
}
// None selected
if (!single_release.first && !json_option.first)
{
cout << "Neither single release nor json file are selected." << endl;
usage(argv[0]);
return -1;
}
// Add path from arguments
if (!args.empty())
{
path = args[0];
}
// Remove trailing slash
if (path[path.size() - 1] == FOLDER_DELIM)
{
path.erase(path.size() - 1);
}
#if 1
// BEGIN TEST PRINTS
if (single_release.first)
{
cout << "SINGLE: " << single_release.second << endl;
}
if (json_option.first)
{
cout << "JSON: " << json_option.second << endl;
}
cout << "PATH: " << path << endl;
// END TEST PRINTS
#endif
vector<string> releases;
vector<string>::iterator it;
// Add single release
if (single_release.first)
{
releases.push_back(single_release.second);
}
// Parse JSON
if (json_option.first)
{
json j = parse_json_file(json_option.second);
if (j.contains("releases") && j["releases"].is_array())
{
for (int i = 0; i < j["releases"].size(); ++i)
{
releases.push_back(j["releases"][i]);
}
}
else if (j.contains("prefix") &&
j.contains("start") && j["start"].is_number_integer() &&
j.contains("end") && j["end"].is_number_integer())
{
string tmp;
for (int i = j["start"]; i <= j["end"]; ++i)
{
// Add release
tmp = j["prefix"];
tmp += num_to_str(i, 3);
releases.push_back(tmp);
// Add release with suffix
if (j.contains("suffix_try") && !j["suffix_try"].empty())
{
tmp += j["suffix_try"];
releases.push_back(tmp);
}
}
}
else
{
cout << "JSON not recognized" << endl;
return -1;
}
}
string release_prefix;
int release_num;
string release_suffix;
bool ok;
ok = login();
if (!ok)
{
cout << "Failed to login" << endl;
return -1;
}
// Break down release string
for (it = releases.begin(); it != releases.end(); ++it)
{
ok = break_down_release_str(*it, release_prefix, release_num, release_suffix);
if (!ok)
{
cout << "Failed to break down - " << *it << endl;
continue;
}
ok = full_donwload(path, release_prefix, release_num, release_suffix);
if (!ok)
{
cout << "Could not download - " << *it << endl;
}
}
ok = logout();
if (!ok)
{
cout << "Failed to logout" << endl;
}
return 0;
}