├── LICENSE ├── README.md ├── nana-develop-1.8 (2024-05-06) ytdlp-interface mod.7z └── ytdlp-interface ├── Youtube-Round.ico ├── forms ├── form_changes.cpp ├── form_colors.cpp ├── form_colors.hpp ├── form_formats.cpp ├── form_json.cpp ├── form_loading.cpp ├── form_playlist.cpp ├── form_sections.cpp ├── form_settings.cpp └── form_suspend.cpp ├── gui.cpp ├── gui.hpp ├── gui_bottom.cpp ├── gui_bottoms.cpp ├── hsl_picker.cpp ├── hsl_picker.hpp ├── icons.hpp ├── json.hpp ├── main.cpp ├── manifest.xml ├── msgbox.hpp ├── msgbox_icons.hpp ├── outbox.cpp ├── progress_ex.hpp ├── queue.cpp ├── resource.h ├── themed_form.cpp ├── themed_form.hpp ├── types.cpp ├── types.hpp ├── util.cpp ├── util.hpp ├── widgets.cpp ├── widgets.hpp ├── ytdlp-interface.rc ├── ytdlp-interface.sln └── ytdlp-interface.vcxproj /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ErrorFlynn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ytdlp-interface 2 | This is a Windows graphical interface for [yt-dlp](https://github.com/yt-dlp/yt-dlp), that is designed as a simple YouTube downloader. Since v1.2, the interface also accepts non-YouTube URLs, so theoretically it can be used to download from any site that `yt-dlp` supports (see [the list](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)). 3 | 4 | To use, unpack the archive in a new folder at a location of your choice, and run `ytdlp-interface.exe`. 5 | 6 | Download link for the latest version (64 bit): https://github.com/ErrorFlynn/ytdlp-interface/releases/download/v2.14.1/ytdlp-interface.7z 7 | 8 | 32 bit build: https://github.com/ErrorFlynn/ytdlp-interface/releases/download/v2.14.1/ytdlp-interface_x86.7z 9 | 10 | 11 | --- 12 | 13 | ## Building the source 14 | The project depends on three static libraries: [Nana C++ GUI library](https://github.com/cnjinhao/nana) v1.8 or later (at the time I'm writing this v1.8 is in development, so you must build branch `develop-1.8`), [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo), and [bit7z](https://github.com/rikyoz/bit7z). 15 | 16 | The project uses a modified version of the Nana library (the file `nana-develop-1.8 (2024-05-06) ytdlp-interface mod.7z`). You can still link against the original library, but the modified version has features and behaviors not present in the original (as of June 2024). Most importantly, the modified library ensures that all interface elements follow the chosen color scheme, and that most interface elements scale properly with the system scale factor. 17 | 18 | The program also uses [JSON for modern C++](https://github.com/nlohmann/json) to get video info from `yt-dlp.exe` and to read/write the settings file, but that's just a header file that's included in the project (you can replace it with its latest version if you really want to). 19 | 20 | --- 21 | 22 | ![ytdlp-interface settings](https://github.com/ErrorFlynn/ytdlp-interface/assets/20293505/adb02d8a-5857-46dc-ad51-fd71c3a6bd96) 23 | 24 | --- 25 | 26 | ![ytdlp-interface queue](https://github.com/ErrorFlynn/ytdlp-interface/assets/20293505/86fd2013-4247-4f1e-8038-334ed31a3d4e) 27 | 28 | --- 29 | 30 | ![ytdlp-interface output](https://github.com/ErrorFlynn/ytdlp-interface/assets/20293505/a99f8e21-95e0-4641-b589-7211a37ee454) 31 | -------------------------------------------------------------------------------- /nana-develop-1.8 (2024-05-06) ytdlp-interface mod.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErrorFlynn/ytdlp-interface/fbca045a416fa7ff3fc84d780dc52fa8e2edfb68/nana-develop-1.8 (2024-05-06) ytdlp-interface mod.7z -------------------------------------------------------------------------------- /ytdlp-interface/Youtube-Round.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErrorFlynn/ytdlp-interface/fbca045a416fa7ff3fc84d780dc52fa8e2edfb68/ytdlp-interface/Youtube-Round.ico -------------------------------------------------------------------------------- /ytdlp-interface/forms/form_changes.cpp: -------------------------------------------------------------------------------- 1 | #include "../gui.hpp" 2 | 3 | 4 | void GUI::fm_changes(nana::window parent) 5 | { 6 | using widgets::theme; 7 | 8 | themed_form fm {nullptr, parent, {}, appear::decorate{}}; 9 | fm.center(1030, 543); 10 | fm.snap(conf.cbsnap); 11 | fm.theme_callback([&](bool dark) 12 | { 13 | apply_theme(dark); 14 | fm.bgcolor(theme::fmbg); 15 | return false; 16 | }); 17 | if(conf.cbtheme == 2) 18 | fm.system_theme(true); 19 | else fm.dark_theme(conf.cbtheme == 0); 20 | 21 | fm.caption("ytdlp-interface - release notes"); 22 | fm.bgcolor(theme::fmbg); 23 | fm.div(R"(vert margin=20 24 | <> >)"); 25 | 26 | widgets::Textbox tb {fm}; 27 | { 28 | fm["tb"] << tb; 29 | nana::API::effects_edge_nimbus(tb, nana::effects::edge_nimbus::none); 30 | tb.typeface(nana::paint::font_info {"Courier New", 12}); 31 | tb.line_wrapped(true); 32 | tb.editable(false); 33 | } 34 | 35 | widgets::cbox cblogview {fm, "Changelog view"}; 36 | fm["cblogview"] << cblogview; 37 | 38 | auto display_release_notes = [&](const unsigned ver) 39 | { 40 | std::string tag_name {releases[ver]["tag_name"]}; 41 | std::string body {releases[ver]["body"]}; 42 | body.erase(0, body.find('-')); 43 | std::string date {releases[ver]["published_at"]}; 44 | date.erase(date.find('T')); 45 | auto title {tag_name + " (" + date + ")\n"}; 46 | title += std::string(title.size() - 1, '=') + "\n\n"; 47 | tb.caption(title + body); 48 | tb.colored_area_access()->clear(); 49 | nana::api::refresh_window(tb); 50 | }; 51 | 52 | auto display_changelog = [&] 53 | { 54 | tb.caption(""); 55 | auto ca {tb.colored_area_access()}; 56 | ca->clear(); 57 | for(const auto &el : releases) 58 | { 59 | std::string 60 | tag_name {el["tag_name"]}, 61 | date {el["published_at"]}, 62 | body {el["body"]}, 63 | block; 64 | 65 | if(!body.empty()) 66 | { 67 | body.erase(0, body.find('-')); 68 | body.erase(body.find_first_of("\r\n", body.rfind("- "))); 69 | } 70 | date.erase(date.find('T')); 71 | auto title {tag_name + " (" + date + ")\n"}; 72 | title += std::string(title.size() - 1, '=') + "\n\n"; 73 | block += title + body + "\n\n"; 74 | if(tag_name != "v1.0.0") 75 | { 76 | block += "---------------------------------------------------------------------------------------------\n\n"; 77 | tb.caption(tb.caption() + block); 78 | auto p {ca->get(tb.text_line_count() - 3)}; 79 | p->count = 1; 80 | p->fgcolor = theme::is_dark() ? nana::color {"#777"} : nana::color {"#bbb"}; 81 | } 82 | else tb.caption(tb.caption() + block); 83 | } 84 | }; 85 | 86 | display_release_notes(0); 87 | 88 | widgets::Label l_history {fm, "Show release notes for:"}; 89 | fm["l_history"] << l_history; 90 | 91 | widgets::Combox com_history {fm}; 92 | { 93 | fm["com_history"] << com_history; 94 | com_history.editable(false); 95 | for(auto &release : releases) 96 | { 97 | std::string text {release["tag_name"]}; 98 | com_history.push_back(text); 99 | } 100 | com_history.option(0); 101 | com_history.events().selected([&] 102 | { 103 | display_release_notes(com_history.option()); 104 | }); 105 | } 106 | 107 | cblogview.events().click([&] 108 | { 109 | if(cblogview.checked()) 110 | display_changelog(); 111 | else display_release_notes(com_history.option()); 112 | com_history.enabled(!cblogview.checked()); 113 | }); 114 | 115 | tb.events().mouse_wheel([&](const nana::arg_wheel &arg) 116 | { 117 | if(!cblogview.checked() && !tb.scroll_operation()->visible(true) && arg.which == nana::arg_wheel::wheel::vertical) 118 | { 119 | const auto idx_sel {com_history.option()}, idx_last {com_history.the_number_of_options() - 1}; 120 | if(arg.upwards) 121 | { 122 | if(idx_sel == 0) 123 | com_history.option(idx_last); 124 | else com_history.option(idx_sel - 1); 125 | } 126 | else 127 | { 128 | if(idx_sel == idx_last) 129 | com_history.option(0); 130 | else com_history.option(idx_sel + 1); 131 | } 132 | nana::api::refresh_window(com_history); 133 | } 134 | }); 135 | 136 | fm.collocate(); 137 | fm.show(); 138 | fm.modality(); 139 | } -------------------------------------------------------------------------------- /ytdlp-interface/forms/form_colors.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../gui.hpp" 4 | #include "../hsl_picker.hpp" 5 | 6 | 7 | class my_tbox : public ::widgets::Textbox 8 | { 9 | public: 10 | nana::color *clr {nullptr}; 11 | nana::hsl_picker *picker {nullptr}; 12 | bool noupdate {false}; 13 | nana::window parent {nullptr}; 14 | 15 | my_tbox(nana::window parent, nana::hsl_picker &picker, nana::color *pclr); 16 | 17 | void apply() 18 | { 19 | picker->color_value(*clr, true); 20 | nana::api::refresh_window(*picker); 21 | } 22 | }; 23 | 24 | 25 | class my_label : public ::widgets::Label 26 | { 27 | public: 28 | my_label(nana::window parent, std::string_view text) : Label {parent, text} 29 | { 30 | text_align(nana::align::left, nana::align_v::center); 31 | } 32 | }; 33 | 34 | 35 | class mybtn : public ::widgets::Button 36 | { 37 | public: 38 | 39 | mybtn() : Button {} {}; 40 | void create(nana::window parent); 41 | }; 42 | 43 | class container : public nana::panel 44 | { 45 | struct fake_field_interface 46 | { 47 | nana::widget *parent {nullptr}, **target {nullptr}; 48 | nana::place *plc {nullptr}; 49 | std::vector *widgets {nullptr}; 50 | std::string field_name; 51 | 52 | fake_field_interface(nana::widget *parent) : parent {parent} {} 53 | nana::place::field_reference operator<<(nana::window wd); 54 | }; 55 | 56 | mybtn btn; 57 | nana::place plc; 58 | fake_field_interface ffi {this}; 59 | 60 | void refresh_btn_theme() { btn.refresh_theme(); } 61 | 62 | public: 63 | 64 | static theme_t *t_custom, *t_conf, *t_def; 65 | static container *selected; 66 | nana::widget *target {nullptr}; 67 | std::vector widgets; 68 | static std::vector containers; 69 | static std::function btn_callback; 70 | 71 | static void refresh_buttons(bool state); 72 | 73 | container() = delete; 74 | container(nana::window parent) { create(parent); } 75 | 76 | void create(nana::window parent); 77 | void refresh_btn_state(); 78 | void refresh_theme() { bgcolor(t_custom->fmbg); } 79 | auto &get_place() { return plc; } 80 | void div(std::string div_text) { plc.div(div_text); } 81 | 82 | auto &operator[](const char *field_name) 83 | { 84 | ffi.field_name = field_name; 85 | return ffi; 86 | } 87 | 88 | }; -------------------------------------------------------------------------------- /ytdlp-interface/forms/form_formats.cpp: -------------------------------------------------------------------------------- 1 | #include "../gui.hpp" 2 | 3 | 4 | void GUI::fm_formats() 5 | { 6 | using namespace nana; 7 | using ::widgets::theme; 8 | auto url {bottoms.visible()}; 9 | auto &bottom {bottoms.at(url)}; 10 | auto &vidinfo {bottom.is_scplaylist ? bottom.playlist_info["entries"][0] : bottom.vidinfo}; 11 | 12 | themed_form fm {nullptr, *this, {}, appear::decorate{}}; 13 | fm.caption(title + " - manual selection of formats"); 14 | fm.bgcolor(theme::fmbg); 15 | fm.snap(conf.cbsnap); 16 | fm.div(R"( 17 | vert margin=20 18 | > 20 | 22 | 23 | > 24 | > 25 | > 26 | > 27 | 28 | > 29 | > 30 | 31 | 32 | <>> 33 | )"); 34 | 35 | ::widgets::Title l_title {fm}; 36 | ::widgets::Label l_dur {fm, "Duration:"}, l_chap {fm, "Chapters:"}, 37 | l_upl {fm, "Uploader:"}, l_date {fm, "Upload date:"}; 38 | ::widgets::Text l_durtext {fm, ""}, l_chaptext {fm, ""}, l_upltext {fm, ""}, l_datetext {fm, ""}; 39 | nana::picture thumb {fm}; 40 | ::widgets::thumb_label thumb_label {fm}; 41 | ::widgets::Separator sep1 {fm}, sep2 {fm}; 42 | ::widgets::cbox cb_streams {fm, "Select multiple audio formats to merge into .mkv file as audio tracks " 43 | "(this passes --audio-multistreams to yt-dlp)"}; 44 | ::widgets::Listbox list {fm, true}; 45 | ::widgets::Button btnok {fm, "Use the selected format(s)"}, btncancel {fm, "Let yt-dlp choose the best formats (default)"}; 46 | 47 | fm["l_title"] << l_title; 48 | fm["l_dur"] << l_dur; 49 | fm["l_durtext"] << l_durtext; 50 | fm["l_chap"] << l_chap; 51 | fm["l_chaptext"] << l_chaptext; 52 | fm["l_upl"] << l_upl; 53 | fm["l_upltext"] << l_upltext; 54 | fm["l_date"] << l_date; 55 | fm["l_datetext"] << l_datetext; 56 | fm["thumb"] << thumb; 57 | fm["thumb_label"] << thumb_label; 58 | fm["sep1"] << sep1; 59 | fm["sep2"] << sep2; 60 | fm["cb_streams"] << cb_streams; 61 | fm["list"] << list; 62 | fm["btncancel"] << btncancel; 63 | fm["btnok"] << btnok; 64 | 65 | thumb.stretchable(true); 66 | cb_streams.check(conf.audio_multistreams); 67 | 68 | list.sortable(false); 69 | list.checkable(true); 70 | list.hilight_checked(true); 71 | list.enable_single(true, false); 72 | list.scheme().text_margin = dpi_scale(10) + (api::screen_dpi(true) > 96) * 4; 73 | list.append_header("format", dpi_scale(280)); 74 | list.append_header("acodec", dpi_scale(90)); 75 | list.append_header("vcodec", dpi_scale(90)); 76 | list.append_header("ext", dpi_scale(50)); 77 | list.append_header("fps", dpi_scale(32)); 78 | list.append_header("ch", dpi_scale(50)); 79 | list.append_header("vbr", dpi_scale(40)); 80 | list.append_header("abr", dpi_scale(40)); 81 | list.append_header("tbr", dpi_scale(40)); 82 | list.append_header("asr", dpi_scale(50)); 83 | list.append_header("filesize", dpi_scale(160)); 84 | 85 | list.events().selected([&](const arg_listbox &arg) 86 | { 87 | if(arg.item.selected()) 88 | arg.item.check(!arg.item.checked()).select(false); 89 | }); 90 | 91 | list.events().mouse_down([&](const arg_mouse &arg) 92 | { 93 | if(conf.audio_multistreams) 94 | { 95 | auto pos {list.cast({arg.pos.x, arg.pos.y})}; 96 | if(pos.is_category() && list.at(pos.cat).text() == "Audio only") 97 | { 98 | list.auto_draw(false); 99 | for(auto ip : list.at(pos.cat)) 100 | ip.check(!ip.checked()); 101 | list.auto_draw(true); 102 | } 103 | } 104 | }); 105 | 106 | list.events().checked([&](const arg_listbox &arg) 107 | { 108 | auto item {arg.item}; 109 | auto pos {item.pos()}; 110 | 111 | if(item.checked()) 112 | { 113 | //item.fgcolor(theme::list_check_highlight_fg); 114 | item.bgcolor(theme::list_check_highlight_bg); 115 | list.auto_draw(false); 116 | if(pos.cat == 0) 117 | { 118 | for(auto ip : list.at(0)) 119 | if(ip != item) 120 | ip.check(false); 121 | if(list.size_categ() == 3) 122 | for(auto ip : list.at(2)) 123 | ip.check(false); 124 | if(list.size_categ() > 1 && (!conf.audio_multistreams || list.at(1).text() == "Video only")) 125 | for(auto ip : list.at(1)) 126 | ip.check(false); 127 | } 128 | else if(list.size_categ() > 1) 129 | { 130 | if(list.at(pos.cat).text() == "Video only") 131 | { 132 | for(auto ip : list.at(0)) 133 | ip.check(false); 134 | for(auto ip : list.at(2)) 135 | if(ip != item) 136 | ip.check(false); 137 | } 138 | else if(!conf.audio_multistreams) 139 | { 140 | for(auto ip : list.at(1)) 141 | if(ip != item) 142 | ip.check(false); 143 | for(auto ip : list.at(0)) 144 | if(ip != item) 145 | ip.check(false); 146 | } 147 | } 148 | list.auto_draw(true); 149 | } 150 | else 151 | { 152 | item.fgcolor(list.fgcolor()); 153 | item.bgcolor(list.bgcolor()); 154 | } 155 | btnok.enable(!list.checked().empty()); 156 | }); 157 | 158 | cb_streams.events().checked([&] 159 | { 160 | conf.audio_multistreams = cb_streams.checked(); 161 | if(!conf.audio_multistreams && list.size_categ() > 1 && list.at(1).text() == "Audio only") 162 | { 163 | list.auto_draw(false); 164 | int count {0}; 165 | for(auto ip : list.at(0)) 166 | if(ip.checked()) 167 | { 168 | count++; 169 | break; 170 | } 171 | for(auto ip : list.at(1)) 172 | if(ip.checked()) 173 | { 174 | if(!count) 175 | { 176 | count++; 177 | continue; 178 | } 179 | ip.check(false); 180 | } 181 | list.auto_draw(true); 182 | } 183 | }); 184 | 185 | btncancel.events().click([&] 186 | { 187 | auto &bottom {bottoms.current()}; 188 | std::string format_id {"---"}, format_note {"---"}, ext {"---"}, filesize {"---"}; 189 | if(bottom.use_strfmt) 190 | { 191 | if(bottom.vidinfo_contains("format_id")) 192 | format_id = bottom.vidinfo["format_id"]; 193 | if(bottom.vidinfo_contains("resolution")) 194 | format_note = bottom.vidinfo["resolution"]; 195 | else if(bottom.vidinfo_contains("format_note")) 196 | format_note = bottom.vidinfo["format_note"]; 197 | if(bottom.vidinfo_contains("ext")) 198 | ext = bottom.vidinfo["ext"]; 199 | if(bottom.vidinfo_contains("filesize")) 200 | { 201 | auto fsize {bottom.vidinfo["filesize"].get()}; 202 | filesize = util::int_to_filesize(fsize, false); 203 | } 204 | else if(bottom.vidinfo_contains("filesize_approx")) 205 | { 206 | auto fsize {bottom.vidinfo["filesize_approx"].get()}; 207 | if(bottom.vidinfo_contains("requested_formats")) 208 | { 209 | auto &reqfmt {bottom.vidinfo["requested_formats"]}; 210 | if(reqfmt.size() == 2) 211 | { 212 | if(reqfmt[0].contains("filesize") && reqfmt[1].contains("filesize")) 213 | filesize = '~' + util::int_to_filesize(fsize, false); 214 | else filesize = "---"; 215 | } 216 | } 217 | else filesize = '~' + util::int_to_filesize(fsize, false); 218 | } 219 | lbq.item_from_value(url).text(4, format_id); 220 | lbq.item_from_value(url).text(5, format_note); 221 | lbq.item_from_value(url).text(6, ext); 222 | lbq.item_from_value(url).text(7, filesize); 223 | } 224 | bottom.use_strfmt = false; 225 | fm.close(); 226 | }); 227 | 228 | auto get_int = [](const nlohmann::json &j, const std::string &key) -> std::string 229 | { 230 | return (j.contains(key) && j[key] != nullptr) ? std::to_string(j[key].get()) : "---"; 231 | }; 232 | 233 | auto get_string = [](const nlohmann::json &j, const std::string &key) -> std::string 234 | { 235 | return (j.contains(key) && j[key] != nullptr) ? j[key].get() : "---"; 236 | }; 237 | 238 | btnok.events().click([&] 239 | { 240 | auto &bottom {bottoms.current()}; 241 | auto &strfmt {bottom.strfmt}, &fmt1 {bottom.fmt1}, &fmt2 {bottom.fmt2}; 242 | fmt1.clear(); 243 | fmt2.clear(); 244 | 245 | size_t vidcat {0}; 246 | if(list.size_categ() == 3) 247 | vidcat = 2; 248 | else if(list.size_categ() == 2 && list.at(1).text() == "Video only") 249 | vidcat = 1; 250 | 251 | bool mergeall {false}; 252 | if(vidcat == 2) 253 | { 254 | size_t cnt {0}; 255 | for(const auto ip : list.at(1)) 256 | if(ip.checked()) 257 | cnt++; 258 | if(list.at(1).size() == cnt) 259 | { 260 | fmt2 = L"mergeall"; 261 | mergeall = true; 262 | } 263 | } 264 | 265 | auto sel {list.checked()}; 266 | for(const auto &pos : sel) 267 | { 268 | auto &val {list.at(pos).value()}; 269 | if(pos.cat == vidcat || pos.cat == 0) 270 | { 271 | fmt1 = std::move(val); 272 | if(mergeall) break; 273 | } 274 | else if(!mergeall) 275 | { 276 | if(!fmt2.empty()) 277 | fmt2 += '+'; 278 | fmt2 += std::move(val); 279 | } 280 | } 281 | strfmt = fmt1; 282 | if(!fmt2.empty()) 283 | { 284 | if(!strfmt.empty()) 285 | strfmt += L'+'; 286 | strfmt += fmt2; 287 | } 288 | 289 | conf.fmt1 = fmt1; 290 | conf.fmt2 = fmt2; 291 | 292 | if(sel.size() == 1) 293 | { 294 | auto item {lbq.at(lbq.selected().front())}; 295 | auto it {std::find_if(vidinfo["formats"].begin(), vidinfo["formats"].end(), [&](const auto &el) 296 | { 297 | return el["format"].get().rfind(nana::to_utf8(strfmt)) != -1; 298 | })}; 299 | std::string fsize {"---"}, fmt_note, ext, fmtid; 300 | if(it != vidinfo["formats"].end()) 301 | { 302 | auto &fmt {*it}; 303 | if(fmt.contains("filesize") && fmt["filesize"] != nullptr) 304 | fsize = util::int_to_filesize(fmt["filesize"].get(), false); 305 | else if(fmt.contains("filesize_approx") && fmt["filesize_approx"] != nullptr) 306 | fsize = util::int_to_filesize(fmt["filesize_approx"].get(), false); 307 | if(list.at(sel.front().cat).text() == "Audio only") 308 | fmt_note = get_string(fmt, "format_note"); 309 | else fmt_note = get_string(fmt, "resolution"); 310 | ext = get_string(fmt, "ext"); 311 | fmtid = get_string(fmt, "format_id"); 312 | item.text(4, fmtid); 313 | item.text(5, fmt_note); 314 | item.text(6, ext); 315 | if(fsize != "---") 316 | fsize = '~' + fsize; 317 | item.text(7, fsize); 318 | } 319 | } 320 | else 321 | { 322 | auto item {lbq.at(lbq.selected().front())}; 323 | auto it1 {std::find_if(vidinfo["formats"].begin(), vidinfo["formats"].end(), [&](const auto &el) 324 | { 325 | return el["format_id"].get() == nana::to_utf8(fmt1); 326 | })}; 327 | auto it2 {std::find_if(vidinfo["formats"].begin(), vidinfo["formats"].end(), [&](const auto &el) 328 | { 329 | return el["format_id"].get() == nana::to_utf8(fmt2); 330 | })}; 331 | 332 | if(it1 != vidinfo["formats"].end() && it2 != vidinfo["formats"].end()) 333 | { 334 | std::string fsize, fsize1 {"---"}, fsize2 {"---"}, fmt_note, ext; 335 | std::uint64_t size1 {0}, size2 {0}; 336 | if(it1->contains("filesize") && (*it1)["filesize"] != nullptr) 337 | size1 = (*it1)["filesize"].get(); 338 | else if(it1->contains("filesize_approx") && (*it1)["filesize_approx"] != nullptr) 339 | size1 = (*it1)["filesize_approx"].get(); 340 | fmt_note = get_string(*it1, "resolution"); 341 | ext = get_string(*it1, "ext"); 342 | if(size1) fsize1 = util::int_to_filesize(size1, false); 343 | 344 | if(it2->contains("filesize") && (*it2)["filesize"] != nullptr) 345 | size2 = (*it2)["filesize"].get(); 346 | else if(it2->contains("filesize_approx") && (*it2)["filesize_approx"] != nullptr) 347 | size2 = (*it2)["filesize_approx"].get(); 348 | if(size2) fsize2 = util::int_to_filesize(size2, false); 349 | if(size1 && size2) 350 | fsize = '~' + util::int_to_filesize(size1 + size2, false); 351 | else fsize = "---"; 352 | 353 | item.text(4, strfmt); 354 | item.text(5, fmt_note); 355 | item.text(6, ext); 356 | item.text(7, fsize); 357 | } 358 | } 359 | 360 | bottom.use_strfmt = true; 361 | if(bottom.using_custom_fmt()) 362 | { 363 | ::widgets::msgbox mbox {fm, "Warning: conflicting -f arguments"}; 364 | std::string text {"The \"Custom arguments\" checkbox is checked, and \"-f\" is present as a custom argument.\n\n" 365 | "If you don't uncheck that checkbox, the \"-f\" custom argument will override the selection you have made here."}; 366 | mbox.icon(nana::msgbox::icon_warning); 367 | (mbox << text)(); 368 | } 369 | fm.close(); 370 | }); 371 | 372 | btnok.enabled(false); 373 | thumb.transparent(true); 374 | 375 | std::string thumb_url, title {"[title missing]"}; 376 | if(bottom.vidinfo_contains("title")) 377 | title = vidinfo["title"]; 378 | if(!bottom.is_ytlink) 379 | { 380 | if(bottom.vidinfo_contains("thumbnail")) 381 | thumb_url = vidinfo["thumbnail"]; 382 | thumb.stretchable(true); 383 | thumb.align(align::center, align_v::center); 384 | } 385 | else 386 | { 387 | auto it {std::find_if(vidinfo["thumbnails"].begin(), vidinfo["thumbnails"].end(), [](const auto &el) 388 | { 389 | return el["url"].get().rfind("mqdefault.jpg") != -1; 390 | })}; 391 | 392 | if(it == vidinfo["thumbnails"].end()) 393 | it = std::find_if(vidinfo["thumbnails"].begin(), vidinfo["thumbnails"].end(), [](const auto &el) 394 | { 395 | return el["url"].get().rfind("mqdefault_live.jpg") != -1; 396 | }); 397 | 398 | if(it != vidinfo["thumbnails"].end()) 399 | thumb_url = (*it)["url"]; 400 | } 401 | 402 | l_title.caption(title); 403 | list.append({"Audio only", "Video only"}); 404 | 405 | int dur {0}; 406 | bool live {bottom.vidinfo_contains("is_live") && bottom.vidinfo["is_live"] || 407 | bottom.vidinfo_contains("live_status") && bottom.vidinfo["live_status"] == "is_live"}; 408 | if(!live && bottom.vidinfo_contains("duration")) 409 | dur = vidinfo["duration"]; 410 | int hr {(dur / 60) / 60}, min {(dur / 60) % 60}, sec {dur % 60}; 411 | if(dur < 60) sec = dur; 412 | std::string durstr {live ? "live" : "---"}; 413 | if(dur) 414 | { 415 | std::stringstream ss; 416 | ss.width(2); 417 | ss.fill('0'); 418 | if(hr) 419 | { 420 | ss << min; 421 | durstr = std::to_string(hr) + ':' + ss.str(); 422 | ss.str(""); 423 | ss.width(2); 424 | ss.fill('0'); 425 | } 426 | else durstr = std::to_string(min); 427 | ss << sec; 428 | durstr += ':' + ss.str(); 429 | } 430 | else if(bottom.vidinfo_contains("duration_string")) 431 | durstr = vidinfo["duration_string"]; 432 | l_durtext.caption(durstr); 433 | 434 | std::string strchap; 435 | if(bottom.vidinfo_contains("chapters")) 436 | strchap = std::to_string(vidinfo["chapters"].size()); 437 | if(strchap.empty() || strchap == "0") 438 | l_chaptext.caption("none"); 439 | else l_chaptext.caption(strchap); 440 | 441 | if(bottom.vidinfo_contains("uploader")) 442 | l_upltext.caption(std::string {vidinfo["uploader"]}); 443 | else l_upltext.caption("---"); 444 | 445 | std::string strdate {"---"}; 446 | if(bottom.vidinfo_contains("upload_date")) 447 | { 448 | strdate = vidinfo["upload_date"]; 449 | strdate = strdate.substr(0, 4) + '-' + strdate.substr(4, 2) + '-' + strdate.substr(6, 2); 450 | } 451 | l_datetext.caption(strdate); 452 | 453 | if(!thumb_url.empty()) 454 | { 455 | thr_thumb = std::thread([&] 456 | { 457 | thumbthr_working = true; 458 | std::string error; 459 | auto thumb_jpg {util::get_inet_res(thumb_url, &error)}; 460 | if(!thumb_jpg.empty()) 461 | { 462 | paint::image img; 463 | img.open(thumb_jpg.data(), thumb_jpg.size()); 464 | if(img.empty()) 465 | { 466 | std::string ext; 467 | auto pos {thumb_url.rfind('.')}; 468 | if(pos != -1) 469 | { 470 | auto idx {pos}; 471 | while(++idx < thumb_url.size() && isalpha(thumb_url[idx])); 472 | ext = thumb_url.substr(pos, idx-pos); 473 | } 474 | if(ext.empty()) 475 | thumb_label.caption("thumbnail format unsupported"); 476 | else thumb_label.caption("thumbnail format unsupported\n(" + ext + ')'); 477 | fm.get_place().field_display("thumb_label", true); 478 | fm.collocate(); 479 | } 480 | else if(thumbthr_working) 481 | thumb.load(img); 482 | thumbthr_working = false; 483 | if(thr_thumb.joinable()) 484 | thr_thumb.detach(); 485 | } 486 | else 487 | { 488 | if(error.empty()) 489 | thumb_label.caption("error downloading thumbnail"); 490 | else thumb_label.caption(error); 491 | fm.get_place().field_display("thumb_label", true); 492 | fm.collocate(); 493 | } 494 | }); 495 | } 496 | else 497 | { 498 | thumb_label.caption("thumbnail not available"); 499 | fm.get_place().field_display("thumb_label", true); 500 | fm.collocate(); 501 | } 502 | 503 | std::string format_id1, format_id2; 504 | if(bottom.vidinfo_contains("format_id")) 505 | { 506 | std::string format_id {vidinfo["format_id"]}; 507 | auto pos(format_id.find('+')); 508 | if(pos != -1) 509 | { 510 | format_id1 = format_id.substr(0, pos); 511 | format_id2 = format_id.substr(pos + 1); 512 | } 513 | else format_id1 = format_id; 514 | } 515 | 516 | list.auto_draw(false); 517 | std::vector colmask(11, false); 518 | for(auto &fmt : vidinfo["formats"]) 519 | { 520 | std::string format {fmt["format"]}, acodec, vcodec, ext, fps, chan, vbr, abr, tbr, asr, filesize {"---"}; 521 | if(format.find("storyboard") == -1) 522 | { 523 | abr = get_int(fmt, "abr"); 524 | vbr = get_int(fmt, "vbr"); 525 | tbr = get_int(fmt, "tbr"); 526 | asr = get_int(fmt, "asr"); 527 | fps = get_int(fmt, "fps"); 528 | chan = get_int(fmt, "audio_channels"); 529 | ext = get_string(fmt, "ext"); 530 | acodec = get_string(fmt, "acodec"); 531 | vcodec = get_string(fmt, "vcodec"); 532 | if(fmt.contains("filesize") && fmt["filesize"] != nullptr) 533 | filesize = util::int_to_filesize(fmt["filesize"].get(), conf.cb_formats_fsize_bytes); 534 | else if(fmt.contains("filesize_approx") && fmt["filesize_approx"] != nullptr) 535 | filesize = "~" + util::int_to_filesize(fmt["filesize_approx"].get(), conf.cb_formats_fsize_bytes); 536 | unsigned catidx {0}; 537 | if(acodec == "none") 538 | catidx = 2; // video only 539 | else if(vcodec == "none") 540 | catidx = 1; // audio only 541 | list.at(catidx).append({format, acodec, vcodec, ext, fps, chan, vbr, abr, tbr, asr, filesize}); 542 | auto idstr {to_wstring(fmt["format_id"].get())}; 543 | auto item {list.at(catidx).back()}; 544 | item.value(idstr); 545 | if(idstr == conf.fmt1 || (conf.audio_multistreams ? conf.fmt2.find(idstr) != -1 : idstr == conf.fmt2)) 546 | list.at(catidx).back().select(true); 547 | if(!format_id1.empty() && format_id1 == to_utf8(idstr)) 548 | item.text(0, format + " *"); 549 | if(!format_id2.empty() && format_id2 == to_utf8(idstr)) 550 | item.text(0, format + " *"); 551 | for(int n {1}; n < 11; n++) 552 | { 553 | const auto text {item.text(n)}; 554 | if(!colmask[n] && text != "---" && text != "none") 555 | colmask[n] = true; 556 | } 557 | } 558 | } 559 | 560 | for(int n {1}; n < 11; n++) 561 | list.column_at(n).visible(colmask[n]); 562 | 563 | if(list.size_categ() == 3) 564 | { 565 | if(list.at(2).size() == 0) 566 | list.erase(2); 567 | if(list.at(1).size() == 0) 568 | list.erase(1); 569 | } 570 | 571 | list.refresh_theme(); 572 | list.fit_column_content(); 573 | list.auto_draw(true); 574 | 575 | fm.events().unload([&] 576 | { 577 | if(thr_thumb.joinable()) 578 | { 579 | thumbthr_working = false; 580 | thr_thumb.detach(); 581 | } 582 | }); 583 | 584 | fm.theme_callback([&](bool dark) 585 | { 586 | apply_theme(dark); 587 | fm.bgcolor(theme::fmbg); 588 | return false; 589 | }); 590 | 591 | if(conf.cbtheme == 2) 592 | fm.system_theme(true); 593 | else fm.dark_theme(conf.cbtheme == 0); 594 | 595 | fm.center(1000, std::max(429.0 + list.item_count() * 20.5, double(600))); 596 | api::track_window_size(fm, dpi_scale_size(900, 600), false); 597 | 598 | fm.collocate(); 599 | fm.modality(); 600 | } -------------------------------------------------------------------------------- /ytdlp-interface/forms/form_json.cpp: -------------------------------------------------------------------------------- 1 | #include "../gui.hpp" 2 | #include 3 | 4 | 5 | void GUI::fm_json() 6 | { 7 | using widgets::theme; 8 | using namespace nana; 9 | 10 | themed_form fm {nullptr, *this, {}, appear::decorate{}}; 11 | fm.center(1000, 1006); 12 | fm.caption(title + " - JSON viewer"); 13 | fm.bgcolor(theme::fmbg); 14 | fm.snap(conf.cbsnap); 15 | 16 | fm.div("vert margin=20