alistair
3 years ago
17 changed files with 1067 additions and 1081 deletions
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
[submodule "tgbot-cpp"] |
||||
path = tgbot-cpp |
||||
url = https://github.com/reo7sp/tgbot-cpp/ |
||||
[submodule "spdlog"] |
||||
path = spdlog |
||||
[submodule "lib/tgbot-cpp"] |
||||
path = lib/tgbot-cpp |
||||
url = https://github.com/reo7sp/tgbot-cpp.git |
||||
[submodule "lib/spdlog"] |
||||
path = lib/spdlog |
||||
url = https://github.com/gabime/spdlog.git |
||||
|
@ -0,0 +1,602 @@
@@ -0,0 +1,602 @@
|
||||
#include "songdb.h" |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#include <iostream> |
||||
#include <istream> |
||||
#include <sstream> |
||||
#include <string> |
||||
#include <optional> |
||||
#include <set> |
||||
#include <map> |
||||
#include <vector> |
||||
#include <memory> |
||||
#include <cmath> |
||||
#include <spdlog/spdlog.h> |
||||
#include "sqlite3.h" |
||||
|
||||
int songdb::callback(void *valmap, int argc, char **argv, char **azColName) { |
||||
std::map<std::string, std::string> *values = (std::map<std::string, std::string> *)valmap; |
||||
|
||||
for (int i = 0; i < argc; i++) { |
||||
values->insert({azColName[i], argv[i]}); |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
bool songdb::setup_tables() { |
||||
char *errmsg; |
||||
int err; |
||||
|
||||
/*
|
||||
* songlists: [list id (autoincrement int), (int64) telegram group id, (string) list name ] |
||||
* |
||||
* songs : [songid int autoincrement, song name NOT NULL, song artist NULL, Song spotify ID (string) ] |
||||
* |
||||
* votes: [song id foreign key, list id foreign key, telegram user id, int rating ] |
||||
* |
||||
*/ |
||||
|
||||
std::string create_songlist_table =
|
||||
"CREATE TABLE IF NOT EXISTS songlists ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " |
||||
"groupid INT NOT NULL, " |
||||
"name VARCHAR(200) NOT NULL " |
||||
");"; |
||||
|
||||
std::string create_votes_table =
|
||||
"CREATE TABLE IF NOT EXISTS votes (" |
||||
"song INT NOT NULL, " |
||||
"list INT NOT NULL, " |
||||
"user INT NOT NULL, " |
||||
"rating NOT NULL, " |
||||
" FOREIGN KEY (song) REFERENCES tracks (id)," |
||||
" FOREIGN KEY (list) REFERENCES songlists (id)," |
||||
" PRIMARY KEY (song, list, user)" |
||||
");"; |
||||
|
||||
std::string create_tracks_table =
|
||||
"CREATE TABLE IF NOT EXISTS tracks ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " |
||||
"name VARCHAR(200) NOT NULL, " |
||||
"artist VARCHAR(200), " |
||||
"spotifyid VARCHAR(200) " |
||||
");"; |
||||
|
||||
err = sqlite3_exec(db, create_songlist_table.c_str(), NULL, 0, &errmsg); |
||||
|
||||
if (err != SQLITE_OK) { |
||||
std::cout << "SQLite Error " << create_songlist_table << "\n" << errmsg << std::endl; |
||||
exit (1); |
||||
} |
||||
|
||||
err = sqlite3_exec(db, create_tracks_table.c_str(), NULL, 0, &errmsg); |
||||
|
||||
if (err != SQLITE_OK) { |
||||
std::cout << "SQLite Error: " << create_tracks_table << "\n" << errmsg << std::endl; |
||||
exit (1); |
||||
} |
||||
|
||||
|
||||
err = sqlite3_exec(db, create_votes_table.c_str(), NULL, 0, &errmsg); |
||||
|
||||
if (err != SQLITE_OK) { |
||||
std::cout << "SQLite Error: " << create_votes_table << "\n" << errmsg << std::endl; |
||||
exit (1); |
||||
} |
||||
|
||||
if (errmsg) |
||||
sqlite3_free(errmsg); |
||||
return false; |
||||
} |
||||
|
||||
int songdb::check_error(int rc) { |
||||
if (rc != SQLITE_OK) { |
||||
spdlog::error("SQLite: {}", sqlite3_errmsg(db)); |
||||
spdlog::dump_backtrace(); |
||||
exit(1); |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
void songdb::create_new_list(int64_t group_id) { |
||||
spdlog::debug("create_new_list {}", group_id); |
||||
|
||||
sqlite3_stmt *statement; |
||||
int err; |
||||
std::string insert_query = "INSERT INTO songlists (groupid, name) VALUES (? , \"default\");"; |
||||
err = sqlite3_prepare_v2(db, insert_query.c_str(), insert_query.length(), &statement, NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 1, group_id); |
||||
check_error(err); |
||||
err = sqlite3_step(statement); |
||||
|
||||
if (err != SQLITE_DONE) { |
||||
check_error(err); |
||||
} |
||||
|
||||
sqlite3_finalize(statement); |
||||
|
||||
} |
||||
|
||||
/* Retrieve or create the song list id for a group.
|
||||
* |
||||
* As is, song lists are unique to telegram groups |
||||
*/ |
||||
int64_t songdb::get_song_list_id(int64_t group_id) { |
||||
spdlog::debug("{} {}", __PRETTY_FUNCTION__, __LINE__); |
||||
|
||||
std::string list_query = "SELECT * FROM songlists WHERE groupid = ?;"; |
||||
/* A constraint of this implementation is that every group can only have
|
||||
* one list
|
||||
*/ |
||||
int err; |
||||
|
||||
|
||||
sqlite3_stmt *statement; |
||||
sqlite3_prepare_v2(db, list_query.c_str(), list_query.length(), &statement, NULL); |
||||
err = sqlite3_bind_int64(statement, 1, group_id); |
||||
check_error(err); |
||||
err = sqlite3_step(statement); |
||||
int list_id; |
||||
|
||||
if (err == SQLITE_ROW && sqlite3_column_count(statement)) { |
||||
|
||||
list_id = sqlite3_column_int(statement, 0); |
||||
|
||||
err = sqlite3_step(statement); |
||||
if (err != SQLITE_DONE) { |
||||
// should only ever be 1 entry
|
||||
spdlog::error("Should only be one list per group?"); |
||||
spdlog::dump_backtrace(); |
||||
exit(1); |
||||
} |
||||
|
||||
|
||||
} else if (err == SQLITE_DONE) { |
||||
// create the list
|
||||
sqlite3_finalize(statement); |
||||
create_new_list(group_id); |
||||
|
||||
err = sqlite3_prepare_v2(db, list_query.c_str(), list_query.length(), &statement, NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 1, group_id); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_step(statement); |
||||
|
||||
if (err == SQLITE_ROW){ |
||||
list_id = sqlite3_column_int64(statement, 0); |
||||
} |
||||
else if (err != SQLITE_DONE) { |
||||
|
||||
check_error(err); |
||||
} |
||||
} else { |
||||
check_error(err); |
||||
} |
||||
|
||||
|
||||
return list_id; |
||||
|
||||
} |
||||
|
||||
std::optional<songdb::track_entry> songdb::get_song(int64_t id) { |
||||
std::string check_exist = "SELECT * FROM tracks WHERE id = ?;"; |
||||
|
||||
int err; |
||||
sqlite3_stmt *statement; |
||||
err = sqlite3_prepare_v2(db, check_exist.c_str(), check_exist.length(), &statement, NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 1, id); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_step(statement); |
||||
if (err == SQLITE_ROW) { |
||||
|
||||
int64_t id = sqlite3_column_int64(statement, 0); |
||||
std::string name {(char *)sqlite3_column_text(statement, 1)}; |
||||
std::string artist {(char *)sqlite3_column_text(statement, 2)}; |
||||
char * spotid = (char *)sqlite3_column_text(statement, 3); |
||||
|
||||
if (spotid) { |
||||
track_entry e {id, name, artist, std::string(spotid)}; |
||||
sqlite3_finalize(statement); |
||||
return e; |
||||
} |
||||
|
||||
track_entry e {id, name, artist}; |
||||
sqlite3_finalize(statement); |
||||
return e; |
||||
} |
||||
sqlite3_finalize(statement); |
||||
return {}; |
||||
} |
||||
|
||||
std::optional<songdb::track_entry> songdb::get_song(std::string name, std::string artist) { |
||||
return get_song(name, artist, {}); |
||||
} |
||||
|
||||
std::optional<songdb::track_entry> songdb::get_song(std::string name, std::string artist, std::optional<std::string> spotify_id) { |
||||
|
||||
spdlog::debug("get_song"); |
||||
|
||||
std::string check_exist_spot = "SELECT * FROM tracks WHERE spotifyid = ?;"; |
||||
std::string check_exist_nospot = "SELECT * FROM tracks WHERE name = ? AND artist = ?;"; |
||||
|
||||
int err; |
||||
sqlite3_stmt *statement; |
||||
|
||||
/* check whether the song exists */ |
||||
if (spotify_id) { |
||||
err = sqlite3_prepare_v2(db, check_exist_spot.c_str(), check_exist_spot.length(), &statement, NULL); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_bind_text(statement, 1, spotify_id->c_str(), spotify_id->length(), NULL); |
||||
check_error(err); |
||||
} else { |
||||
err = sqlite3_prepare_v2(db, check_exist_nospot.c_str(), check_exist_nospot.length(), &statement, NULL); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_bind_text(statement, 1, name.c_str(), name.length(), NULL); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_bind_text(statement, 2, artist.c_str(), artist.length(), NULL); |
||||
check_error(err); |
||||
} |
||||
|
||||
err = sqlite3_step(statement); |
||||
if (err == SQLITE_ROW) { |
||||
|
||||
int64_t id = sqlite3_column_int64(statement, 0); |
||||
std::string name {(char *)sqlite3_column_text(statement, 1)}; |
||||
std::string artist {(char *)sqlite3_column_text(statement, 2)}; |
||||
char * spotid = (char *)sqlite3_column_text(statement, 3); |
||||
|
||||
if (spotid) { |
||||
track_entry e {id, name, artist, std::string(spotid)}; |
||||
sqlite3_finalize(statement); |
||||
return e; |
||||
} |
||||
track_entry e {id, name, artist}; |
||||
sqlite3_finalize(statement); |
||||
return e; |
||||
spdlog::info("Entry exists."); |
||||
} else if (err != SQLITE_DONE) { |
||||
check_error(err); |
||||
sqlite3_finalize(statement); |
||||
return {}; |
||||
} |
||||
|
||||
sqlite3_finalize(statement); |
||||
return {}; |
||||
} |
||||
|
||||
std::optional<songdb::track_entry> songdb::insert_song(std::string name, std::string artist, std::optional<std::string> spotify_id) { |
||||
|
||||
spdlog::debug("insert_song"); |
||||
|
||||
std::string ins_query_spot = "INSERT INTO tracks (name, artist, spotifyid) VALUES(?, ?, ?);"; |
||||
std::string ins_query_nospot = "INSERT INTO tracks (name, artist) VALUES(?, ?);"; |
||||
|
||||
// int list_id = get_song_list_id(group_id);
|
||||
sqlite3_stmt *statement; |
||||
int err; |
||||
|
||||
auto e = get_song(name, artist, spotify_id); |
||||
if (e) { |
||||
return e; |
||||
} |
||||
|
||||
if (spotify_id) { |
||||
err = sqlite3_prepare_v2(db, ins_query_spot.c_str(), ins_query_spot.length(), &statement, NULL); |
||||
} else { |
||||
err = sqlite3_prepare_v2(db, ins_query_nospot.c_str(), ins_query_nospot.length(), &statement, NULL); |
||||
} |
||||
check_error(err); |
||||
|
||||
err = sqlite3_bind_text(statement, 1, name.c_str(), name.length(), NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_text(statement, 2, artist.c_str(), artist.length(), NULL); |
||||
check_error(err); |
||||
|
||||
if (spotify_id) { |
||||
err = sqlite3_bind_text(statement, 3, spotify_id->c_str(), spotify_id->length(), NULL); |
||||
check_error(err); |
||||
} |
||||
|
||||
err = sqlite3_step(statement); |
||||
if (err != SQLITE_DONE) { |
||||
sqlite3_finalize(statement); |
||||
spdlog::warn("Failed insertion Sqlite: {}", sqlite3_errstr(err)); |
||||
spdlog::dump_backtrace(); |
||||
sqlite3_finalize(statement); |
||||
return {}; |
||||
} |
||||
|
||||
sqlite3_finalize(statement); |
||||
return get_song(name, artist, spotify_id); |
||||
} |
||||
|
||||
bool songdb::insert_vote(int64_t user, int64_t group, int value, int64_t songid) { |
||||
spdlog::debug("insert_vote"); |
||||
auto song = get_song(songid); |
||||
if (!song) { |
||||
spdlog::error("Failed to add vote, couldnt find song id: {}", songid); |
||||
return true; |
||||
} |
||||
|
||||
int64_t list = get_song_list_id(group); |
||||
|
||||
|
||||
/* find existing vote */ |
||||
std::string ins_query = "INSERT OR REPLACE INTO votes (song, list, user, rating) " |
||||
"VALUES (?, ?, ?, ?);"; |
||||
|
||||
sqlite3_stmt *statement; |
||||
int err; |
||||
err = sqlite3_prepare_v2(db, ins_query.c_str(), ins_query.length(), &statement, NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 1, song->id); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 2, list); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 3, user); |
||||
check_error(err); |
||||
err= sqlite3_bind_int64(statement, 4, value); |
||||
check_error(err); |
||||
|
||||
err = sqlite3_step(statement); |
||||
if (err != SQLITE_DONE) { |
||||
check_error(err); |
||||
sqlite3_finalize(statement); |
||||
return true; |
||||
}
|
||||
|
||||
sqlite3_finalize(statement); |
||||
return false; |
||||
} |
||||
|
||||
std::vector<songdb::vote> songdb::get_votes_list(int64_t song_list) { |
||||
std::string query = "SELECT * FROM votes WHERE list = ?"; |
||||
|
||||
sqlite3_stmt *statement; |
||||
int err; |
||||
err = sqlite3_prepare_v2(db, query.c_str(), query.length(), &statement, NULL); |
||||
check_error(err); |
||||
err = sqlite3_bind_int64(statement, 1, song_list); |
||||
check_error(err); |
||||
|
||||
|
||||
std::vector<vote> votes; |
||||
|
||||
err = sqlite3_step(statement); |
||||
while (err == SQLITE_ROW) { |
||||
int song = sqlite3_column_int(statement, 0); |
||||
int list = sqlite3_column_int(statement, 1); |
||||
int user = sqlite3_column_int(statement, 2); |
||||
int value= sqlite3_column_int(statement, 3); |
||||
|
||||
votes.push_back({song, list,user,value}); |
||||
err = sqlite3_step(statement); |
||||
}
|
||||
|
||||
if (err != SQLITE_DONE) { |
||||
check_error(err); |
||||
} |
||||
|
||||
sqlite3_finalize(statement); |
||||
return votes; |
||||
}; |
||||
|
||||
songdb::base_weight_vector songdb::get_base_weights (int64_t song_list) { |
||||
spdlog::debug("{} {}", __PRETTY_FUNCTION__, __LINE__); |
||||
std::vector<vote> list = get_votes_list(song_list); |
||||
std::set<int64_t> chat_members; |
||||
|
||||
// {song, {user, vote}}
|
||||
std::map<int, std::map<int64_t, double>> vote_info {}; |
||||
|
||||
for (auto v : list) { |
||||
chat_members.insert(v.user); |
||||
|
||||
vote_info[v.song] = {{v.user, v.value}}; |
||||
} |
||||
|
||||
for (auto v : vote_info) { |
||||
// Insert zero values for members who have not voted
|
||||
for (auto m : chat_members) { |
||||
if (!v.second.count(m)) { |
||||
v.second.insert({m, 0.0}); |
||||
} |
||||
} |
||||
|
||||
// normalise weightings
|
||||
double total = 0; |
||||
for (auto m : v.second) { |
||||
total += m.second; |
||||
} |
||||
|
||||
for (auto m : v.second) { |
||||
v.second[m.first] = m.second / total; |
||||
} |
||||
} |
||||
|
||||
if (vote_info.size() == 0) { |
||||
return {}; |
||||
} |
||||
|
||||
/* turn it into a nice easy to use vector
|
||||
* Relying on the fact maps are sorted and things will always be in the |
||||
* same order. |
||||
*/ |
||||
base_weight_vector v; |
||||
|
||||
for (auto song : vote_info) { |
||||
v.song_order.push_back(song.first); |
||||
} |
||||
|
||||
auto a = vote_info.begin(); |
||||
for (auto user : a->second) { |
||||
v.person_order.push_back(user.first); |
||||
} |
||||
|
||||
for (auto song: vote_info) { |
||||
std::vector<double> user_votes; |
||||
for (auto user : song.second) { |
||||
user_votes.push_back(user.second); |
||||
} |
||||
|
||||
assert(user_votes.size() == v.person_order.size()); |
||||
|
||||
v.weights.push_back(user_votes); |
||||
} |
||||
|
||||
return v; |
||||
} |
||||
|
||||
double songdb::dot_product(const std::vector<double> &a, const std::vector<double> &b) { |
||||
assert(a.size() == b.size()); |
||||
|
||||
double dot = 0; |
||||
|
||||
for (int i = 0; i < a.size(); i++) { |
||||
dot += a.at(i) * b.at(i); |
||||
} |
||||
|
||||
double dot2 = sqrt(dot); |
||||
return dot2; |
||||
} |
||||
|
||||
double songdb::weight_badness_inner_product(const std::vector<double> ¤t_badness, const std::vector<double> &song_goodness) { |
||||
return dot_product(current_badness,song_goodness); |
||||
} |
||||
|
||||
std::vector<double> songdb::update_badness(std::vector<double> old_badness, std::vector<double> song_goodness) { |
||||
auto new_badness = old_badness; |
||||
for (int i = 0; i < old_badness.size(); i++) { |
||||
new_badness[i] = new_badness[i] - song_goodness[i]; |
||||
} |
||||
|
||||
return new_badness; |
||||
} |
||||
|
||||
/**
|
||||
* The returned base weight vector has the songs in the sorted order, |
||||
* the weights field is the badness vector used for the next song in the |
||||
* list. |
||||
* |
||||
* /param num: the number songs to return |
||||
*/ |
||||
songdb::base_weight_vector songdb::get_top_songs(songdb::base_weight_vector input, std::vector<double> starting_badness, int num) { |
||||
spdlog::debug("{} {}", __PRETTY_FUNCTION__, __LINE__); |
||||
|
||||
if (num > input.song_order.size()) { |
||||
num = input.song_order.size(); |
||||
} |
||||
|
||||
base_weight_vector result {}; |
||||
auto current_badness = starting_badness; |
||||
result.person_order = input.person_order; |
||||
|
||||
struct score { |
||||
int64_t song; |
||||
double score; |
||||
std::vector<double> base_weight; |
||||
}; |
||||
|
||||
|
||||
// create scores vector
|
||||
std::vector<score> scores {};
|
||||
for (int i = 0; i < input.song_order.size(); i++) { |
||||
|
||||
scores.push_back({input.song_order.at(i),
|
||||
0, input.weights.at(i)}); |
||||
} |
||||
|
||||
for (int i = 0; i < num; i++) { |
||||
// Compute scores based on badness
|
||||
for (int j = 0; j < scores.size(); j++) { |
||||
scores[j].score = weight_badness_inner_product(current_badness, scores[j].base_weight); |
||||
} |
||||
|
||||
// sort scores
|
||||
std::sort(scores.rbegin(), scores.rend(),
|
||||
[](const score &a, const score &b) |
||||
{ |
||||
return a.score > b.score; |
||||
} |
||||
); |
||||
|
||||
// chose the song with the best score
|
||||
auto chosen = scores.at(scores.size() - 1); |
||||
result.song_order.push_back(chosen.song); |
||||
|
||||
// update badness vector
|
||||
current_badness = update_badness(current_badness, chosen.base_weight); |
||||
result.weights.push_back(current_badness); |
||||
|
||||
// run algorithm again on the subset not containing the chosen
|
||||
// score, with the updated badness vector
|
||||
scores.pop_back(); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
std::string songdb::get_top_5_songs(int64_t telegram_group) { |
||||
spdlog::debug("{} {}", __PRETTY_FUNCTION__, __LINE__); |
||||
int64_t song_list = get_song_list_id(telegram_group); |
||||
auto songs = get_base_weights(song_list); |
||||
|
||||
if (songs.weights.size() == 0) { |
||||
return {}; |
||||
} |
||||
|
||||
std::vector<double> starting_badness {}; |
||||
for (int i = 0; i < songs.person_order.size(); i++) { |
||||
starting_badness.push_back(1); |
||||
} |
||||
|
||||
auto chosen = get_top_songs(songs, starting_badness, 5); |
||||
|
||||
std::string slist = "Top 5 Songs:\n\n"; |
||||
for (int i = 0; i < chosen.song_order.size(); i++) { |
||||
int64_t songid = chosen.song_order.at(i); |
||||
auto song = get_song(songid); |
||||
if (!song) { |
||||
throw std::runtime_error("Invalid state achieved."); |
||||
} |
||||
|
||||
slist += song->name; |
||||
slist += " "; |
||||
slist += song->artist; |
||||
slist += "\n"; |
||||
|
||||
} |
||||
return slist; |
||||
} |
||||
|
||||
std::vector<songdb::track_entry> songdb::generate_track_list(int64_t song_list) { |
||||
auto base_weights = get_base_weights(song_list); |
||||
|
||||
for (int i = 0; i < base_weights.song_order.size(); i++) { |
||||
spdlog::info("song {} nppl {}", base_weights.song_order[i], base_weights.weights.size()); |
||||
} |
||||
|
||||
std::vector<track_entry> retlist {}; |
||||
return retlist; |
||||
} |
||||
|
||||
songdb::songdb(std::string filepath): filepath(filepath) { |
||||
int err = sqlite3_open(filepath.c_str(), &db); |
||||
if (err) { |
||||
std::cout << "Failed to open database: " << sqlite3_errmsg(db); |
||||
exit(1); |
||||
} |
||||
|
||||
setup_tables(); |
||||
} |
||||
|
||||
songdb::~songdb () { |
||||
sqlite3_close(db); |
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
#pragma once |
||||
|
||||
#include "sqlite3.h" |
||||
#include <string> |
||||
#include <cstdio> |
||||
#include <vector> |
||||
#include <tuple> |
||||
#include <optional> |
||||
|
||||
class songdb { |
||||
std::string filepath; |
||||
sqlite3 *db; |
||||
|
||||
public: |
||||
|
||||
struct runtime_vals { }; |
||||
runtime_vals runtime_data; |
||||
|
||||
protected: |
||||
|
||||
enum error_codes { |
||||
NOT_FOUND, |
||||
ALREADY_ADDED |
||||
}; |
||||
|
||||
static int callback(void *valmap, int argc, char **argv, char **azColName); |
||||
bool setup_tables(); |
||||
int check_error(int rc); |
||||
|
||||
public: |
||||
|
||||
struct track_entry { |
||||
int64_t id; |
||||
std::string name; |
||||
std::string artist; |
||||
std::optional<std::string> spotify_id; |
||||
|
||||
}; |
||||
|
||||
struct vote { |
||||
int song; |
||||
int list; |
||||
int64_t user; |
||||
double value; |
||||
}; |
||||
|
||||
struct base_weight_vector { |
||||
std::vector<int64_t> person_order; |
||||
std::vector<int64_t> song_order; |
||||
std::vector<std::vector<double>> weights;
|
||||
}; |
||||
|
||||
void create_new_list(int64_t group_id); |
||||
int64_t get_song_list_id(int64_t group_id); |
||||
std::optional<track_entry> get_song(int64_t id); |
||||
std::optional<track_entry> get_song(std::string name, std::string artist); |
||||
std::optional<track_entry> get_song(std::string name, std::string artist, std::optional<std::string> spotify_id); |
||||
std::optional<track_entry> insert_song(std::string name, std::string artist, std::optional<std::string> spotify_id); |
||||
bool insert_vote(int64_t user, int64_t group, int value, int64_t songid); |
||||
std::vector<vote> get_votes_list(int64_t song_list); |
||||
base_weight_vector get_base_weights (int64_t song_list); |
||||
double dot_product(const std::vector<double> &a, const std::vector<double> &b); |
||||
double weight_badness_inner_product(const std::vector<double> ¤t_badness, const std::vector<double> &song_goodness); |
||||
std::vector<double> update_badness(std::vector<double> old_badness, std::vector<double> song_goodness); |
||||
base_weight_vector get_top_songs(base_weight_vector input, std::vector<double> starting_badness, int num); |
||||
std::string get_top_5_songs(int64_t telegram_group); |
||||
std::vector<track_entry> generate_track_list(int64_t song_list); |
||||
songdb(std::string filepath); |
||||
~songdb(); |
||||
}; |
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
#include "spotify.h" |
||||
#include "cpr/cprtypes.h" |
||||
#include "cpr/parameters.h" |
||||
#include <nlohmann/json.hpp> |
||||
#include <cpr/cpr.h> |
||||
#include <istream> |
||||
#include <sstream> |
||||
#include <string> |
||||
#include <spdlog/spdlog.h> |
||||
#include <iostream> |
||||
#include <cstdio> |
||||
#include "Base64.hpp" |
||||
#include <optional> |
||||
|
||||
using json = nlohmann::json; |
||||
|
||||
void spotify::verify_logged_in() { |
||||
/*
|
||||
* |
||||
* Would be a good idea to just requrst a page to chek. but the /me |
||||
* endpoint is for logged in users i guess? and this iis jut an app.
|
||||
* |
||||
cpr::Response r = cpr::Get(cpr::Url{API_NAME_BASE + "v1/me"},cpr::Header{{"Authorization", "Bearer " + access_token}}); |
||||
std::istringstream isj {r.text}; |
||||
json auth_response; |
||||
try { |
||||
isj >> auth_response; |
||||
} catch (json::exception & e) { |
||||
std::cout << e.id << r.text; |
||||
} |
||||
|
||||
if (auth_response.count("display_name")) { |
||||
std::cout << "Logged in as " << auth_response["display_name"] << std::endl << auth_response["href"] << std::endl; |
||||
} else { |
||||
std::cout << "Failed to log in" << std::endl << auth_response.dump(4) << std::endl; |
||||
} |
||||
*/ |
||||
} |
||||
|
||||
spotify::spotify(std::string access_token) : access_token(access_token) { |
||||
auth_header = cpr::Header{{"Authorization", "Bearer " + access_token}}; |
||||
verify_logged_in(); |
||||
} |
||||
|
||||
spotify::spotify(std::string client_id, std::string client_secret) { |
||||
auto ascii_token = client_id + ":" + client_secret; |
||||
|
||||
size_t buf_length = base64::get_encoded_length(ascii_token.length()); |
||||
auto buf = std::make_unique<uint8_t[]>(buf_length); |
||||
std::string auth_token; |
||||
|
||||
base64::encode((uint8_t *)(ascii_token.c_str()), ascii_token.length(), buf.get(), buf_length); |
||||
auth_token = std::string {(char *)buf.get(), buf_length}; |
||||
|
||||
cpr::Response r = cpr::Post(cpr::Url{"https://accounts.spotify.com/api/token"},cpr::Header{{"Authorization", "Basic " + auth_token}}, cpr::Parameters{{"grant_type", "client_credentials"}}); |
||||
|
||||
std::istringstream isj {r.text}; |
||||
|
||||
json auth_response; |
||||
if (r.status_code == 200) { |
||||
isj >> auth_response; |
||||
} else { |
||||
spdlog::error("Login error {} {}",r.status_code, r.status_line); |
||||
} |
||||
|
||||
|
||||
if (auth_response.count("access_token")) { |
||||
access_token = auth_response["access_token"]; |
||||
std::cout << "Successfully logged into spotify" << std::endl << "Access token: " << access_token << std::endl; |
||||
} else { |
||||
std::cout << "Unable to log into spotify:" << std::endl << auth_response.dump(4) << std::endl; |
||||
exit (1); |
||||
} |
||||
|
||||
auth_header = cpr::Header{{"Authorization", "Bearer " + access_token}}; |
||||
verify_logged_in(); |
||||
} |
||||
|
||||
std::optional<json> spotify::get_track(std::string track_id) { |
||||
|
||||
auto r = cpr::Get(cpr::Url{API_NAME_BASE + "v1/tracks/" + track_id}, auth_header); |
||||
if (r.status_code == 200) { |
||||
std::istringstream isj {r.text}; |
||||
json info ; |
||||
try { |
||||
isj >> info; |
||||
} catch (json::exception & e) { |
||||
std::cout << "Json error" << std::endl; |
||||
} |
||||
return info; |
||||
} else { |
||||
std::cout << r.text << std::endl; |
||||
return {}; |
||||
} |
||||
} |
||||
|
||||
/*
|
||||
* Parse track link from a spotify url like: |
||||
* |
||||
* https://open.spotify.com/track/4UO1pfxi5fDbxshrwwznJ2?si=BtN9Yn_JQXSHGUa4CEZKvQ&utm_source=copy-link
|
||||
* |
||||
* |
||||
*/ |
||||
std::optional<std::string> spotify::track_id_from_link(std::string link) { |
||||
const std::string start = "spotify.com/track/"; |
||||
auto f = link.find(start); |
||||
if (f == std::string::npos) { |
||||
return {}; |
||||
} |
||||
|
||||
auto end = link.find("?", f); |
||||
auto begin = f + start.length(); |
||||
return link.substr(begin, end - begin); |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
#pragma once |
||||
|
||||
#include "spotify.h" |
||||
#include "cpr/cprtypes.h" |
||||
#include "cpr/parameters.h" |
||||
#include <nlohmann/json.hpp> |
||||
#include <cpr/cpr.h> |
||||
#include <istream> |
||||
#include <sstream> |
||||
#include <string> |
||||
#include <spdlog/spdlog.h> |
||||
#include <iostream> |
||||
#include <cstdio> |
||||
#include "Base64.hpp" |
||||
#include <optional> |
||||
|
||||
using json = nlohmann::json; |
||||
|
||||
class spotify { |
||||
|
||||
const std::string API_NAME_BASE = "https://api.spotify.com/"; |
||||
std::string access_token; |
||||
cpr::Header auth_header; |
||||
|
||||
public: |
||||
void verify_logged_in(); |
||||
spotify(std::string access_token); |
||||
spotify(std::string client_id, std::string client_secret); |
||||
std::optional<json> get_track(std::string track_id) ; |
||||
std::optional<std::string> track_id_from_link(std::string link); |
||||
}; |
@ -0,0 +1,184 @@
@@ -0,0 +1,184 @@
|
||||
#include <csignal> |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#include <exception> |
||||
#include <istream> |
||||
#include <sstream> |
||||
#include <stdexcept> |
||||
#include <string> |
||||
|
||||
#include <spdlog/spdlog.h> |
||||
#include <tgbot/tgbot.h> |
||||
#include <nlohmann/json.hpp> |
||||
|
||||
#include "util.h" |
||||
#include "spotify.h" |
||||
#include "songdb.h" |
||||
|
||||
using namespace TgBot; |
||||
using json = nlohmann::json; |
||||
|
||||
int main() { |
||||
spdlog::set_level(spdlog::level::info); // Set global log level to debug
|
||||
spdlog::enable_backtrace(32); |
||||
|
||||
char *teletok = getenv("TELEGRAM_TOKEN"); |
||||
|
||||
// needed if file not exist
|
||||
char *spotid = getenv("SPOTIFY_ID"); |
||||
char *spotsecret = getenv("SPOTIFY_SECRET"); |
||||
char *spotaccess_token = getenv("SPOTIFY_TOKEN"); |
||||
|
||||
if (!teletok) { |
||||
std::cout << "Need to set environment variable TELEGRAM_TOKEN" << std::endl; |
||||
exit(1); |
||||
} |
||||
|
||||
if (!spotaccess_token) { |
||||
if (!spotid) { |
||||
std::cout << "Need to set environment variable SPOTIFY_ID or SPOTIFY_TOKEN" << std::endl; |
||||
exit(1); |
||||
} |
||||
if (!spotsecret) { |
||||
std::cout << "Need to set environment variable SPOTIFY_SECRET of SPOTIFY_TOKEN" << std::endl; |
||||
exit(1); |
||||
} |
||||
} |
||||
|
||||
spotify *s; |
||||
if (spotaccess_token)
|
||||
s = new spotify(spotaccess_token); |
||||
else
|
||||
s = new spotify(spotid, spotsecret); |
||||
|
||||
signal(SIGINT, [](int s) { |
||||
spdlog::info("Shutting down..."); |
||||
exit(0); |
||||
}); |
||||
|
||||
songdb data {"test.db"}; |
||||
std::string teletoken {teletok}; |
||||
Bot bot(teletoken); |
||||
|
||||
InlineKeyboardMarkup::Ptr keyboard(new InlineKeyboardMarkup); |
||||
std::vector<InlineKeyboardButton::Ptr> row0; |
||||
|
||||
for(int i = 0; i <= 4; i++) { |
||||
InlineKeyboardButton::Ptr btn(new InlineKeyboardButton); |
||||
btn->text = std::to_string(i); |
||||
btn->callbackData= std::to_string(i); |
||||
row0.push_back(btn); |
||||
} |
||||
|
||||
keyboard->inlineKeyboard.push_back(row0); |
||||
|
||||
bot.getEvents().onCallbackQuery([&bot, &keyboard, &data](CallbackQuery::Ptr query) { |
||||
if ((query->data == "1") || (query->data == "2") || (query->data == "3") || (query->data == "4") || (query->data == "0")) { |
||||
std::istringstream is {query->data}; |
||||
int value; |
||||
is >> value; |
||||
|
||||
std::string songidflag = "songid:"; |
||||
auto a = query->message->text.find(songidflag); |
||||
auto b = query->message->text.find("\n", a); |
||||
|
||||
if (a == std::string::npos || b == std::string::npos) { |
||||
spdlog::error("Parse songid"); |
||||
spdlog::dump_backtrace(); |
||||
return; |
||||
} |
||||
a += songidflag.length(); |
||||
|
||||
std::istringstream is2 {query->message->text.substr(a, b - a)}; |
||||
int64_t songid; |
||||
is2 >> songid; |
||||
auto song = data.get_song(songid); |
||||
if (!song) { |
||||
spdlog::error ("bad song id"); |
||||
} |
||||
|
||||
data.insert_vote(query->from->id, query->message->chat->id, value, songid); |
||||
} |
||||
}); |
||||
|
||||
bot.getEvents().onCommand("add", [&bot, &keyboard, &data, s](Message::Ptr message) { |
||||
std::string title; |
||||
std::string artist; |
||||
int songid; |
||||
|
||||
if (message->text.find("spotify.com") != std::string::npos) { |
||||
std::string link = util::trim_whitespace(message->text.substr(message->text.find("add") + 3)); |
||||
auto resp = s->track_id_from_link(link); |
||||
if (!resp) { |
||||
bot.getApi().sendMessage(message->chat->id, "Sorry, I don't understand that link."); |
||||
return; |
||||
} |
||||
|
||||
auto spot_resp = s->get_track(*resp); |
||||
|
||||
if (!spot_resp) { |
||||
bot.getApi().sendMessage(message->chat->id, "Sorry, I cannot find that track in spotify."); |
||||
return; |
||||
} |
||||
|
||||
json track_data = *spot_resp; |
||||
|
||||
title = track_data["name"]; |
||||
artist = track_data["artists"][0]["name"]; |
||||
auto song = data.insert_song(title, artist, *resp); |
||||
songid = song->id; |
||||
} else { |
||||
title = util::trim_whitespace(message->text.substr(message->text.find("add") + 3)); |
||||
artist = ""; |
||||
auto song = data.insert_song(title, artist, {}); |
||||
songid = song->id; |
||||
} |
||||
|
||||
std::string response = "Added song: " + title; |
||||
if (artist != "") |
||||
response += ", by " + artist; |
||||
|
||||
response += "\n\n"; |
||||
std::ostringstream os; |
||||
os << songid; |
||||
|
||||
response += "songid:" + os.str() + "\n\r\n\r"; |
||||
response += "Everyone, please rate how well you know this song /5"; |
||||
|
||||
bot.getApi().sendMessage(message->chat->id, response, false, 0, keyboard, "Markdown"); |
||||
}); |
||||
|
||||
bot.getEvents().onCommand("vote", [&bot](Message::Ptr message) { |
||||
bot.getApi().sendMessage(message->chat->id, "Hi!"); |
||||
}); |
||||
|
||||
|
||||
bot.getEvents().onCommand("start", [&bot, &data](Message::Ptr message) { |
||||
bot.getApi().sendMessage(message->chat->id, "Hi!"); |
||||
}); |
||||
|
||||
bot.getEvents().onCommand("list", [&bot, &data](Message::Ptr message) { |
||||
try { |
||||
std::string response = data.get_top_5_songs(message->chat->id); |
||||
bot.getApi().sendMessage(message->chat->id, response); |
||||
} catch (std::exception const &e) { |
||||
spdlog::error("exp: {}", e.what()); |
||||
spdlog::dump_backtrace(); |
||||
} |
||||
}); |
||||
|
||||
try { |
||||
printf("Bot username: %s\n", bot.getApi().getMe()->username.c_str()); |
||||
bot.getApi().deleteWebhook(); |
||||
TgLongPoll longPoll(bot); |
||||
while (true) { |
||||
printf("Long poll started\n"); |
||||
longPoll.start(); |
||||
} |
||||
} catch (std::exception& e) { |
||||
printf("error: %s\n", e.what()); |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
#include "util.h" |
||||
#include <string> |
||||
#include <cstdlib> |
||||
#include <cstdio> |
||||
#include <istream> |
||||
#include <fstream> |
||||
#include <sstream> |
||||
#include <iostream> |
||||
|
||||
namespace util { |
||||
|
||||
std::string
|
||||
trim_whitespace(std::string s)
|
||||
{ |
||||
int ff = s.find_first_not_of(" \n\t"); |
||||
int ll = s.find_last_not_of(" \n\t"); |
||||
return s.substr(ff, ll - ff + 1); |
||||
} |
||||
|
||||
|
||||
|
||||
std::string
|
||||
read_file(std::string const &fpath)
|
||||
{ |
||||
std::ostringstream sstr; |
||||
std::ifstream in (fpath); |
||||
sstr << in.rdbuf(); |
||||
return sstr.str(); |
||||
} |
||||
|
||||
void write_file(std::string const &fpath, std::string const &content) { |
||||
std::fstream s; |
||||
s.open(fpath, std::ios_base::out); |
||||
if (!s.is_open()) { |
||||
std::cerr << "Error: failed to open file "<< fpath; |
||||
return; |
||||
} |
||||
s << content; |
||||
s.close(); |
||||
} |
||||
|
||||
}; |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
|
||||
namespace util { |
||||
std::string trim_whitespace(std::string s); |
||||
std::string read_file(std::string const &fpath); |
||||
void write_file(std::string const &fpath, std::string const &content); |
||||
} |
Loading…
Reference in new issue