Add virtual channel for all unwatched videos
This commit is contained in:
parent
160fb1cfff
commit
c571e5fec4
105
main.cpp
105
main.cpp
|
@ -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
45
yt.cpp
|
@ -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
18
yt.h
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue