Add virtual channel for all unwatched videos

This commit is contained in:
Daniel Schulte 2020-11-26 13:27:11 +01:00
parent 160fb1cfff
commit c571e5fec4
3 changed files with 118 additions and 50 deletions

105
main.cpp
View File

@ -22,13 +22,14 @@ static std::string user_home;
std::vector<Channel> channels; std::vector<Channel> channels;
std::unordered_map<std::string, std::vector<Video>> videos; std::unordered_map<std::string, std::vector<Video>> videos;
std::optional<size_t> selected_channel; size_t selected_channel;
size_t current_video_count = 0; size_t current_video_count = 0;
size_t selected_video = 0; size_t selected_video = 0;
size_t videos_per_page = 0; size_t videos_per_page = 0;
size_t current_page_count = 0; size_t current_page_count = 0;
size_t title_offset = 0; size_t title_offset = 0;
bool any_title_in_next_half = false; bool any_title_in_next_half = false;
bool clear_channels_on_change = false;
static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false) static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false)
{ {
@ -58,7 +59,7 @@ void draw_channel_list(const std::vector<Video> &videos)
const int pages = videos.size() / available_rows; const int pages = videos.size() / available_rows;
const int cur_page = selected_video / available_rows; const int cur_page = selected_video / available_rows;
const std::string channel_name = std::string("Channel: ") + channels[*selected_channel].name.c_str(); const std::string channel_name = std::string("Channel: ") + channels[selected_channel].name.c_str();
termpaint_surface_write_with_attr(surface, 0, 0, channel_name.c_str(), get_attr(ASNormal)); termpaint_surface_write_with_attr(surface, 0, 0, channel_name.c_str(), get_attr(ASNormal));
if(pages > 1) { if(pages > 1) {
@ -130,40 +131,67 @@ void draw_no_channels_msg()
} }
} }
void load_videos_for_channel(const std::string &channelId, bool force=false) void load_videos_for_channel(const Channel &channel, bool force=false)
{ {
if(videos.find(channelId) != videos.end() && !force) if(!force && videos.find(channel.id) != videos.end() && !videos[channel.id].empty())
return; return;
std::vector<Video> &channelVideos = videos[channelId]; std::vector<Video> &channelVideos = videos[channel.id];
channelVideos = Video::get_all_for_channel(channelId); if(channel.is_virtual) {
channelVideos = Video::get_all_with_flag_value(channel.virtual_flag, channel.virtual_flag_value);
} else {
channelVideos = Video::get_all_for_channel(channel.id);
}
for(Video &video: channelVideos) { for(Video &video: channelVideos) {
video.tui_title_width = string_width(video.title); video.tui_title_width = string_width(video.title);
} }
if(channels[*selected_channel].id == channelId) if(channels[selected_channel].id == channel.id)
selected_video = 0; selected_video = 0;
} }
void fetch_videos_for_channel(Channel &ch, bool name_in_title=false) void fetch_videos_for_channel(Channel &channel, bool name_in_title=false)
{ {
if(channel.is_virtual) {
std::vector<Video> &channelVideos = videos[channel.id];
channelVideos = Video::get_all_with_flag_value(channel.virtual_flag, channel.virtual_flag_value);
channel.video_count = channelVideos.size();
for(Video &video: channelVideos) {
video.tui_title_width = string_width(video.title);
}
return;
}
std::string title("Refreshing"); std::string title("Refreshing");
if(name_in_title) if(name_in_title)
title.append(" ").append(ch.name); title.append(" ").append(channel.name);
title.append(""); title.append("");
progress_info *info = begin_progress(title, 30); progress_info *info = begin_progress(title, 30);
ch.fetch_new_videos(db, info); channel.fetch_new_videos(db, info);
end_progress(info); end_progress(info);
load_videos_for_channel(channels[*selected_channel].id, true); load_videos_for_channel(channels[selected_channel], true);
ch.load_info(db); channel.load_info(db);
}
bool startswith(const std::string &what, const std::string &with)
{
const size_t len = with.length();
return what.substr(0, len) == with;
} }
void select_channel_by_index(const int index) { void select_channel_by_index(const int index) {
if(clear_channels_on_change) {
for(auto &[k, v]: videos) {
v.clear();
}
}
selected_channel = index; selected_channel = index;
const Channel &channel = channels.at(*selected_channel); const Channel &channel = channels.at(selected_channel);
selected_video = 0; selected_video = 0;
current_video_count = channel.video_count; current_video_count = channel.video_count;
load_videos_for_channel(channel.id); load_videos_for_channel(channel, clear_channels_on_change);
clear_channels_on_change = channel.is_virtual;
} }
void select_channel_by_name(const std::string &channel_name) { void select_channel_by_name(const std::string &channel_name) {
@ -185,19 +213,29 @@ void add_channel_to_list(Channel &channel)
channel.load_info(db); channel.load_info(db);
std::string selected_channel_id; std::string selected_channel_id;
if(selected_channel) { if(selected_channel < channels.size())
selected_channel_id = channels[*selected_channel].id; selected_channel_id = channels[selected_channel].id;
}
channels.push_back(channel); channels.push_back(channel);
std::sort(channels.begin(), channels.end(), [](const Channel &a, const Channel &b){ return a.name < b.name; });
if(selected_channel) { std::sort(channels.begin(), channels.end(), [](const Channel &a, const Channel &b){ if(a.is_virtual != b.is_virtual) { return a.is_virtual > b.is_virtual; } return a.name < b.name; });
if(!selected_channel_id.empty()) {
const size_t new_index = std::distance(channels.cbegin(), std::find_if(channels.cbegin(), channels.cend(), [&](const Channel &ch){ return ch.id == selected_channel_id; })); const size_t new_index = std::distance(channels.cbegin(), std::find_if(channels.cbegin(), channels.cend(), [&](const Channel &ch){ return ch.id == selected_channel_id; }));
selected_channel = new_index; selected_channel = new_index;
} }
} }
void make_virtual_unwatched_channel()
{
Channel channel = Channel::add_virtual("All Unwatched", kWatched, false);
std::vector<Video> &channelVideos = videos[channel.id];
channelVideos = Video::get_all_with_flag_value(channel.virtual_flag, channel.virtual_flag_value);
for(Video &video: channelVideos) {
video.tui_title_width = string_width(video.title);
}
add_channel_to_list(channel);
}
void action_add_channel_by_name() void action_add_channel_by_name()
{ {
std::string channel_name = get_string("Add new Channel", "Enter channel name"); std::string channel_name = get_string("Add new Channel", "Enter channel name");
@ -249,7 +287,7 @@ void action_select_channel() {
std::vector<std::string> names; std::vector<std::string> names;
for(const Channel &c: channels) for(const Channel &c: channels)
names.push_back(c.name + " (" + std::to_string(c.unwatched) + ")"); names.push_back(c.name + " (" + std::to_string(c.unwatched) + ")");
const int channel = get_selection("Switch Channel", names, *selected_channel, Align::VCenter | Align::Left); const int channel = get_selection("Switch Channel", names, selected_channel, Align::VCenter | Align::Left);
if(channel != -1) if(channel != -1)
select_channel_by_index(channel); select_channel_by_index(channel);
} else { } else {
@ -258,8 +296,7 @@ void action_select_channel() {
} }
void action_refresh_channel() { void action_refresh_channel() {
Channel &ch = channels[*selected_channel]; fetch_videos_for_channel(channels.at(selected_channel));
fetch_videos_for_channel(ch);
} }
void action_refresh_all_channels() { void action_refresh_all_channels() {
@ -271,14 +308,14 @@ void action_refresh_all_channels() {
} }
void action_mark_video_watched() { void action_mark_video_watched() {
Channel &ch = channels[*selected_channel]; Channel &ch = channels.at(selected_channel);
Video &video = videos[ch.id][selected_video]; Video &video = videos[ch.id][selected_video];
video.set_flag(db, kWatched); video.set_flag(db, kWatched);
ch.load_info(db); ch.load_info(db);
} }
void action_watch_video() { void action_watch_video() {
Channel &ch = channels[*selected_channel]; Channel &ch = channels.at(selected_channel);
Video &video = videos[ch.id][selected_video]; Video &video = videos[ch.id][selected_video];
const std::string url = "https://youtube.com/watch?v="; const std::string url = "https://youtube.com/watch?v=";
@ -293,14 +330,14 @@ void action_watch_video() {
} }
void action_mark_video_unwatched() { void action_mark_video_unwatched() {
Channel &ch = channels[*selected_channel]; Channel &ch = channels.at(selected_channel);
Video &selected = videos[ch.id][selected_video]; Video &selected = videos[ch.id][selected_video];
selected.set_flag(db, kWatched, false); selected.set_flag(db, kWatched, false);
ch.load_info(db); ch.load_info(db);
} }
void action_mark_all_videos_watched() { void action_mark_all_videos_watched() {
Channel &ch = channels[*selected_channel]; Channel &ch = channels.at(selected_channel);
if(message_box("Mark all as watched", ("Do you want to mark all videos of " + ch.name + " as watched?").c_str(), Button::Yes | Button::No, Button::No) != Button::Yes) if(message_box("Mark all as watched", ("Do you want to mark all videos of " + ch.name + " as watched?").c_str(), Button::Yes | Button::No, Button::No) != Button::Yes)
return; return;
{ {
@ -313,13 +350,13 @@ void action_mark_all_videos_watched() {
} }
void action_select_prev_channel() { void action_select_prev_channel() {
if(selected_channel && *selected_channel > 0) if(selected_channel > 0)
select_channel_by_index(*selected_channel - 1); select_channel_by_index(selected_channel - 1);
} }
void action_select_next_channel() { void action_select_next_channel() {
if(selected_channel && *selected_channel < channels.size() - 1) if(selected_channel < channels.size() - 1)
select_channel_by_index(*selected_channel + 1); select_channel_by_index(selected_channel + 1);
} }
void action_select_prev_video() { void action_select_prev_video() {
@ -427,6 +464,7 @@ int main()
} }
db_init(database_filename); db_init(database_filename);
make_virtual_unwatched_channel();
for(Channel &channel: Channel::get_all(db)) { for(Channel &channel: Channel::get_all(db)) {
add_channel_to_list(channel); add_channel_to_list(channel);
} }
@ -464,10 +502,7 @@ int main()
do { do {
termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR); termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
if(selected_channel) draw_channel_list(videos[channels.at(selected_channel).id]);
draw_channel_list(videos[channels.at(*selected_channel).id]);
else
draw_no_channels_msg();
tp_flush(force_repaint); tp_flush(force_repaint);
force_repaint = false; force_repaint = false;

45
yt.cpp
View File

@ -66,11 +66,11 @@ static json api_request(const std::string &url, std::map<std::string, std::strin
return {}; return {};
} }
Channel::Channel(sqlite3_stmt *row): id(get_string(row, 0)), name(get_string(row, 1)) Channel::Channel(sqlite3_stmt *row): id(get_string(row, 0)), name(get_string(row, 1)), is_virtual(false), virtual_flag(kNone), virtual_flag_value(false)
{ {
} }
Channel::Channel(const std::string &id, const std::string &name): id(id), name(name) Channel::Channel(const std::string &id, const std::string &name): id(id), name(name), is_virtual(false), virtual_flag(kNone), virtual_flag_value(false)
{ {
} }
@ -104,6 +104,17 @@ Channel Channel::add(sqlite3 *db, const std::string &selector, const std::string
return Channel(channel_id, channel_name); return Channel(channel_id, channel_name);
} }
Channel Channel::add_virtual(const std::string &name, const VideoFlag virtual_flag, const bool virtual_flag_value)
{
std::string id = name;
std::transform(id.begin(), id.end(), id.begin(), [](char c){ return std::isalnum(c) ? std::tolower(c) : '-'; });
Channel channel("virtual-" + id, name);
channel.is_virtual = true;
channel.virtual_flag = virtual_flag;
channel.virtual_flag_value = virtual_flag_value;
return channel;
}
std::vector<Channel> Channel::get_all(sqlite3 *db) std::vector<Channel> Channel::get_all(sqlite3 *db)
{ {
std::vector<Channel> channels; std::vector<Channel> channels;
@ -231,6 +242,10 @@ void Channel::load_info(sqlite3 *db)
video_count = 0; video_count = 0;
unwatched = 0; unwatched = 0;
if(is_virtual) {
return;
}
sqlite3_stmt *query; sqlite3_stmt *query;
SC(sqlite3_prepare_v2(db, "SELECT flags, count(*) as videos FROM videos where channelId = ?1 GROUP by flags;", -1, &query, nullptr)); SC(sqlite3_prepare_v2(db, "SELECT flags, count(*) as videos FROM videos where channelId = ?1 GROUP by flags;", -1, &query, nullptr));
SC(sqlite3_bind_text(query, 1, id.c_str(), -1, SQLITE_TRANSIENT)); SC(sqlite3_bind_text(query, 1, id.c_str(), -1, SQLITE_TRANSIENT));
@ -245,17 +260,13 @@ void Channel::load_info(sqlite3 *db)
SC(sqlite3_finalize(query)); SC(sqlite3_finalize(query));
} }
void Channel::videos_watched(int count)
{
unwatched -= count;
}
bool Channel::is_valid() const bool Channel::is_valid() const
{ {
return !id.empty() && !name.empty(); return !id.empty() && !name.empty();
} }
Video::Video(sqlite3_stmt *row): id(get_string(row, 0)), title(get_string(row, 2)), description(get_string(row, 3)), flags(sqlite3_column_int(row, 4)), published(get_string(row, 5)) Video::Video(sqlite3_stmt *row): id(get_string(row, 0)), title(get_string(row, 2)), description(get_string(row, 3)),
flags(sqlite3_column_int(row, 4)), published(get_string(row, 5)), tui_title_width(0)
{ {
} }
@ -291,3 +302,21 @@ std::vector<Video> Video::get_all_for_channel(const std::string &channel_id)
return videos; return videos;
} }
std::vector<Video> Video::get_all_with_flag_value(const VideoFlag flag, const int value)
{
std::vector<Video> videos;
sqlite3_stmt *query;
SC(sqlite3_prepare_v2(db, "SELECT * FROM videos WHERE flags & ?1 = ?2 ORDER BY published DESC;", -1, &query, nullptr));
SC(sqlite3_bind_int(query, 1, flag));
SC(sqlite3_bind_int(query, 2, value));
while(sqlite3_step(query) == SQLITE_ROW) {
videos.emplace_back(query);
Video video(query);
}
SC(sqlite3_finalize(query));
return videos;
}

18
yt.h
View File

@ -14,20 +14,29 @@ extern struct yt_config {
std::map<std::string, std::string> extra_headers; std::map<std::string, std::string> extra_headers;
} yt_config; } yt_config;
enum VideoFlag {
kNone = 0,
kWatched = (1<<0),
kDownloaded = (1<<1),
};
class Channel class Channel
{ {
public: public:
std::string id; std::string id;
std::string name; std::string name;
bool is_virtual;
VideoFlag virtual_flag;
bool virtual_flag_value;
Channel(sqlite3_stmt *row); Channel(sqlite3_stmt *row);
static Channel add(sqlite3 *db, const std::string &selector, const std::string &value); static Channel add(sqlite3 *db, const std::string &selector, const std::string &value);
static Channel add_virtual(const std::string &name, const VideoFlag virtual_flag=kNone, const bool virtual_flag_value=true);
static std::vector<Channel> get_all(sqlite3 *db); static std::vector<Channel> get_all(sqlite3 *db);
std::string upload_playlist() const; std::string upload_playlist() const;
void fetch_new_videos(sqlite3 *db, progress_info *info=nullptr, std::optional<std::string> after={}, std::optional<int> max_count={}); void fetch_new_videos(sqlite3 *db, progress_info *info=nullptr, std::optional<std::string> after={}, std::optional<int> max_count={});
void load_info(sqlite3 *db); void load_info(sqlite3 *db);
void videos_watched(int count);
bool is_valid() const; bool is_valid() const;
unsigned int video_count; unsigned int video_count;
@ -36,12 +45,6 @@ private:
Channel(const std::string &id, const std::string &name); Channel(const std::string &id, const std::string &name);
}; };
enum VideoFlag {
kNone = 0,
kWatched = (1<<0),
kDownloaded = (1<<1),
};
struct Video struct Video
{ {
std::string id; std::string id;
@ -53,6 +56,7 @@ struct Video
Video(sqlite3_stmt *row); Video(sqlite3_stmt *row);
void set_flag(sqlite3 *db, VideoFlag flag, bool value=true); void set_flag(sqlite3 *db, VideoFlag flag, bool value=true);
static std::vector<Video> get_all_for_channel(const std::string &channel_id); static std::vector<Video> get_all_for_channel(const std::string &channel_id);
static std::vector<Video> get_all_with_flag_value(const VideoFlag flag, const int value);
size_t tui_title_width; size_t tui_title_width;
}; };