CoolFace
Modelpublic

cwenzi/neuroflow-cpp

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
1likes
video_downloader.cpp1397 linesDownload Raw Back to scripts
1/**2 * 万能视频下载器 — C++ 版本3 *4 * 对标 D:\电影\万能视频下载器.exe (PyInstaller + yt-dlp + tkinter)5 *6 * 功能:7 *   - 多站点视频提取 (YouTube, Bilibili, 抖音, etc.) 通过 yt-dlp 子进程8 *   - 格式/质量选择9 *   - 多线程下载 + 断点续传10 *   - 实时进度显示11 *   - HLS/DASH 流支持12 *   - 代理支持13 *14 * 编译 (MSVC):15 *   cl /EHsc /O2 /std:c++17 video_downloader.cpp /Fe:video_downloader.exe16 *   需要: yt-dlp.exe 在 PATH 中 (或同目录)17 *18 * 编译 (MinGW):19 *   g++ -O2 -std=c++17 video_downloader.cpp -lwinhttp -lshlwapi -o video_downloader.exe20 */21 22#define WIN32_LEAN_AND_MEAN23#define _WIN32_WINNT 0x060124#include <windows.h>25#include <winhttp.h>26#include <shlwapi.h>27#include <commctrl.h>28 29#include <algorithm>30#include <atomic>31#include <condition_variable>32#include <cstdio>33#include <cstring>34#include <ctime>35#include <deque>36#include <filesystem>37#include <fstream>38#include <functional>39#include <iomanip>40#include <iostream>41#include <map>42#include <memory>43#include <mutex>44#include <optional>45#include <queue>46#include <regex>47#include <sstream>48#include <string>49#include <string_view>50#include <thread>51#include <unordered_map>52#include <vector>53 54#pragma comment(lib, "winhttp.lib")55#pragma comment(lib, "shlwapi.lib")56#pragma comment(lib, "comctl32.lib")57 58namespace fs = std::filesystem;59 60// ============================================================================61// 工具函数62// ============================================================================63 64static std::string wchar_to_utf8(const wchar_t* wstr, int len = -1) {65    if (!wstr) return {};66    int n = WideCharToMultiByte(CP_UTF8, 0, wstr, len, nullptr, 0, nullptr, nullptr);67    std::string result(n, '\0');68    WideCharToMultiByte(CP_UTF8, 0, wstr, len, &result[0], n, nullptr, nullptr);69    return result;70}71 72static std::wstring utf8_to_wchar(std::string_view str) {73    if (str.empty()) return {};74    int n = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0);75    std::wstring result(n, L'\0');76    MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], n);77    return result;78}79 80static std::string format_bytes(uint64_t bytes) {81    const char* units[] = {"B", "KB", "MB", "GB", "TB"};82    int idx = 0;83    double size = (double)bytes;84    while (size >= 1024.0 && idx < 4) {85        size /= 1024.0;86        idx++;87    }88    char buf[64];89    snprintf(buf, sizeof(buf), "%.*f %s", (idx == 0 ? 0 : 1), size, units[idx]);90    return buf;91}92 93static std::string format_speed(double bytes_per_sec) {94    return format_bytes((uint64_t)bytes_per_sec) + "/s";95}96 97static std::string format_duration(int64_t seconds) {98    int64_t h = seconds / 3600;99    int64_t m = (seconds % 3600) / 60;100    int64_t s = seconds % 60;101    char buf[32];102    if (h > 0)103        snprintf(buf, sizeof(buf), "%lld:%02lld:%02lld", h, m, s);104    else105        snprintf(buf, sizeof(buf), "%lld:%02lld", m, s);106    return buf;107}108 109static std::vector<std::string> split(std::string_view str, char delim) {110    std::vector<std::string> parts;111    size_t start = 0, end;112    while ((end = str.find(delim, start)) != std::string_view::npos) {113        parts.emplace_back(str.substr(start, end - start));114        start = end + 1;115    }116    if (start < str.size()) parts.emplace_back(str.substr(start));117    return parts;118}119 120static std::string strip(std::string_view s) {121    while (!s.empty() && (s.front() == ' ' || s.front() == '\t' || s.front() == '\n' || s.front() == '\r'))122        s.remove_prefix(1);123    while (!s.empty() && (s.back() == ' ' || s.back() == '\t' || s.back() == '\n' || s.back() == '\r'))124        s.remove_suffix(1);125    return std::string(s);126}127 128// ============================================================================129// 简易 JSON 解析器 (无需外部依赖)130// ============================================================================131 132struct JsonValue {133    enum Type { NUL, BOOL, INT, FLOAT, STRING, ARRAY, OBJECT };134    Type type = NUL;135    bool bval = false;136    int64_t ival = 0;137    double fval = 0.0;138    std::string sval;139    std::vector<JsonValue> arr;140    std::unordered_map<std::string, JsonValue> obj;141    std::vector<std::string> obj_keys;  // 保持插入顺序142 143    static JsonValue parse(std::string_view json);144    std::string dump(int indent = 0) const;145 146    const JsonValue& operator[](const char* key) const {147        static JsonValue null_val;148        auto it = obj.find(key);149        return it != obj.end() ? it->second : null_val;150    }151    const JsonValue& operator[](size_t idx) const {152        static JsonValue null_val;153        return idx < arr.size() ? arr[idx] : null_val;154    }155    bool has(const char* key) const { return obj.find(key) != obj.end(); }156    size_t size() const { return type == ARRAY ? arr.size() : type == OBJECT ? obj.size() : 0; }157    std::string str_or(const char* def = "") const {158        return type == STRING ? sval : def;159    }160    int64_t int_or(int64_t def = 0) const {161        if (type == INT) return ival;162        if (type == FLOAT) return (int64_t)fval;163        return def;164    }165    double float_or(double def = 0.0) const {166        if (type == FLOAT) return fval;167        if (type == INT) return (double)ival;168        return def;169    }170    bool bool_or(bool def = false) const {171        return type == BOOL ? bval : def;172    }173};174 175// 简易递归下降 JSON 解析器176struct JsonParser {177    std::string_view json;178    size_t pos = 0;179 180    JsonParser(std::string_view j) : json(j) {}181 182    void skip_ws() {183        while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||184                                      json[pos] == '\n' || json[pos] == '\r'))185            pos++;186    }187 188    char peek() { skip_ws(); return pos < json.size() ? json[pos] : '\0'; }189    char next() { skip_ws(); return pos < json.size() ? json[pos++] : '\0'; }190 191    JsonValue parse_value() {192        skip_ws();193        if (pos >= json.size()) return JsonValue{};194 195        char c = json[pos];196        if (c == '"') return parse_string();197        if (c == '{') return parse_object();198        if (c == '[') return parse_array();199        if (c == 't' || c == 'f') return parse_bool();200        if (c == 'n') return parse_null();201        return parse_number();202    }203 204    JsonValue parse_string() {205        next(); // skip '"'206        std::string s;207        while (pos < json.size() && json[pos] != '"') {208            if (json[pos] == '\\' && pos + 1 < json.size()) {209                pos++;210                switch (json[pos]) {211                    case '"':  s += '"'; break;212                    case '\\': s += '\\'; break;213                    case '/':  s += '/'; break;214                    case 'b':  s += '\b'; break;215                    case 'f':  s += '\f'; break;216                    case 'n':  s += '\n'; break;217                    case 'r':  s += '\r'; break;218                    case 't':  s += '\t'; break;219                    case 'u': {220                        if (pos + 4 < json.size()) {221                            unsigned cp = 0;222                            for (int i = 1; i <= 4; i++)223                                cp = (cp << 4) | hex_val(json[pos + i]);224                            pos += 4;225                            if (cp < 0x80) s += (char)cp;226                            else if (cp < 0x800) { s += (char)(0xC0 | (cp >> 6)); s += (char)(0x80 | (cp & 0x3F)); }227                            else { s += (char)(0xE0 | (cp >> 12)); s += (char)(0x80 | ((cp>>6) & 0x3F)); s += (char)(0x80 | (cp & 0x3F)); }228                        }229                        break;230                    }231                }232                pos++;233            } else {234                s += json[pos++];235            }236        }237        if (pos < json.size()) pos++; // skip closing '"'238        JsonValue v; v.type = JsonValue::STRING; v.sval = s;239        return v;240    }241 242    JsonValue parse_object() {243        next(); // skip '{'244        JsonValue v; v.type = JsonValue::OBJECT;245        while (peek() != '}' && pos < json.size()) {246            JsonValue key = parse_string();247            if (peek() == ':') next();248            v.obj_keys.push_back(key.sval);249            v.obj[key.sval] = parse_value();250            if (peek() == ',') next();251        }252        if (pos < json.size()) pos++; // skip '}'253        return v;254    }255 256    JsonValue parse_array() {257        next(); // skip '['258        JsonValue v; v.type = JsonValue::ARRAY;259        while (peek() != ']' && pos < json.size()) {260            v.arr.push_back(parse_value());261            if (peek() == ',') next();262        }263        if (pos < json.size()) pos++; // skip ']'264        return v;265    }266 267    JsonValue parse_bool() {268        JsonValue v; v.type = JsonValue::BOOL;269        if (json.substr(pos, 4) == "true") { v.bval = true; pos += 4; }270        else { v.bval = false; pos += 5; }271        return v;272    }273 274    JsonValue parse_null() {275        pos += 4; // skip "null"276        return JsonValue{};277    }278 279    JsonValue parse_number() {280        JsonValue v;281        size_t start = pos;282        bool is_float = false;283        if (pos < json.size() && json[pos] == '-') pos++;284        while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++;285        if (pos < json.size() && json[pos] == '.') { is_float = true; pos++; }286        while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++;287        if (pos < json.size() && (json[pos] == 'e' || json[pos] == 'E')) {288            is_float = true; pos++;289            if (pos < json.size() && (json[pos] == '+' || json[pos] == '-')) pos++;290            while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++;291        }292        auto num_str = std::string(json.substr(start, pos - start));293        if (is_float) {294            v.type = JsonValue::FLOAT;295            v.fval = std::stod(num_str);296        } else {297            v.type = JsonValue::INT;298            v.ival = std::stoll(num_str);299        }300        return v;301    }302 303    static int hex_val(char c) {304        if (c >= '0' && c <= '9') return c - '0';305        if (c >= 'a' && c <= 'f') return c - 'a' + 10;306        if (c >= 'A' && c <= 'F') return c - 'A' + 10;307        return 0;308    }309};310 311JsonValue JsonValue::parse(std::string_view json) {312    return JsonParser(json).parse_value();313}314 315// ============================================================================316// 视频格式信息317// ============================================================================318 319struct VideoFormat {320    std::string format_id;321    std::string ext;           // mp4, webm, mkv, etc.322    std::string resolution;    // 1920x1080323    std::string vcodec;        // h264, vp9, av1324    std::string acodec;        // aac, opus, mp4a325    std::string note;          // 备注 (e.g. "1080p", "best")326    int64_t filesize = 0;      // 字节 (0 = unknown)327    int64_t filesize_approx = 0;328    int width = 0;329    int height = 0;330    double fps = 0.0;331    double tbr = 0.0;          // 平均码率332    double abr = 0.0;          // 音频码率333    double vbr = 0.0;          // 视频码率334    bool has_video = true;335    bool has_audio = true;336    std::string protocol;      // https, m3u8, dash337};338 339struct VideoInfo {340    std::string title;341    std::string description;342    std::string uploader;343    std::string upload_date;344    std::string webpage_url;345    std::string thumbnail_url;346    int64_t duration = 0;       // 秒347    int64_t view_count = 0;348    int64_t like_count = 0;349    std::vector<VideoFormat> formats;350    std::vector<std::string> categories;351    std::vector<std::string> tags;352 353    // 选择最佳格式354    const VideoFormat* best_video(int max_height = 99999) const {355        const VideoFormat* best = nullptr;356        for (auto& f : formats) {357            if (!f.has_video) continue;358            if (f.height > max_height) continue;359            if (!best || f.height > best->height ||360                (f.height == best->height && f.tbr > best->tbr))361                best = &f;362        }363        return best;364    }365 366    const VideoFormat* best_audio() const {367        const VideoFormat* best = nullptr;368        for (auto& f : formats) {369            if (!f.has_audio || f.has_video) continue;370            if (!best || f.abr > best->abr) best = &f;371        }372        if (!best) best = best_video();373        return best;374    }375};376 377// ============================================================================378// yt-dlp 子进程调用 (提取视频信息)379// ============================================================================380 381class YtDlpExtractor {382public:383    static std::optional<VideoInfo> extract(const std::string& url,384                                             const std::string& proxy = "",385                                             const std::string& cookies = "") {386        std::string cmd = "yt-dlp.exe";387        cmd += " --dump-json --no-playlist --ignore-errors --no-warnings";388        if (!proxy.empty()) cmd += " --proxy " + proxy;389        if (!cookies.empty()) cmd += " --cookies " + cookies;390        cmd += " \"" + url + "\"";391        cmd += " 2>nul";392 393        std::string output = exec_command(cmd);394        if (output.empty()) {395            // 尝试 python -m yt_dlp396            cmd = "python -m yt_dlp --dump-json --no-playlist --ignore-errors --no-warnings";397            if (!proxy.empty()) cmd += " --proxy " + proxy;398            cmd += " \"" + url + "\"";399            cmd += " 2>nul";400            output = exec_command(cmd);401        }402        if (output.empty()) return std::nullopt;403        return parse_info(output);404    }405 406    static std::optional<std::vector<VideoInfo>> extract_playlist(407        const std::string& url, const std::string& proxy = "") {408        std::string cmd = "yt-dlp.exe";409        cmd += " --dump-json --ignore-errors --no-warnings";410        if (!proxy.empty()) cmd += " --proxy " + proxy;411        cmd += " \"" + url + "\"";412        cmd += " 2>nul";413 414        std::string output = exec_command(cmd);415        if (output.empty()) return std::nullopt;416 417        std::vector<VideoInfo> results;418        size_t pos = 0;419        while (pos < output.size()) {420            // 找到每个 JSON 对象边界421            size_t start = output.find('{', pos);422            if (start == std::string::npos) break;423            int depth = 0;424            size_t end = start;425            bool in_str = false;426            while (end < output.size()) {427                char c = output[end];428                if (c == '"' && (end == 0 || output[end-1] != '\\')) in_str = !in_str;429                if (!in_str) {430                    if (c == '{') depth++;431                    else if (c == '}') { depth--; if (depth == 0) { end++; break; } }432                }433                end++;434            }435            if (depth == 0) {436                auto vi = parse_info(output.substr(start, end - start));437                if (vi) results.push_back(*vi);438            }439            pos = end;440        }441        return results.empty() ? std::nullopt : std::make_optional(results);442    }443 444private:445    static std::string exec_command(const std::string& cmd) {446        SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};447        HANDLE hRead, hWrite;448        if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return "";449 450        SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);451 452        STARTUPINFOW si = {sizeof(STARTUPINFOW)};453        si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;454        si.wShowWindow = SW_HIDE;455        si.hStdOutput = hWrite;456        si.hStdError = hWrite;457 458        PROCESS_INFORMATION pi = {};459        std::wstring wcmd = utf8_to_wchar(cmd);460        std::unique_ptr<wchar_t[]> cmd_buf(new wchar_t[wcmd.size() + 1]);461        wcscpy(cmd_buf.get(), wcmd.c_str());462 463        std::string result;464        if (CreateProcessW(nullptr, cmd_buf.get(), nullptr, nullptr, TRUE,465                           CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {466            CloseHandle(hWrite);467            WaitForSingleObject(pi.hProcess, 30000);  // 30s timeout for extract468            char buf[4096];469            DWORD read;470            while (ReadFile(hRead, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) {471                buf[read] = '\0';472                result += buf;473            }474            CloseHandle(hRead);475            CloseHandle(pi.hProcess);476            CloseHandle(pi.hThread);477        } else {478            CloseHandle(hWrite);479            CloseHandle(hRead);480        }481        return result;482    }483 484    static std::optional<VideoInfo> parse_info(const std::string& json_str) {485        try {486            auto j = JsonValue::parse(json_str);487            if (j.type != JsonValue::OBJECT) return std::nullopt;488 489            VideoInfo vi;490            vi.title = j["title"].str_or("Unknown");491            vi.description = j["description"].str_or();492            vi.uploader = j["uploader"].str_or(j["channel"].str_or("Unknown"));493            vi.upload_date = j["upload_date"].str_or();494            vi.webpage_url = j["webpage_url"].str_or();495            vi.thumbnail_url = j["thumbnail"].str_or();496            vi.duration = j["duration"].int_or(0);497            vi.view_count = j["view_count"].int_or(0);498            vi.like_count = j["like_count"].int_or(0);499 500            // categories501            if (j.has("categories") && j["categories"].type == JsonValue::ARRAY) {502                for (auto& c : j["categories"].arr)503                    if (c.type == JsonValue::STRING) vi.categories.push_back(c.sval);504            }505            // tags506            if (j.has("tags") && j["tags"].type == JsonValue::ARRAY) {507                for (auto& t : j["tags"].arr)508                    if (t.type == JsonValue::STRING) vi.tags.push_back(t.sval);509            }510 511            // formats512            if (j.has("formats") && j["formats"].type == JsonValue::ARRAY) {513                for (auto& fj : j["formats"].arr) {514                    if (fj.type != JsonValue::OBJECT) continue;515                    VideoFormat f;516                    f.format_id = fj["format_id"].str_or();517                    f.ext = fj["ext"].str_or("mp4");518                    f.resolution = fj["resolution"].str_or();519                    f.vcodec = fj["vcodec"].str_or("none");520                    f.acodec = fj["acodec"].str_or("none");521                    f.note = fj["format_note"].str_or();522                    f.filesize = fj["filesize"].int_or(0);523                    f.filesize_approx = fj["filesize_approx"].int_or(0);524                    f.width = (int)fj["width"].int_or(0);525                    f.height = (int)fj["height"].int_or(0);526                    f.fps = fj["fps"].float_or(0.0);527                    f.tbr = fj["tbr"].float_or(0.0);528                    f.abr = fj["abr"].float_or(0.0);529                    f.vbr = fj["vbr"].float_or(0.0);530                    f.protocol = fj["protocol"].str_or("https");531                    f.has_video = f.vcodec != "none";532                    f.has_audio = f.acodec != "none";533                    vi.formats.push_back(std::move(f));534                }535            }536            return vi;537        } catch (...) {538            return std::nullopt;539        }540    }541};542 543// ============================================================================544// HTTP 下载器 (WinHTTP, 支持断点续传 + 多线程)545// ============================================================================546 547struct DownloadProgress {548    std::atomic<uint64_t> downloaded{0};549    std::atomic<uint64_t> total{0};550    std::atomic<bool> paused{false};551    std::atomic<bool> cancelled{false};552    std::atomic<bool> finished{false};553    std::string status;  // "downloading", "merging", "done", "error"554    std::string error_msg;555    std::chrono::steady_clock::time_point start_time;556};557 558class HttpDownloader {559public:560    struct Options {561        std::string url;562        std::string output_path;563        int64_t resume_from = 0;  // 断点续传位置564        int num_threads = 4;565        int max_retries = 3;566        int connect_timeout_ms = 15000;567        int read_timeout_ms = 30000;568        std::string proxy;        // http://127.0.0.1:7890569        std::string user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";570        std::string cookies;571        std::map<std::string, std::string> extra_headers;572    };573 574    static bool download(const Options& opts, DownloadProgress* progress = nullptr) {575        if (progress) {576            progress->downloaded = 0;577            progress->total = 0;578            progress->finished = false;579            progress->status = "connecting";580            progress->start_time = std::chrono::steady_clock::now();581        }582 583        // 解析 URL584        std::wstring url = utf8_to_wchar(opts.url);585        URL_COMPONENTS urlc = {sizeof(URL_COMPONENTS)};586        wchar_t host[256] = {0}, path[2048] = {0};587        urlc.lpszHostName = host;588        urlc.dwHostNameLength = 255;589        urlc.lpszUrlPath = path;590        urlc.dwUrlPathLength = 2047;591 592        if (!WinHttpCrackUrl(url.c_str(), (DWORD)url.size(), 0, &urlc)) {593            if (progress) progress->status = "error: bad url";594            return false;595        }596 597        bool use_ssl = (urlc.nScheme == INTERNET_SCHEME_HTTPS);598 599        HINTERNET hSession = WinHttpOpen(600            utf8_to_wchar(opts.user_agent).c_str(),601            opts.proxy.empty() ? WINHTTP_ACCESS_TYPE_DEFAULT_PROXY : WINHTTP_ACCESS_TYPE_NAMED_PROXY,602            opts.proxy.empty() ? WINHTTP_NO_PROXY_NAME : utf8_to_wchar(opts.proxy).c_str(),603            WINHTTP_NO_PROXY_BYPASS, 0);604 605        if (!hSession) { if (progress) progress->status = "error: session"; return false; }606 607        WinHttpSetTimeouts(hSession, opts.connect_timeout_ms, opts.connect_timeout_ms,608                           opts.read_timeout_ms, opts.read_timeout_ms);609 610        HINTERNET hConnect = WinHttpConnect(hSession, host, urlc.nPort, 0);611        if (!hConnect) { WinHttpCloseHandle(hSession); return false; }612 613        HINTERNET hRequest = WinHttpOpenRequest(614            hConnect, L"GET", path, nullptr, WINHTTP_NO_REFERER,615            WINHTTP_DEFAULT_ACCEPT_TYPES,616            use_ssl ? WINHTTP_FLAG_SECURE : 0);617 618        if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return false; }619 620        // 断点续传621        if (opts.resume_from > 0) {622            wchar_t range[64];623            swprintf(range, 64, L"bytes=%lld-", (long long)opts.resume_from);624            WinHttpAddRequestHeaders(hRequest, range, (DWORD)wcslen(range), WINHTTP_ADDREQ_FLAG_ADD);625        }626 627        // 自定义 headers628        for (auto& [k, v] : opts.extra_headers) {629            std::string hdr = k + ": " + v;630            std::wstring whdr = utf8_to_wchar(hdr);631            WinHttpAddRequestHeaders(hRequest, whdr.c_str(), (DWORD)whdr.size(),632                                     WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);633        }634 635        if (!opts.cookies.empty()) {636            std::wstring cookie_hdr = L"Cookie: " + utf8_to_wchar(opts.cookies);637            WinHttpAddRequestHeaders(hRequest, cookie_hdr.c_str(), (DWORD)cookie_hdr.size(),638                                     WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);639        }640 641        if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,642                                WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||643            !WinHttpReceiveResponse(hRequest, nullptr)) {644            WinHttpCloseHandle(hRequest);645            WinHttpCloseHandle(hConnect);646            WinHttpCloseHandle(hSession);647            if (progress) progress->status = "error: request failed";648            return false;649        }650 651        // 获取文件大小652        wchar_t content_len[32] = {0};653        DWORD cl_len = sizeof(content_len);654        uint64_t total_size = 0;655        if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH,656                                WINHTTP_HEADER_NAME_BY_INDEX, content_len, &cl_len, WINHTTP_NO_HEADER_INDEX)) {657            total_size = _wcstoui64(content_len, nullptr, 10);658        }659        if (progress) {660            progress->total = total_size + opts.resume_from;661            progress->downloaded = opts.resume_from;662            progress->status = "downloading";663        }664 665        // 打开输出文件666        std::ofstream outfile;667        if (!opts.output_path.empty()) {668            auto mode = (opts.resume_from > 0) ? std::ios::binary | std::ios::app669                                                : std::ios::binary;670            outfile.open(opts.output_path, mode);671            if (!outfile) {672                WinHttpCloseHandle(hRequest);673                WinHttpCloseHandle(hConnect);674                WinHttpCloseHandle(hSession);675                if (progress) progress->status = "error: cannot open output";676                return false;677            }678        }679 680        // 下载循环681        std::vector<uint8_t> buf(65536);682        uint64_t total_dl = opts.resume_from;683        auto last_update = std::chrono::steady_clock::now();684 685        while (true) {686            if (progress && progress->cancelled) break;687 688            // 暂停处理689            while (progress && progress->paused) {690                progress->status = "paused";691                std::this_thread::sleep_for(std::chrono::milliseconds(200));692            }693            if (progress && !progress->paused) progress->status = "downloading";694 695            DWORD available = 0;696            if (!WinHttpQueryDataAvailable(hRequest, &available)) break;697            if (available == 0) break;698 699            DWORD to_read = std::min(available, (DWORD)buf.size());700            DWORD read = 0;701            if (!WinHttpReadData(hRequest, buf.data(), to_read, &read)) break;702 703            if (outfile.is_open()) outfile.write((char*)buf.data(), read);704            total_dl += read;705 706            auto now = std::chrono::steady_clock::now();707            if (progress && now - last_update > std::chrono::milliseconds(100)) {708                progress->downloaded = total_dl;709                last_update = now;710            }711        }712 713        if (outfile.is_open()) outfile.close();714        WinHttpCloseHandle(hRequest);715        WinHttpCloseHandle(hConnect);716        WinHttpCloseHandle(hSession);717 718        if (progress) {719            progress->finished = true;720            progress->status = progress->cancelled ? "cancelled" : "done";721            progress->downloaded = total_dl;722        }723        return true;724    }725};726 727// ============================================================================728// 多线程分片下载器729// ============================================================================730 731class ChunkedDownloader {732public:733    static bool download(const HttpDownloader::Options& base_opts,734                         uint64_t total_size,735                         int num_chunks,736                         DownloadProgress* progress = nullptr) {737        if (num_chunks <= 1 || total_size < 4 * 1024 * 1024) {738            // 单线程下载739            return HttpDownloader::download(base_opts, progress);740        }741 742        struct ChunkState {743            std::string tmp_path;744            std::atomic<uint64_t> downloaded{0};745            bool ok = false;746        };747 748        uint64_t chunk_size = total_size / num_chunks;749        std::vector<ChunkState> chunks(num_chunks);750        std::vector<std::thread> workers;751 752        fs::path output_path(base_opts.output_path);753        fs::path tmp_dir = output_path.parent_path() / (output_path.stem().string() + ".tmp");754        fs::create_directories(tmp_dir);755 756        std::mutex progress_mtx;757 758        for (int i = 0; i < num_chunks; i++) {759            workers.emplace_back([&, i]() {760                auto opts = base_opts;761                uint64_t start = i * chunk_size;762                uint64_t end = (i == num_chunks - 1) ? total_size - 1 : (i + 1) * chunk_size - 1;763                opts.resume_from = start;764                chunks[i].tmp_path = (tmp_dir / ("chunk_" + std::to_string(i))).string();765                opts.output_path = chunks[i].tmp_path;766 767                // 添加 Range 头768                opts.extra_headers["Range"] = "bytes=" + std::to_string(start) + "-" + std::to_string(end);769 770                chunks[i].ok = HttpDownloader::download(opts, nullptr);771 772                // 部分更新进度 (简化版 — 用文件大小估计)773                if (chunks[i].ok) {774                    std::error_code ec;775                    chunks[i].downloaded = fs::file_size(chunks[i].tmp_path, ec);776                }777            });778        }779 780        // 等待所有线程781        for (auto& t : workers) t.join();782 783        // 检查是否全部成功784        bool all_ok = true;785        for (auto& c : chunks) if (!c.ok) { all_ok = false; break; }786 787        if (!all_ok) {788            // 回退到单线程789            return HttpDownloader::download(base_opts, progress);790        }791 792        // 合并分片793        if (progress) progress->status = "merging";794        std::ofstream out(base_opts.output_path, std::ios::binary);795        if (!out) return false;796 797        std::vector<uint8_t> merge_buf(1 * 1024 * 1024);  // 1MB buffer798        for (auto& c : chunks) {799            std::ifstream in(c.tmp_path, std::ios::binary);800            while (in) {801                in.read((char*)merge_buf.data(), merge_buf.size());802                out.write((char*)merge_buf.data(), in.gcount());803            }804            in.close();805            fs::remove(c.tmp_path);806        }807        out.close();808        fs::remove_all(tmp_dir);809 810        if (progress) {811            progress->finished = true;812            progress->status = "done";813            progress->downloaded = total_size;814        }815        return true;816    }817};818 819// ============================================================================820// 视频下载管理器821// ============================================================================822 823class VideoDownloadManager {824public:825    struct DownloadTask {826        std::string url;827        std::string output_dir = ".";828        std::string output_template = "%(title)s.%(ext)s";829        std::string selected_format = "best";  // "best", "bestvideo+bestaudio", format_id830        int max_height = 1080;831        int num_threads = 4;832        std::string proxy;833        std::string cookies;834        bool audio_only = false;835        bool extract_audio = false;836        bool embed_thumbnail = false;837 838        std::unique_ptr<DownloadProgress> progress;839        std::unique_ptr<VideoInfo> info;840    };841 842    static bool download(DownloadTask& task) {843        // 1. 提取视频信息844        std::cout << "[1/3] 正在获取视频信息..." << std::endl;845        auto vi = YtDlpExtractor::extract(task.url, task.proxy, task.cookies);846        if (!vi) {847            std::cerr << "错误: 无法提取视频信息 (请确认 yt-dlp.exe 在 PATH 中)" << std::endl;848            return false;849        }850        task.info = std::make_unique<VideoInfo>(*vi);851        std::cout << "  标题: " << vi->title << std::endl;852        std::cout << "  上传: " << vi->uploader << std::endl;853        std::cout << "  时长: " << format_duration(vi->duration) << std::endl;854        std::cout << "  格式数: " << vi->formats.size() << std::endl;855 856        // 2. 选择格式857        const VideoFormat* selected_fmt = nullptr;858        if (task.audio_only || task.extract_audio) {859            selected_fmt = vi->best_audio();860        } else {861            selected_fmt = vi->best_video(task.max_height);862        }863        if (!selected_fmt) {864            std::cerr << "错误: 没有找到合适的格式" << std::endl;865            return false;866        }867 868        std::cout << "\n[2/3] 选择格式: " << selected_fmt->format_id869                  << " (" << selected_fmt->ext << ", "870                  << selected_fmt->resolution871                  << ", " << format_bytes(selected_fmt->filesize) << ")" << std::endl;872 873        // 3. 使用 yt-dlp 下载 (一站式: 提取 + 下载 + 合并)874        std::cout << "\n[3/3] 开始下载..." << std::endl;875 876        std::string output_path = task.output_dir;877        if (!output_path.empty() && output_path.back() != '\\' && output_path.back() != '/')878            output_path += "\\";879        output_path += task.output_template;880 881        std::string cmd = "yt-dlp.exe";882        cmd += " -f " + selected_fmt->format_id;883        cmd += " -o \"" + output_path + "\"";884        cmd += " --no-playlist";885        if (!task.proxy.empty()) cmd += " --proxy " + task.proxy;886        if (!task.cookies.empty()) cmd += " --cookies " + task.cookies;887        if (task.num_threads > 1) cmd += " --concurrent-fragments " + std::to_string(task.num_threads);888        cmd += " --newline --progress";889        cmd += " \"" + task.url + "\"";890 891        std::cout << "  " << cmd << std::endl << std::endl;892 893        // 直接运行 (实时输出到控制台)894        return run_command_realtime(cmd);895    }896 897    static void show_formats(const VideoInfo& vi) {898        std::cout << "\n可用格式:\n";899        std::cout << std::left900                  << std::setw(12) << "ID"901                  << std::setw(6) << "EXT"902                  << std::setw(14) << "分辨率"903                  << std::setw(10) << "大小"904                  << std::setw(8) << "FPS"905                  << std::setw(12) << "编码"906                  << "备注\n";907        std::cout << std::string(80, '-') << "\n";908 909        for (auto& f : vi.formats) {910            std::string size_str = f.filesize > 0 ? format_bytes(f.filesize) :911                                    f.filesize_approx > 0 ? "~" + format_bytes(f.filesize_approx) : "?";912            std::string codec = f.vcodec;913            if (codec.size() > 10) codec = codec.substr(0, 10);914            if (f.has_video && f.has_audio) codec += "+audio";915            else if (!f.has_video) codec = "audio only";916            else if (!f.has_audio) codec = "video only";917 918            std::cout << std::left919                      << std::setw(12) << f.format_id920                      << std::setw(6) << f.ext921                      << std::setw(14) << (f.resolution.empty() ? "audio" : f.resolution)922                      << std::setw(10) << size_str923                      << std::setw(8) << (f.fps > 0 ? std::to_string((int)f.fps) : "-")924                      << std::setw(12) << codec925                      << f.note << "\n";926        }927    }928 929private:930    static bool run_command_realtime(const std::string& cmd) {931        SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};932        HANDLE hRead, hWrite;933        if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return false;934        SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);935 936        STARTUPINFOW si = {sizeof(STARTUPINFOW)};937        si.dwFlags = STARTF_USESTDHANDLES;938        si.hStdOutput = hWrite;939        si.hStdError = hWrite;940 941        PROCESS_INFORMATION pi = {};942        std::wstring wcmd = utf8_to_wchar(cmd);943        std::unique_ptr<wchar_t[]> cmd_buf(new wchar_t[wcmd.size() + 1]);944        wcscpy(cmd_buf.get(), wcmd.c_str());945 946        if (!CreateProcessW(nullptr, cmd_buf.get(), nullptr, nullptr, TRUE,947                            0, nullptr, nullptr, &si, &pi)) {948            CloseHandle(hWrite);949            CloseHandle(hRead);950            return false;951        }952 953        CloseHandle(hWrite);954 955        char buf[4096];956        DWORD read;957        while (ReadFile(hRead, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) {958            buf[read] = '\0';959            std::cout << buf;960        }961 962        CloseHandle(hRead);963        WaitForSingleObject(pi.hProcess, INFINITE);964 965        DWORD exit_code = 0;966        GetExitCodeProcess(pi.hProcess, &exit_code);967        CloseHandle(pi.hProcess);968        CloseHandle(pi.hThread);969 970        return exit_code == 0;971    }972};973 974// ============================================================================975// Win32 GUI (对标 tkinter 界面)976// ============================================================================977 978#define IDC_URL_INPUT      1001979#define IDC_BROWSE_BTN     1002980#define IDC_OUTPUT_INPUT   1003981#define IDC_QUALITY_COMBO  1004982#define IDC_FORMAT_LIST    1005983#define IDC_EXTRACT_BTN    1006984#define IDC_DOWNLOAD_BTN   1007985#define IDC_PROGRESS_BAR   1008986#define IDC_STATUS_TEXT    1009987#define IDC_AUDIO_ONLY     1010988 989struct GuiState {990    HWND hUrlInput = nullptr;991    HWND hOutputInput = nullptr;992    HWND hQualityCombo = nullptr;993    HWND hFormatList = nullptr;994    HWND hExtractBtn = nullptr;995    HWND hDownloadBtn = nullptr;996    HWND hProgressBar = nullptr;997    HWND hStatusText = nullptr;998    HWND hAudioOnly = nullptr;999 1000    std::unique_ptr<VideoInfo> current_info;1001    std::unique_ptr<std::thread> download_thread;1002    std::unique_ptr<DownloadProgress> progress;1003    std::wstring last_output_dir = L".";1004};1005 1006static GuiState g_gui;1007 1008static void gui_set_status(const wchar_t* text) {1009    SetWindowTextW(g_gui.hStatusText, text);1010}1011 1012static void gui_update_progress() {1013    if (!g_gui.progress) return;1014    uint64_t dl = g_gui.progress->downloaded;1015    uint64_t total = g_gui.progress->total;1016    if (total > 0) {1017        int pct = (int)(dl * 100 / total);1018        SendMessageW(g_gui.hProgressBar, PBM_SETPOS, (WPARAM)pct, 0);1019        wchar_t txt[128];1020        swprintf(txt, 128, L"下载中... %d%% (%s / %s)",1021                 pct,1022                 utf8_to_wchar(format_bytes(dl)).c_str(),1023                 utf8_to_wchar(format_bytes(total)).c_str());1024        gui_set_status(txt);1025    }1026}1027 1028static void gui_on_extract() {1029    wchar_t url[2048] = {0};1030    GetWindowTextW(g_gui.hUrlInput, url, 2047);1031    if (wcslen(url) == 0) {1032        MessageBoxW(nullptr, L"请输入视频 URL", L"提示", MB_OK | MB_ICONINFORMATION);1033        return;1034    }1035 1036    gui_set_status(L"正在获取视频信息...");1037    EnableWindow(g_gui.hExtractBtn, FALSE);1038    EnableWindow(g_gui.hDownloadBtn, FALSE);1039 1040    auto vi = YtDlpExtractor::extract(wchar_to_utf8(url));1041    if (!vi) {1042        gui_set_status(L"获取失败: 请确认 URL 正确且 yt-dlp.exe 可用");1043        EnableWindow(g_gui.hExtractBtn, TRUE);1044        return;1045    }1046 1047    g_gui.current_info = std::make_unique<VideoInfo>(*vi);1048 1049    // 更新格式列表1050    ListView_DeleteAllItems(g_gui.hFormatList);1051    for (size_t i = 0; i < vi->formats.size(); i++) {1052        auto& f = vi->formats[i];1053        wchar_t id[32]; swprintf(id, 32, L"%S", f.format_id.c_str());1054        wchar_t ext[16]; swprintf(ext, 16, L"%S", f.ext.c_str());1055        wchar_t res[32]; swprintf(res, 32, L"%S", f.resolution.c_str());1056        wchar_t size_str[32]; swprintf(size_str, 32, L"%S", format_bytes(f.filesize).c_str());1057        wchar_t note[128]; swprintf(note, 128, L"%S", f.note.c_str());1058 1059        LVITEMW item = {};1060        item.mask = LVIF_TEXT;1061        item.iItem = (int)i;1062        item.pszText = id;1063        ListView_InsertItem(g_gui.hFormatList, &item);1064 1065        ListView_SetItemText(g_gui.hFormatList, (int)i, 1, ext);1066        ListView_SetItemText(g_gui.hFormatList, (int)i, 2, res);1067        ListView_SetItemText(g_gui.hFormatList, (int)i, 3, size_str);1068        ListView_SetItemText(g_gui.hFormatList, (int)i, 4, note);1069    }1070 1071    wchar_t status[256];1072    swprintf(status, 256, L"已获取: %S (%S) - %zu 个格式",1073             vi->title.c_str(), format_duration(vi->duration).c_str(), vi->formats.size());1074    gui_set_status(status);1075 1076    EnableWindow(g_gui.hExtractBtn, TRUE);1077    EnableWindow(g_gui.hDownloadBtn, TRUE);1078}1079 1080static void gui_on_download() {1081    if (!g_gui.current_info) {1082        gui_on_extract();1083        if (!g_gui.current_info) return;1084    }1085 1086    wchar_t url[2048] = {0};1087    wchar_t out_dir[1024] = {0};1088    GetWindowTextW(g_gui.hUrlInput, url, 2047);1089    GetWindowTextW(g_gui.hOutputInput, out_dir, 1023);1090 1091    bool audio_only = (SendMessageW(g_gui.hAudioOnly, BM_GETCHECK, 0, 0) == BST_CHECKED);1092 1093    // 获取选中的格式1094    int sel = ListView_GetNextItem(g_gui.hFormatList, -1, LVNI_SELECTED);1095    std::string format_id = "best";1096    if (sel >= 0 && sel < (int)g_gui.current_info->formats.size()) {1097        format_id = g_gui.current_info->formats[sel].format_id;1098    }1099    if (audio_only) format_id = "bestaudio";1100 1101    VideoDownloadManager::DownloadTask task;1102    task.url = wchar_to_utf8(url);1103    task.output_dir = wchar_to_utf8(out_dir);1104    if (task.output_dir.empty()) task.output_dir = ".";1105    task.selected_format = format_id;1106    task.audio_only = audio_only;1107 1108    g_gui.progress = std::make_unique<DownloadProgress>();1109    gui_set_status(L"开始下载...");1110    SendMessageW(g_gui.hProgressBar, PBM_SETPOS, 0, 0);1111    EnableWindow(g_gui.hDownloadBtn, FALSE);1112 1113    // 在后台线程下载1114    g_gui.download_thread = std::make_unique<std::thread>([task = std::move(task)]() mutable {1115        bool ok = VideoDownloadManager::download(task);1116        std::wstring msg = ok ? L"下载完成!" : L"下载失败";1117        gui_set_status(msg.c_str());1118        EnableWindow(g_gui.hDownloadBtn, TRUE);1119    });1120}1121 1122static LRESULT CALLBACK gui_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {1123    switch (msg) {1124        case WM_CREATE: {1125            // 创建控件 (对标 tkinter 布局)1126            int y = 10;1127 1128            // URL 输入1129            CreateWindowW(L"STATIC", L"视频 URL:", WS_CHILD | WS_VISIBLE,1130                          10, y, 60, 20, hwnd, nullptr, nullptr, nullptr);1131            g_gui.hUrlInput = CreateWindowW(L"EDIT", L"",1132                WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,1133                80, y - 2, 500, 22, hwnd, (HMENU)IDC_URL_INPUT, nullptr, nullptr);1134 1135            // 提取按钮1136            g_gui.hExtractBtn = CreateWindowW(L"BUTTON", L"获取信息",1137                WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,1138                590, y - 2, 80, 24, hwnd, (HMENU)IDC_EXTRACT_BTN, nullptr, nullptr);1139            y += 28;1140 1141            // 输出路径1142            CreateWindowW(L"STATIC", L"保存到:", WS_CHILD | WS_VISIBLE,1143                          10, y, 60, 20, hwnd, nullptr, nullptr, nullptr);1144            g_gui.hOutputInput = CreateWindowW(L"EDIT", L".",1145                WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,1146                80, y - 2, 500, 22, hwnd, (HMENU)IDC_OUTPUT_INPUT, nullptr, nullptr);1147            g_gui.hDownloadBtn = CreateWindowW(L"BUTTON", L"下载",1148                WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,1149                590, y - 2, 80, 24, hwnd, (HMENU)IDC_DOWNLOAD_BTN, nullptr, nullptr);1150            y += 28;1151 1152            // 仅音频1153            g_gui.hAudioOnly = CreateWindowW(L"BUTTON", L"仅下载音频",1154                WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,1155                80, y, 100, 20, hwnd, (HMENU)IDC_AUDIO_ONLY, nullptr, nullptr);1156            y += 24;1157 1158            // 格式列表1159            g_gui.hFormatList = CreateWindowW(WC_LISTVIEWW, L"",1160                WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT | LVS_SINGLESEL,1161                10, y, 660, 180, hwnd, (HMENU)IDC_FORMAT_LIST, nullptr, nullptr);1162 1163            // 添加列1164            LVCOLUMNW col = {};1165            col.mask = LVCF_TEXT | LVCF_WIDTH;1166            col.cx = 80; col.pszText = (LPWSTR)L"格式ID"; ListView_InsertColumn(g_gui.hFormatList, 0, &col);1167            col.cx = 50; col.pszText = (LPWSTR)L"扩展"; ListView_InsertColumn(g_gui.hFormatList, 1, &col);1168            col.cx = 100; col.pszText = (LPWSTR)L"分辨率"; ListView_InsertColumn(g_gui.hFormatList, 2, &col);1169            col.cx = 80; col.pszText = (LPWSTR)L"大小"; ListView_InsertColumn(g_gui.hFormatList, 3, &col);1170            col.cx = 330; col.pszText = (LPWSTR)L"备注"; ListView_InsertColumn(g_gui.hFormatList, 4, &col);1171            y += 185;1172 1173            // 进度条1174            g_gui.hProgressBar = CreateWindowW(PROGRESS_CLASSW, L"",1175                WS_CHILD | WS_VISIBLE | PBS_SMOOTH,1176                10, y, 550, 22, hwnd, (HMENU)IDC_PROGRESS_BAR, nullptr, nullptr);1177            SendMessageW(g_gui.hProgressBar, PBM_SETRANGE, 0, MAKELPARAM(0, 100));1178            y += 28;1179 1180            // 状态栏1181            g_gui.hStatusText = CreateWindowW(L"STATIC", L"就绪 - 粘贴 URL 并点击 [获取信息]",1182                WS_CHILD | WS_VISIBLE | SS_LEFT,1183                10, y, 660, 20, hwnd, (HMENU)IDC_STATUS_TEXT, nullptr, nullptr);1184 1185            // 设置字体1186            HFONT hFont = CreateFontW(16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,1187                DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,1188                CLEARTYPE_QUALITY, FF_DONTCARE, L"Microsoft YaHei UI");1189            EnumChildWindows(hwnd, [](HWND child, LPARAM lf) -> BOOL {1190                SendMessageW(child, WM_SETFONT, (WPARAM)(HFONT)lf, TRUE);1191                return TRUE;1192            }, (LPARAM)hFont);1193            break;1194        }1195 1196        case WM_COMMAND: {1197            WORD id = LOWORD(wp);1198            if (id == IDC_EXTRACT_BTN) gui_on_extract();1199            else if (id == IDC_DOWNLOAD_BTN) gui_on_download();1200            break;

Showing the first 1,200 of 1397 lines. Download the file for the rest.