<commit_before>#include "utils.h"

#include <sys/stat.h>

#include <algorithm>
#include <cctype> // ::isspace
#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <stdexcept>

#include <glibmm/fileutils.h>
#include <glibmm/uriutils.h>

#include "debug.h"
#include "ui.h"
#include "gettext.h"

using namespace std;
using namespace Glib;

const DB_playItem_t *last;

static const ustring LW_FMT = "http://lyrics.wikia.com/api.php?action=lyrics&fmt=xml&artist=%1&song=%2";
static const char *home_cache = getenv("XDG_CACHE_HOME");
static const string lyrics_dir = (home_cache ? string(home_cache) : string(getenv("HOME")) + "/.cache")
                               + "/deadbeef/lyrics/";

static experimental::optional<ustring>(*const providers[])(DB_playItem_t *) = {&get_lyrics_from_script, &download_lyrics_from_lyricwiki};

inline string cached_filename(string artist, string title) {
	replace(artist.begin(), artist.end(), '/', '_');
	replace(title.begin(), title.end(), '/', '_');

	return lyrics_dir + artist + '-' + title;
}

extern "C"
bool is_cached(const char *artist, const char *title) {
	return artist && title && access(cached_filename(artist, title).c_str(), 0) == 0;
}

extern "C"
void ensure_lyrics_path_exists() {
	mkpath(lyrics_dir, 0755);
}

/**
 * Loads the cached lyrics
 * @param artist The artist name
 * @param title  The song title
 * @note         Have no idea about the encodings, so a bug possible here
 */
experimental::optional<ustring> load_cached_lyrics(const char *artist, const char *title) {
	string filename = cached_filename(artist, title);
	debug_out << "filename = '" << filename << "'\n";
	try {
		return {file_get_contents(filename)};
	} catch (const FileError& error) {
		debug_out << error.what();
		return {};
	}
}

bool save_cached_lyrics(const string &artist, const string &title, const string &lyrics) {
	string filename = cached_filename(artist, title);
	ofstream t(filename);
	if (!t) {
		cerr << "lyricbar: could not open file for writing: " << filename << endl;
		return false;
	}
	t << lyrics;
	return true;
}

bool is_playing(DB_playItem_t *track) {
	DB_playItem_t *pl_track = deadbeef->streamer_get_playing_track();
	if (!pl_track)
		return false;

	deadbeef->pl_item_unref(pl_track);
	return pl_track == track;
}

static
experimental::optional<ustring> get_lyrics_from_metadata(DB_playItem_t *track) {
	pl_lock_guard guard;
	const char *lyrics = deadbeef->pl_find_meta(track, "lyrics")
	                  ?: deadbeef->pl_find_meta(track, "unsynced lyrics");
	if (lyrics)
		return ustring{lyrics};
	else return {};
}

experimental::optional<ustring> get_lyrics_from_script(DB_playItem_t *track) {
	array<char, 4096> buf;
	deadbeef->conf_get_str("lyricbar.customcmd", nullptr, buf.data(), buf.size());
	if (!buf[0]) {
		return {};
	}
	auto tf_code = deadbeef->tf_compile(buf.data());
	if (!tf_code) {
	    std::cerr << "lyricbar: Invalid script command!\n";
	    return {};
	}
	ddb_tf_context_t ctx{};
	ctx._size = sizeof(ctx);
	ctx.it = track;

	int command_len = deadbeef->tf_eval(&ctx, tf_code, buf.data(), buf.size());
	deadbeef->tf_free(tf_code);

	if (command_len < 0) {
		std::cerr << "lyricbar: Invalid script command!\n";
		return {};
	}

	std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(buf.data(), "r"), pclose);
	if (!pipe) {
		perror("lyricbar: popen script command");
		return {};
	}
	size_t nbytes;
	string ans;
	do {
		nbytes = fread(buf.data(), 1, buf.size(), pipe.get());
		ans.append(buf.data(), nbytes);
	} while (nbytes != 0);
	if (ferror(pipe.get())) {
		perror("lyricbar: fread from script output");
		return {};
	}
	if (ans.empty()) {
		return {};
	}
	auto res = ustring{std::move(ans)};
	if (!res.validate()) {
		cerr << "lyricbar: script output is not a valid UTF8 string!\n";
		return {};
	}
	return {std::move(res)};
}

void char_asciify(gunichar c, ustring &out) {
	switch (c) {
		case U'’':
		case U'´':
		case U'`':
			c = '\'';
			break;
		case U'“':
		case U'”':
			c = '"';
			break;
		case U'–':
		case U'—':
			c = '-';
			break;
		case U'…':
			out.append(3, '.');
			return;
		}
	out.push_back(c);
}
<commit_msg>Switch to gio-based script spawning<commit_after>#include "utils.h"

#include <sys/stat.h>

#include <algorithm>
#include <cctype> // ::isspace
#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <stdexcept>

#include <glibmm/fileutils.h>
#include <glibmm/uriutils.h>
#include <giomm.h>

#include "debug.h"
#include "ui.h"
#include "gettext.h"

using namespace std;
using namespace Glib;

const DB_playItem_t *last;

static const ustring LW_FMT = "http://lyrics.wikia.com/api.php?action=lyrics&fmt=xml&artist=%1&song=%2";
static const char *home_cache = getenv("XDG_CACHE_HOME");
static const string lyrics_dir = (home_cache ? string(home_cache) : string(getenv("HOME")) + "/.cache")
                               + "/deadbeef/lyrics/";

static experimental::optional<ustring>(*const providers[])(DB_playItem_t *) = {&get_lyrics_from_script, &download_lyrics_from_lyricwiki};

inline string cached_filename(string artist, string title) {
	replace(artist.begin(), artist.end(), '/', '_');
	replace(title.begin(), title.end(), '/', '_');

	return lyrics_dir + artist + '-' + title;
}

extern "C"
bool is_cached(const char *artist, const char *title) {
	return artist && title && access(cached_filename(artist, title).c_str(), 0) == 0;
}

extern "C"
void ensure_lyrics_path_exists() {
	mkpath(lyrics_dir, 0755);
}

/**
 * Loads the cached lyrics
 * @param artist The artist name
 * @param title  The song title
 * @note         Have no idea about the encodings, so a bug possible here
 */
experimental::optional<ustring> load_cached_lyrics(const char *artist, const char *title) {
	string filename = cached_filename(artist, title);
	debug_out << "filename = '" << filename << "'\n";
	try {
		return {file_get_contents(filename)};
	} catch (const FileError& error) {
		debug_out << error.what();
		return {};
	}
}

bool save_cached_lyrics(const string &artist, const string &title, const string &lyrics) {
	string filename = cached_filename(artist, title);
	ofstream t(filename);
	if (!t) {
		cerr << "lyricbar: could not open file for writing: " << filename << endl;
		return false;
	}
	t << lyrics;
	return true;
}

bool is_playing(DB_playItem_t *track) {
	DB_playItem_t *pl_track = deadbeef->streamer_get_playing_track();
	if (!pl_track)
		return false;

	deadbeef->pl_item_unref(pl_track);
	return pl_track == track;
}

static
experimental::optional<ustring> get_lyrics_from_metadata(DB_playItem_t *track) {
	pl_lock_guard guard;
	const char *lyrics = deadbeef->pl_find_meta(track, "lyrics")
	                  ?: deadbeef->pl_find_meta(track, "unsynced lyrics");
	if (lyrics)
		return ustring{lyrics};
	else return {};
}

experimental::optional<ustring> get_lyrics_from_script(DB_playItem_t *track) {
	std::string buf = std::string(4096, '\0');
	deadbeef->conf_get_str("lyricbar.customcmd", nullptr, &buf[0], buf.size());
	if (!buf[0]) {
		return {};
	}
	auto tf_code = deadbeef->tf_compile(buf.data());
	if (!tf_code) {
	    std::cerr << "lyricbar: Invalid script command!\n";
	    return {};
	}
	ddb_tf_context_t ctx{};
	ctx._size = sizeof(ctx);
	ctx.it = track;

	int command_len = deadbeef->tf_eval(&ctx, tf_code, &buf[0], buf.size());
	deadbeef->tf_free(tf_code);
	if (command_len < 0) {
		std::cerr << "lyricbar: Invalid script command!\n";
		return {};
	}

	buf.resize(command_len);

	std::string script_output;
	int exit_status = 0;
	spawn_command_line_sync(buf, &script_output, nullptr, &exit_status);

	if (script_output.empty() || exit_status != 0) {
		return {};
	}

	auto res = ustring{std::move(script_output)};
	if (!res.validate()) {
		cerr << "lyricbar: script output is not a valid UTF8 string!\n";
		return {};
	}
	return {std::move(res)};
}

void char_asciify(gunichar c, ustring &out) {
	switch (c) {
		case U'’':
		case U'´':
		case U'`':
			c = '\'';
			break;
		case U'“':
		case U'”':
			c = '"';
			break;
		case U'–':
		case U'—':
			c = '-';
			break;
		case U'…':
			out.append(3, '.');
			return;
		}
	out.push_back(c);
}

{'fraction_non_alphanumeric': 0.1660132320509679, 'fraction_numerical': 0.004778240627297231, 'mean_word_length': 4.482202820685023, 'pattern_counts': {'":': 0, '<': 82, '<?xml version=': 0, '>': 52, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1593085', 'n_tokens_mistral': 3162, 'n_tokens_neox': 3025, 'n_words': 690}