2020-11-28 14:40:46 +00:00
// SPDX-License-Identifier: MIT
2020-12-20 23:00:01 +00:00
# include "application.h"
2020-11-22 15:17:14 +00:00
# define _X_OPEN_SOURCE
# include "tui.h"
# include "yt.h"
2020-11-22 18:18:45 +00:00
# include "db.h"
2020-11-26 18:30:26 +00:00
# include "subprocess.h"
2020-11-22 15:17:14 +00:00
# include <algorithm>
2020-12-02 22:13:57 +00:00
# include <chrono>
2020-11-22 15:17:14 +00:00
# include <cstdlib>
# include <cstring>
# include <ctime>
# include <unordered_map>
# include <fstream>
# include <time.h>
2020-11-26 18:41:48 +00:00
# include <libgen.h>
2020-11-22 15:17:14 +00:00
# include <stdio.h>
# include <curl/curl.h>
# include <nlohmann/json.hpp>
2021-07-18 14:41:19 +00:00
std : : string user_home ;
2020-11-22 15:17:14 +00:00
2021-07-18 14:11:16 +00:00
std : : vector < UserFlag > userFlags ;
2020-11-22 15:17:14 +00:00
std : : vector < Channel > channels ;
std : : unordered_map < std : : string , std : : vector < Video > > videos ;
2020-11-26 12:27:11 +00:00
size_t selected_channel ;
2020-11-22 15:17:14 +00:00
size_t current_video_count = 0 ;
size_t selected_video = 0 ;
size_t videos_per_page = 0 ;
size_t current_page_count = 0 ;
size_t title_offset = 0 ;
bool any_title_in_next_half = false ;
2020-11-26 12:27:11 +00:00
bool clear_channels_on_change = false ;
2020-11-22 15:17:14 +00:00
2020-12-20 23:00:01 +00:00
static application_host * host = nullptr ;
2020-11-22 15:17:14 +00:00
static termpaint_attr * get_attr ( const AttributeSetType type , const bool highlight = false )
{
return highlight ? attributes [ type ] . highlight : attributes [ type ] . normal ;
}
2020-12-06 18:40:10 +00:00
void draw_channel_list ( const std : : vector < Video > & videos , bool show_channel_name = false )
2020-11-22 15:17:14 +00:00
{
const size_t cols = termpaint_surface_width ( surface ) ;
const size_t rows = termpaint_surface_height ( surface ) ;
const size_t column_spacing = 2 ;
2020-11-26 12:28:51 +00:00
const size_t date_column = 0 ;
2020-11-22 15:17:14 +00:00
const size_t date_width = std : : string ( " xxxx-xx-xx xx:xx " ) . size ( ) ;
const size_t start_row = 2 ;
const size_t available_rows = rows - 2 ;
videos_per_page = available_rows ;
2020-12-06 18:40:10 +00:00
const int cur_page = selected_video / available_rows ;
2021-07-18 14:41:19 +00:00
const int pages = videos . size ( ) / available_rows ;
2020-12-06 18:40:10 +00:00
size_t cur_entry = 0 ;
std : : map < std : : string , std : : string > channel_name_lookup ;
const size_t channel_name_column = date_column + date_width + column_spacing ;
size_t channel_name_width = show_channel_name * std : : string ( " Channel " ) . size ( ) ;
if ( show_channel_name ) {
for ( size_t i = cur_page * available_rows ; i < videos . size ( ) ; i + + ) {
const Video & video = videos . at ( i ) ;
for ( size_t c = 0 ; c < channels . size ( ) ; c + + ) {
const Channel & channel = channels . at ( c ) ;
if ( video . channel_id = = channel . id ) {
channel_name_lookup [ channel . id ] = channel . name ;
channel_name_width = std : : max ( channel_name_width , channel . tui_name_width ) ;
break ;
}
}
if ( + + cur_entry > available_rows )
break ;
}
}
const size_t first_name_column = date_column + date_width + column_spacing + channel_name_width + ( channel_name_width > 0 ) * column_spacing ;
const size_t last_name_column = cols ;
const size_t name_quater = ( last_name_column - first_name_column ) / 4 ;
2020-11-22 15:17:14 +00:00
2021-07-18 14:41:19 +00:00
const std : : string channel_name = std : : string ( " Channel: " ) + channels [ selected_channel ] . name ;
2020-11-22 15:17:14 +00:00
termpaint_surface_write_with_attr ( surface , 0 , 0 , channel_name . c_str ( ) , get_attr ( ASNormal ) ) ;
if ( pages > 1 ) {
std : : string text = " (Page " + std : : to_string ( cur_page + 1 ) + " / " + std : : to_string ( pages + 1 ) + " ) " ;
const size_t w = string_width ( text ) ;
termpaint_surface_write_with_attr ( surface , cols - w , 0 , text . c_str ( ) , attributes [ ASNormal ] . normal ) ;
}
termpaint_surface_write_with_attr ( surface , date_column , 1 , " Date " , get_attr ( ASNormal ) ) ;
2020-12-06 18:40:10 +00:00
if ( show_channel_name )
termpaint_surface_write_with_attr ( surface , channel_name_column , 1 , " Channel " , get_attr ( ASNormal ) ) ;
2020-11-22 15:17:14 +00:00
termpaint_surface_write_with_attr ( surface , first_name_column , 1 , " Title " , get_attr ( ASNormal ) ) ;
any_title_in_next_half = false ;
2020-12-06 18:40:10 +00:00
cur_entry = 0 ;
2020-11-22 15:17:14 +00:00
for ( size_t i = cur_page * available_rows ; i < videos . size ( ) ; i + + ) {
const size_t row = start_row + cur_entry ;
const bool selected = i = = selected_video ;
const Video & video = videos . at ( i ) ;
termpaint_attr * attr = get_attr ( video . flags & kWatched ? ASWatched : ASUnwatched , selected ) ;
termpaint_surface_clear_rect_with_attr ( surface , 0 , row , cols , 1 , attr ) ;
std : : vector < char > dt ( date_width + 10 , 0 ) ;
struct tm tm ;
memset ( & tm , 0 , sizeof ( tm ) ) ;
2021-07-18 14:39:32 +00:00
const std : : string & date_str = ! video . published . empty ( ) ? video . published : video . added_to_playlist ;
if ( strptime ( date_str . c_str ( ) , " %FT%T%z " , & tm ) ! = nullptr ) {
2020-11-22 15:17:14 +00:00
strftime ( dt . data ( ) , date_width + 10 , " %F %H:%M " , & tm ) ;
}
termpaint_surface_write_with_attr ( surface , date_column , row , dt . data ( ) , attr ) ;
2020-12-06 18:40:10 +00:00
if ( show_channel_name ) {
termpaint_surface_write_with_attr ( surface , channel_name_column , row , channel_name_lookup [ video . channel_id ] . c_str ( ) , attr ) ;
}
2020-11-22 15:17:14 +00:00
bool in_this_quater = title_offset * name_quater < video . tui_title_width ;
any_title_in_next_half = any_title_in_next_half | | ( ( title_offset + 2 ) * name_quater ) < video . tui_title_width ;
if ( in_this_quater )
termpaint_surface_write_with_attr_clipped ( surface , first_name_column , row , video . title . c_str ( ) + ( name_quater * title_offset ) , attr , first_name_column , last_name_column ) ;
else
termpaint_surface_write_with_attr ( surface , first_name_column , row , " ← " , attr ) ;
if ( + + cur_entry > available_rows )
break ;
}
if ( ! any_title_in_next_half & & title_offset > 0 )
title_offset - - ;
}
void draw_no_channels_msg ( )
{
const size_t cols = termpaint_surface_width ( surface ) ;
const size_t rows = termpaint_surface_height ( surface ) ;
const std : : string text ( " No configured channels. \n "
" Press the following keys to add one: \n "
2020-11-22 18:19:28 +00:00
" a Add channel by name \n "
" A Add channel by ID \n "
" Or press F1 for help or C-q to quit. " ) ;
2020-11-22 15:17:14 +00:00
{
std : : pair < size_t , size_t > size = string_size ( text ) ;
write_multiline_string ( ( cols - size . first ) / 2 , ( rows - size . second ) / 2 , text , get_attr ( ASNormal ) ) ;
}
}
2020-11-26 12:27:11 +00:00
void load_videos_for_channel ( const Channel & channel , bool force = false )
2020-11-22 15:17:14 +00:00
{
2020-11-26 12:27:11 +00:00
if ( ! force & & videos . find ( channel . id ) ! = videos . end ( ) & & ! videos [ channel . id ] . empty ( ) )
2020-11-22 15:17:14 +00:00
return ;
2020-11-26 12:27:11 +00:00
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
if ( channel . is_virtual ) {
2021-07-18 21:57:44 +00:00
channelVideos = Video : : get_all_with_filter ( channel . filter ) ;
2020-11-26 12:27:11 +00:00
} else {
channelVideos = Video : : get_all_for_channel ( channel . id ) ;
}
2020-11-22 20:29:45 +00:00
for ( Video & video : channelVideos ) {
2020-11-22 15:17:14 +00:00
video . tui_title_width = string_width ( video . title ) ;
}
2020-11-22 20:29:45 +00:00
2020-11-26 12:27:11 +00:00
if ( channels [ selected_channel ] . id = = channel . id )
2020-11-22 20:29:45 +00:00
selected_video = 0 ;
2020-11-22 15:17:14 +00:00
}
2020-12-01 19:37:08 +00:00
int fetch_videos_for_channel ( Channel & channel , bool name_in_title = false )
2020-11-22 15:17:14 +00:00
{
2020-11-26 12:27:11 +00:00
if ( channel . is_virtual ) {
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
2021-07-18 21:57:44 +00:00
channelVideos = Video : : get_all_with_filter ( channel . filter ) ;
2020-11-26 12:27:11 +00:00
for ( Video & video : channelVideos ) {
video . tui_title_width = string_width ( video . title ) ;
}
2020-12-04 17:05:59 +00:00
return 0 ;
2020-11-26 12:27:11 +00:00
}
2020-11-22 15:17:14 +00:00
std : : string title ( " Refreshing " ) ;
if ( name_in_title )
2020-11-26 12:27:11 +00:00
title . append ( " " ) . append ( channel . name ) ;
2020-11-22 15:17:14 +00:00
title . append ( " … " ) ;
progress_info * info = begin_progress ( title , 30 ) ;
2020-12-01 19:37:08 +00:00
const int new_video_count = channel . fetch_new_videos ( db , info ) ;
2020-11-22 15:17:14 +00:00
end_progress ( info ) ;
2020-11-26 12:27:11 +00:00
load_videos_for_channel ( channels [ selected_channel ] , true ) ;
channel . load_info ( db ) ;
2020-12-01 19:37:08 +00:00
return new_video_count ;
2020-11-26 12:27:11 +00:00
}
2020-11-26 18:40:39 +00:00
bool startswith ( const std : : string & str , const std : : string & with )
2020-11-26 12:27:11 +00:00
{
const size_t len = with . length ( ) ;
2020-11-26 18:40:39 +00:00
return str . substr ( 0 , len ) = = with ;
2020-11-22 15:17:14 +00:00
}
2020-11-26 18:30:26 +00:00
std : : string replace ( const std : : string & str , const std : : string & what , const std : : string & with )
{
const size_t replace_length = what . size ( ) ;
std : : string out = str ;
size_t pos = 0 ;
while ( ( pos = out . find ( what , pos ) ) ! = std : : string : : npos ) {
out . replace ( pos , replace_length , with ) ;
}
return out ;
}
2020-11-22 15:17:14 +00:00
void select_channel_by_index ( const int index ) {
2020-11-26 12:27:11 +00:00
if ( clear_channels_on_change ) {
for ( auto & [ k , v ] : videos ) {
v . clear ( ) ;
}
}
2020-11-22 15:17:14 +00:00
selected_channel = index ;
2020-11-26 12:27:11 +00:00
const Channel & channel = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
selected_video = 0 ;
2020-11-26 12:27:11 +00:00
load_videos_for_channel ( channel , clear_channels_on_change ) ;
2020-11-26 18:44:37 +00:00
current_video_count = videos [ channel . id ] . size ( ) ;
2020-11-26 12:27:11 +00:00
clear_channels_on_change = channel . is_virtual ;
2020-11-22 15:17:14 +00:00
}
void select_channel_by_name ( const std : : string & channel_name ) {
auto it = std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . name = = channel_name ; } ) ;
if ( it = = channels . cend ( ) )
return ;
select_channel_by_index ( std : : distance ( channels . cbegin ( ) , it ) ) ;
}
void select_channel_by_id ( const std : : string & channel_id ) {
auto it = std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . id = = channel_id ; } ) ;
if ( it = = channels . cend ( ) )
return ;
select_channel_by_index ( std : : distance ( channels . cbegin ( ) , it ) ) ;
}
void add_channel_to_list ( Channel & channel )
{
channel . load_info ( db ) ;
2020-12-06 18:40:10 +00:00
channel . tui_name_width = string_width ( channel . name ) ;
2020-11-22 15:17:14 +00:00
std : : string selected_channel_id ;
2020-11-26 12:27:11 +00:00
if ( selected_channel < channels . size ( ) )
selected_channel_id = channels [ selected_channel ] . id ;
2020-11-22 15:17:14 +00:00
channels . push_back ( channel ) ;
2020-11-26 12:27:11 +00:00
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 ( ) ) {
2020-11-22 15:17:14 +00:00
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 ;
}
}
2020-11-26 12:27:11 +00:00
void make_virtual_unwatched_channel ( )
{
2021-07-18 21:57:44 +00:00
ChannelFilter filter ;
filter . video_mask = kWatched ;
Channel channel = Channel : : add_virtual ( " All Unwatched " , filter ) ;
2020-11-26 12:27:11 +00:00
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
2021-07-18 21:57:44 +00:00
channelVideos = Video : : get_all_with_filter ( channel . filter ) ;
2020-11-26 12:27:11 +00:00
for ( Video & video : channelVideos ) {
video . tui_title_width = string_width ( video . title ) ;
}
add_channel_to_list ( channel ) ;
}
2020-11-22 20:43:30 +00:00
void action_add_channel_by_name ( )
2020-11-22 15:17:14 +00:00
{
std : : string channel_name = get_string ( " Add new Channel " , " Enter channel name " ) ;
if ( channel_name . empty ( ) ) {
return ;
} else if ( std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . name = = channel_name ; } ) ! = channels . cend ( ) ) {
message_box ( " Can't add channel " , " A channel with this name is already in the list! " ) ;
return ;
} else {
Channel ch = Channel : : add ( db , " forUsername " , channel_name ) ;
if ( ch . is_valid ( ) ) {
add_channel_to_list ( ch ) ;
select_channel_by_id ( ch . id ) ;
tp_flush ( ) ;
if ( message_box ( " Update now? " , " Fetch videos for this channel now? " , Button : : Yes | Button : : No , Button : : Yes ) = = Button : : Yes ) {
fetch_videos_for_channel ( ch ) ;
}
} else {
message_box ( " Can't add channel " , " There is no channel with this name! " ) ;
}
}
}
2020-11-22 20:43:30 +00:00
void action_add_channel_by_id ( )
2020-11-22 15:17:14 +00:00
{
std : : string channel_id = get_string ( " Add new Channel " , " Enter channel ID " ) ;
if ( channel_id . empty ( ) ) {
return ;
} else if ( std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . id = = channel_id ; } ) ! = channels . cend ( ) ) {
message_box ( " Can't add channel " , " A channel with this ID is already in the list! " ) ;
return ;
} else {
Channel ch = Channel : : add ( db , " id " , channel_id ) ;
if ( ch . is_valid ( ) ) {
add_channel_to_list ( ch ) ;
select_channel_by_id ( ch . id ) ;
tp_flush ( ) ;
if ( message_box ( " Update now? " , " Fetch videos for this channel now? " , Button : : Yes | Button : : No , Button : : Yes ) = = Button : : Yes ) {
fetch_videos_for_channel ( ch ) ;
}
} else {
message_box ( " Can't add channel " , " There is no channel with this ID! " ) ;
}
}
}
void action_select_channel ( ) {
2021-07-18 14:41:19 +00:00
if ( channels . empty ( ) ) {
message_box ( " Can't select channel " , " No channels configured. \n Please configure one. " ) ;
} else {
2020-11-22 15:17:14 +00:00
std : : vector < std : : string > names ;
2021-07-18 14:41:19 +00:00
names . reserve ( channels . size ( ) ) ;
2020-11-22 15:17:14 +00:00
for ( const Channel & c : channels )
names . push_back ( c . name + " ( " + std : : to_string ( c . unwatched ) + " ) " ) ;
2020-11-26 12:27:11 +00:00
const int channel = get_selection ( " Switch Channel " , names , selected_channel , Align : : VCenter | Align : : Left ) ;
2020-11-22 15:17:14 +00:00
if ( channel ! = - 1 )
select_channel_by_index ( channel ) ;
}
}
2020-12-01 19:36:14 +00:00
bool run_command ( const std : : vector < std : : string > & cmd , const std : : vector < std : : pair < std : : string , std : : string > > & placeholders = { } ) {
const size_t cmd_size = cmd . size ( ) ;
if ( ! cmd_size )
return true ;
const char * cmdline [ cmd_size + 1 ] ;
for ( size_t c = 0 ; c < cmd_size ; c + + ) {
std : : string arg = cmd [ c ] ;
for ( size_t p = 0 ; p < placeholders . size ( ) ; p + + ) {
const auto & [ what , with ] = placeholders . at ( p ) ;
arg = replace ( arg , what , with ) ;
}
cmdline [ c ] = strdup ( arg . c_str ( ) ) ;
}
cmdline [ cmd_size ] = nullptr ;
subprocess_s proc ;
const int rc = subprocess_create ( cmdline , subprocess_option_inherit_environment , & proc ) ;
if ( rc ! = 0 ) {
const std : : string message = cmd . at ( 0 ) + " failed with error " + std : : to_string ( rc ) ;
message_box ( " Failed to run command " , message ) ;
}
subprocess_join ( & proc , nullptr ) ;
for ( size_t i = 0 ; i < cmd_size ; i + + ) {
free ( ( void * ) cmdline [ i ] ) ;
}
return rc = = 0 ;
}
2020-12-01 22:20:10 +00:00
std : : vector < std : : string > notify_channel_new_video_command ;
std : : vector < std : : string > notify_channel_new_videos_command ;
std : : vector < std : : string > notify_channels_new_videos_command ;
2020-11-22 15:17:14 +00:00
void action_refresh_channel ( ) {
2020-12-01 22:20:10 +00:00
Channel & ch = channels . at ( selected_channel ) ;
const int new_videos = fetch_videos_for_channel ( channels . at ( selected_channel ) ) ;
2020-12-04 17:05:59 +00:00
if ( new_videos = = 0 | | ch . is_virtual )
2020-12-01 22:20:10 +00:00
return ;
2020-12-20 23:00:01 +00:00
if ( new_videos = = 1 ) {
if ( host & & host - > notify_channel_single_video ) {
host - > notify_channel_single_video ( ch . name , videos [ ch . id ] . front ( ) . title ) ;
} else if ( ! notify_channel_new_video_command . empty ( ) ) {
run_command ( notify_channel_new_video_command , {
{ " {{channelName}} " , ch . name } ,
{ " {{videoTitle}} " , videos [ ch . id ] . front ( ) . title } ,
} ) ;
}
} else {
if ( host & & host - > notify_channel_multiple_videos ) {
host - > notify_channel_multiple_videos ( ch . name , new_videos ) ;
2021-07-18 14:41:19 +00:00
} else if ( ! notify_channel_new_videos_command . empty ( ) ) {
2020-12-20 23:00:01 +00:00
run_command ( notify_channel_new_videos_command , {
{ " {{channelName}} " , ch . name } ,
{ " {{newVideos}} " , std : : to_string ( new_videos ) }
} ) ;
}
2020-12-01 22:20:10 +00:00
}
2020-11-22 15:17:14 +00:00
}
2020-12-02 22:13:57 +00:00
void action_refresh_all_channels ( bool ask = true ) {
2021-07-18 14:41:19 +00:00
if ( ask & & message_box ( " Refresh all channels? " , " Do you want to refresh all " + std : : to_string ( channels . size ( ) ) + " channels? " , Button : : Yes | Button : : No , Button : : No ) ! = Button : : Yes )
2020-11-22 15:17:14 +00:00
return ;
2020-12-01 22:20:10 +00:00
int updated_channels = 0 ;
int new_videos = 0 ;
2020-11-22 15:17:14 +00:00
for ( Channel & channel : channels ) {
2020-12-01 22:20:10 +00:00
const int count = fetch_videos_for_channel ( channel , true ) ;
2020-12-04 17:05:59 +00:00
if ( channel . is_virtual )
continue ;
2020-12-01 22:20:10 +00:00
new_videos + = count ;
if ( count )
updated_channels + + ;
}
2020-12-20 23:00:01 +00:00
if ( updated_channels & & new_videos ) {
if ( host & & host - > notify_channels_multiple_videos ) {
host - > notify_channels_multiple_videos ( updated_channels , new_videos ) ;
} else if ( ! notify_channels_new_videos_command . empty ( ) ) {
run_command ( notify_channels_new_videos_command , {
{ " {{updatedChannels}} " , std : : to_string ( updated_channels ) } ,
{ " {{newVideos}} " , std : : to_string ( new_videos ) }
} ) ;
}
2020-11-22 15:17:14 +00:00
}
}
void action_mark_video_watched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & video = videos [ ch . id ] [ selected_video ] ;
video . set_flag ( db , kWatched ) ;
ch . load_info ( db ) ;
}
2020-11-26 18:30:26 +00:00
std : : vector < std : : string > watch_command = { " xdg-open " , " https://youtube.com/watch?v={{vid}} " } ;
2020-11-22 15:17:14 +00:00
void action_watch_video ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & video = videos [ ch . id ] [ selected_video ] ;
2020-12-01 19:36:14 +00:00
if ( run_command ( watch_command , { { " {{vid}} " , video . id } } ) ) {
2020-11-22 15:17:14 +00:00
video . set_flag ( db , kWatched ) ;
ch . load_info ( db ) ;
}
}
void action_mark_video_unwatched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & selected = videos [ ch . id ] [ selected_video ] ;
selected . set_flag ( db , kWatched , false ) ;
ch . load_info ( db ) ;
}
void action_mark_all_videos_watched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2021-07-18 14:41:19 +00:00
if ( message_box ( " Mark all as watched " , " Do you want to mark all videos of " + ch . name + " as watched? " , Button : : Yes | Button : : No , Button : : No ) ! = Button : : Yes )
2020-11-22 15:17:14 +00:00
return ;
2020-11-22 20:43:30 +00:00
{
db_transaction transaction ;
for ( Video & video : videos [ ch . id ] ) {
video . set_flag ( db , kWatched ) ;
}
}
ch . load_info ( db ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_prev_channel ( ) {
2020-11-26 12:27:11 +00:00
if ( selected_channel > 0 )
select_channel_by_index ( selected_channel - 1 ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_next_channel ( ) {
2020-11-26 12:27:11 +00:00
if ( selected_channel < channels . size ( ) - 1 )
select_channel_by_index ( selected_channel + 1 ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_prev_video ( ) {
if ( selected_video > 0 )
selected_video - - ;
}
void action_select_next_video ( ) {
if ( selected_video < current_video_count - 1 )
selected_video + + ;
}
void action_select_prev_video_page ( ) {
if ( selected_video > 0 )
selected_video - = std : : min ( selected_video , videos_per_page ) ;
}
void action_select_next_video_page ( ) {
if ( selected_video < current_video_count - 1 )
2020-11-22 23:02:51 +00:00
selected_video + = std : : min ( current_video_count - 1 - selected_video , videos_per_page ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_first_video ( ) {
selected_video = 0 ;
}
void action_select_last_video ( ) {
selected_video = current_video_count - 1 ;
}
void action_scroll_title_left ( ) {
if ( title_offset > 0 )
title_offset - - ;
}
void action_scroll_title_right ( ) {
if ( any_title_in_next_half )
title_offset + + ;
}
void action_show_video_detail ( ) {
2020-12-16 18:41:58 +00:00
const size_t cols = termpaint_surface_width ( surface ) ;
const Channel & ch = channels . at ( selected_channel ) ;
const Video & selected = videos [ ch . id ] [ selected_video ] ;
2021-07-18 14:40:25 +00:00
std : : string text ;
text . append ( " Video: \t " ) . append ( selected . title ) . append ( " \n " ) ;
text . append ( " Published: \t " ) . append ( selected . published ) . append ( " \n " ) ;
text . append ( " Added to playlist: \t " ) . append ( selected . added_to_playlist ) . append ( " \n " ) ;
text . append ( " \n " ) . append ( selected . description ) ;
message_box ( " Video Information " , text_wrap ( text , cols / 8 * 7 ) ) ;
2020-11-22 15:17:14 +00:00
}
2021-07-18 14:11:16 +00:00
void action_add_new_user_flag ( ) {
std : : string name = get_string ( " Flag name " ) ;
if ( name . empty ( ) )
return ;
userFlags . push_back ( UserFlag : : create ( db , name ) ) ;
}
2021-07-18 21:27:20 +00:00
void action_rename_user_flag ( ) {
std : : vector < std : : string > names ;
for ( const UserFlag & flag : userFlags ) {
names . push_back ( flag . name ) ;
}
int index = get_selection ( " Select flag to rename " , names ) ;
if ( index < 0 )
return ;
UserFlag & flag = userFlags . at ( index ) ;
std : : string name = edit_string ( " Enter new name " , std : : string ( ) , flag . name ) ;
if ( name . empty ( ) )
return ;
flag . name = name ;
flag . save ( db ) ;
}
2021-07-18 14:11:16 +00:00
void action_manage_user_flags ( ) {
bool done = false ;
constexpr const char userflag_keys [ ] = " 1234567890 "
" abcdefghij "
" klmnopqrst "
" uv " ;
const std : : string userflag_keys_str ( userflag_keys ) ;
// +1 to accomodate for the 0 byte at the end
static_assert ( sizeof ( userflag_keys ) = = UserFlag : : max_flag_count + 1 , " There must be a key for each UserFlag " ) ;
size_t channel_name_width = 0 ;
for ( const Channel & c : channels ) {
channel_name_width = std : : max ( channel_name_width , c . tui_name_width ) ;
}
const char * box_chars [ ] = { " ┬ " , " │ " , " ┴ " } ;
size_t selected_channel = 0 ;
char key_buf [ ] = " " ;
std : : vector < action > actions = {
{ TERMPAINT_EV_KEY , " F2 " , 0 , action_add_new_user_flag , " Add new user flag " } ,
2021-07-18 21:27:20 +00:00
{ TERMPAINT_EV_KEY , " F3 " , 0 , action_rename_user_flag , " Rename user flag " } ,
2021-07-18 14:11:16 +00:00
{ TERMPAINT_EV_KEY , " Escape " , 0 , [ & ] { done = true ; } , " Stop user flag management " } ,
{ TERMPAINT_EV_KEY , " ArrowUp " , 0 , [ & ] { if ( selected_channel > 0 ) selected_channel - - ; } , " Previous channel " } ,
{ TERMPAINT_EV_KEY , " ArrowDown " , 0 , [ & ] { if ( selected_channel < channels . size ( ) ) selected_channel + + ; } , " Next channel " } ,
{ EV_IGNORE , " 1..0,a..v " , 0 , nullptr , " Toggle user flags for selected channel " } ,
} ;
do {
size_t flag_name_width = 0 ;
for ( const UserFlag & flag : userFlags ) {
flag_name_width = std : : max ( flag_name_width , string_width ( flag . name ) ) ;
}
const size_t content_rows_needed = std : : max ( userFlags . size ( ) , channels . size ( ) ) ;
const size_t box_cols = 1 + channel_name_width + 3 + 2 + flag_name_width + 1 ;
const size_t box_rows = 1 + content_rows_needed + 1 ;
const size_t channel_name_pos = 1 ;
const size_t divider_pos = channel_name_pos + channel_name_width + 1 ;
const size_t flag_char_pos = divider_pos + 1 ;
const size_t flag_name_pos = flag_char_pos + 2 ;
Channel & current_channel = channels [ selected_channel ] ;
draw_box_with_caption ( 0 , 0 , box_cols , box_rows ) ;
for ( size_t row = 0 ; row < box_rows ; row + + ) {
if ( row < channels . size ( ) ) {
termpaint_surface_write_with_attr ( surface , channel_name_pos , 1 + row , channels [ row ] . name . c_str ( ) , get_attr ( ASNormal , selected_channel = = row ) ) ;
}
const char * box_char = box_chars [ ( row > 0 ) + ( row + 1 = = box_rows ) ] ;
termpaint_surface_write_with_attr ( surface , divider_pos , row , box_char , get_attr ( ASNormal , false ) ) ;
if ( ! current_channel . is_virtual & & row < userFlags . size ( ) ) {
const UserFlag & flag = userFlags . at ( row ) ;
termpaint_attr * attr = get_attr ( current_channel . user_flags & flag . id ? ASUnwatched : ASWatched , false ) ;
key_buf [ 0 ] = userflag_keys [ int ( log2 ( flag . id ) ) ] ;
termpaint_surface_write_with_attr ( surface , flag_char_pos , 1 + row , key_buf , attr ) ;
termpaint_surface_write_with_attr ( surface , flag_name_pos , 1 + row , flag . name . c_str ( ) , attr ) ;
}
}
tp_flush ( false ) ;
auto event = tp_wait_for_event ( ) ;
if ( ! event )
abort ( ) ;
if ( ! tui_handle_action ( * event , actions ) ) {
if ( event - > type = = TERMPAINT_EV_CHAR & & event - > string . length ( ) = = 1 ) {
if ( current_channel . is_virtual )
continue ;
const size_t index = userflag_keys_str . find ( event - > string [ 0 ] ) ;
if ( index = = std : : string : : npos )
continue ;
if ( std : : find_if ( userFlags . cbegin ( ) , userFlags . cend ( ) ,
[ & ] ( const UserFlag & f ) { return f . id = = ( 1 < < index ) ; } ) = = userFlags . cend ( ) )
continue ;
current_channel . user_flags ^ = ( 1 < < index ) ;
current_channel . save_user_flags ( db ) ;
}
}
} while ( ! done ) ;
}
2020-11-22 15:17:14 +00:00
using json = nlohmann : : json ;
std : : optional < json > load_json ( const std : : string & filename ) {
std : : ifstream ifs ( filename ) ;
if ( ! ifs . is_open ( ) )
return { } ;
try {
json data ;
ifs > > data ;
return data ;
} catch ( json : : parse_error & err ) {
2020-12-01 22:20:57 +00:00
tui_abort ( " Failed to parse JSON file %s: %s " , filename . c_str ( ) , err . what ( ) ) ;
2020-11-22 15:17:14 +00:00
return { } ;
}
}
bool save_json ( const std : : string & filename , const json & data ) {
std : : ofstream ofs ( filename ) ;
if ( ! ofs . is_open ( ) )
return false ;
try {
ofs < < data ;
return true ;
} catch ( json : : exception & err ) {
return false ;
}
}
2020-11-26 18:41:48 +00:00
static std : : string get_module_path ( ) {
std : : string str ;
char * exe = realpath ( " /proc/self/exe " , nullptr ) ;
if ( exe ) {
str = std : : string ( exe ) ;
free ( exe ) ;
exe = nullptr ;
} else {
perror ( " realpath " ) ;
}
exe = dirname ( ( char * ) str . c_str ( ) ) ;
return std : : string ( exe ) ;
}
2020-11-22 18:18:45 +00:00
2020-12-01 21:56:48 +00:00
void config_get_string_list ( std : : vector < std : : string > & buffer , const json & obj , const std : : string & key ) {
if ( ! obj . contains ( key ) | | ! obj [ key ] . is_array ( ) )
return ;
buffer . clear ( ) ;
for ( const json & elem : obj [ key ] ) {
if ( ! elem . is_string ( ) ) {
tui_abort ( " Configuration error: " + key + " element " + elem . dump ( ) + " is not a string but " + elem . type_name ( ) + " . " ) ;
}
buffer . push_back ( elem ) ;
}
}
2020-12-16 18:41:58 +00:00
static void run ( )
2020-11-22 15:17:14 +00:00
{
2020-11-26 18:41:48 +00:00
const std : : string module_path = get_module_path ( ) ;
2020-11-22 15:17:14 +00:00
user_home = std : : string ( std : : getenv ( " HOME " ) ) ;
2020-11-26 18:38:00 +00:00
std : : string database_filename = user_home + " /.local/share/yttui.db " ;
2020-11-22 15:17:14 +00:00
curl_global_init ( CURL_GLOBAL_ALL ) ;
2020-11-26 18:41:48 +00:00
const std : : vector < std : : string > config_locations { user_home + " /.config " , module_path } ;
2020-11-26 18:40:11 +00:00
std : : string config_file ;
2020-11-22 15:17:14 +00:00
nlohmann : : json config ;
for ( const std : : string & location : config_locations ) {
2020-11-26 18:40:11 +00:00
config_file = location + " /yttui.conf " ;
2020-11-22 15:17:14 +00:00
auto config_data = load_json ( config_file ) ;
if ( config_data ) {
config = * config_data ;
break ;
}
}
2020-12-16 18:56:50 +00:00
2020-11-22 22:44:33 +00:00
if ( config . count ( " apiKey " ) & & config [ " apiKey " ] . is_string ( ) ) {
2020-11-22 18:28:51 +00:00
yt_config . api_key = config [ " apiKey " ] ;
} else {
2020-11-26 18:40:11 +00:00
tui_abort ( " A YouTube API key is required for this application to function. \n Please provide one in the config file. \n \n Current config file: \n " + config_file ) ;
2020-11-22 18:28:51 +00:00
}
2020-11-22 22:44:33 +00:00
if ( config . count ( " extraHeaders " ) & & config [ " extraHeaders " ] . is_array ( ) ) {
2020-11-22 18:28:51 +00:00
for ( const json & elem : config [ " extraHeaders " ] ) {
2020-11-22 22:44:33 +00:00
if ( elem . count ( " key " ) & & elem [ " key " ] . is_string ( ) & & elem . count ( " value " ) & & elem [ " value " ] . is_string ( ) ) {
2020-11-22 18:28:51 +00:00
yt_config . extra_headers . emplace ( elem [ " key " ] , elem [ " value " ] ) ;
2020-11-22 15:17:14 +00:00
}
}
2020-11-22 18:28:51 +00:00
}
2020-11-22 22:44:33 +00:00
if ( config . count ( " database " ) & & config [ " database " ] . is_string ( ) ) {
2020-11-26 18:38:00 +00:00
database_filename = replace ( config [ " database " ] , " $HOME " , user_home ) ;
2020-11-22 15:17:14 +00:00
}
2020-12-01 21:56:48 +00:00
config_get_string_list ( watch_command , config , " watchCommand " ) ;
2020-12-01 22:20:10 +00:00
if ( config . contains ( " notifications " ) & & config [ " notifications " ] . is_object ( ) ) {
const json & notifications = config [ " notifications " ] ;
config_get_string_list ( notify_channel_new_video_command , notifications , " channelNewVideoCommand " ) ;
config_get_string_list ( notify_channel_new_videos_command , notifications , " channelNewVideosCommand " ) ;
config_get_string_list ( notify_channels_new_videos_command , notifications , " channelsNewVideosCommand " ) ;
}
2020-11-22 15:17:14 +00:00
2020-12-02 22:13:57 +00:00
int auto_refresh_interval = - 1 ; // In seconds
if ( config . count ( " autoRefreshInterval " ) & & config [ " autoRefreshInterval " ] . is_number_integer ( ) ) {
auto_refresh_interval = config [ " autoRefreshInterval " ] ;
auto_refresh_interval = std : : max ( - 1 , auto_refresh_interval ) ;
}
std : : chrono : : system_clock : : time_point next_update = std : : chrono : : system_clock : : now ( ) + std : : chrono : : seconds ( auto_refresh_interval ) ;
std : : chrono : : system_clock : : time_point last_user_action ;
2020-11-22 20:29:45 +00:00
db_init ( database_filename ) ;
2021-07-18 14:11:16 +00:00
userFlags = UserFlag : : get_all ( db ) ;
2020-11-26 12:27:11 +00:00
make_virtual_unwatched_channel ( ) ;
2020-11-22 20:29:45 +00:00
for ( Channel & channel : Channel : : get_all ( db ) ) {
2020-11-22 15:17:14 +00:00
add_channel_to_list ( channel ) ;
}
2021-07-18 14:41:19 +00:00
if ( ! channels . empty ( ) ) {
2020-11-22 15:17:14 +00:00
select_channel_by_index ( 0 ) ;
2020-12-16 18:56:50 +00:00
}
2020-11-22 15:17:14 +00:00
bool exit = false ;
2020-11-26 12:25:25 +00:00
bool force_repaint = false ;
2020-11-22 15:17:14 +00:00
std : : vector < action > actions = {
2020-11-22 20:43:30 +00:00
{ TERMPAINT_EV_CHAR , " a " , 0 , action_add_channel_by_name , " Add channel by name " } ,
{ TERMPAINT_EV_CHAR , " A " , 0 , action_add_channel_by_id , " Add channel by Id " } ,
2020-11-22 15:17:14 +00:00
{ TERMPAINT_EV_CHAR , " c " , 0 , action_select_channel , " Select channel " } ,
{ TERMPAINT_EV_CHAR , " j " , 0 , action_select_prev_channel , " Select previous channel " } ,
{ TERMPAINT_EV_CHAR , " k " , 0 , action_select_next_channel , " Select next channel " } ,
{ TERMPAINT_EV_CHAR , " r " , 0 , action_refresh_channel , " Refresh selected channel " } ,
2020-12-02 22:13:57 +00:00
{ TERMPAINT_EV_CHAR , " R " , 0 , [ & ] ( ) { action_refresh_all_channels ( ) ; } , " Refresh all channels " } ,
2020-11-22 15:17:14 +00:00
{ TERMPAINT_EV_CHAR , " w " , 0 , action_watch_video , " Watch video " } ,
{ TERMPAINT_EV_CHAR , " w " , TERMPAINT_MOD_ALT , action_mark_video_watched , " Mark video as watched " } ,
{ TERMPAINT_EV_CHAR , " u " , 0 , action_mark_video_unwatched , " Mark video as unwatched " } ,
{ TERMPAINT_EV_CHAR , " W " , 0 , action_mark_all_videos_watched , " Mark channel as watched " } ,
2020-11-26 18:40:39 +00:00
{ TERMPAINT_EV_CHAR , " q " , TERMPAINT_MOD_CTRL , [ & ] ( ) { exit = true ; } , " Quit " } ,
2020-11-22 15:17:14 +00:00
2021-07-18 14:40:25 +00:00
{ TERMPAINT_EV_KEY , " Enter " , 0 , action_show_video_detail , " Show video details " } ,
{ TERMPAINT_EV_KEY , " Space " , 0 , action_show_video_detail , " Show video details " } ,
2020-11-22 15:17:14 +00:00
{ TERMPAINT_EV_KEY , " ArrowUp " , 0 , action_select_prev_video , " Previous video " } ,
{ TERMPAINT_EV_KEY , " ArrowDown " , 0 , action_select_next_video , " Next video " } ,
{ TERMPAINT_EV_KEY , " PageUp " , 0 , action_select_prev_video_page , " Previous video page " } ,
{ TERMPAINT_EV_KEY , " PageDown " , 0 , action_select_next_video_page , " Next video page " } ,
{ TERMPAINT_EV_KEY , " Home " , 0 , action_select_first_video , " First video " } ,
{ TERMPAINT_EV_KEY , " End " , 0 , action_select_last_video , " Last video " } ,
{ TERMPAINT_EV_KEY , " ArrowLeft " , 0 , action_scroll_title_left , " Scroll title left " } ,
{ TERMPAINT_EV_KEY , " ArrowRight " , 0 , action_scroll_title_right , " Scroll title right " } ,
2020-11-26 12:25:25 +00:00
{ TERMPAINT_EV_CHAR , " l " , TERMPAINT_MOD_CTRL , [ & ] ( ) { force_repaint = true ; } , " Force redraw " } ,
2021-07-18 14:11:16 +00:00
{ TERMPAINT_EV_KEY , " F2 " , 0 , action_manage_user_flags , " Manage user flags " } ,
2020-11-22 15:17:14 +00:00
} ;
2020-12-02 22:13:57 +00:00
bool draw = true ;
2020-11-22 15:17:14 +00:00
do {
2020-12-20 23:00:01 +00:00
if ( host & & host - > quit & & host - > quit ( ) ) {
break ;
}
2020-12-02 22:13:57 +00:00
if ( draw ) {
2020-12-06 18:40:10 +00:00
Channel & channel = channels . at ( selected_channel ) ;
2020-12-02 22:13:57 +00:00
termpaint_surface_clear ( surface , TERMPAINT_DEFAULT_COLOR , TERMPAINT_DEFAULT_COLOR ) ;
2020-12-06 18:40:10 +00:00
draw_channel_list ( videos [ channel . id ] , channel . is_virtual ) ;
2020-12-02 22:13:57 +00:00
tp_flush ( force_repaint ) ;
force_repaint = false ;
}
draw = true ;
2020-11-22 15:17:14 +00:00
2020-12-20 23:00:01 +00:00
auto event = tp_wait_for_event ( 500 ) ;
2020-11-22 15:17:14 +00:00
if ( ! event )
abort ( ) ;
2020-12-02 22:13:57 +00:00
if ( event - > type = = EV_TIMEOUT ) {
draw = false ;
const bool update_pending = next_update < std : : chrono : : system_clock : : now ( ) ;
const bool inactivity_threshold = ( std : : chrono : : system_clock : : now ( ) - last_user_action ) > std : : chrono : : seconds ( 30 ) ;
if ( auto_refresh_interval ! = - 1 & & update_pending & & inactivity_threshold ) {
action_refresh_all_channels ( false ) ;
2020-12-03 08:19:34 +00:00
next_update = std : : chrono : : system_clock : : now ( ) + std : : chrono : : seconds ( auto_refresh_interval ) ;
2020-12-16 18:50:16 +00:00
draw = true ;
2020-12-02 22:13:57 +00:00
}
} else if ( tui_handle_action ( * event , actions ) ) {
2020-12-16 18:56:50 +00:00
last_user_action = std : : chrono : : system_clock : : now ( ) ;
2020-12-02 22:13:57 +00:00
}
2020-11-22 15:17:14 +00:00
} while ( ! exit ) ;
2020-11-22 20:29:45 +00:00
db_shutdown ( ) ;
2020-11-22 15:17:14 +00:00
curl_global_cleanup ( ) ;
2020-12-16 18:41:58 +00:00
}
void run_standalone ( )
{
tp_init ( ) ;
run ( ) ;
tp_shutdown ( ) ;
}
2020-11-22 15:17:14 +00:00
2020-12-20 23:00:01 +00:00
void run_embedded ( int pty_fd , application_host * _host )
2020-12-16 18:41:58 +00:00
{
tp_init_from_fd ( pty_fd ) ;
2020-12-20 23:00:01 +00:00
host = _host ;
2020-12-16 18:41:58 +00:00
run ( ) ;
tp_shutdown ( ) ;
2020-11-22 15:17:14 +00:00
}