600 lines
12 KiB
C++
600 lines
12 KiB
C++
#include "monstercat_dl.h"
|
|
#include "common.h"
|
|
#include "curl_dl.h"
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
using std::endl;
|
|
using std::map;
|
|
using std::ofstream;
|
|
using std::sort;
|
|
using std::string;
|
|
using std::stringstream;
|
|
using std::to_string;
|
|
|
|
using nlohmann::json;
|
|
|
|
string Monstercat_DL::calc_proper_artist(const string& artist_raw)
|
|
{
|
|
size_t pos;
|
|
string result;
|
|
|
|
pos = artist_raw.find("feat.");
|
|
if (string::npos == pos)
|
|
{
|
|
return artist_raw;
|
|
}
|
|
|
|
result = artist_raw.substr(0, pos);
|
|
result = trim_whitespace(result);
|
|
|
|
return result;
|
|
}
|
|
|
|
string Monstercat_DL::calc_proper_title(const string& artist_raw,
|
|
const string& title_raw, const string& version_raw)
|
|
{
|
|
size_t pos;
|
|
string result;
|
|
|
|
result = trim_whitespace(title_raw);
|
|
|
|
pos = artist_raw.find("feat.");
|
|
if (string::npos != pos)
|
|
{
|
|
result += " (";
|
|
result += trim_whitespace(artist_raw.substr(pos));
|
|
result += ")";
|
|
}
|
|
|
|
if (!version_raw.empty())
|
|
{
|
|
result += " (";
|
|
result += trim_whitespace(version_raw);
|
|
result += ")";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Monstercat_DL::Monstercat_DL()
|
|
: m_log(&std::cout), m_base_url("https://player.monstercat.app/api/"),
|
|
m_is_logged_in(false)
|
|
{}
|
|
|
|
Monstercat_DL::~Monstercat_DL()
|
|
{
|
|
}
|
|
|
|
bool Monstercat_DL::login(const string& user, const string& pass)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
json data;
|
|
string url;
|
|
stringstream out;
|
|
|
|
data["Email"] = user;
|
|
data["Password"] = pass;
|
|
|
|
url = m_base_url;
|
|
url += "sign-in";
|
|
|
|
ok = curl.post_json(url, data, &out);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not post json" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
data.clear();
|
|
out >> data;
|
|
|
|
if (data.contains("Needs2FA") &&
|
|
data["Needs2FA"].is_boolean() &&
|
|
data["Needs2FA"] == false)
|
|
{
|
|
m_is_logged_in = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Monstercat_DL::logout()
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
json data;
|
|
string url;
|
|
|
|
if (!m_is_logged_in)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
url = m_base_url;
|
|
url += "sign-out";
|
|
|
|
ok = curl.post_json(url, data, nullptr);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not post json" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
json Monstercat_DL::get_release_json(const string& catalog_id)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
json result;
|
|
string url;
|
|
stringstream out;
|
|
|
|
url = m_base_url;
|
|
url += "catalog/release/";
|
|
url += catalog_id;
|
|
|
|
ok = curl.download(url, &out);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not download json" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
out >> result;
|
|
return result;
|
|
}
|
|
|
|
json Monstercat_DL::get_browse_json(const string& release_id)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
json result;
|
|
string url;
|
|
stringstream out;
|
|
|
|
url = m_base_url;
|
|
url += "catalog/browse?offset=0&limit=0&search=&sort=&nogold=false&releaseId=";
|
|
url += release_id;
|
|
|
|
ok = curl.download(url, &out);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not download json" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
out >> result;
|
|
return result;
|
|
}
|
|
|
|
Release Monstercat_DL::parse_release_json(const json& release_json)
|
|
{
|
|
Release result;
|
|
json release_object;
|
|
json tracks_object;
|
|
string artist_raw;
|
|
string title_raw;
|
|
string version_raw;
|
|
Track temp_track;
|
|
|
|
if (release_json.contains("Release"))
|
|
{
|
|
release_object = release_json["Release"];
|
|
}
|
|
else
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Parse Release
|
|
json_extract(release_object, "CatalogId", result.catalog_id);
|
|
json_extract(release_object, "Id", result.id);
|
|
json_extract(release_object, "Type", result.type);
|
|
json_extract(release_object, "ReleaseDate", result.release_date);
|
|
{
|
|
size_t pos = result.release_date.find('T');
|
|
if (pos != string::npos)
|
|
{
|
|
result.release_date.erase(pos);
|
|
}
|
|
}
|
|
|
|
json_extract(release_object, "ArtistsTitle", artist_raw);
|
|
json_extract(release_object, "Title", title_raw);
|
|
json_extract(release_object, "Version", version_raw);
|
|
|
|
result.artist = calc_proper_artist(artist_raw);
|
|
result.title = calc_proper_title(artist_raw, title_raw, version_raw);
|
|
|
|
if (release_json.contains("Tracks") && release_json["Tracks"].is_array())
|
|
{
|
|
tracks_object = release_json["Tracks"];
|
|
}
|
|
else
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Parse Tracks
|
|
for (json& curr_track : tracks_object)
|
|
{
|
|
if (curr_track.contains("TrackNumber") &&
|
|
curr_track["TrackNumber"].is_number_integer())
|
|
{
|
|
temp_track.number = curr_track["TrackNumber"];
|
|
}
|
|
json_extract(curr_track, "Id", temp_track.id);
|
|
|
|
json_extract(curr_track, "ArtistsTitle", artist_raw);
|
|
json_extract(curr_track, "Title", title_raw);
|
|
json_extract(curr_track, "Version", version_raw);
|
|
|
|
temp_track.artist = calc_proper_artist(artist_raw);
|
|
temp_track.title = calc_proper_title(artist_raw, title_raw, version_raw);
|
|
|
|
result.tracks.push_back(temp_track);
|
|
}
|
|
|
|
// Sort tracks by number
|
|
sort(result.tracks.begin(), result.tracks.end(), [](const Track& t1, const Track& t2)
|
|
{
|
|
return t1.number < t2.number;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
void Monstercat_DL::add_extended_mixes(Release& release, const json& browse_json)
|
|
{
|
|
int track_num;
|
|
json data_obj;
|
|
json file_obj;
|
|
Track *track;
|
|
string mime_type;
|
|
|
|
if (browse_json.contains("Data") && browse_json["Data"].is_array())
|
|
{
|
|
data_obj = browse_json["Data"];
|
|
}
|
|
|
|
for (json& track_json : data_obj)
|
|
{
|
|
// Get the track number
|
|
track_num = 0;
|
|
if (track_json.contains("TrackNumber") && track_json["TrackNumber"].is_number_integer())
|
|
{
|
|
track_num = track_json["TrackNumber"];
|
|
}
|
|
|
|
// File must exist and be an object
|
|
if (!track_json.contains("File") || !track_json["File"].is_object())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Find track
|
|
track = nullptr;
|
|
for (int i = 0; i < release.tracks.size(); ++i)
|
|
{
|
|
if (release.tracks[i].number == track_num)
|
|
{
|
|
track = &(release.tracks[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nullptr == track)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not find track number " << track_num << " for catalog id " << release.catalog_id << endl;
|
|
}
|
|
return;
|
|
}
|
|
|
|
file_obj = track_json["File"];
|
|
json_extract(file_obj, "Id", track->extended_mix_file_id);
|
|
json_extract(file_obj, "MimeType", mime_type);
|
|
|
|
if ("audio/wav" == mime_type)
|
|
{
|
|
track->extended_mix_extension = ".wav";
|
|
}
|
|
else
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Unknown MIME type for catalog id " << release.catalog_id << " - " << mime_type << endl;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
string Monstercat_DL::calc_release_folder(const Release& release, const string& main_folder)
|
|
{
|
|
string result;
|
|
string folder_name;
|
|
|
|
ensure_folder(main_folder, release.type);
|
|
|
|
folder_name = release.release_date;
|
|
folder_name += " - ";
|
|
folder_name += clean_filename(release.catalog_id);
|
|
folder_name += " - ";
|
|
folder_name += clean_filename(release.artist);
|
|
folder_name += " - ";
|
|
folder_name += clean_filename(release.title);
|
|
|
|
result = build_fname(main_folder, release.type, folder_name);
|
|
ensure_folder(result, "");
|
|
|
|
return result;
|
|
}
|
|
|
|
string Monstercat_DL::calc_track_filename(const Release& release, int track_num)
|
|
{
|
|
string result;
|
|
bool use_number;
|
|
bool use_artist;
|
|
const Track *track = nullptr;
|
|
|
|
// Find the track
|
|
for (int i = 0; i < release.tracks.size(); ++i)
|
|
{
|
|
if (release.tracks[i].number == track_num)
|
|
{
|
|
track = &(release.tracks[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nullptr == track)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Cannot find track number " << track_num << " in catalog id " << release.catalog_id << endl;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
// Determine whether number is needed
|
|
if (1 == release.tracks.size())
|
|
{
|
|
use_number = false;
|
|
}
|
|
else
|
|
{
|
|
use_number = true;
|
|
}
|
|
|
|
// Determine whether artist is needed
|
|
if (track->artist == release.artist)
|
|
{
|
|
use_artist = false;
|
|
}
|
|
else
|
|
{
|
|
use_artist = true;
|
|
}
|
|
|
|
// Build filename
|
|
result = "";
|
|
if (use_number)
|
|
{
|
|
// Always use 2 digits
|
|
result += i_to_str(track->number, 2, '0');
|
|
result += " - ";
|
|
}
|
|
|
|
if (use_artist)
|
|
{
|
|
result += track->artist;
|
|
result += " - ";
|
|
}
|
|
|
|
result += track->title;
|
|
|
|
return clean_filename(result);
|
|
}
|
|
|
|
bool Monstercat_DL::download_cover(const string& catalog_id, const string& path)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
string url;
|
|
stringstream out;
|
|
ofstream out_file;
|
|
map<string, string> out_headers;
|
|
string filepath;
|
|
|
|
url = "https://www.monstercat.com/release/";
|
|
url += catalog_id;
|
|
url += "/cover";
|
|
|
|
ok = curl.download(url, &out, nullptr, &out_headers);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not download image" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
filepath = build_fname(path, "", "Cover");
|
|
if (0 == out_headers.count("content-type"))
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "No content-type" << endl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (out_headers["content-type"] == "image/jpeg")
|
|
{
|
|
filepath += ".jpg";
|
|
}
|
|
else if (out_headers["content-type"] == "image/png")
|
|
{
|
|
filepath += ".png";
|
|
}
|
|
else
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Unknown Content-Type for cover - " << out_headers["content-type"] << endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
out_file.open(filepath, std::ios::binary);
|
|
if (!out_file.is_open())
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not open file for write" << endl;
|
|
*m_log << filepath << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out_file << out.rdbuf();
|
|
out_file.close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Monstercat_DL::download_track(const string& release_id,
|
|
const string& track_id, const string& filepath, bool is_mp3)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
string url;
|
|
stringstream out_data;
|
|
ofstream out_file;
|
|
|
|
url = m_base_url;
|
|
url += "release/";
|
|
url += release_id;
|
|
url += "/track-download/";
|
|
url += track_id;
|
|
if (is_mp3)
|
|
{
|
|
url += "?format=mp3_320";
|
|
}
|
|
else
|
|
{
|
|
url += "?format=flac";
|
|
}
|
|
|
|
ok = curl.download(url, &out_data);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not download track" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out_file.open(filepath, std::ios::binary);
|
|
if (!out_file.is_open())
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not open file for write" << endl;
|
|
*m_log << filepath << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out_file << out_data.rdbuf();
|
|
out_file.close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Monstercat_DL::download_file(const string& file_id, const string& filepath)
|
|
{
|
|
CURL_DL& curl = CURL_DL::get_handle();
|
|
bool ok;
|
|
string url;
|
|
stringstream out_data;
|
|
ofstream out_file;
|
|
|
|
url = m_base_url;
|
|
url += "file/";
|
|
url += file_id;
|
|
url += "/open?download=true";
|
|
|
|
ok = curl.download(url, &out_data);
|
|
if (!ok)
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not download track" << endl;
|
|
*m_log << "CURL:" << curl.get_error() << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out_file.open(filepath, std::ios::binary);
|
|
if (!out_file.is_open())
|
|
{
|
|
if (nullptr != m_log)
|
|
{
|
|
*m_log << __PRETTY_FUNCTION__ << endl;
|
|
*m_log << "Could not open file for write" << endl;
|
|
*m_log << filepath << endl;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out_file << out_data.rdbuf();
|
|
out_file.close();
|
|
|
|
return true;
|
|
}
|