Buckets:
| /** | |
| * 万能视频下载器 — C++ 版本 | |
| * | |
| * 对标 D:\电影\万能视频下载器.exe (PyInstaller + yt-dlp + tkinter) | |
| * | |
| * 功能: | |
| * - 多站点视频提取 (YouTube, Bilibili, 抖音, etc.) 通过 yt-dlp 子进程 | |
| * - 格式/质量选择 | |
| * - 多线程下载 + 断点续传 | |
| * - 实时进度显示 | |
| * - HLS/DASH 流支持 | |
| * - 代理支持 | |
| * | |
| * 编译 (MSVC): | |
| * cl /EHsc /O2 /std:c++17 video_downloader.cpp /Fe:video_downloader.exe | |
| * 需要: yt-dlp.exe 在 PATH 中 (或同目录) | |
| * | |
| * 编译 (MinGW): | |
| * g++ -O2 -std=c++17 video_downloader.cpp -lwinhttp -lshlwapi -o video_downloader.exe | |
| */ | |
| namespace fs = std::filesystem; | |
| // ============================================================================ | |
| // 工具函数 | |
| // ============================================================================ | |
| static std::string wchar_to_utf8(const wchar_t* wstr, int len = -1) { | |
| if (!wstr) return {}; | |
| int n = WideCharToMultiByte(CP_UTF8, 0, wstr, len, nullptr, 0, nullptr, nullptr); | |
| std::string result(n, '\0'); | |
| WideCharToMultiByte(CP_UTF8, 0, wstr, len, &result[0], n, nullptr, nullptr); | |
| return result; | |
| } | |
| static std::wstring utf8_to_wchar(std::string_view str) { | |
| if (str.empty()) return {}; | |
| int n = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); | |
| std::wstring result(n, L'\0'); | |
| MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], n); | |
| return result; | |
| } | |
| static std::string format_bytes(uint64_t bytes) { | |
| const char* units[] = {"B", "KB", "MB", "GB", "TB"}; | |
| int idx = 0; | |
| double size = (double)bytes; | |
| while (size >= 1024.0 && idx < 4) { | |
| size /= 1024.0; | |
| idx++; | |
| } | |
| char buf[64]; | |
| snprintf(buf, sizeof(buf), "%.*f %s", (idx == 0 ? 0 : 1), size, units[idx]); | |
| return buf; | |
| } | |
| static std::string format_speed(double bytes_per_sec) { | |
| return format_bytes((uint64_t)bytes_per_sec) + "/s"; | |
| } | |
| static std::string format_duration(int64_t seconds) { | |
| int64_t h = seconds / 3600; | |
| int64_t m = (seconds % 3600) / 60; | |
| int64_t s = seconds % 60; | |
| char buf[32]; | |
| if (h > 0) | |
| snprintf(buf, sizeof(buf), "%lld:%02lld:%02lld", h, m, s); | |
| else | |
| snprintf(buf, sizeof(buf), "%lld:%02lld", m, s); | |
| return buf; | |
| } | |
| static std::vector<std::string> split(std::string_view str, char delim) { | |
| std::vector<std::string> parts; | |
| size_t start = 0, end; | |
| while ((end = str.find(delim, start)) != std::string_view::npos) { | |
| parts.emplace_back(str.substr(start, end - start)); | |
| start = end + 1; | |
| } | |
| if (start < str.size()) parts.emplace_back(str.substr(start)); | |
| return parts; | |
| } | |
| static std::string strip(std::string_view s) { | |
| while (!s.empty() && (s.front() == ' ' || s.front() == '\t' || s.front() == '\n' || s.front() == '\r')) | |
| s.remove_prefix(1); | |
| while (!s.empty() && (s.back() == ' ' || s.back() == '\t' || s.back() == '\n' || s.back() == '\r')) | |
| s.remove_suffix(1); | |
| return std::string(s); | |
| } | |
| // ============================================================================ | |
| // 简易 JSON 解析器 (无需外部依赖) | |
| // ============================================================================ | |
| struct JsonValue { | |
| enum Type { NUL, BOOL, INT, FLOAT, STRING, ARRAY, OBJECT }; | |
| Type type = NUL; | |
| bool bval = false; | |
| int64_t ival = 0; | |
| double fval = 0.0; | |
| std::string sval; | |
| std::vector<JsonValue> arr; | |
| std::unordered_map<std::string, JsonValue> obj; | |
| std::vector<std::string> obj_keys; // 保持插入顺序 | |
| static JsonValue parse(std::string_view json); | |
| std::string dump(int indent = 0) const; | |
| const JsonValue& operator[](const char* key) const { | |
| static JsonValue null_val; | |
| auto it = obj.find(key); | |
| return it != obj.end() ? it->second : null_val; | |
| } | |
| const JsonValue& operator[](size_t idx) const { | |
| static JsonValue null_val; | |
| return idx < arr.size() ? arr[idx] : null_val; | |
| } | |
| bool has(const char* key) const { return obj.find(key) != obj.end(); } | |
| size_t size() const { return type == ARRAY ? arr.size() : type == OBJECT ? obj.size() : 0; } | |
| std::string str_or(const char* def = "") const { | |
| return type == STRING ? sval : def; | |
| } | |
| int64_t int_or(int64_t def = 0) const { | |
| if (type == INT) return ival; | |
| if (type == FLOAT) return (int64_t)fval; | |
| return def; | |
| } | |
| double float_or(double def = 0.0) const { | |
| if (type == FLOAT) return fval; | |
| if (type == INT) return (double)ival; | |
| return def; | |
| } | |
| bool bool_or(bool def = false) const { | |
| return type == BOOL ? bval : def; | |
| } | |
| }; | |
| // 简易递归下降 JSON 解析器 | |
| struct JsonParser { | |
| std::string_view json; | |
| size_t pos = 0; | |
| JsonParser(std::string_view j) : json(j) {} | |
| void skip_ws() { | |
| while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || | |
| json[pos] == '\n' || json[pos] == '\r')) | |
| pos++; | |
| } | |
| char peek() { skip_ws(); return pos < json.size() ? json[pos] : '\0'; } | |
| char next() { skip_ws(); return pos < json.size() ? json[pos++] : '\0'; } | |
| JsonValue parse_value() { | |
| skip_ws(); | |
| if (pos >= json.size()) return JsonValue{}; | |
| char c = json[pos]; | |
| if (c == '"') return parse_string(); | |
| if (c == '{') return parse_object(); | |
| if (c == '[') return parse_array(); | |
| if (c == 't' || c == 'f') return parse_bool(); | |
| if (c == 'n') return parse_null(); | |
| return parse_number(); | |
| } | |
| JsonValue parse_string() { | |
| next(); // skip '"' | |
| std::string s; | |
| while (pos < json.size() && json[pos] != '"') { | |
| if (json[pos] == '\\' && pos + 1 < json.size()) { | |
| pos++; | |
| switch (json[pos]) { | |
| case '"': s += '"'; break; | |
| case '\\': s += '\\'; break; | |
| case '/': s += '/'; break; | |
| case 'b': s += '\b'; break; | |
| case 'f': s += '\f'; break; | |
| case 'n': s += '\n'; break; | |
| case 'r': s += '\r'; break; | |
| case 't': s += '\t'; break; | |
| case 'u': { | |
| if (pos + 4 < json.size()) { | |
| unsigned cp = 0; | |
| for (int i = 1; i <= 4; i++) | |
| cp = (cp << 4) | hex_val(json[pos + i]); | |
| pos += 4; | |
| if (cp < 0x80) s += (char)cp; | |
| else if (cp < 0x800) { s += (char)(0xC0 | (cp >> 6)); s += (char)(0x80 | (cp & 0x3F)); } | |
| else { s += (char)(0xE0 | (cp >> 12)); s += (char)(0x80 | ((cp>>6) & 0x3F)); s += (char)(0x80 | (cp & 0x3F)); } | |
| } | |
| break; | |
| } | |
| } | |
| pos++; | |
| } else { | |
| s += json[pos++]; | |
| } | |
| } | |
| if (pos < json.size()) pos++; // skip closing '"' | |
| JsonValue v; v.type = JsonValue::STRING; v.sval = s; | |
| return v; | |
| } | |
| JsonValue parse_object() { | |
| next(); // skip '{' | |
| JsonValue v; v.type = JsonValue::OBJECT; | |
| while (peek() != '}' && pos < json.size()) { | |
| JsonValue key = parse_string(); | |
| if (peek() == ':') next(); | |
| v.obj_keys.push_back(key.sval); | |
| v.obj[key.sval] = parse_value(); | |
| if (peek() == ',') next(); | |
| } | |
| if (pos < json.size()) pos++; // skip '}' | |
| return v; | |
| } | |
| JsonValue parse_array() { | |
| next(); // skip '[' | |
| JsonValue v; v.type = JsonValue::ARRAY; | |
| while (peek() != ']' && pos < json.size()) { | |
| v.arr.push_back(parse_value()); | |
| if (peek() == ',') next(); | |
| } | |
| if (pos < json.size()) pos++; // skip ']' | |
| return v; | |
| } | |
| JsonValue parse_bool() { | |
| JsonValue v; v.type = JsonValue::BOOL; | |
| if (json.substr(pos, 4) == "true") { v.bval = true; pos += 4; } | |
| else { v.bval = false; pos += 5; } | |
| return v; | |
| } | |
| JsonValue parse_null() { | |
| pos += 4; // skip "null" | |
| return JsonValue{}; | |
| } | |
| JsonValue parse_number() { | |
| JsonValue v; | |
| size_t start = pos; | |
| bool is_float = false; | |
| if (pos < json.size() && json[pos] == '-') pos++; | |
| while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++; | |
| if (pos < json.size() && json[pos] == '.') { is_float = true; pos++; } | |
| while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++; | |
| if (pos < json.size() && (json[pos] == 'e' || json[pos] == 'E')) { | |
| is_float = true; pos++; | |
| if (pos < json.size() && (json[pos] == '+' || json[pos] == '-')) pos++; | |
| while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') pos++; | |
| } | |
| auto num_str = std::string(json.substr(start, pos - start)); | |
| if (is_float) { | |
| v.type = JsonValue::FLOAT; | |
| v.fval = std::stod(num_str); | |
| } else { | |
| v.type = JsonValue::INT; | |
| v.ival = std::stoll(num_str); | |
| } | |
| return v; | |
| } | |
| static int hex_val(char c) { | |
| if (c >= '0' && c <= '9') return c - '0'; | |
| if (c >= 'a' && c <= 'f') return c - 'a' + 10; | |
| if (c >= 'A' && c <= 'F') return c - 'A' + 10; | |
| return 0; | |
| } | |
| }; | |
| JsonValue JsonValue::parse(std::string_view json) { | |
| return JsonParser(json).parse_value(); | |
| } | |
| // ============================================================================ | |
| // 视频格式信息 | |
| // ============================================================================ | |
| struct VideoFormat { | |
| std::string format_id; | |
| std::string ext; // mp4, webm, mkv, etc. | |
| std::string resolution; // 1920x1080 | |
| std::string vcodec; // h264, vp9, av1 | |
| std::string acodec; // aac, opus, mp4a | |
| std::string note; // 备注 (e.g. "1080p", "best") | |
| int64_t filesize = 0; // 字节 (0 = unknown) | |
| int64_t filesize_approx = 0; | |
| int width = 0; | |
| int height = 0; | |
| double fps = 0.0; | |
| double tbr = 0.0; // 平均码率 | |
| double abr = 0.0; // 音频码率 | |
| double vbr = 0.0; // 视频码率 | |
| bool has_video = true; | |
| bool has_audio = true; | |
| std::string protocol; // https, m3u8, dash | |
| }; | |
| struct VideoInfo { | |
| std::string title; | |
| std::string description; | |
| std::string uploader; | |
| std::string upload_date; | |
| std::string webpage_url; | |
| std::string thumbnail_url; | |
| int64_t duration = 0; // 秒 | |
| int64_t view_count = 0; | |
| int64_t like_count = 0; | |
| std::vector<VideoFormat> formats; | |
| std::vector<std::string> categories; | |
| std::vector<std::string> tags; | |
| // 选择最佳格式 | |
| const VideoFormat* best_video(int max_height = 99999) const { | |
| const VideoFormat* best = nullptr; | |
| for (auto& f : formats) { | |
| if (!f.has_video) continue; | |
| if (f.height > max_height) continue; | |
| if (!best || f.height > best->height || | |
| (f.height == best->height && f.tbr > best->tbr)) | |
| best = &f; | |
| } | |
| return best; | |
| } | |
| const VideoFormat* best_audio() const { | |
| const VideoFormat* best = nullptr; | |
| for (auto& f : formats) { | |
| if (!f.has_audio || f.has_video) continue; | |
| if (!best || f.abr > best->abr) best = &f; | |
| } | |
| if (!best) best = best_video(); | |
| return best; | |
| } | |
| }; | |
| // ============================================================================ | |
| // yt-dlp 子进程调用 (提取视频信息) | |
| // ============================================================================ | |
| class YtDlpExtractor { | |
| public: | |
| static std::optional<VideoInfo> extract(const std::string& url, | |
| const std::string& proxy = "", | |
| const std::string& cookies = "") { | |
| std::string cmd = "yt-dlp.exe"; | |
| cmd += " --dump-json --no-playlist --ignore-errors --no-warnings"; | |
| if (!proxy.empty()) cmd += " --proxy " + proxy; | |
| if (!cookies.empty()) cmd += " --cookies " + cookies; | |
| cmd += " \"" + url + "\""; | |
| cmd += " 2>nul"; | |
| std::string output = exec_command(cmd); | |
| if (output.empty()) { | |
| // 尝试 python -m yt_dlp | |
| cmd = "python -m yt_dlp --dump-json --no-playlist --ignore-errors --no-warnings"; | |
| if (!proxy.empty()) cmd += " --proxy " + proxy; | |
| cmd += " \"" + url + "\""; | |
| cmd += " 2>nul"; | |
| output = exec_command(cmd); | |
| } | |
| if (output.empty()) return std::nullopt; | |
| return parse_info(output); | |
| } | |
| static std::optional<std::vector<VideoInfo>> extract_playlist( | |
| const std::string& url, const std::string& proxy = "") { | |
| std::string cmd = "yt-dlp.exe"; | |
| cmd += " --dump-json --ignore-errors --no-warnings"; | |
| if (!proxy.empty()) cmd += " --proxy " + proxy; | |
| cmd += " \"" + url + "\""; | |
| cmd += " 2>nul"; | |
| std::string output = exec_command(cmd); | |
| if (output.empty()) return std::nullopt; | |
| std::vector<VideoInfo> results; | |
| size_t pos = 0; | |
| while (pos < output.size()) { | |
| // 找到每个 JSON 对象边界 | |
| size_t start = output.find('{', pos); | |
| if (start == std::string::npos) break; | |
| int depth = 0; | |
| size_t end = start; | |
| bool in_str = false; | |
| while (end < output.size()) { | |
| char c = output[end]; | |
| if (c == '"' && (end == 0 || output[end-1] != '\\')) in_str = !in_str; | |
| if (!in_str) { | |
| if (c == '{') depth++; | |
| else if (c == '}') { depth--; if (depth == 0) { end++; break; } } | |
| } | |
| end++; | |
| } | |
| if (depth == 0) { | |
| auto vi = parse_info(output.substr(start, end - start)); | |
| if (vi) results.push_back(*vi); | |
| } | |
| pos = end; | |
| } | |
| return results.empty() ? std::nullopt : std::make_optional(results); | |
| } | |
| private: | |
| static std::string exec_command(const std::string& cmd) { | |
| SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; | |
| HANDLE hRead, hWrite; | |
| if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return ""; | |
| SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); | |
| STARTUPINFOW si = {sizeof(STARTUPINFOW)}; | |
| si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; | |
| si.wShowWindow = SW_HIDE; | |
| si.hStdOutput = hWrite; | |
| si.hStdError = hWrite; | |
| PROCESS_INFORMATION pi = {}; | |
| std::wstring wcmd = utf8_to_wchar(cmd); | |
| std::unique_ptr<wchar_t[]> cmd_buf(new wchar_t[wcmd.size() + 1]); | |
| wcscpy(cmd_buf.get(), wcmd.c_str()); | |
| std::string result; | |
| if (CreateProcessW(nullptr, cmd_buf.get(), nullptr, nullptr, TRUE, | |
| CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { | |
| CloseHandle(hWrite); | |
| WaitForSingleObject(pi.hProcess, 30000); // 30s timeout for extract | |
| char buf[4096]; | |
| DWORD read; | |
| while (ReadFile(hRead, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) { | |
| buf[read] = '\0'; | |
| result += buf; | |
| } | |
| CloseHandle(hRead); | |
| CloseHandle(pi.hProcess); | |
| CloseHandle(pi.hThread); | |
| } else { | |
| CloseHandle(hWrite); | |
| CloseHandle(hRead); | |
| } | |
| return result; | |
| } | |
| static std::optional<VideoInfo> parse_info(const std::string& json_str) { | |
| try { | |
| auto j = JsonValue::parse(json_str); | |
| if (j.type != JsonValue::OBJECT) return std::nullopt; | |
| VideoInfo vi; | |
| vi.title = j["title"].str_or("Unknown"); | |
| vi.description = j["description"].str_or(); | |
| vi.uploader = j["uploader"].str_or(j["channel"].str_or("Unknown")); | |
| vi.upload_date = j["upload_date"].str_or(); | |
| vi.webpage_url = j["webpage_url"].str_or(); | |
| vi.thumbnail_url = j["thumbnail"].str_or(); | |
| vi.duration = j["duration"].int_or(0); | |
| vi.view_count = j["view_count"].int_or(0); | |
| vi.like_count = j["like_count"].int_or(0); | |
| // categories | |
| if (j.has("categories") && j["categories"].type == JsonValue::ARRAY) { | |
| for (auto& c : j["categories"].arr) | |
| if (c.type == JsonValue::STRING) vi.categories.push_back(c.sval); | |
| } | |
| // tags | |
| if (j.has("tags") && j["tags"].type == JsonValue::ARRAY) { | |
| for (auto& t : j["tags"].arr) | |
| if (t.type == JsonValue::STRING) vi.tags.push_back(t.sval); | |
| } | |
| // formats | |
| if (j.has("formats") && j["formats"].type == JsonValue::ARRAY) { | |
| for (auto& fj : j["formats"].arr) { | |
| if (fj.type != JsonValue::OBJECT) continue; | |
| VideoFormat f; | |
| f.format_id = fj["format_id"].str_or(); | |
| f.ext = fj["ext"].str_or("mp4"); | |
| f.resolution = fj["resolution"].str_or(); | |
| f.vcodec = fj["vcodec"].str_or("none"); | |
| f.acodec = fj["acodec"].str_or("none"); | |
| f.note = fj["format_note"].str_or(); | |
| f.filesize = fj["filesize"].int_or(0); | |
| f.filesize_approx = fj["filesize_approx"].int_or(0); | |
| f.width = (int)fj["width"].int_or(0); | |
| f.height = (int)fj["height"].int_or(0); | |
| f.fps = fj["fps"].float_or(0.0); | |
| f.tbr = fj["tbr"].float_or(0.0); | |
| f.abr = fj["abr"].float_or(0.0); | |
| f.vbr = fj["vbr"].float_or(0.0); | |
| f.protocol = fj["protocol"].str_or("https"); | |
| f.has_video = f.vcodec != "none"; | |
| f.has_audio = f.acodec != "none"; | |
| vi.formats.push_back(std::move(f)); | |
| } | |
| } | |
| return vi; | |
| } catch (...) { | |
| return std::nullopt; | |
| } | |
| } | |
| }; | |
| // ============================================================================ | |
| // HTTP 下载器 (WinHTTP, 支持断点续传 + 多线程) | |
| // ============================================================================ | |
| struct DownloadProgress { | |
| std::atomic<uint64_t> downloaded{0}; | |
| std::atomic<uint64_t> total{0}; | |
| std::atomic<bool> paused{false}; | |
| std::atomic<bool> cancelled{false}; | |
| std::atomic<bool> finished{false}; | |
| std::string status; // "downloading", "merging", "done", "error" | |
| std::string error_msg; | |
| std::chrono::steady_clock::time_point start_time; | |
| }; | |
| class HttpDownloader { | |
| public: | |
| struct Options { | |
| std::string url; | |
| std::string output_path; | |
| int64_t resume_from = 0; // 断点续传位置 | |
| int num_threads = 4; | |
| int max_retries = 3; | |
| int connect_timeout_ms = 15000; | |
| int read_timeout_ms = 30000; | |
| std::string proxy; // http://127.0.0.1:7890 | |
| std::string user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"; | |
| std::string cookies; | |
| std::map<std::string, std::string> extra_headers; | |
| }; | |
| static bool download(const Options& opts, DownloadProgress* progress = nullptr) { | |
| if (progress) { | |
| progress->downloaded = 0; | |
| progress->total = 0; | |
| progress->finished = false; | |
| progress->status = "connecting"; | |
| progress->start_time = std::chrono::steady_clock::now(); | |
| } | |
| // 解析 URL | |
| std::wstring url = utf8_to_wchar(opts.url); | |
| URL_COMPONENTS urlc = {sizeof(URL_COMPONENTS)}; | |
| wchar_t host[256] = {0}, path[2048] = {0}; | |
| urlc.lpszHostName = host; | |
| urlc.dwHostNameLength = 255; | |
| urlc.lpszUrlPath = path; | |
| urlc.dwUrlPathLength = 2047; | |
| if (!WinHttpCrackUrl(url.c_str(), (DWORD)url.size(), 0, &urlc)) { | |
| if (progress) progress->status = "error: bad url"; | |
| return false; | |
| } | |
| bool use_ssl = (urlc.nScheme == INTERNET_SCHEME_HTTPS); | |
| HINTERNET hSession = WinHttpOpen( | |
| utf8_to_wchar(opts.user_agent).c_str(), | |
| opts.proxy.empty() ? WINHTTP_ACCESS_TYPE_DEFAULT_PROXY : WINHTTP_ACCESS_TYPE_NAMED_PROXY, | |
| opts.proxy.empty() ? WINHTTP_NO_PROXY_NAME : utf8_to_wchar(opts.proxy).c_str(), | |
| WINHTTP_NO_PROXY_BYPASS, 0); | |
| if (!hSession) { if (progress) progress->status = "error: session"; return false; } | |
| WinHttpSetTimeouts(hSession, opts.connect_timeout_ms, opts.connect_timeout_ms, | |
| opts.read_timeout_ms, opts.read_timeout_ms); | |
| HINTERNET hConnect = WinHttpConnect(hSession, host, urlc.nPort, 0); | |
| if (!hConnect) { WinHttpCloseHandle(hSession); return false; } | |
| HINTERNET hRequest = WinHttpOpenRequest( | |
| hConnect, L"GET", path, nullptr, WINHTTP_NO_REFERER, | |
| WINHTTP_DEFAULT_ACCEPT_TYPES, | |
| use_ssl ? WINHTTP_FLAG_SECURE : 0); | |
| if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return false; } | |
| // 断点续传 | |
| if (opts.resume_from > 0) { | |
| wchar_t range[64]; | |
| swprintf(range, 64, L"bytes=%lld-", (long long)opts.resume_from); | |
| WinHttpAddRequestHeaders(hRequest, range, (DWORD)wcslen(range), WINHTTP_ADDREQ_FLAG_ADD); | |
| } | |
| // 自定义 headers | |
| for (auto& [k, v] : opts.extra_headers) { | |
| std::string hdr = k + ": " + v; | |
| std::wstring whdr = utf8_to_wchar(hdr); | |
| WinHttpAddRequestHeaders(hRequest, whdr.c_str(), (DWORD)whdr.size(), | |
| WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE); | |
| } | |
| if (!opts.cookies.empty()) { | |
| std::wstring cookie_hdr = L"Cookie: " + utf8_to_wchar(opts.cookies); | |
| WinHttpAddRequestHeaders(hRequest, cookie_hdr.c_str(), (DWORD)cookie_hdr.size(), | |
| WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE); | |
| } | |
| if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, | |
| WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || | |
| !WinHttpReceiveResponse(hRequest, nullptr)) { | |
| WinHttpCloseHandle(hRequest); | |
| WinHttpCloseHandle(hConnect); | |
| WinHttpCloseHandle(hSession); | |
| if (progress) progress->status = "error: request failed"; | |
| return false; | |
| } | |
| // 获取文件大小 | |
| wchar_t content_len[32] = {0}; | |
| DWORD cl_len = sizeof(content_len); | |
| uint64_t total_size = 0; | |
| if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH, | |
| WINHTTP_HEADER_NAME_BY_INDEX, content_len, &cl_len, WINHTTP_NO_HEADER_INDEX)) { | |
| total_size = _wcstoui64(content_len, nullptr, 10); | |
| } | |
| if (progress) { | |
| progress->total = total_size + opts.resume_from; | |
| progress->downloaded = opts.resume_from; | |
| progress->status = "downloading"; | |
| } | |
| // 打开输出文件 | |
| std::ofstream outfile; | |
| if (!opts.output_path.empty()) { | |
| auto mode = (opts.resume_from > 0) ? std::ios::binary | std::ios::app | |
| : std::ios::binary; | |
| outfile.open(opts.output_path, mode); | |
| if (!outfile) { | |
| WinHttpCloseHandle(hRequest); | |
| WinHttpCloseHandle(hConnect); | |
| WinHttpCloseHandle(hSession); | |
| if (progress) progress->status = "error: cannot open output"; | |
| return false; | |
| } | |
| } | |
| // 下载循环 | |
| std::vector<uint8_t> buf(65536); | |
| uint64_t total_dl = opts.resume_from; | |
| auto last_update = std::chrono::steady_clock::now(); | |
| while (true) { | |
| if (progress && progress->cancelled) break; | |
| // 暂停处理 | |
| while (progress && progress->paused) { | |
| progress->status = "paused"; | |
| std::this_thread::sleep_for(std::chrono::milliseconds(200)); | |
| } | |
| if (progress && !progress->paused) progress->status = "downloading"; | |
| DWORD available = 0; | |
| if (!WinHttpQueryDataAvailable(hRequest, &available)) break; | |
| if (available == 0) break; | |
| DWORD to_read = std::min(available, (DWORD)buf.size()); | |
| DWORD read = 0; | |
| if (!WinHttpReadData(hRequest, buf.data(), to_read, &read)) break; | |
| if (outfile.is_open()) outfile.write((char*)buf.data(), read); | |
| total_dl += read; | |
| auto now = std::chrono::steady_clock::now(); | |
| if (progress && now - last_update > std::chrono::milliseconds(100)) { | |
| progress->downloaded = total_dl; | |
| last_update = now; | |
| } | |
| } | |
| if (outfile.is_open()) outfile.close(); | |
| WinHttpCloseHandle(hRequest); | |
| WinHttpCloseHandle(hConnect); | |
| WinHttpCloseHandle(hSession); | |
| if (progress) { | |
| progress->finished = true; | |
| progress->status = progress->cancelled ? "cancelled" : "done"; | |
| progress->downloaded = total_dl; | |
| } | |
| return true; | |
| } | |
| }; | |
| // ============================================================================ | |
| // 多线程分片下载器 | |
| // ============================================================================ | |
| class ChunkedDownloader { | |
| public: | |
| static bool download(const HttpDownloader::Options& base_opts, | |
| uint64_t total_size, | |
| int num_chunks, | |
| DownloadProgress* progress = nullptr) { | |
| if (num_chunks <= 1 || total_size < 4 * 1024 * 1024) { | |
| // 单线程下载 | |
| return HttpDownloader::download(base_opts, progress); | |
| } | |
| struct ChunkState { | |
| std::string tmp_path; | |
| std::atomic<uint64_t> downloaded{0}; | |
| bool ok = false; | |
| }; | |
| uint64_t chunk_size = total_size / num_chunks; | |
| std::vector<ChunkState> chunks(num_chunks); | |
| std::vector<std::thread> workers; | |
| fs::path output_path(base_opts.output_path); | |
| fs::path tmp_dir = output_path.parent_path() / (output_path.stem().string() + ".tmp"); | |
| fs::create_directories(tmp_dir); | |
| std::mutex progress_mtx; | |
| for (int i = 0; i < num_chunks; i++) { | |
| workers.emplace_back([&, i]() { | |
| auto opts = base_opts; | |
| uint64_t start = i * chunk_size; | |
| uint64_t end = (i == num_chunks - 1) ? total_size - 1 : (i + 1) * chunk_size - 1; | |
| opts.resume_from = start; | |
| chunks[i].tmp_path = (tmp_dir / ("chunk_" + std::to_string(i))).string(); | |
| opts.output_path = chunks[i].tmp_path; | |
| // 添加 Range 头 | |
| opts.extra_headers["Range"] = "bytes=" + std::to_string(start) + "-" + std::to_string(end); | |
| chunks[i].ok = HttpDownloader::download(opts, nullptr); | |
| // 部分更新进度 (简化版 — 用文件大小估计) | |
| if (chunks[i].ok) { | |
| std::error_code ec; | |
| chunks[i].downloaded = fs::file_size(chunks[i].tmp_path, ec); | |
| } | |
| }); | |
| } | |
| // 等待所有线程 | |
| for (auto& t : workers) t.join(); | |
| // 检查是否全部成功 | |
| bool all_ok = true; | |
| for (auto& c : chunks) if (!c.ok) { all_ok = false; break; } | |
| if (!all_ok) { | |
| // 回退到单线程 | |
| return HttpDownloader::download(base_opts, progress); | |
| } | |
| // 合并分片 | |
| if (progress) progress->status = "merging"; | |
| std::ofstream out(base_opts.output_path, std::ios::binary); | |
| if (!out) return false; | |
| std::vector<uint8_t> merge_buf(1 * 1024 * 1024); // 1MB buffer | |
| for (auto& c : chunks) { | |
| std::ifstream in(c.tmp_path, std::ios::binary); | |
| while (in) { | |
| in.read((char*)merge_buf.data(), merge_buf.size()); | |
| out.write((char*)merge_buf.data(), in.gcount()); | |
| } | |
| in.close(); | |
| fs::remove(c.tmp_path); | |
| } | |
| out.close(); | |
| fs::remove_all(tmp_dir); | |
| if (progress) { | |
| progress->finished = true; | |
| progress->status = "done"; | |
| progress->downloaded = total_size; | |
| } | |
| return true; | |
| } | |
| }; | |
| // ============================================================================ | |
| // 视频下载管理器 | |
| // ============================================================================ | |
| class VideoDownloadManager { | |
| public: | |
| struct DownloadTask { | |
| std::string url; | |
| std::string output_dir = "."; | |
| std::string output_template = "%(title)s.%(ext)s"; | |
| std::string selected_format = "best"; // "best", "bestvideo+bestaudio", format_id | |
| int max_height = 1080; | |
| int num_threads = 4; | |
| std::string proxy; | |
| std::string cookies; | |
| bool audio_only = false; | |
| bool extract_audio = false; | |
| bool embed_thumbnail = false; | |
| std::unique_ptr<DownloadProgress> progress; | |
| std::unique_ptr<VideoInfo> info; | |
| }; | |
| static bool download(DownloadTask& task) { | |
| // 1. 提取视频信息 | |
| std::cout << "[1/3] 正在获取视频信息..." << std::endl; | |
| auto vi = YtDlpExtractor::extract(task.url, task.proxy, task.cookies); | |
| if (!vi) { | |
| std::cerr << "错误: 无法提取视频信息 (请确认 yt-dlp.exe 在 PATH 中)" << std::endl; | |
| return false; | |
| } | |
| task.info = std::make_unique<VideoInfo>(*vi); | |
| std::cout << " 标题: " << vi->title << std::endl; | |
| std::cout << " 上传: " << vi->uploader << std::endl; | |
| std::cout << " 时长: " << format_duration(vi->duration) << std::endl; | |
| std::cout << " 格式数: " << vi->formats.size() << std::endl; | |
| // 2. 选择格式 | |
| const VideoFormat* selected_fmt = nullptr; | |
| if (task.audio_only || task.extract_audio) { | |
| selected_fmt = vi->best_audio(); | |
| } else { | |
| selected_fmt = vi->best_video(task.max_height); | |
| } | |
| if (!selected_fmt) { | |
| std::cerr << "错误: 没有找到合适的格式" << std::endl; | |
| return false; | |
| } | |
| std::cout << "\n[2/3] 选择格式: " << selected_fmt->format_id | |
| << " (" << selected_fmt->ext << ", " | |
| << selected_fmt->resolution | |
| << ", " << format_bytes(selected_fmt->filesize) << ")" << std::endl; | |
| // 3. 使用 yt-dlp 下载 (一站式: 提取 + 下载 + 合并) | |
| std::cout << "\n[3/3] 开始下载..." << std::endl; | |
| std::string output_path = task.output_dir; | |
| if (!output_path.empty() && output_path.back() != '\\' && output_path.back() != '/') | |
| output_path += "\\"; | |
| output_path += task.output_template; | |
| std::string cmd = "yt-dlp.exe"; | |
| cmd += " -f " + selected_fmt->format_id; | |
| cmd += " -o \"" + output_path + "\""; | |
| cmd += " --no-playlist"; | |
| if (!task.proxy.empty()) cmd += " --proxy " + task.proxy; | |
| if (!task.cookies.empty()) cmd += " --cookies " + task.cookies; | |
| if (task.num_threads > 1) cmd += " --concurrent-fragments " + std::to_string(task.num_threads); | |
| cmd += " --newline --progress"; | |
| cmd += " \"" + task.url + "\""; | |
| std::cout << " " << cmd << std::endl << std::endl; | |
| // 直接运行 (实时输出到控制台) | |
| return run_command_realtime(cmd); | |
| } | |
| static void show_formats(const VideoInfo& vi) { | |
| std::cout << "\n可用格式:\n"; | |
| std::cout << std::left | |
| << std::setw(12) << "ID" | |
| << std::setw(6) << "EXT" | |
| << std::setw(14) << "分辨率" | |
| << std::setw(10) << "大小" | |
| << std::setw(8) << "FPS" | |
| << std::setw(12) << "编码" | |
| << "备注\n"; | |
| std::cout << std::string(80, '-') << "\n"; | |
| for (auto& f : vi.formats) { | |
| std::string size_str = f.filesize > 0 ? format_bytes(f.filesize) : | |
| f.filesize_approx > 0 ? "~" + format_bytes(f.filesize_approx) : "?"; | |
| std::string codec = f.vcodec; | |
| if (codec.size() > 10) codec = codec.substr(0, 10); | |
| if (f.has_video && f.has_audio) codec += "+audio"; | |
| else if (!f.has_video) codec = "audio only"; | |
| else if (!f.has_audio) codec = "video only"; | |
| std::cout << std::left | |
| << std::setw(12) << f.format_id | |
| << std::setw(6) << f.ext | |
| << std::setw(14) << (f.resolution.empty() ? "audio" : f.resolution) | |
| << std::setw(10) << size_str | |
| << std::setw(8) << (f.fps > 0 ? std::to_string((int)f.fps) : "-") | |
| << std::setw(12) << codec | |
| << f.note << "\n"; | |
| } | |
| } | |
| private: | |
| static bool run_command_realtime(const std::string& cmd) { | |
| SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; | |
| HANDLE hRead, hWrite; | |
| if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return false; | |
| SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); | |
| STARTUPINFOW si = {sizeof(STARTUPINFOW)}; | |
| si.dwFlags = STARTF_USESTDHANDLES; | |
| si.hStdOutput = hWrite; | |
| si.hStdError = hWrite; | |
| PROCESS_INFORMATION pi = {}; | |
| std::wstring wcmd = utf8_to_wchar(cmd); | |
| std::unique_ptr<wchar_t[]> cmd_buf(new wchar_t[wcmd.size() + 1]); | |
| wcscpy(cmd_buf.get(), wcmd.c_str()); | |
| if (!CreateProcessW(nullptr, cmd_buf.get(), nullptr, nullptr, TRUE, | |
| 0, nullptr, nullptr, &si, &pi)) { | |
| CloseHandle(hWrite); | |
| CloseHandle(hRead); | |
| return false; | |
| } | |
| CloseHandle(hWrite); | |
| char buf[4096]; | |
| DWORD read; | |
| while (ReadFile(hRead, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) { | |
| buf[read] = '\0'; | |
| std::cout << buf; | |
| } | |
| CloseHandle(hRead); | |
| WaitForSingleObject(pi.hProcess, INFINITE); | |
| DWORD exit_code = 0; | |
| GetExitCodeProcess(pi.hProcess, &exit_code); | |
| CloseHandle(pi.hProcess); | |
| CloseHandle(pi.hThread); | |
| return exit_code == 0; | |
| } | |
| }; | |
| // ============================================================================ | |
| // Win32 GUI (对标 tkinter 界面) | |
| // ============================================================================ | |
| struct GuiState { | |
| HWND hUrlInput = nullptr; | |
| HWND hOutputInput = nullptr; | |
| HWND hQualityCombo = nullptr; | |
| HWND hFormatList = nullptr; | |
| HWND hExtractBtn = nullptr; | |
| HWND hDownloadBtn = nullptr; | |
| HWND hProgressBar = nullptr; | |
| HWND hStatusText = nullptr; | |
| HWND hAudioOnly = nullptr; | |
| std::unique_ptr<VideoInfo> current_info; | |
| std::unique_ptr<std::thread> download_thread; | |
| std::unique_ptr<DownloadProgress> progress; | |
| std::wstring last_output_dir = L"."; | |
| }; | |
| static GuiState g_gui; | |
| static void gui_set_status(const wchar_t* text) { | |
| SetWindowTextW(g_gui.hStatusText, text); | |
| } | |
| static void gui_update_progress() { | |
| if (!g_gui.progress) return; | |
| uint64_t dl = g_gui.progress->downloaded; | |
| uint64_t total = g_gui.progress->total; | |
| if (total > 0) { | |
| int pct = (int)(dl * 100 / total); | |
| SendMessageW(g_gui.hProgressBar, PBM_SETPOS, (WPARAM)pct, 0); | |
| wchar_t txt[128]; | |
| swprintf(txt, 128, L"下载中... %d%% (%s / %s)", | |
| pct, | |
| utf8_to_wchar(format_bytes(dl)).c_str(), | |
| utf8_to_wchar(format_bytes(total)).c_str()); | |
| gui_set_status(txt); | |
| } | |
| } | |
| static void gui_on_extract() { | |
| wchar_t url[2048] = {0}; | |
| GetWindowTextW(g_gui.hUrlInput, url, 2047); | |
| if (wcslen(url) == 0) { | |
| MessageBoxW(nullptr, L"请输入视频 URL", L"提示", MB_OK | MB_ICONINFORMATION); | |
| return; | |
| } | |
| gui_set_status(L"正在获取视频信息..."); | |
| EnableWindow(g_gui.hExtractBtn, FALSE); | |
| EnableWindow(g_gui.hDownloadBtn, FALSE); | |
| auto vi = YtDlpExtractor::extract(wchar_to_utf8(url)); | |
| if (!vi) { | |
| gui_set_status(L"获取失败: 请确认 URL 正确且 yt-dlp.exe 可用"); | |
| EnableWindow(g_gui.hExtractBtn, TRUE); | |
| return; | |
| } | |
| g_gui.current_info = std::make_unique<VideoInfo>(*vi); | |
| // 更新格式列表 | |
| ListView_DeleteAllItems(g_gui.hFormatList); | |
| for (size_t i = 0; i < vi->formats.size(); i++) { | |
| auto& f = vi->formats[i]; | |
| wchar_t id[32]; swprintf(id, 32, L"%S", f.format_id.c_str()); | |
| wchar_t ext[16]; swprintf(ext, 16, L"%S", f.ext.c_str()); | |
| wchar_t res[32]; swprintf(res, 32, L"%S", f.resolution.c_str()); | |
| wchar_t size_str[32]; swprintf(size_str, 32, L"%S", format_bytes(f.filesize).c_str()); | |
| wchar_t note[128]; swprintf(note, 128, L"%S", f.note.c_str()); | |
| LVITEMW item = {}; | |
| item.mask = LVIF_TEXT; | |
| item.iItem = (int)i; | |
| item.pszText = id; | |
| ListView_InsertItem(g_gui.hFormatList, &item); | |
| ListView_SetItemText(g_gui.hFormatList, (int)i, 1, ext); | |
| ListView_SetItemText(g_gui.hFormatList, (int)i, 2, res); | |
| ListView_SetItemText(g_gui.hFormatList, (int)i, 3, size_str); | |
| ListView_SetItemText(g_gui.hFormatList, (int)i, 4, note); | |
| } | |
| wchar_t status[256]; | |
| swprintf(status, 256, L"已获取: %S (%S) - %zu 个格式", | |
| vi->title.c_str(), format_duration(vi->duration).c_str(), vi->formats.size()); | |
| gui_set_status(status); | |
| EnableWindow(g_gui.hExtractBtn, TRUE); | |
| EnableWindow(g_gui.hDownloadBtn, TRUE); | |
| } | |
| static void gui_on_download() { | |
| if (!g_gui.current_info) { | |
| gui_on_extract(); | |
| if (!g_gui.current_info) return; | |
| } | |
| wchar_t url[2048] = {0}; | |
| wchar_t out_dir[1024] = {0}; | |
| GetWindowTextW(g_gui.hUrlInput, url, 2047); | |
| GetWindowTextW(g_gui.hOutputInput, out_dir, 1023); | |
| bool audio_only = (SendMessageW(g_gui.hAudioOnly, BM_GETCHECK, 0, 0) == BST_CHECKED); | |
| // 获取选中的格式 | |
| int sel = ListView_GetNextItem(g_gui.hFormatList, -1, LVNI_SELECTED); | |
| std::string format_id = "best"; | |
| if (sel >= 0 && sel < (int)g_gui.current_info->formats.size()) { | |
| format_id = g_gui.current_info->formats[sel].format_id; | |
| } | |
| if (audio_only) format_id = "bestaudio"; | |
| VideoDownloadManager::DownloadTask task; | |
| task.url = wchar_to_utf8(url); | |
| task.output_dir = wchar_to_utf8(out_dir); | |
| if (task.output_dir.empty()) task.output_dir = "."; | |
| task.selected_format = format_id; | |
| task.audio_only = audio_only; | |
| g_gui.progress = std::make_unique<DownloadProgress>(); | |
| gui_set_status(L"开始下载..."); | |
| SendMessageW(g_gui.hProgressBar, PBM_SETPOS, 0, 0); | |
| EnableWindow(g_gui.hDownloadBtn, FALSE); | |
| // 在后台线程下载 | |
| g_gui.download_thread = std::make_unique<std::thread>([task = std::move(task)]() mutable { | |
| bool ok = VideoDownloadManager::download(task); | |
| std::wstring msg = ok ? L"下载完成!" : L"下载失败"; | |
| gui_set_status(msg.c_str()); | |
| EnableWindow(g_gui.hDownloadBtn, TRUE); | |
| }); | |
| } | |
| static LRESULT CALLBACK gui_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { | |
| switch (msg) { | |
| case WM_CREATE: { | |
| // 创建控件 (对标 tkinter 布局) | |
| int y = 10; | |
| // URL 输入 | |
| CreateWindowW(L"STATIC", L"视频 URL:", WS_CHILD | WS_VISIBLE, | |
| 10, y, 60, 20, hwnd, nullptr, nullptr, nullptr); | |
| g_gui.hUrlInput = CreateWindowW(L"EDIT", L"", | |
| WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, | |
| 80, y - 2, 500, 22, hwnd, (HMENU)IDC_URL_INPUT, nullptr, nullptr); | |
| // 提取按钮 | |
| g_gui.hExtractBtn = CreateWindowW(L"BUTTON", L"获取信息", | |
| WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, | |
| 590, y - 2, 80, 24, hwnd, (HMENU)IDC_EXTRACT_BTN, nullptr, nullptr); | |
| y += 28; | |
| // 输出路径 | |
| CreateWindowW(L"STATIC", L"保存到:", WS_CHILD | WS_VISIBLE, | |
| 10, y, 60, 20, hwnd, nullptr, nullptr, nullptr); | |
| g_gui.hOutputInput = CreateWindowW(L"EDIT", L".", | |
| WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, | |
| 80, y - 2, 500, 22, hwnd, (HMENU)IDC_OUTPUT_INPUT, nullptr, nullptr); | |
| g_gui.hDownloadBtn = CreateWindowW(L"BUTTON", L"下载", | |
| WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, | |
| 590, y - 2, 80, 24, hwnd, (HMENU)IDC_DOWNLOAD_BTN, nullptr, nullptr); | |
| y += 28; | |
| // 仅音频 | |
| g_gui.hAudioOnly = CreateWindowW(L"BUTTON", L"仅下载音频", | |
| WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, | |
| 80, y, 100, 20, hwnd, (HMENU)IDC_AUDIO_ONLY, nullptr, nullptr); | |
| y += 24; | |
| // 格式列表 | |
| g_gui.hFormatList = CreateWindowW(WC_LISTVIEWW, L"", | |
| WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT | LVS_SINGLESEL, | |
| 10, y, 660, 180, hwnd, (HMENU)IDC_FORMAT_LIST, nullptr, nullptr); | |
| // 添加列 | |
| LVCOLUMNW col = {}; | |
| col.mask = LVCF_TEXT | LVCF_WIDTH; | |
| col.cx = 80; col.pszText = (LPWSTR)L"格式ID"; ListView_InsertColumn(g_gui.hFormatList, 0, &col); | |
| col.cx = 50; col.pszText = (LPWSTR)L"扩展"; ListView_InsertColumn(g_gui.hFormatList, 1, &col); | |
| col.cx = 100; col.pszText = (LPWSTR)L"分辨率"; ListView_InsertColumn(g_gui.hFormatList, 2, &col); | |
| col.cx = 80; col.pszText = (LPWSTR)L"大小"; ListView_InsertColumn(g_gui.hFormatList, 3, &col); | |
| col.cx = 330; col.pszText = (LPWSTR)L"备注"; ListView_InsertColumn(g_gui.hFormatList, 4, &col); | |
| y += 185; | |
| // 进度条 | |
| g_gui.hProgressBar = CreateWindowW(PROGRESS_CLASSW, L"", | |
| WS_CHILD | WS_VISIBLE | PBS_SMOOTH, | |
| 10, y, 550, 22, hwnd, (HMENU)IDC_PROGRESS_BAR, nullptr, nullptr); | |
| SendMessageW(g_gui.hProgressBar, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); | |
| y += 28; | |
| // 状态栏 | |
| g_gui.hStatusText = CreateWindowW(L"STATIC", L"就绪 - 粘贴 URL 并点击 [获取信息]", | |
| WS_CHILD | WS_VISIBLE | SS_LEFT, | |
| 10, y, 660, 20, hwnd, (HMENU)IDC_STATUS_TEXT, nullptr, nullptr); | |
| // 设置字体 | |
| HFONT hFont = CreateFontW(16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, | |
| DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, | |
| CLEARTYPE_QUALITY, FF_DONTCARE, L"Microsoft YaHei UI"); | |
| EnumChildWindows(hwnd, [](HWND child, LPARAM lf) -> BOOL { | |
| SendMessageW(child, WM_SETFONT, (WPARAM)(HFONT)lf, TRUE); | |
| return TRUE; | |
| }, (LPARAM)hFont); | |
| break; | |
| } | |
| case WM_COMMAND: { | |
| WORD id = LOWORD(wp); | |
| if (id == IDC_EXTRACT_BTN) gui_on_extract(); | |
| else if (id == IDC_DOWNLOAD_BTN) gui_on_download(); | |
| break; | |
| } | |
| case WM_TIMER: { | |
| // 定期更新进度 | |
| if (wp == 1 && g_gui.progress) { | |
| gui_update_progress(); | |
| if (g_gui.progress->finished) { | |
| KillTimer(hwnd, 1); | |
| } | |
| } | |
| break; | |
| } | |
| case WM_DESTROY: | |
| if (g_gui.download_thread && g_gui.download_thread->joinable()) | |
| g_gui.download_thread->detach(); | |
| PostQuitMessage(0); | |
| break; | |
| } | |
| return DefWindowProcW(hwnd, msg, wp, lp); | |
| } | |
| static int run_gui(HINSTANCE hInstance) { | |
| WNDCLASSEXW wc = {sizeof(WNDCLASSEXW)}; | |
| wc.lpfnWndProc = gui_wndproc; | |
| wc.hInstance = hInstance; | |
| wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); | |
| wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); | |
| wc.lpszClassName = L"VideoDownloaderWindow"; | |
| RegisterClassExW(&wc); | |
| int screen_w = GetSystemMetrics(SM_CXSCREEN); | |
| int screen_h = GetSystemMetrics(SM_CYSCREEN); | |
| int w = 700, h = 430; | |
| int x = (screen_w - w) / 2; | |
| int y = (screen_h - h) / 2; | |
| HWND hwnd = CreateWindowExW(0, L"VideoDownloaderWindow", | |
| L"万能视频下载器 v2.0 (C++ Native)", | |
| WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX), | |
| x, y, w, h, nullptr, nullptr, hInstance, nullptr); | |
| ShowWindow(hwnd, SW_SHOW); | |
| SetTimer(hwnd, 1, 200, nullptr); | |
| MSG msg = {}; | |
| while (GetMessageW(&msg, nullptr, 0, 0)) { | |
| TranslateMessage(&msg); | |
| DispatchMessageW(&msg); | |
| } | |
| return (int)msg.wParam; | |
| } | |
| // ============================================================================ | |
| // CLI 模式 | |
| // ============================================================================ | |
| static void print_banner() { | |
| std::cout << R"( | |
| ╔══════════════════════════════════════════════════════════╗ | |
| ║ 万能视频下载器 v2.0 (C++ Native Edition) ║ | |
| ║ 对标 PyInstaller + yt-dlp + tkinter 版本 ║ | |
| ╚══════════════════════════════════════════════════════════╝ | |
| )" << std::endl; | |
| } | |
| static void print_usage(const char* prog) { | |
| std::cout << "用法:\n" | |
| << " " << prog << " [选项] <URL>\n" | |
| << " " << prog << " --gui 启动图形界面\n\n" | |
| << "选项:\n" | |
| << " -o, --output <path> 输出路径/模板 (默认: ./%(title)s.%(ext)s)\n" | |
| << " -f, --format <id> 选择格式 (默认: best)\n" | |
| << " -F, --list-formats 列出所有可用格式\n" | |
| << " -q, --quality <height> 最高画质 (e.g. 720, 1080, 2160)\n" | |
| << " -a, --audio-only 仅下载音频\n" | |
| << " -p, --proxy <url> 代理 (e.g. http://127.0.0.1:7890)\n" | |
| << " -c, --cookies <file> Cookies 文件\n" | |
| << " -t, --threads <n> 并发线程数 (默认: 4)\n" | |
| << " -P, --playlist 下载播放列表\n" | |
| << " -h, --help 显示帮助\n" | |
| << std::endl; | |
| } | |
| // ============================================================================ | |
| // main 入口 | |
| // ============================================================================ | |
| int main(int argc, char* argv[]) { | |
| // 初始化 COM 和通用控件 | |
| INITCOMMONCONTROLSEX icex = {sizeof(INITCOMMONCONTROLSEX), ICC_PROGRESS_CLASS | ICC_LISTVIEW_CLASSES}; | |
| InitCommonControlsEx(&icex); | |
| // 无参数启动 → GUI 模式 | |
| if (argc == 1) { | |
| return run_gui(GetModuleHandleW(nullptr)); | |
| } | |
| // 解析参数 | |
| std::string url; | |
| std::string output = "."; | |
| std::string format = "best"; | |
| std::string proxy; | |
| std::string cookies; | |
| int max_height = 1080; | |
| int threads = 4; | |
| bool list_formats = false; | |
| bool audio_only = false; | |
| bool playlist = false; | |
| bool gui_mode = false; | |
| for (int i = 1; i < argc; i++) { | |
| std::string arg = argv[i]; | |
| if (arg == "--gui") { gui_mode = true; } | |
| else if (arg == "-h" || arg == "--help") { print_usage(argv[0]); return 0; } | |
| else if (arg == "-F" || arg == "--list-formats") { list_formats = true; } | |
| else if (arg == "-a" || arg == "--audio-only") { audio_only = true; } | |
| else if (arg == "-P" || arg == "--playlist") { playlist = true; } | |
| else if ((arg == "-o" || arg == "--output") && i + 1 < argc) { output = argv[++i]; } | |
| else if ((arg == "-f" || arg == "--format") && i + 1 < argc) { format = argv[++i]; } | |
| else if ((arg == "-q" || arg == "--quality") && i + 1 < argc) { max_height = atoi(argv[++i]); } | |
| else if ((arg == "-p" || arg == "--proxy") && i + 1 < argc) { proxy = argv[++i]; } | |
| else if ((arg == "-c" || arg == "--cookies") && i + 1 < argc) { cookies = argv[++i]; } | |
| else if ((arg == "-t" || arg == "--threads") && i + 1 < argc) { threads = atoi(argv[++i]); } | |
| else if (arg[0] != '-') { url = arg; } | |
| } | |
| if (gui_mode) { | |
| return run_gui(GetModuleHandleW(nullptr)); | |
| } | |
| if (url.empty()) { | |
| print_banner(); | |
| print_usage(argv[0]); | |
| return 1; | |
| } | |
| print_banner(); | |
| // 列表模式 | |
| if (list_formats) { | |
| auto vi = YtDlpExtractor::extract(url, proxy, cookies); | |
| if (!vi) { | |
| std::cerr << "无法提取视频信息" << std::endl; | |
| return 1; | |
| } | |
| std::cout << "标题: " << vi->title << "\n"; | |
| VideoDownloadManager::show_formats(*vi); | |
| return 0; | |
| } | |
| // 播放列表模式 | |
| if (playlist) { | |
| auto vids = YtDlpExtractor::extract_playlist(url, proxy); | |
| if (!vids) { | |
| std::cerr << "无法提取播放列表" << std::endl; | |
| return 1; | |
| } | |
| std::cout << "播放列表: " << vids->size() << " 个视频\n\n"; | |
| for (size_t i = 0; i < vids->size(); i++) { | |
| std::cout << "[" << (i + 1) << "/" << vids->size() << "] " | |
| << (*vids)[i].title << std::endl; | |
| VideoDownloadManager::DownloadTask task; | |
| task.url = (*vids)[i].webpage_url.empty() ? url : (*vids)[i].webpage_url; | |
| task.output_dir = output; | |
| task.selected_format = format; | |
| task.max_height = max_height; | |
| task.num_threads = threads; | |
| task.proxy = proxy; | |
| task.cookies = cookies; | |
| task.audio_only = audio_only; | |
| if (!VideoDownloadManager::download(task)) { | |
| std::cerr << " 下载失败,继续下一个..." << std::endl; | |
| } | |
| } | |
| std::cout << "\n播放列表下载完成" << std::endl; | |
| return 0; | |
| } | |
| // 单视频下载 | |
| VideoDownloadManager::DownloadTask task; | |
| task.url = url; | |
| task.output_dir = output; | |
| task.selected_format = format; | |
| task.max_height = max_height; | |
| task.num_threads = threads; | |
| task.proxy = proxy; | |
| task.cookies = cookies; | |
| task.audio_only = audio_only; | |
| bool ok = VideoDownloadManager::download(task); | |
| std::cout << (ok ? "\n下载完成!" : "\n下载失败") << std::endl; | |
| return ok ? 0 : 1; | |
| } | |
Xet Storage Details
- Size:
- 52.3 kB
- Xet hash:
- fe5a1100197c5180dcad2665c8fb093939b009500ceebf63065359281d82d727
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.