diff options
| author | Mole Shang <135e2@135e2.dev> | 2023-07-25 09:27:26 +0800 | 
|---|---|---|
| committer | Mole Shang <135e2@135e2.dev> | 2023-08-05 23:19:46 +0800 | 
| commit | ed8f6df90b0c39835198d5b7af4bbd391362f180 (patch) | |
| tree | 907ba31bac854eb5dc8a2781825e24c049b10580 | |
| download | hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.tar.gz hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.tar.bz2 hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.zip | |
hinata: initial commit
| -rw-r--r-- | .clang-format | 192 | ||||
| -rw-r--r-- | .gitignore | 17 | ||||
| -rw-r--r-- | src/constants.h | 18 | ||||
| -rw-r--r-- | src/extractors/bilibili.c | 475 | ||||
| -rw-r--r-- | src/extractors/bilibili.h | 94 | ||||
| -rw-r--r-- | src/extractors/extractor.c | 24 | ||||
| -rw-r--r-- | src/extractors/extractor.h | 32 | ||||
| -rw-r--r-- | src/logger.c | 32 | ||||
| -rw-r--r-- | src/logger.h | 30 | ||||
| -rw-r--r-- | src/main.c | 163 | ||||
| -rw-r--r-- | src/main.h | 8 | ||||
| -rw-r--r-- | src/process_url.c | 526 | ||||
| -rw-r--r-- | src/process_url.h | 59 | ||||
| -rw-r--r-- | src/style.h | 133 | ||||
| -rw-r--r-- | src/ui.c | 98 | ||||
| -rw-r--r-- | src/ui.h | 16 | ||||
| -rw-r--r-- | src/utils.c | 204 | ||||
| -rw-r--r-- | src/utils.h | 61 | ||||
| -rw-r--r-- | xmake.lua | 153 | 
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(§ion->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(§ion->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) | 
