summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format192
-rw-r--r--.gitignore17
-rw-r--r--src/constants.h18
-rw-r--r--src/extractors/bilibili.c475
-rw-r--r--src/extractors/bilibili.h94
-rw-r--r--src/extractors/extractor.c24
-rw-r--r--src/extractors/extractor.h32
-rw-r--r--src/logger.c32
-rw-r--r--src/logger.h30
-rw-r--r--src/main.c163
-rw-r--r--src/main.h8
-rw-r--r--src/process_url.c526
-rw-r--r--src/process_url.h59
-rw-r--r--src/style.h133
-rw-r--r--src/ui.c98
-rw-r--r--src/ui.h16
-rw-r--r--src/utils.c204
-rw-r--r--src/utils.h61
-rw-r--r--xmake.lua153
19 files changed, 2335 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..41577ff
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,192 @@
+---
+Language: Cpp
+BasedOnStyle: LLVM
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Right
+AlignOperands: Align
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: All
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: MultiLine
+AttributeMacros:
+ - __capability
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ AfterExternBlock: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+ColumnLimit: 80
+CommentPragmas: '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle: ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^(<|"(gtest|gmock|isl|json)/)'
+ Priority: 3
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires: false
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Right
+PPIndentWidth: -1
+ReferenceAlignment: Pointer
+ReflowComments: true
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes: CaseSensitive
+SortJavaStaticImport: Before
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+ AfterControlStatements: true
+ AfterForeachMacros: true
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: true
+ AfterOverloadedOperator: false
+ BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard: Latest
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TabWidth: 8
+UseCRLF: false
+UseTab: Never
+WhitespaceSensitiveMacros:
+ - STRINGIZE
+ - PP_STRINGIZE
+ - BOOST_PP_STRINGIZE
+ - NS_SWIFT_NAME
+ - CF_SWIFT_NAME
+...
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ef7f3d2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+# Xmake cache
+.xmake/
+build/
+
+# MacOS Cache
+.DS_Store
+
+compile_flags.txt
+
+# Fonts
+*.ttf
+
+# Tests
+*test*
+
+# Binary
+*binary*
diff --git a/src/constants.h b/src/constants.h
new file mode 100644
index 0000000..5891d7a
--- /dev/null
+++ b/src/constants.h
@@ -0,0 +1,18 @@
+#ifndef CONSTANTS_H_
+#define CONSTANTS_H_
+
+#define APP_NAME "Hinata"
+#define MAX_VALUE 100
+
+#define CEIL_DIV(a, b) (((a) + (b)-1) / (b))
+#define MAX(x, y) ((x) > (y)) ? (x) : (y)
+
+#ifdef _WIN32
+#define SPLITTER_CHAR '\\'
+#define SPLITTER_STR "\\"
+#else
+#define SPLITTER_CHAR '/'
+#define SPLITTER_STR "/"
+#endif
+
+#endif
diff --git a/src/extractors/bilibili.c b/src/extractors/bilibili.c
new file mode 100644
index 0000000..874a605
--- /dev/null
+++ b/src/extractors/bilibili.c
@@ -0,0 +1,475 @@
+#include <cjson/cJSON.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include "c11threads.h"
+#else
+#include <threads.h>
+#endif
+
+#include "../logger.h"
+#include "../process_url.h"
+#include "../utils.h"
+#include "bilibili.h"
+#include "extractor.h"
+
+static int get_multipagedata(char *pagedata, Multipage *multipage_struct,
+ bool *is_page) {
+ const char *patterns_str[1] = {"window.__INITIAL_STATE__=(.+?);\\(function"};
+ const str_array_t patterns = {(char **)patterns_str, 1};
+ str_array_t results = create_str_array(0);
+
+ int r = regex_match(pagedata, patterns, &results);
+ if (!r) {
+ for (unsigned short i = 0; i < results.n; i++) {
+ // DEBUG_PRINT("%s\n", results.str[i]);
+ if (results.str[i]) {
+ multipage_struct->json = cJSON_Parse(get_str_element(&results, i));
+ }
+ }
+ free_str_array(&results);
+
+ cJSON *aid_obj = cJSON_GetObjectItem(multipage_struct->json, "aid");
+ cJSON *bvid_obj = cJSON_GetObjectItem(multipage_struct->json, "bvid");
+ cJSON *sections_obj =
+ cJSON_GetObjectItem(multipage_struct->json, "sections");
+ cJSON *videoData_obj =
+ cJSON_GetObjectItem(multipage_struct->json, "videoData");
+ if (aid_obj && bvid_obj && sections_obj && videoData_obj) {
+ multipage_struct->aid = aid_obj->valueint;
+ multipage_struct->bvid = bvid_obj->valuestring;
+ multipage_struct->sections = create_array(
+ sizeof(Multi_episode_data), cJSON_GetArraySize(sections_obj));
+
+ /* sections */
+ if (!cJSON_GetArraySize(sections_obj)) {
+ DEBUG_PRINT("This video does not have sections, meaning that it is a "
+ "multi-p video with only one av/bvid\n");
+ *is_page = 1;
+ }
+
+ cJSON *e;
+ int i = 0;
+ cJSON_ArrayForEach(e, sections_obj) {
+ Multi_episode_data *section =
+ get_element(&multipage_struct->sections, i);
+ cJSON *season_id_obj = cJSON_GetObjectItem(e, "season_id");
+ cJSON *episodes_obj = cJSON_GetObjectItem(e, "episodes");
+ if (season_id_obj && episodes_obj) {
+ section->season_id = season_id_obj->valueint;
+ DEBUG_PRINT("sections[%d] season_id: %d\n", i, section->season_id);
+
+ section->episodes =
+ create_array(sizeof(Episode), cJSON_GetArraySize(episodes_obj));
+
+ cJSON *e;
+ int j = 0;
+ cJSON_ArrayForEach(e, episodes_obj) {
+ cJSON *aid_obj = cJSON_GetObjectItem(e, "aid");
+ cJSON *bvid_obj = cJSON_GetObjectItem(e, "bvid");
+ cJSON *cid_obj = cJSON_GetObjectItem(e, "cid");
+ cJSON *title_obj = cJSON_GetObjectItem(e, "title");
+ if (aid_obj && bvid_obj && cid_obj && title_obj) {
+ Episode *episode = get_element(&section->episodes, j);
+ episode->aid = aid_obj->valueint;
+ episode->bvid = bvid_obj->valuestring;
+ episode->cid = cid_obj->valueint;
+ episode->title = title_obj->valuestring;
+ DEBUG_PRINT("sections[%d].episodes[%d] aid: %d\n", i, j,
+ episode->aid);
+ DEBUG_PRINT("sections[%d].episodes[%d] bvid: %s\n", i, j,
+ episode->bvid);
+ DEBUG_PRINT("sections[%d].episodes[%d] cid: %d\n", i, j,
+ episode->cid);
+ DEBUG_PRINT("sections[%d].episodes[%d] title: %s\n", i, j,
+ episode->title);
+
+ j++;
+ continue;
+ }
+ r = 1;
+ LOG("cJSON", "Read JSON.sections[%d].episodes[%d] failed.\n", i, j);
+ return r;
+ }
+
+ i++;
+ continue;
+ }
+ r = 1;
+ LOG("cJSON", "Read JSON.sections[%d] failed.\n", i);
+ return r;
+ }
+
+ /* videoData */
+ Multipage_video_data *videoData = &multipage_struct->videoData;
+ cJSON *title_obj = cJSON_GetObjectItem(videoData_obj, "title");
+ cJSON *pages_obj = cJSON_GetObjectItem(videoData_obj, "pages");
+ if (title_obj && pages_obj) {
+ videoData->title = title_obj->valuestring;
+ DEBUG_PRINT("videoData.title: %s\n", videoData->title);
+
+ videoData->pages = create_array(sizeof(Video_pages_data),
+ cJSON_GetArraySize(pages_obj));
+ int i = 0;
+ cJSON *e;
+ cJSON_ArrayForEach(e, pages_obj) {
+ cJSON *cid_obj = cJSON_GetObjectItem(e, "cid");
+ cJSON *part_obj = cJSON_GetObjectItem(e, "part");
+ cJSON *page_obj = cJSON_GetObjectItem(e, "page");
+ if (cid_obj && part_obj && page_obj) {
+ Video_pages_data *page = get_element(&videoData->pages, i);
+ page->cid = cid_obj->valueint;
+ page->part = part_obj->valuestring;
+ page->page = page_obj->valueint;
+ DEBUG_PRINT("videoData.pages[%d].cid: %d\n", i, page->cid);
+ DEBUG_PRINT("videoData.pages[%d].part: %s\n", i, page->part);
+ DEBUG_PRINT("videoData.pages[%d].page: %d\n", i, page->page);
+
+ i++;
+ continue;
+ }
+ LOG("cJSON", "Read JSON.videodata.pages[%d] failed.\n", i);
+ return 1;
+ }
+ } else {
+ LOG("cJSON", "Read JSON.videodata failed.\n");
+ return 1;
+ }
+ } else {
+ r = 1;
+ LOG("cJSON", "Parse pagedata JSON failed.\n");
+ }
+ }
+ return r;
+}
+
+static int get_dash(const char *api_resp, Dash *dash) {
+ dash->json = cJSON_Parse(api_resp);
+
+ cJSON *code_obj = cJSON_GetObjectItem(dash->json, "code");
+ cJSON *message_obj = cJSON_GetObjectItem(dash->json, "message");
+ cJSON *dashinfo_obj = cJSON_GetObjectItem(dash->json, "data");
+ if (cJSON_IsInvalid(dashinfo_obj)) {
+ dashinfo_obj = cJSON_GetObjectItem(dash->json, "result");
+ }
+
+ if (!code_obj || !message_obj || !dashinfo_obj) {
+ LOG("cJSON", "Parse API resp_json failed.\n");
+ return 1;
+ }
+ dash->code = code_obj->valueint;
+ dash->message = code_obj->valuestring;
+
+ /* dashinfo: "data" or "result" */
+ DEBUG_PRINT("Key of dashinfo: %s\n", dashinfo_obj->string);
+ Dash_info *dashinfo = &dash->dashinfo;
+ cJSON *quality_obj = cJSON_GetObjectItem(dashinfo_obj, "quality");
+ cJSON *accept_description_obj =
+ cJSON_GetObjectItem(dashinfo_obj, "accept_description");
+ cJSON *accept_quality_obj =
+ cJSON_GetObjectItem(dashinfo_obj, "accept_quality");
+ cJSON *dash_streams_obj = cJSON_GetObjectItem(dashinfo_obj, "dash");
+ cJSON *format_obj = cJSON_GetObjectItem(dashinfo_obj, "format");
+ cJSON *durl_obj = cJSON_GetObjectItem(dashinfo_obj, "durl"); // NOTE: Optional
+
+ if (!quality_obj || !accept_description_obj || !accept_quality_obj ||
+ !dash_streams_obj || !format_obj) {
+ LOG("cJSON", "Read API resp_json.%s failed.\n", dashinfo_obj->string);
+ return 1;
+ }
+
+ dashinfo->quality = quality_obj->valueint;
+ DEBUG_PRINT("quality: %d\n", dashinfo->quality);
+
+ dashinfo->format = format_obj->valuestring;
+ DEBUG_PRINT("format: %s\n", dashinfo->format);
+
+ dashinfo->accept_description =
+ create_str_array(cJSON_GetArraySize(accept_quality_obj));
+ str_array_t *ac_d = &dashinfo->accept_description;
+ for (unsigned char n = 0; n < cJSON_GetArraySize(accept_description_obj);
+ n++) {
+ cJSON *i = cJSON_GetArrayItem(accept_description_obj, n);
+
+ if (!i) {
+ LOG("cJSON", "Read API resp_json.%s.accept_description failed.\n",
+ dashinfo_obj->string);
+ return 1;
+ }
+ set_str_element(ac_d, n, i->valuestring);
+ DEBUG_PRINT("accept_description[%hhu]: %s\n", n, get_str_element(ac_d, n));
+ }
+
+ dashinfo->accept_quality =
+ create_array(sizeof(int), cJSON_GetArraySize(accept_quality_obj));
+ generic_array_t *ac_q = &dashinfo->accept_quality;
+ for (unsigned char n = 0; n < cJSON_GetArraySize(accept_quality_obj); n++) {
+ cJSON *i = cJSON_GetArrayItem(accept_quality_obj, n);
+
+ if (!i) {
+ LOG("cJSON", "Read API resp_json.%s.accept_quality failed.\n",
+ dashinfo_obj->string);
+ return 1;
+ }
+ int *v = get_element(ac_q, n);
+ *v = i->valueint;
+ DEBUG_PRINT("accept_quality[%hhu]: %d\n", n, *v);
+ }
+
+ cJSON *video_obj = cJSON_GetObjectItem(dash_streams_obj, "video");
+ cJSON *audio_obj = cJSON_GetObjectItem(dash_streams_obj, "audio");
+
+ if (!video_obj || !audio_obj) {
+ LOG("cJSON", "Read API resp_json.%s.dash failed.\n", dashinfo_obj->string);
+ return 1;
+ }
+
+ dashinfo->dash.video =
+ create_array(sizeof(Dash_stream), cJSON_GetArraySize(video_obj));
+ dashinfo->dash.audio =
+ create_array(sizeof(Dash_stream), cJSON_GetArraySize(audio_obj));
+ generic_array_t *target;
+ cJSON *dash_stream_obj;
+ for (dash_stream_obj = video_obj, target = &dashinfo->dash.video;;) {
+ int i = 0;
+ cJSON *e;
+ cJSON_ArrayForEach(e, dash_stream_obj) {
+ cJSON *id_obj = cJSON_GetObjectItem(e, "id");
+ cJSON *baseUrl_obj = cJSON_GetObjectItem(e, "baseUrl");
+ cJSON *bandwidth_obj = cJSON_GetObjectItem(e, "bandwidth");
+ cJSON *mimeType_obj = cJSON_GetObjectItem(e, "mimeType");
+ cJSON *codecid_obj = cJSON_GetObjectItem(e, "codecid");
+ cJSON *codecs_obj = cJSON_GetObjectItem(e, "codecs");
+
+ if (!id_obj || !baseUrl_obj || !bandwidth_obj || !mimeType_obj ||
+ !codecid_obj || !codecs_obj) {
+ LOG("cJSON", "Read API resp_json.%s.dash.%s[%d] failed.\n",
+ dashinfo_obj->string, dash_stream_obj->string, i);
+ return 1;
+ }
+ Dash_stream *ds = get_element(target, i);
+ ds->id = id_obj->valueint;
+ ds->baseUrl = baseUrl_obj->valuestring;
+ ds->bandwidth = bandwidth_obj->valueint;
+ ds->mimeType = mimeType_obj->valuestring;
+ ds->codecid = codecid_obj->valueint;
+ ds->codecs = codecs_obj->valuestring;
+
+ DEBUG_PRINT("%s[%d].id: %d\n", dash_stream_obj->string, i, ds->id);
+ DEBUG_PRINT("%s[%d].baseUrl: %s\n", dash_stream_obj->string, i,
+ ds->baseUrl);
+ DEBUG_PRINT("%s[%d].bandwidth: %d\n", dash_stream_obj->string, i,
+ ds->bandwidth);
+ DEBUG_PRINT("%s[%d].mimeType: %s\n", dash_stream_obj->string, i,
+ ds->mimeType);
+ DEBUG_PRINT("%s[%d].codecid: %d\n", dash_stream_obj->string, i,
+ ds->codecid);
+ DEBUG_PRINT("%s[%d].codecs: %s\n", dash_stream_obj->string, i,
+ ds->codecs);
+
+ i++;
+ }
+
+ if (dash_stream_obj == video_obj) {
+ dash_stream_obj = audio_obj;
+ target = &dashinfo->dash.audio;
+ } else {
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int get_page_in_query(char *query, int *page) {
+ const char *pattern = "p=(\\d+)";
+ str_array_t results = {0};
+ int r = regex_match(query, (str_array_t){(char **)&pattern, 1}, &results);
+ if (!r) {
+ // for (unsigned short i = 0; i < results.n; i++) {
+ // DEBUG_PRINT("%s\n", results.str[i]);
+ // }
+ *page = results.n ? atoi(results.str[0]) : 1; // Download p1 by default
+ }
+ return r;
+}
+
+static int generate_api(Bilibili_options *bilibili_options, const int quality) {
+ char params[UCHAR_MAX];
+ snprintf(params, sizeof(params),
+ "avid=%d&cid=%d&bvid=%s&qn=%d&type=&otype=json&fourk=1&fnver=0&"
+ "fnval=2000",
+ bilibili_options->aid, bilibili_options->cid, bilibili_options->bvid,
+ quality);
+ bilibili_options->api = malloc(strlen(BILIBILI_API) + strlen(params) + 1);
+ strcpy(bilibili_options->api, BILIBILI_API);
+ strcat(bilibili_options->api, params);
+ return 0;
+}
+
+static const char *mimeType2ext(const char *mimeType) {
+ static char mimeType_l[CHAR_MAX];
+ strcpy(mimeType_l, mimeType);
+ const char *exts[2];
+ size_t extsCount = 0;
+
+ char *token = strtok(mimeType_l, "/");
+ while (token != NULL && extsCount < 2) {
+ exts[extsCount++] = token;
+ token = strtok(NULL, "/");
+ }
+
+ if (extsCount == 2) {
+ return exts[1];
+ }
+
+ return "mp4"; // Cannot parse, use default
+}
+
+static const char *id2quality_desc(int id) {
+ const char *desc;
+ switch (id) {
+ case 127:
+ desc = "超高清 8K";
+ break;
+ case 120:
+ desc = "超清 4K";
+ break;
+ case 112:
+ desc = "高清 1080P+";
+ break;
+ case 80:
+ desc = "高清 1080P";
+ break;
+ case 48:
+ desc = "高清 720P";
+ break;
+ case 32:
+ desc = "清晰 480P";
+ break;
+ case 16:
+ desc = "流畅 360P";
+ break;
+ default:
+ desc = "Unknown resolution";
+ break;
+ }
+ return desc;
+}
+
+static void multipage_cleanup(Multipage *multipage_struct) {
+ for (unsigned short i = 0; i < multipage_struct->sections.n; i++) {
+ // free_and_nullify(multipage_struct->sections[i].episodes);
+ Multi_episode_data *section = get_element(&multipage_struct->sections, i);
+ free_array(&section->episodes);
+ }
+ free_array(&multipage_struct->sections);
+ free_array(&multipage_struct->videoData.pages);
+ cJSON_Delete(multipage_struct->json);
+ multipage_struct->json = NULL;
+}
+
+static void dash_cleanup(Dash *dash) {
+ cJSON_Delete(dash->json);
+ free_str_array(&dash->dashinfo.accept_description);
+ free_array(&dash->dashinfo.accept_quality);
+ free_array(&dash->dashinfo.dash.audio);
+ free_array(&dash->dashinfo.dash.video);
+}
+
+static int download(Bilibili_options *bilibili_options) {
+ Dash dash = {0};
+ char *resp;
+ get(bilibili_options->api, &resp);
+ if (get_dash(resp, &dash)) {
+ LOG("Bilibili", "Get dash failed.");
+ free_and_nullify(resp);
+ dash_cleanup(&dash);
+ return 1;
+ };
+
+ // Download the highest resolution
+ Dash_stream *video = get_element(&dash.dashinfo.dash.video, 0);
+ Dash_stream *audio = get_element(&dash.dashinfo.dash.audio, 0);
+ const char *quality_desc = id2quality_desc(video->id);
+
+ {
+ char fn[USHRT_MAX];
+ sprintf(fn, "%s[%s]-%s.%s", bilibili_options->title, quality_desc, "video",
+ mimeType2ext(video->mimeType));
+ add_url(video->baseUrl, NULL, fn, "https://www.bilibili.com");
+ }
+
+ {
+ char fn[USHRT_MAX];
+ sprintf(fn, "%s[%s]-%s.%s", bilibili_options->title,
+ quality_desc, "audio", mimeType2ext(audio->mimeType));
+ add_url(audio->baseUrl, NULL, fn, "https://www.bilibili.com");
+ }
+
+ free_and_nullify(resp);
+ dash_cleanup(&dash);
+ return 0;
+}
+
+void bilibili_extract(struct options *options) {
+ Multipage multipage_struct = {0};
+ Bilibili_options bilibili_options = {options->URL};
+ int p = 1;
+ char *api;
+
+ if (get(options->URL, &options->pagedata)) {
+ append_log("[Bilibili] Download pagedata failed.\n");
+ return;
+ }
+ bilibili_options.html = options->pagedata;
+
+ if (get_multipagedata(options->pagedata, &multipage_struct,
+ &bilibili_options.is_page)) {
+ multipage_cleanup(&multipage_struct);
+ append_log("[Bilibili] Parse pagedata failed.\n");
+ return;
+ };
+
+ if (get_page_in_query(options->query, &p) || p < 1 ||
+ p > multipage_struct.videoData.pages.n) {
+ multipage_cleanup(&multipage_struct);
+ append_log("[Bilibili] Parse query failed.\n");
+ return;
+ }
+
+ Video_pages_data *page =
+ get_element(&multipage_struct.videoData.pages, p - 1);
+
+ bilibili_options.aid = multipage_struct.aid;
+ bilibili_options.bvid = multipage_struct.bvid;
+ bilibili_options.cid = page->cid;
+ bilibili_options.page = p;
+ bilibili_options.title = multipage_struct.videoData.title;
+
+ DEBUG_PRINT("aid: %d\n", bilibili_options.aid);
+ DEBUG_PRINT("bvid: %s\n", bilibili_options.bvid);
+ DEBUG_PRINT("cid: %d\n", bilibili_options.cid);
+ DEBUG_PRINT("is_page: %s\n", bilibili_options.is_page ? "yes" : "no");
+ DEBUG_PRINT("page: %d\n", bilibili_options.page);
+ DEBUG_PRINT("title: %s\n", bilibili_options.title);
+
+ if (generate_api(&bilibili_options, 127)) {
+ free_and_nullify(bilibili_options.api);
+ multipage_cleanup(&multipage_struct);
+ return;
+ }
+ DEBUG_PRINT("Generated API: %s\n", bilibili_options.api);
+
+ if (download(&bilibili_options)) {
+ free_and_nullify(bilibili_options.api);
+ multipage_cleanup(&multipage_struct);
+ return;
+ }
+
+ free_and_nullify(bilibili_options.api);
+ multipage_cleanup(&multipage_struct);
+}
diff --git a/src/extractors/bilibili.h b/src/extractors/bilibili.h
new file mode 100644
index 0000000..5d609b0
--- /dev/null
+++ b/src/extractors/bilibili.h
@@ -0,0 +1,94 @@
+#ifndef BILIBILI_H_
+#define BILIBILI_H_
+
+#include "../utils.h"
+#include "extractor.h"
+#include <stddef.h>
+
+#define BILIBILI_API "https://api.bilibili.com/x/player/playurl?"
+#define BILIBILI_BANGUMI_API "https://api.bilibili.com/pgc/player/web/playurl?"
+#define BILIBILI_TOKEN_API "https://api.bilibili.com/x/player/playurl/token?"
+
+typedef struct video_pages_data {
+ int cid;
+ char *part;
+ int page;
+} Video_pages_data;
+
+typedef struct multipage_video_data {
+ char *title;
+ generic_array_t pages;
+} Multipage_video_data;
+
+typedef struct episode {
+ int aid;
+ char *bvid;
+ int cid;
+ char *title;
+} Episode;
+
+typedef struct multi_episode_data {
+ int season_id;
+ generic_array_t episodes;
+} Multi_episode_data;
+
+typedef struct multipage {
+ int aid;
+ char *bvid;
+ generic_array_t sections;
+ Multipage_video_data videoData;
+ cJSON *json;
+} Multipage;
+
+typedef struct bilibili_options {
+ char *url;
+ char *html;
+ char *api;
+ char *cookie;
+ bool is_bangumi;
+ bool is_page;
+ int aid;
+ int cid;
+ char *bvid;
+ int page;
+ char *title;
+} Bilibili_options;
+
+typedef struct durl {
+ char *url;
+ size_t size;
+} Durl;
+
+typedef struct dash_stream {
+ int id;
+ char *baseUrl;
+ int bandwidth;
+ char *mimeType;
+ int codecid;
+ char *codecs;
+} Dash_stream;
+
+typedef struct dash_streams {
+ generic_array_t video;
+ generic_array_t audio;
+} Dash_streams;
+
+typedef struct dash_info {
+ int quality;
+ str_array_t accept_description;
+ generic_array_t accept_quality;
+ Dash_streams dash;
+ char *format;
+ generic_array_t durl;
+} Dash_info;
+
+typedef struct dash {
+ int code;
+ char *message;
+ Dash_info dashinfo;
+ cJSON *json;
+} Dash;
+
+void bilibili_extract(struct options *);
+
+#endif
diff --git a/src/extractors/extractor.c b/src/extractors/extractor.c
new file mode 100644
index 0000000..91f34d2
--- /dev/null
+++ b/src/extractors/extractor.c
@@ -0,0 +1,24 @@
+#include <stdlib.h>
+
+#include "bilibili.h"
+#include "extractor.h"
+
+Site_map site_map = {{{"www.bilibili.com", SITE_BILIBILI}}, 1};
+
+void options_cleanup(Options *options) {
+ free_and_nullify(options->URL);
+ free_and_nullify(options->path);
+ free_and_nullify(options->query);
+ free_and_nullify(options->pagedata);
+}
+
+int extract(void *v) {
+ Options *options = (Options *)v;
+ switch (options->site) {
+ case SITE_BILIBILI:
+ bilibili_extract(options);
+ break;
+ }
+ options_cleanup(options);
+ return 0;
+}
diff --git a/src/extractors/extractor.h b/src/extractors/extractor.h
new file mode 100644
index 0000000..d3ebeec
--- /dev/null
+++ b/src/extractors/extractor.h
@@ -0,0 +1,32 @@
+#ifndef EXTRACTOR_H_
+#define EXTRACTOR_H_
+
+#include <cjson/cJSON.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+enum site { SITE_BILIBILI };
+typedef enum site site_t;
+
+typedef struct site_map {
+ struct {
+ char domain[SHRT_MAX];
+ site_t site;
+ } pairs[1];
+ unsigned char size;
+} Site_map;
+
+typedef struct options {
+ site_t site;
+ char *URL;
+ char *path;
+ char *query;
+ char *pagedata;
+} Options;
+
+void options_cleanup(Options*);
+
+int extract(void *);
+
+#endif
diff --git a/src/logger.c b/src/logger.c
new file mode 100644
index 0000000..0fef20a
--- /dev/null
+++ b/src/logger.c
@@ -0,0 +1,32 @@
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "logger.h"
+#include "nuklear.h"
+
+static struct logger logger = {0};
+
+struct logger *setup_logger(void) {
+ return &logger;
+}
+
+void append_log(const char *fmt, ...) {
+ va_list ap1;
+ va_start(ap1, fmt);
+ va_list ap2;
+ va_copy(ap2, ap1);
+ char buf[vsnprintf(NULL, 0, fmt, ap1) + 1];
+ va_end(ap1);
+ vsnprintf(buf, sizeof(buf), fmt, ap2);
+ va_end(ap2);
+ nk_str_append_str_char(&logger.text_edit->string, buf);
+ logger.box_lines++;
+ logger.scrollbar->y += logger.extend_box ? (logger.font_height + 2) : 0;
+ DEBUG_PRINT("scrollbar w: %u, h: %u\n", logger.scrollbar->x,
+ logger.scrollbar->y);
+}
+
+void clear_log() {
+ nk_textedit_delete(logger.text_edit, 0, logger.text_edit->string.len);
+ logger.box_lines = 0;
+}
diff --git a/src/logger.h b/src/logger.h
new file mode 100644
index 0000000..dbc059f
--- /dev/null
+++ b/src/logger.h
@@ -0,0 +1,30 @@
+#ifndef LOGGER_H_
+#define LOGGER_H_
+
+#include <stdbool.h>
+
+#ifdef DEBUG
+#define DEBUG_PRINT(fmt, args...) \
+ fprintf(stderr, "DEBUG: %s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, \
+ ##args)
+#else
+#define DEBUG_PRINT(fmt, args...) /* Don't do anything in release builds */
+#endif
+
+#define LOG(component, fmt, args...) append_log("[%s] " fmt, component, ##args)
+
+struct logger {
+ struct nk_text_edit *text_edit;
+ unsigned long box_lines;
+ struct nk_scroll *scrollbar;
+ bool extend_box;
+ int font_height;
+};
+
+struct logger *setup_logger(void);
+
+void append_log(const char *fmt, ...);
+
+void clear_log();
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..d60112e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,163 @@
+#include <GL/glew.h>
+#include <GLFW/glfw3.h>
+#include <curl/curl.h>
+#include <nfd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MAX_VERTEX_BUFFER 512 * 1024
+#define MAX_ELEMENT_BUFFER 128 * 1024
+#define NK_INCLUDE_FIXED_TYPES
+#define NK_INCLUDE_STANDARD_IO
+#define NK_INCLUDE_STANDARD_VARARGS
+#define NK_INCLUDE_DEFAULT_ALLOCATOR
+#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
+#define NK_INCLUDE_FONT_BAKING
+#define NK_INCLUDE_DEFAULT_FONT
+#define NK_IMPLEMENTATION
+#define NK_GLFW_GL3_IMPLEMENTATION
+#define NK_KEYSTATE_BASED_INPUT
+#include "nuklear.h"
+#include "nuklear_glfw_gl3.h"
+
+#include "constants.h"
+#include "unifont.h"
+#include "logger.h"
+#include "main.h"
+#include "process_url.h"
+#include "style.h"
+#include "ui.h"
+
+extern int win_width, win_height;
+extern void load_ui(struct ui_struct *);
+static void error_callback(int e, const char *d) {
+ printf("Error %d: %s\n", e, d);
+}
+
+int main(void) {
+ struct ui_struct ui;
+
+ /* Curl */
+ curl_init();
+
+ /* GLFW */
+ struct nk_glfw glfw = {0};
+ static GLFWwindow *win;
+ glfwSetErrorCallback(error_callback);
+ if (!glfwInit()) {
+ fprintf(stdout, "[GFLW] failed to init!\n");
+ exit(EXIT_FAILURE);
+ }
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+#ifdef __APPLE__
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
+#endif
+
+ win = glfwCreateWindow(win_width, win_height, APP_NAME, NULL, NULL);
+
+ if (!win) {
+ // Window creation failed
+ fprintf(stdout, "[GLFW] failed to create window. Does this platform "
+ "support OpenGL 3.3+?\n");
+ printf("GL_VERSION_3_3: %s",
+ glewIsSupported("GL_VERSION_3_3") ? "yes" : "no");
+ exit(EXIT_FAILURE);
+ }
+
+ glfwMakeContextCurrent(win);
+
+ /* Glew */
+ glewExperimental = GL_TRUE;
+ GLenum err;
+ if ((err = glewInit()) != GLEW_OK) {
+ /* Problem: glewInit failed, something is seriously wrong. */
+ fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
+ exit(EXIT_FAILURE);
+ }
+
+ /* Native File Dialog */
+ NFD_Init();
+
+ /* Logger setup */
+ struct logger *logger = setup_logger();
+ // Put it in ui_struct
+ ui.logger = logger;
+
+ /* UI stat */
+ status_t stat = {0};
+ stat.is_done = true;
+ ui.stat = &stat;
+
+ /* create context */
+ ui.ctx = nk_glfw3_init(&glfw, win, NK_GLFW3_INSTALL_CALLBACKS);
+
+ /* Font */
+ ui.logger->font_height = 24;
+ {
+ struct nk_font_atlas *atlas;
+ struct nk_font_config cfg = nk_font_config(0);
+ /* NOTICE: Some CJK fonts may have incorrect glyph indexes.
+ https://github.com/Immediate-Mode-UI/Nuklear/issues/399
+ https://github.com/Immediate-Mode-UI/Nuklear/issues/542
+ FIX: https://github.com/Immediate-Mode-UI/Nuklear/pull/531
+ */
+ cfg.range = nk_font_chinese_glyph_ranges();
+ // const nk_rune ranges[] = {0x4E00, 0x9FAF, 0x0020, 0x00FF, 0x3000,
+ // 0x30FF, 0x31F0, 0x31FF, 0xFF00, 0xFFEF,
+ //
+ // 0};
+ // cfg.range = ranges;
+ cfg.oversample_h = 1;
+ cfg.oversample_v = 1;
+ nk_glfw3_font_stash_begin(&glfw, &atlas);
+ atlas->default_font = nk_font_atlas_add_compressed_base85(
+ atlas, unifont_compressed_data_base85, ui.logger->font_height, &cfg);
+ nk_glfw3_font_stash_end(&glfw);
+ }
+
+ /* TextEdit */
+ struct nk_text_edit text_edit;
+ nk_textedit_init_default(&text_edit);
+
+ logger->text_edit = &text_edit;
+
+ /* Scrollbar */
+ struct nk_scroll scrollbar = {0};
+ logger->scrollbar = &scrollbar;
+
+ /* Theming */
+ set_style(ui.ctx, THEME_DARK);
+
+ while (!glfwWindowShouldClose(win)) {
+ /* Input */
+ glfwPollEvents();
+ nk_glfw3_new_frame(&glfw);
+
+ /* Response to window resize */
+ glfwGetWindowSize(win, &win_width, &win_height);
+ nk_window_set_bounds(ui.ctx, APP_NAME,
+ nk_rect(0, 0, win_width, win_height));
+
+ /* GUI */
+ if (nk_begin(ui.ctx, APP_NAME, nk_rect(0, 0, win_width, win_height),
+ NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_TITLE)) {
+ load_ui(&ui);
+ }
+ nk_end(ui.ctx);
+
+ /* Draw */
+ glfwGetWindowSize(win, &win_width, &win_height);
+ glViewport(0, 0, win_width, win_height);
+ glClear(GL_COLOR_BUFFER_BIT);
+ nk_glfw3_render(&glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER,
+ MAX_ELEMENT_BUFFER);
+ glfwSwapBuffers(win);
+ }
+ NFD_Quit();
+ curl_cleanup(&stat);
+ nk_glfw3_shutdown(&glfw);
+ glfwTerminate();
+ return 0;
+}
diff --git a/src/main.h b/src/main.h
new file mode 100644
index 0000000..6a319d4
--- /dev/null
+++ b/src/main.h
@@ -0,0 +1,8 @@
+#ifndef MAIN_H_
+#define MAIN_H_
+
+#include <stdio.h>
+
+int win_width = 800, win_height = 600;
+
+#endif
diff --git a/src/process_url.c b/src/process_url.c
new file mode 100644
index 0000000..4bfce8d
--- /dev/null
+++ b/src/process_url.c
@@ -0,0 +1,526 @@
+#include <curl/curl.h>
+#include <curl/easy.h>
+#include <curl/header.h>
+#include <curl/system.h>
+#include <curl/urlapi.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include "c11threads.h"
+#else
+#include <threads.h>
+#endif
+
+#include "nuklear.h"
+
+#include "constants.h"
+#include "extractors/extractor.h"
+#include "logger.h"
+#include "process_url.h"
+
+/* NOTICE: the global curl_conf pointer will only stay valid during downloading,
+ * otherwise, ALWAYS point it to NULL. */
+static curl_conf_t *curl_conf;
+extern Site_map site_map;
+Options options;
+static queue_t dl_queue;
+
+thrd_t tid[MAX_THREAD];
+mtx_t mtx;
+cnd_t cnd;
+bool corrupted;
+static const char *outdir_g, *referer_g;
+static CURLU *h;
+
+/*NOTE: Use logger(X) (defined as a generic macro) to log errors. */
+static bool logerr_b(CURLcode r) {
+ if (r && !corrupted) {
+ LOG("libcurl", "Error %d: %s\n", r, ERRTOSTRING(r));
+ corrupted = true;
+ }
+ return r;
+}
+
+static bool logerr_h(CURLHcode r) {
+ if (r) {
+ const char *err_str;
+ switch (r) {
+ case CURLHE_BADINDEX:
+ err_str = "header exists but not with this index";
+ break;
+ case CURLHE_MISSING:
+ // Allow no headers
+ err_str = "no such header exists";
+ DEBUG_PRINT("Header Error %d: %s\n", r, err_str);
+ return r;
+ break;
+ case CURLHE_NOHEADERS:
+ err_str = "no headers at all exist (yet)";
+ break;
+ case CURLHE_NOREQUEST:
+ err_str = "no request with this number was used";
+ break;
+ case CURLHE_OUT_OF_MEMORY:
+ err_str = "out of memory while processing";
+ break;
+ case CURLHE_BAD_ARGUMENT:
+ err_str = "a function argument was not okay";
+ break;
+ case CURLHE_NOT_BUILT_IN:
+ err_str = "if API was disabled in the build";
+ break;
+ default:
+ err_str = "unknown error";
+ break;
+ }
+ LOG("libcurl", "Header Error %d: %s\n", r, err_str);
+ corrupted = true;
+ }
+ return r;
+}
+
+static bool logerr_u(CURLUcode r) {
+ switch (r) {
+ case CURLUE_NO_QUERY:
+ // Accept no query
+ DEBUG_PRINT("The URL has no query.\n");
+ break;
+ case 0:
+ break;
+ default:
+ LOG("libcurl", "Parse Error %d: Invalid URL\n", r);
+ break;
+ }
+ return r;
+}
+
+static void curl_easy_setcommonopts(CURL *curl) {
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1L);
+ curl_easy_setopt(
+ curl, CURLOPT_USERAGENT,
+ "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0");
+ curl_easy_setopt(curl, CURLOPT_REFERER, referer_g);
+ /* enable all supported built-in compressions,
+ * since serveral sites enable gzip encoding */
+ curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
+}
+
+static int progress_callback(void *clientp, curl_off_t dltotal,
+ curl_off_t dlnow, curl_off_t ultotal,
+ curl_off_t ulnow) {
+ thrd_info_t *ti = (thrd_info_t *)clientp;
+ ti->curl_c->dlnow_per_thrd[ti->no] = dlnow;
+ if (ti->curl_c->total_thrd == 1) {
+ ti->curl_c->dltotal = dltotal;
+ }
+ // Return non-zero to abort download
+ return corrupted;
+}
+
+static size_t write2str(void *ptr, size_t size, size_t nmemb, str_data_t *s) {
+ size_t new_len = s->len + size * nmemb;
+ s->string = realloc(s->string, new_len + 1);
+ memcpy(s->string + s->len, ptr, size * nmemb);
+ s->string[new_len] = '\0';
+ s->len = new_len;
+
+ return size * nmemb;
+}
+
+static int parse_url(const char *URL, const char *outdir, char *fn) {
+ CURLUcode ue = logerr(curl_url_set(h, CURLUPART_URL, URL, 0));
+ if (ue && ue != CURLUE_NO_QUERY) {
+ return 1;
+ }
+ char *domain, *path, *query;
+
+ if (ue == CURLUE_NO_QUERY) {
+ query = NULL;
+ } else {
+ ue = logerr(curl_url_get(h, CURLUPART_QUERY, &query, 0));
+ }
+ ue = curl_url_get(h, CURLUPART_HOST, &domain, 0);
+ if (ue) {
+ return 1;
+ }
+ ue = logerr(curl_url_get(h, CURLUPART_PATH, &path, 0));
+ if (ue) {
+ return 1;
+ }
+
+ DEBUG_PRINT("Domain: %s\n", domain);
+ DEBUG_PRINT("Path: %s\n", path);
+ DEBUG_PRINT("Query: %s\n", query);
+
+ for (unsigned short i = 0; i < site_map.size; i++) {
+ if (!strcmp(domain, site_map.pairs[i].domain)) {
+ append_log("Got site: %s\n", domain);
+ thrd_t t;
+ options.site = site_map.pairs[i].site;
+ options.URL = malloc(strlen(domain) + strlen(path) + 10);
+ sprintf(options.URL, "https://%s%s", domain, path);
+ options.path = malloc(strlen(path) + 1);
+ strcpy(options.path, path);
+ if (query) {
+ options.query = malloc(strlen(query) + 1);
+ strcpy(options.query, query);
+ } else {
+ options.query = calloc(1, sizeof(char));
+ }
+
+ append_log("pagedata URL: %s\n", options.URL);
+
+ thrd_create(&t, extract, &options);
+ thrd_detach(t);
+
+ curl_free(domain);
+ curl_free(path);
+ curl_free(query);
+ return 0;
+ };
+ }
+
+ curl_conf_t *curl_c = malloc(sizeof(curl_conf_t));
+ curl_c->URL = malloc(strlen(URL) + 1);
+ strcpy(curl_c->URL, URL);
+
+ /* filename */
+
+ if (fn == NULL) {
+ const char *patterns_str[1] = {"(?:.+\\/)([^#/?]+)"};
+ str_array_t results = create_str_array(0);
+ const str_array_t patterns = {(char **)patterns_str, 1};
+ regex_match(path, patterns, &results);
+ for (unsigned short i = 0; i < results.n; i++) {
+ if (results.str[i]) {
+ DEBUG_PRINT("[%d] %s\n", i, results.str[i]);
+ sprintf(curl_c->outfn, "%s%s%s", outdir,
+ outdir[strlen(outdir) - 1] == SPLITTER_CHAR ? "" : SPLITTER_STR,
+ results.str[i]);
+ }
+ }
+ free_str_array(&results);
+ if (curl_c->outfn[0] == '\0') {
+ // sprintf(curl_c->outfn, "%s%c%s", outdir, SPLITTER,
+ // "test");
+ LOG("libcurl",
+ "Infer filename failed, please specify a valid filename.\n");
+ curl_free(domain);
+ curl_free(path);
+ curl_free(query);
+ return 1;
+ }
+ } else {
+ sprintf(curl_c->outfn, "%s%s%s", outdir,
+ outdir[strlen(outdir) - 1] == SPLITTER_CHAR ? "" : SPLITTER_STR,
+ fn);
+ free_and_nullify(fn);
+ }
+ DEBUG_PRINT("File will be saved as: %s\n", curl_c->outfn);
+ DEBUG_PRINT("Got regular URL: %s\n", curl_c->URL);
+
+ enqueue(&dl_queue, (void *)curl_c);
+
+ curl_free(domain);
+ curl_free(path);
+ curl_free(query);
+
+ return 0;
+}
+
+static bool get_info(const char *URL, long *psize) {
+ CURL *curl;
+ long resp_code;
+ bool support_range = false;
+ struct curl_header *pch;
+ curl = curl_easy_init();
+ curl_easy_setopt(curl, CURLOPT_URL, URL);
+ curl_easy_setcommonopts(curl);
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL);
+ CURLcode r = curl_easy_perform(curl);
+ if (logerr(r)) {
+ curl_easy_cleanup(curl);
+ return support_range;
+ }
+ r = curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
+ (curl_off_t *)psize);
+ if (logerr(r)) {
+ curl_easy_cleanup(curl);
+ return support_range;
+ }
+ CURLHcode rh =
+ curl_easy_header(curl, "Accept-Ranges", 0, CURLH_HEADER, -1, &pch);
+ if (logerr(rh) || strcmp(pch->value, "bytes")) {
+ curl_easy_cleanup(curl);
+ return support_range;
+ }
+ char *ct = NULL;
+ r = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
+ if (logerr(r)) {
+ curl_easy_cleanup(curl);
+ return support_range;
+ }
+
+ support_range = true;
+ curl_easy_cleanup(curl);
+ return support_range;
+}
+
+static int pull_part(void *a) {
+ CURLcode res;
+ thrd_info_t *ti = (thrd_info_t *)a;
+ curl_conf_t *curl_c = ti->curl_c;
+ unsigned char n = ti->no;
+ // Here we need to manually control str_array_t
+ curl_c->partfn.str[n] = malloc(strlen(curl_c->outfn) + 4);
+ sprintf(curl_c->partfn.str[n], "%s.%d", curl_c->outfn, n);
+ DEBUG_PRINT("[THRD %hhu] partfn: %s, range: %s\n", n,
+ get_str_element(&curl_c->partfn, n), ti->range);
+ {
+ curl_c->fplist[n] = fopen(get_str_element(&curl_c->partfn, n), "wb+");
+ CURL *curl;
+
+ curl = curl_easy_init();
+ curl_easy_setopt(curl, CURLOPT_URL, curl_c->URL);
+ curl_easy_setcommonopts(curl);
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
+ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 30L);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_c->fplist[n]);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
+ if (ti->curl_c->total_thrd != 1) {
+ curl_easy_setopt(curl, CURLOPT_RANGE, ti->range);
+ }
+ curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback);
+ curl_easy_setopt(curl, CURLOPT_XFERINFODATA, ti);
+ res = curl_easy_perform(curl);
+ rewind(curl_c->fplist[n]);
+ append_log("[THRD %hhu] File downloaded.\n", n);
+ curl_easy_cleanup(curl);
+ logerr(res);
+ }
+ mtx_lock(&mtx);
+ curl_c->success_thrd += 1;
+ cnd_signal(&cnd); // Unblocks the waiting cleanup thread. If no threads are
+ // blocked, does nothing and returns thrd_success.
+
+ mtx_unlock(&mtx);
+ return (int)res;
+}
+
+static int merge_and_cleanup(curl_conf_t *curl_c) {
+ if (corrupted) {
+ append_log("Cancelling...\n");
+ } else {
+ append_log("Merging files...\n");
+ }
+
+ FILE *fop;
+ fop = fopen(curl_c->outfn, "wb");
+ if (fop == NULL) {
+ // User quitted before downloading, return directly
+ return 1;
+ }
+ for (unsigned short i = 0; i < curl_c->total_thrd; i++) {
+ if (!corrupted) {
+ char buffer[1024];
+ size_t bytesRead = 0;
+ while ((bytesRead = fread(buffer, 1, sizeof(buffer), curl_c->fplist[i])) >
+ 0) {
+ fwrite(buffer, 1, bytesRead, fop);
+ }
+ }
+ fclose(curl_c->fplist[i]);
+ if (remove(get_str_element(&curl_c->partfn, i)) != 0) {
+ append_log("Error deleting partial file %s\n",
+ get_str_element(&curl_c->partfn, i));
+ }
+ }
+ fclose(fop);
+
+ if (corrupted) {
+ // Also delete dst file
+ if (remove(curl_c->outfn) != 0) {
+ append_log("Error deleting file %s\n", curl_c->outfn);
+ }
+ }
+ // Reset stat
+ corrupted = false;
+ curl_c->success_thrd = 0;
+ curl_c->total_thrd = 0;
+ free_and_nullify(curl_c->URL);
+
+ return 0;
+}
+
+static int download(curl_conf_t *curl_c) {
+ /* Reset thrd info. */
+ // if (curl_c->success_thrd == curl_c->total_thrd) {
+ curl_c->success_thrd = 0;
+ // }
+
+ CURL *curl;
+ curl_off_t cl = 0L, begin = 0L, end;
+
+ static thrd_info_t thrd_info[MAX_THREAD] = {0};
+
+ bool support_range = get_info(curl_c->URL, &cl);
+ DEBUG_PRINT("Size: %ld bytes.\n", cl);
+ if (support_range && cl > 0L) {
+ curl_c->dltotal = cl;
+ curl_c->total_thrd = (unsigned char)CEIL_DIV(cl, MAX_THREAD_SIZE);
+ if (curl_c->total_thrd > MAX_THREAD) {
+ curl_c->total_thrd = MAX_THREAD;
+ }
+ LOG("libcurl", "Server supports range header, setting threads to %hhu\n",
+ curl_c->total_thrd);
+ } else {
+ LOG("libcurl", "Server doesn't claim range header "
+ "support, falling back to single thread.\n");
+ curl_c->total_thrd = 1;
+ }
+ curl_off_t size_per_thrd = (cl / curl_c->total_thrd);
+
+ curl_c->partfn = create_str_array(curl_c->total_thrd);
+
+ for (unsigned char i = 0; i < curl_c->total_thrd; i++) {
+ curl_off_t chunk_size;
+ thrd_info[i].no = i;
+ if (i + 1 == curl_c->total_thrd)
+ chunk_size = cl - (curl_c->total_thrd - 1) * size_per_thrd;
+ else
+ chunk_size = size_per_thrd;
+ end = begin + chunk_size - 1;
+ if (curl_c->total_thrd != 1) {
+ sprintf(thrd_info[i].range,
+ "%" CURL_FORMAT_CURL_OFF_T "-%" CURL_FORMAT_CURL_OFF_T, begin,
+ end);
+ }
+ thrd_info[i].curl_c = curl_c;
+ int error = thrd_create(&tid[i], pull_part, &thrd_info[i]);
+ if (error)
+ append_log("Couldn't run thread number %d, errno %d\n", i, error);
+ begin = end + 1;
+ }
+ return 0;
+}
+
+void curl_init(curl_conf_t *curl) {
+ curl_global_init(CURL_GLOBAL_ALL);
+ h = curl_url();
+ dl_queue = create_queue();
+ mtx_init(&mtx, mtx_plain);
+ cnd_init(&cnd);
+}
+
+void curl_cleanup(status_t *stat) {
+ /* We only need to cleanup
+ * the currently active thread. */
+ if (curl_conf) {
+
+ corrupted = true; // In case libcurl is still downloading
+ /* Now Wait for all threads to cancel... */
+ mtx_lock(&mtx);
+ while (curl_conf->success_thrd != curl_conf->total_thrd) {
+ cnd_wait(&cnd, &mtx);
+ }
+ mtx_unlock(&mtx);
+ if (!stat->is_done) {
+ merge_and_cleanup(curl_conf);
+ }
+ mtx_destroy(&mtx);
+ cnd_destroy(&cnd);
+ }
+ free_queue(&dl_queue);
+ curl_url_cleanup(h);
+ curl_global_cleanup();
+}
+
+void poll_status(status_t *stat) {
+ if (!is_empty_queue(&dl_queue) && stat->is_done) {
+ /* extract_done is a flag used to signal that
+ * the extractor process is done. */
+ curl_conf = (curl_conf_t *)dequeue(&dl_queue);
+ if (download(curl_conf)) {
+ // Something went wrong when creating download task
+ DEBUG_PRINT("Creating download task failed.\n");
+ };
+ stat->is_done = false;
+ }
+ if (curl_conf) {
+ curl_conf->dlnow = 0L;
+ for (unsigned char i = 0; i < curl_conf->total_thrd; i++) {
+ curl_conf->dlnow += curl_conf->dlnow_per_thrd[i];
+ }
+ stat->cur = curl_conf->dlnow;
+ stat->total = curl_conf->dltotal;
+ DEBUG_PRINT("success_thrd: %hhu, total_thrd: %hhu, is_done: %s\n",
+ curl_conf->success_thrd, curl_conf->total_thrd,
+ stat->is_done ? "yes" : "no");
+ mtx_lock(&mtx);
+ if (curl_conf->success_thrd == curl_conf->total_thrd &&
+ (curl_conf->total_thrd && !stat->is_done)) {
+ stat->is_done = true;
+ for (unsigned short i = 0; i < curl_conf->total_thrd; i++) {
+ int r;
+ thrd_join(tid[i], &r);
+ }
+ merge_and_cleanup(curl_conf);
+ append_log("Download %s finished.\n", curl_conf->outfn);
+ curl_conf = NULL;
+ }
+ mtx_unlock(&mtx);
+ }
+}
+
+int get(const char *URL, char **pdstr) {
+ CURL *curl = curl_easy_init();
+ str_data_t pagedata = {0};
+ pagedata.string = malloc(1);
+ pagedata.string[0] = '\0';
+ curl_easy_setopt(curl, CURLOPT_URL, URL);
+ curl_easy_setcommonopts(curl);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write2str);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&pagedata);
+ CURLcode res = logerr(curl_easy_perform(curl));
+ *pdstr = malloc(pagedata.len + 1);
+ strcpy(*pdstr, pagedata.string);
+ curl_easy_cleanup(curl);
+ return res;
+}
+
+/* Add an URL to dl_queue.
+ * - If outdir is NULL or a empty string, reuse the cached outdir_g
+ * - If fn is NULL or a empty string, infer the filename from URL (otherwise
+ * fail and quit')
+ * - If referer is NULL or a empty string, uses NULL */
+void add_url(const char *URL, const char *outdir, const char *fn,
+ const char *referer) {
+ if (outdir && outdir[0] != '\0') {
+ outdir_g = outdir;
+ }
+ referer_g = referer;
+ if (referer && referer[0] == '\0') {
+ referer_g = NULL;
+ }
+ DEBUG_PRINT("referer_g: %s\n", referer_g);
+
+ char *filename;
+ if (fn == NULL || fn[0] == '\0') {
+ filename = NULL;
+ } else {
+ filename = malloc(strlen(fn) + 1);
+ strcpy(filename, fn);
+ }
+
+ // Pass our cache (outdir_g) to parse_url()
+ if (parse_url(URL, outdir_g, filename)) {
+ DEBUG_PRINT("parse_url() failed with error.\n");
+ return; // Parse failed, quit the task directly
+ };
+}
diff --git a/src/process_url.h b/src/process_url.h
new file mode 100644
index 0000000..03023b5
--- /dev/null
+++ b/src/process_url.h
@@ -0,0 +1,59 @@
+#ifndef PROCESS_URL_H_
+#define PROCESS_URL_H_
+
+#include <curl/curl.h>
+#include <limits.h>
+#include <stdbool.h>
+
+#include "utils.h"
+
+#define MAX_THREAD 6
+#define MAX_THREAD_SIZE 10485760
+
+#define ERRTOSTRING(err) curl_easy_strerror(err)
+#define logerr(X) \
+ _Generic((X), CURLcode \
+ : logerr_b, CURLHcode \
+ : logerr_h, CURLUcode \
+ : logerr_u)(X)
+
+typedef struct curl_conf {
+ curl_off_t dlnow_per_thrd[MAX_THREAD];
+ curl_off_t dlnow;
+ curl_off_t dltotal;
+ unsigned char success_thrd;
+ unsigned char total_thrd;
+ char *URL;
+ char outfn[USHRT_MAX];
+ str_array_t partfn;
+ FILE *fplist[MAX_THREAD];
+} curl_conf_t;
+
+typedef struct thrd_info {
+ unsigned char no;
+ curl_conf_t *curl_c;
+ char range[UCHAR_MAX];
+} thrd_info_t;
+
+typedef struct status {
+ curl_off_t cur;
+ curl_off_t total;
+ bool is_done;
+} status_t;
+
+typedef struct str_data {
+ char *string;
+ size_t len;
+} str_data_t;
+
+void curl_init();
+
+void curl_cleanup(status_t *);
+
+void poll_status(status_t *);
+
+int get(const char *, char **);
+
+void add_url(const char *, const char *, const char *, const char *);
+
+#endif
diff --git a/src/style.h b/src/style.h
new file mode 100644
index 0000000..cb7387b
--- /dev/null
+++ b/src/style.h
@@ -0,0 +1,133 @@
+/*
+ https://github.com/Immediate-Mode-UI/Nuklear/blob/b4b94b0486c0c43045d0176d03cce016190fe3ff/demo/common/style.c
+*/
+enum theme {THEME_BLACK, THEME_WHITE, THEME_RED, THEME_BLUE, THEME_DARK};
+
+static void
+set_style(struct nk_context *ctx, enum theme theme)
+{
+ struct nk_color table[NK_COLOR_COUNT];
+ if (theme == THEME_WHITE) {
+ table[NK_COLOR_TEXT] = nk_rgba(70, 70, 70, 255);
+ table[NK_COLOR_WINDOW] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_HEADER] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_BORDER] = nk_rgba(0, 0, 0, 255);
+ table[NK_COLOR_BUTTON] = nk_rgba(185, 185, 185, 255);
+ table[NK_COLOR_BUTTON_HOVER] = nk_rgba(170, 170, 170, 255);
+ table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(160, 160, 160, 255);
+ table[NK_COLOR_TOGGLE] = nk_rgba(150, 150, 150, 255);
+ table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(120, 120, 120, 255);
+ table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_SELECT] = nk_rgba(190, 190, 190, 255);
+ table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_SLIDER] = nk_rgba(190, 190, 190, 255);
+ table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(80, 80, 80, 255);
+ table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(70, 70, 70, 255);
+ table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(60, 60, 60, 255);
+ table[NK_COLOR_PROPERTY] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_EDIT] = nk_rgba(150, 150, 150, 255);
+ table[NK_COLOR_EDIT_CURSOR] = nk_rgba(0, 0, 0, 255);
+ table[NK_COLOR_COMBO] = nk_rgba(175, 175, 175, 255);
+ table[NK_COLOR_CHART] = nk_rgba(160, 160, 160, 255);
+ table[NK_COLOR_CHART_COLOR] = nk_rgba(45, 45, 45, 255);
+ table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255);
+ table[NK_COLOR_SCROLLBAR] = nk_rgba(180, 180, 180, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(140, 140, 140, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(150, 150, 150, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(160, 160, 160, 255);
+ table[NK_COLOR_TAB_HEADER] = nk_rgba(180, 180, 180, 255);
+ nk_style_from_table(ctx, table);
+ } else if (theme == THEME_RED) {
+ table[NK_COLOR_TEXT] = nk_rgba(190, 190, 190, 255);
+ table[NK_COLOR_WINDOW] = nk_rgba(30, 33, 40, 215);
+ table[NK_COLOR_HEADER] = nk_rgba(181, 45, 69, 220);
+ table[NK_COLOR_BORDER] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_BUTTON] = nk_rgba(181, 45, 69, 255);
+ table[NK_COLOR_BUTTON_HOVER] = nk_rgba(190, 50, 70, 255);
+ table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(195, 55, 75, 255);
+ table[NK_COLOR_TOGGLE] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(45, 60, 60, 255);
+ table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(181, 45, 69, 255);
+ table[NK_COLOR_SELECT] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(181, 45, 69, 255);
+ table[NK_COLOR_SLIDER] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(181, 45, 69, 255);
+ table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(186, 50, 74, 255);
+ table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(191, 55, 79, 255);
+ table[NK_COLOR_PROPERTY] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_EDIT] = nk_rgba(51, 55, 67, 225);
+ table[NK_COLOR_EDIT_CURSOR] = nk_rgba(190, 190, 190, 255);
+ table[NK_COLOR_COMBO] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_CHART] = nk_rgba(51, 55, 67, 255);
+ table[NK_COLOR_CHART_COLOR] = nk_rgba(170, 40, 60, 255);
+ table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255);
+ table[NK_COLOR_SCROLLBAR] = nk_rgba(30, 33, 40, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(64, 84, 95, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(70, 90, 100, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(75, 95, 105, 255);
+ table[NK_COLOR_TAB_HEADER] = nk_rgba(181, 45, 69, 220);
+ nk_style_from_table(ctx, table);
+ } else if (theme == THEME_BLUE) {
+ table[NK_COLOR_TEXT] = nk_rgba(20, 20, 20, 255);
+ table[NK_COLOR_WINDOW] = nk_rgba(202, 212, 214, 215);
+ table[NK_COLOR_HEADER] = nk_rgba(137, 182, 224, 220);
+ table[NK_COLOR_BORDER] = nk_rgba(140, 159, 173, 255);
+ table[NK_COLOR_BUTTON] = nk_rgba(137, 182, 224, 255);
+ table[NK_COLOR_BUTTON_HOVER] = nk_rgba(142, 187, 229, 255);
+ table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(147, 192, 234, 255);
+ table[NK_COLOR_TOGGLE] = nk_rgba(177, 210, 210, 255);
+ table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(182, 215, 215, 255);
+ table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(137, 182, 224, 255);
+ table[NK_COLOR_SELECT] = nk_rgba(177, 210, 210, 255);
+ table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(137, 182, 224, 255);
+ table[NK_COLOR_SLIDER] = nk_rgba(177, 210, 210, 255);
+ table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(137, 182, 224, 245);
+ table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(142, 188, 229, 255);
+ table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(147, 193, 234, 255);
+ table[NK_COLOR_PROPERTY] = nk_rgba(210, 210, 210, 255);
+ table[NK_COLOR_EDIT] = nk_rgba(210, 210, 210, 225);
+ table[NK_COLOR_EDIT_CURSOR] = nk_rgba(20, 20, 20, 255);
+ table[NK_COLOR_COMBO] = nk_rgba(210, 210, 210, 255);
+ table[NK_COLOR_CHART] = nk_rgba(210, 210, 210, 255);
+ table[NK_COLOR_CHART_COLOR] = nk_rgba(137, 182, 224, 255);
+ table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255);
+ table[NK_COLOR_SCROLLBAR] = nk_rgba(190, 200, 200, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(64, 84, 95, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(70, 90, 100, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(75, 95, 105, 255);
+ table[NK_COLOR_TAB_HEADER] = nk_rgba(156, 193, 220, 255);
+ nk_style_from_table(ctx, table);
+ } else if (theme == THEME_DARK) {
+ table[NK_COLOR_TEXT] = nk_rgba(210, 210, 210, 255);
+ table[NK_COLOR_WINDOW] = nk_rgba(57, 67, 71, 215);
+ table[NK_COLOR_HEADER] = nk_rgba(51, 51, 56, 220);
+ table[NK_COLOR_BORDER] = nk_rgba(46, 46, 46, 255);
+ table[NK_COLOR_BUTTON] = nk_rgba(48, 83, 111, 255);
+ table[NK_COLOR_BUTTON_HOVER] = nk_rgba(58, 93, 121, 255);
+ table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(63, 98, 126, 255);
+ table[NK_COLOR_TOGGLE] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(45, 53, 56, 255);
+ table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(48, 83, 111, 255);
+ table[NK_COLOR_SELECT] = nk_rgba(57, 67, 61, 255);
+ table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(48, 83, 111, 255);
+ table[NK_COLOR_SLIDER] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(48, 83, 111, 245);
+ table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(53, 88, 116, 255);
+ table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(58, 93, 121, 255);
+ table[NK_COLOR_PROPERTY] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_EDIT] = nk_rgba(50, 58, 61, 225);
+ table[NK_COLOR_EDIT_CURSOR] = nk_rgba(210, 210, 210, 255);
+ table[NK_COLOR_COMBO] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_CHART] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_CHART_COLOR] = nk_rgba(48, 83, 111, 255);
+ table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba(255, 0, 0, 255);
+ table[NK_COLOR_SCROLLBAR] = nk_rgba(50, 58, 61, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(48, 83, 111, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(53, 88, 116, 255);
+ table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(58, 93, 121, 255);
+ table[NK_COLOR_TAB_HEADER] = nk_rgba(48, 83, 111, 255);
+ nk_style_from_table(ctx, table);
+ } else {
+ nk_style_default(ctx);
+ }
+}
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..7f1dc52
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,98 @@
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "nuklear.h"
+#include <nfd.h>
+
+#include "constants.h"
+#include "logger.h"
+#include "process_url.h"
+#include "ui.h"
+
+static nk_size pct;
+static nfdchar_t *outPath;
+
+void load_ui(struct ui_struct *ui) {
+ static char text[USHRT_MAX], box_buffer[UINT16_MAX], status_string[UCHAR_MAX];
+ float logGroup_height =
+ nk_window_get_height(ui->ctx) - 80 - 45 - 50 - 35 - 80;
+ ui->logger->extend_box =
+ (logGroup_height - 15) <
+ ((ui->logger->box_lines + 1) * (ui->logger->font_height + 2));
+
+ poll_status(ui->stat);
+ if (ui->stat->total) {
+ pct = (ui->stat->cur) * 100 / (ui->stat->total);
+ sprintf(status_string, "%lu/%lu, %lu%%", ui->stat->cur, ui->stat->total,
+ pct);
+ }
+ if (ui->stat->is_done) {
+ (ui->stat->total) = 0; // To prevent nuklear further updating status_string
+ }
+
+ nk_layout_row_dynamic(ui->ctx, 80, 1);
+ nk_label(ui->ctx, "Hinata", NK_TEXT_CENTERED);
+
+ nk_layout_row_template_begin(ui->ctx, 45);
+ nk_layout_row_template_push_static(ui->ctx, 5);
+ nk_layout_row_template_push_dynamic(ui->ctx);
+ nk_layout_row_template_push_static(ui->ctx, 5);
+ nk_layout_row_template_push_static(ui->ctx, 100);
+ nk_layout_row_template_push_static(ui->ctx, 5);
+ nk_layout_row_template_end(ui->ctx);
+
+ SPACER;
+ nk_edit_string_zero_terminated(ui->ctx, NK_EDIT_FIELD | NK_EDIT_AUTO_SELECT,
+ text, USHRT_MAX - 1, nk_filter_ascii);
+ SPACER;
+ if (nk_button_label(ui->ctx, "下载")) {
+ // Clear logger text
+ clear_log();
+
+ nfdresult_t result = NFD_PickFolder(&outPath, "");
+ if (result == NFD_OKAY) {
+ DEBUG_PRINT("[NFD] outPath: %s\n", outPath);
+ } else if (result == NFD_ERROR) {
+ LOG("NFD", "Error: %s\n", NFD_GetError());
+ }
+
+ if (outPath) {
+ append_log("Got URL: %s\n", text);
+ add_url(text, outPath, NULL, NULL);
+ } else {
+ LOG("NFD", "Please specify a valid file PATH to write to!\n");
+ }
+ }
+ SPACER;
+
+ nk_layout_row_dynamic(ui->ctx, 50, 1);
+ nk_label(ui->ctx, status_string, NK_TEXT_CENTERED);
+
+ nk_layout_row_template_begin(ui->ctx, 35);
+ nk_layout_row_template_push_static(ui->ctx, 5);
+ nk_layout_row_template_push_dynamic(ui->ctx);
+ nk_layout_row_template_push_static(ui->ctx, 5);
+ nk_layout_row_template_end(ui->ctx);
+ SPACER;
+ if (nk_progress(ui->ctx, &pct, MAX_VALUE, !NK_MODIFIABLE)) {
+ // the value of the progress bar changed, the new value is stored in
+ // currentValue
+ }
+ SPACER;
+
+ nk_layout_row_dynamic(ui->ctx, logGroup_height, 1);
+ ui->ctx->style.window.group_padding = nk_vec2(0, 0);
+ if (nk_group_scrolled_begin(ui->ctx, ui->logger->scrollbar, "LogGroup", 0)) {
+ nk_layout_row_dynamic(
+ ui->ctx,
+ MAX(logGroup_height - 15,
+ (ui->logger->box_lines + 1) * (ui->logger->font_height + 2)),
+ 1);
+ nk_edit_buffer(ui->ctx, NK_EDIT_BOX | NK_EDIT_READ_ONLY | NK_EDIT_MULTILINE,
+ ui->logger->text_edit, nk_filter_default);
+ nk_group_scrolled_end(ui->ctx);
+ }
+}
diff --git a/src/ui.h b/src/ui.h
new file mode 100644
index 0000000..300e950
--- /dev/null
+++ b/src/ui.h
@@ -0,0 +1,16 @@
+#ifndef UI_H_
+#define UI_H_
+
+#define SPACER nk_spacer(ui->ctx)
+
+#include "process_url.h"
+
+struct ui_struct {
+ struct nk_context *ctx;
+ struct logger *logger;
+ status_t *stat;
+};
+
+void load_ui(struct ui_struct *);
+
+#endif
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..1132a94
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,204 @@
+#include <pcre2.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "logger.h"
+#include "utils.h"
+
+int regex_match(const char *subject, str_array_t patterns,
+ str_array_t *results) {
+ pcre2_code *re;
+
+ int errornumber;
+ int rc = PCRE2_ERROR_NOMATCH;
+
+ PCRE2_SIZE subject_len = strlen(subject);
+ PCRE2_SIZE offset = 0, erroroffset;
+ PCRE2_SIZE *ovector;
+
+ pcre2_match_data *match_data;
+ for (unsigned short i = 0; i < patterns.n; i++) {
+ DEBUG_PRINT("Gets pattern: %s\n", patterns.str[i]);
+ re = pcre2_compile((PCRE2_SPTR)patterns.str[i], PCRE2_ZERO_TERMINATED, 0,
+ &errornumber, &erroroffset, NULL);
+
+ if (re == NULL) {
+ PCRE2_UCHAR buffer[256];
+ pcre2_get_error_message(errornumber, buffer, sizeof(buffer));
+ LOG("PCRE2", "compilation failed at offset %d: %s\n", (int)erroroffset,
+ buffer);
+ return 1;
+ }
+
+ match_data = pcre2_match_data_create_from_pattern(re, NULL);
+
+ unsigned char i = 0;
+ while (offset < subject_len &&
+ (rc = pcre2_match(re, (PCRE2_SPTR)subject, (PCRE2_SIZE)subject_len,
+ offset, 0, match_data, NULL)) > 0) {
+
+ // results->str = realloc(results->str, sizeof(char *) * (rc +
+ // results->n));
+ resize_str_array(results, rc + i);
+
+ ovector = pcre2_get_ovector_pointer(match_data);
+ DEBUG_PRINT("Get %d captures.\n", rc - 1);
+ DEBUG_PRINT("Match succeeded at offset %d.\n", (int)ovector[0]);
+
+ for (unsigned short j = 1; j < rc; j++) {
+ PCRE2_SIZE substring_length = ovector[2 * j + 1] - ovector[2 * j];
+ PCRE2_SPTR substring = (PCRE2_SPTR)subject + ovector[2 * j];
+ /* Here we need to manually control the str array,
+ * as PCRE2_SPTR == const unsigned char
+ * (which cannot be directly casted) */
+ results->str[j] = malloc(substring_length + 1);
+ sprintf(results->str[j], "%.*s", (int)substring_length, substring);
+ DEBUG_PRINT("index: %2d, substring_length: %d\n", j,
+ (int)substring_length);
+ }
+ offset = ovector[1];
+ DEBUG_PRINT("offset: %zu, subject_len: %zu\n", offset, subject_len);
+ i++;
+ }
+ pcre2_match_data_free(match_data);
+ pcre2_code_free(re);
+ if (rc <= 0) {
+
+ switch (rc) {
+ case PCRE2_ERROR_NOMATCH:
+ DEBUG_PRINT("No match found.\n");
+ return 0;
+ break;
+ case 0:
+ LOG("PCRE2",
+ "ovector was not big enough for all the captured substrings\n");
+ break;
+ default:
+ LOG("PCRE2", "Matching error %d\n", rc);
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+generic_array_t create_array(size_t elem_size, size_t n) {
+ generic_array_t array;
+ array.data = n ? malloc(elem_size * n) : NULL;
+ array.elem_size = elem_size;
+ array.n = n;
+ return array;
+}
+
+void free_array(generic_array_t *array) {
+ free_and_nullify(array->data);
+ array->n = 0;
+}
+
+void resize_array(generic_array_t *array, size_t new_size) {
+ array->data = realloc(array->data, array->elem_size * new_size);
+ array->n = new_size;
+}
+
+void *get_element(generic_array_t *array, size_t index) {
+ if (index >= array->n) {
+ return NULL; // Out of bounds
+ }
+ return (char *)array->data + index * array->elem_size;
+}
+
+/* A more specific impl, specially for string (char *) */
+
+str_array_t create_str_array(size_t n) {
+ str_array_t array;
+ array.str = n ? malloc(n * sizeof(char *)) : NULL;
+ array.n = n;
+ for (size_t i = 0; i < n; i++) {
+ array.str[i] = NULL;
+ }
+ return array;
+}
+
+void free_str_array(str_array_t *array) {
+ for (size_t i = 0; i < array->n; i++) {
+ free(array->str[i]);
+ }
+ free_and_nullify(array->str);
+ array->n = 0;
+}
+
+void resize_str_array(str_array_t *array, size_t new_size) {
+ array->str = realloc(array->str, sizeof(char *) * new_size);
+ for (size_t i = array->n; i < new_size; i++) {
+ array->str[i] = NULL;
+ }
+ array->n = new_size;
+}
+
+int set_str_element(str_array_t *array, size_t index, const char *value) {
+ if (index >= array->n) {
+ return 1; // Out of bounds
+ }
+ array->str[index] = malloc(strlen(value) + 1);
+ strcpy(array->str[index], value);
+ return 0;
+}
+
+const char *get_str_element(str_array_t *array, size_t index) {
+ if (index >= array->n) {
+ return NULL; // Out of bounds
+ }
+ return array->str[index];
+}
+
+queue_t create_queue(void) {
+ queue_t queue;
+ queue.front = queue.rear = NULL;
+ return queue;
+}
+
+int is_empty_queue(queue_t *queue) { return queue->front == NULL; }
+
+void enqueue(queue_t *queue, data_t data) {
+ node_t *node = malloc(sizeof(node_t));
+ node->data = data;
+ node->next = NULL;
+ if (queue->rear == NULL) {
+ queue->rear = queue->front = node;
+ } else {
+ queue->rear->next = node;
+ queue->rear = node;
+ }
+}
+
+data_t dequeue(queue_t *queue) {
+ if (is_empty_queue(queue)) {
+ DEBUG_PRINT("Queue is empty.\n");
+ return NULL;
+ }
+
+ node_t *temp = queue->front;
+ data_t data = temp->data;
+ queue->front = temp->next;
+ free_and_nullify(temp);
+
+ if (queue->front == NULL) {
+ queue->rear = NULL;
+ }
+
+ return data;
+}
+
+void free_queue(queue_t *queue) {
+ while (!is_empty_queue(queue)) {
+ dequeue(queue);
+ }
+}
+
+void free_and_nullify(void *p) {
+ if (p) {
+ free(p);
+ p = NULL;
+ }
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..e4140a6
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,61 @@
+#ifndef UTILS_H_
+#define UTILS_H_
+
+#include <stddef.h>
+
+typedef void *data_t;
+
+typedef struct str_array {
+ char **str;
+ size_t n;
+} str_array_t;
+
+typedef struct generic_array {
+ void *data;
+ size_t elem_size;
+ size_t n;
+} generic_array_t;
+
+typedef struct node {
+ data_t data;
+ struct node *next;
+} node_t;
+
+typedef struct queue {
+ node_t *front;
+ node_t *rear;
+} queue_t;
+
+int regex_match(const char *, str_array_t, str_array_t *);
+
+generic_array_t create_array(size_t elem_size, size_t n);
+
+void free_array(generic_array_t *array);
+
+void resize_array(generic_array_t *array, size_t new_size);
+
+void *get_element(generic_array_t *array, size_t index);
+
+void free_and_nullify(void *p);
+
+str_array_t create_str_array(size_t n);
+
+void free_str_array(str_array_t *array);
+
+void resize_str_array(str_array_t *array, size_t new_size);
+
+int set_str_element(str_array_t *array, size_t index, const char *value);
+
+const char *get_str_element(str_array_t *array, size_t index);
+
+queue_t create_queue(void);
+
+int is_empty_queue(queue_t *queue);
+
+void enqueue(queue_t *queue, data_t data);
+
+data_t dequeue(queue_t *queue);
+
+void free_queue(queue_t *queue);
+
+#endif
diff --git a/xmake.lua b/xmake.lua
new file mode 100644
index 0000000..d5f481c
--- /dev/null
+++ b/xmake.lua
@@ -0,0 +1,153 @@
+add_rules("mode.debug", "mode.release", "mode.releasedbg", "mode.minsizerel")
+
+add_requires("nuklear", "nuklear_glfw_gl3", "nuklear_fonts", "glew", "glfw", "libcurl", "nativefiledialog-extended",
+ "c11threads", "pcre2",
+ "cjson")
+
+if is_plat("linux") then
+ set_toolchains("clang")
+end
+
+target("hinata")
+set_kind("binary")
+add_files("src/*.c")
+add_files("src/*/*.c")
+set_languages("c11")
+if is_mode("debug") then
+ add_defines("DEBUG")
+end
+add_packages("nuklear", "nuklear_glfw_gl3", "nuklear_fonts", "glew", "glfw", "libcurl", "nativefiledialog-extended",
+ "c11threads", "pcre2",
+ "cjson")
+
+
+package("nativefiledialog-extended")
+
+set_homepage("https://github.com/btzy/nativefiledialog-extended")
+set_description(
+ "Cross platform (Windows, Mac, Linux) native file dialog library with C and C++ bindings, based on mlabbe/nativefiledialog.")
+
+add_urls("https://github.com/btzy/nativefiledialog-extended/archive/refs/tags/$(version).zip",
+ "https://github.com/btzy/nativefiledialog-extended.git")
+add_versions("v1.1.0", "5827d17b6bddc8881406013f419c534e8459b38f34c2f266d9c1da8a7a7464bc")
+
+add_configs("portal", { description = "Use xdg-desktop-portal instead of GTK.", default = true, type = "boolean" })
+if is_plat("windows") then
+ add_configs("shared", { description = "Build shared library.", default = false, type = "boolean", readonly = true })
+end
+
+add_deps("cmake")
+if is_plat("windows") or is_plat("mingw") then
+ add_syslinks("shell32", "ole32", "uuid")
+elseif is_plat("macosx") then
+ add_frameworks("AppKit", "UniformTypeIdentifiers")
+end
+on_load("linux", function(package)
+ if package:config("portal") then
+ package:add("deps", "dbus")
+ else
+ package:add("deps", "gtk+3")
+ end
+end)
+
+on_install("windows", "macosx", "linux", "mingw", function(package)
+ local configs = { "-DNFD_BUILD_TESTS=OFF" }
+ table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release"))
+ table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
+ table.insert(configs, "-DNFD_PORTAL=" .. (package:config("portal") and "ON" or "OFF"))
+ import("package.tools.cmake").install(package, configs)
+end)
+
+on_test(function(package)
+ assert(package:check_cxxsnippets({
+ test = [[
+ void test() {
+ NFD_Init();
+ nfdchar_t *outPath = NULL;
+ nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}};
+ nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, NULL);
+ NFD_Quit();
+ }
+ ]]
+ }, { includes = "nfd.h" }))
+end)
+
+package("c11threads")
+
+set_homepage("https://github.com/jtsiomb/c11threads")
+set_description("Portable C11 threads implementation over POSIX threads and win32 threads.")
+
+add_urls("https://github.com/jtsiomb/c11threads/archive/ec95e1aa82079aefe109d18e5069b79711692064.zip")
+add_versions("1.0-ec95e1a", "9eb5eab9db4c32418eea39543d4883022c81167ea5e37f820f5af972bea7a30e")
+
+on_install("linux", "macosx", function(package)
+ io.writefile("xmake.lua", [[
+ add_rules("mode.debug", "mode.release")
+ target("c11threads")
+ set_kind("static")
+ add_headerfiles("c11threads.h")
+ ]])
+ import("package.tools.xmake").install(package)
+end)
+
+on_install("windows", "mingw", function(package)
+ io.writefile("xmake.lua", [[
+ add_rules("mode.debug", "mode.release")
+ target("c11threads")
+ set_kind("static")
+ add_files("c11threads_win32.c")
+ add_headerfiles("c11threads.h")
+ ]])
+ import("package.tools.xmake").install(package)
+end)
+
+package("nuklear_glfw_gl3")
+
+set_homepage("https://github.com/Immediate-Mode-UI/Nuklear")
+set_description("A single-header ANSI C immediate mode cross-platform GUI library (GLFW OpenGL 3 Binding)")
+set_license("MIT")
+
+add_urls("https://github.com/Immediate-Mode-UI/Nuklear/archive/refs/tags/$(version).tar.gz",
+ "https://github.com/Immediate-Mode-UI/Nuklear.git")
+
+add_versions("4.10.5", "6c80cbd0612447421fa02ad92f4207da2cd019a14d94885dfccac1aadc57926a")
+
+on_install(function(package)
+ os.cp("demo/glfw_opengl3/nuklear_glfw_gl3.h", package:installdir("include"))
+end)
+
+package("nuklear_fonts")
+
+add_urls(
+"https://gist.github.com/135e2/656614a4a86cf6f8e9aea9f0de850634/archive/8d1dc2a079cef20d97acbc2a9874b1b77b25070b.zip")
+
+add_versions("8d1dc2a", "3dedfd45900cf68fed49ee50963c37c59af01c7f8deb327224f710c010565f87")
+
+on_install(function(package)
+ os.cp("unifont.h", package:installdir("include"))
+ os.cp("NotoSansCJK.h", package:installdir("include"))
+end)
+
+package("cjson")
+
+set_homepage("https://github.com/DaveGamble/cJSON")
+set_description("Ultralightweight JSON parser in ANSI C.")
+set_license("MIT")
+
+set_urls("https://github.com/DaveGamble/cJSON/archive/v$(version).zip",
+ "https://github.com/DaveGamble/cJSON.git")
+
+add_versions("1.7.16", "ea60a2477e5b7f41418eeb70488f437c56c56f998802e23be22b859e4be3ef44")
+
+add_deps("cmake")
+
+on_install("windows", "macosx", "linux", "mingw", "iphoneos", "android", function(package)
+ local configs = { "-DENABLE_CJSON_TEST=OFF" }
+ table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release"))
+ table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
+ import("package.tools.cmake").install(package, configs)
+end)
+
+on_test(function(package)
+ assert(package:has_cfuncs("cJSON_malloc", { includes = "cjson/cJSON.h" }))
+end)