NanaSakura 发表于 2023-9-20 17:11:23

以目的为驱动的开发实例

本帖最后由 Shizuku- 于 2023-9-20 17:15 编辑

- 本文和Minecraft毫无关联。
- 本文涉及的语言为C。
- 项目地址:https://github.com/wxkj123/osu-utils,GPL V3 Licence

本文将详细叙述开发过程。

首先不要被结构如此复杂的源码吓到,其实本身是单文件,但是因为有些复杂,以及个人习惯问题导致单文件观感不佳,所以拆分成了这个样子。

然后安利一下这个游戏,叫osu!,没错是个音游。它是由社区驱动的,因此谱面文件都是由玩家制作并上传的。音乐嘛,有个很闹心的事情,就是会经常性地产生各种版权问题,osu!这里也不例外,那么问题就来了,打音游得听喜欢的歌,但是喜欢的歌往往又找不到,于是乎进入死循环,我根本不知道什么歌好听,找歌范围又过于宽泛,毫无目的性。

于是想了点办法,诶,别人打过的歌……应该……不会太难听吧,那可以照着别人的Best Play抄嘛(Best Play是直接挂在个人主页的)。

于是……就有了本项目。

这个程序是用来扒别人的Best Play的,当然也可以扒自己的。

想要扒这个东西,那就得要找到定位的抓手,即如何找到想要的谱面,这是其一;用什么下载,这是其二;用什么库,这是其三。解决好这几个问题,这个程序也就写出来了。

定位问题,随便打开一个谱面的info页,留心观察就会发现:



找到一个谱面需要有这两个数字,前者称作sid(即BeatmapSet ID),后者称作bid(Beatmap ID)。

那么获取这个数字就成了首要问题了。

既然音游群有机器人,曾经也问过某开发者适配问题,他是这么答的:



哦,有API啊,那好说,直接看文档就是了。



看来还有OAuth的问题。



挺好的,没什么难度。

因此批量获取谱面的方法是:先向API请求token,然后抓着这个token以及uid去问API要bplist,要到了之后,就可以从返回的json里面找到sid,然后下载。

选库方面,本来打算使用libuv+cJSON,后来发现我经验不够,uv根本不会,于是改用了libcurl+cJSON练手(其实也可以找借口说uv太重了)。

于是可以开始写了。

首先需要拿到token,那么这就是目标。首先请求完会拿到一个JSON文件,然后使用cJSON即可将token取出。

代码如下:

inc/util/OAuth.h:

#ifndef OAuth_h
#define OAuth_h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cJSON.h"

void read_token(char* tokenstring);
void on_token_expire(void);
void get_token(int clientid,const char* clientsec);
size_t token_cb(char* buffer,size_t size,size_t nitems,void* userdata);

#endif /* OAuth_h */


src/core/auth/OAuth.c:

#include "OAuth.h"

void read_token(char* tokenstring){
    FILE* fp=fopen("Cache/token.json","r");
    int failcount=0;
    if(fp){
      fseek(fp,0,SEEK_END);
      long fsize=ftell(fp);
      if(fsize==0){
            on_token_expire();
            exit(-1);
      }
      fseek(fp,0,SEEK_SET);
      char ch;
      char* string=(char*) malloc(fsize);
      char* ptr=string;
      while(!feof(fp)){
            ch=fgetc(fp);
            * ptr++=ch;
      }
      fclose(fp);
      cJSON* root=cJSON_Parse(string);
      free(string);
      cJSON* timethen=cJSON_GetObjectItem(root,"time");
      time_t previous_time=timethen->valueint;
      time_t current_time=time(NULL);
      if(current_time-previous_time>=86400){
            on_token_expire();
            exit(-2);
      }
      cJSON* token=cJSON_GetObjectItem(root,"token");
      sprintf(tokenstring,"%s",token->valuestring);
      cJSON_Delete(root);
    }
    else{
      if(failcount>1){
            fprintf(stderr,"Failed to read and get token.\n");
      }
      on_token_expire();
      read_token(tokenstring);
    }
}

void on_token_expire(void){
    fprintf(stderr,"Failed to read token, will get another.\n");
    int clientid=5;
    char* clientsec="FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
    get_token(clientid,clientsec);
}

void get_token(int clientid,const char* clientsec){
    CURL* eh=curl_easy_init();
    if(eh){
      struct curl_slist* list;
      char data;
      sprintf(data,"client_id=%d&client_secret=%s&grant_type=client_credentials&scope=public",clientid,clientsec);
      list=curl_slist_append(NULL,"Accept: application/json");
      list=curl_slist_append(list,"Content-Type: application/x-www-form-urlencoded");
      curl_easy_setopt(eh,CURLOPT_URL,"https://osu.ppy.sh/oauth/token");
      curl_easy_setopt(eh,CURLOPT_PROXY,"http://localhost:7890");
      curl_easy_setopt(eh,CURLOPT_HTTPHEADER,list);
      curl_easy_setopt(eh,CURLOPT_POSTFIELDSIZE,(long)strlen(data));
      curl_easy_setopt(eh,CURLOPT_POSTFIELDS,data);
      curl_easy_setopt(eh,CURLOPT_TIMEOUT,30);
      curl_easy_setopt(eh,CURLOPT_WRITEFUNCTION,token_cb);
      curl_easy_perform(eh);
      curl_easy_cleanup(eh);
    }
    else{
      fprintf(stderr,"Failed to get token.\n");
      exit(1);
    }
}

size_t token_cb(char* buffer,size_t size,size_t nitems,void* userdata){
    FILE* read=(FILE*) userdata;
    size_t retcode=fread(buffer,size,nitems,read);
    cJSON* root=cJSON_Parse(buffer);
    cJSON* item=cJSON_GetObjectItem(root,"access_token");
    char* token=item->valuestring;
    time_t current=time(NULL);
    cJSON* data=cJSON_CreateObject();
    cJSON_AddNumberToObject(data,"time",current);
    cJSON_AddStringToObject(data,"token",token);
    char* jsondata=cJSON_Print(data);
    FILE* fp;
    fp=fopen("Cache/token.json","w+");
    fprintf(fp,"%s",jsondata);
    fclose(fp);
    cJSON_Delete(root);
    return retcode;
}

oauth这个模块只做了一件事情,就是判断是否有token文件存在,如果有就读取,没有就再来一个。

token本身是有时效性的,如果token过期了,那就得重新请求,否则可以尽情复用,所以需要文件存储。

cJSON只是一个工具,C本身不具备解析JSON的功能,但是有了这个库后就有了。

请求完token就可以请求bplist,默认已知uid,查询范围。

代码如下:

inc/util/Getbplist.h:

#ifndef Getbplist
#define Getbplist

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "cJSON.h"

#include "util/Basic.h"
#include "Mode.h"

void get_bplist(int uid,uint8_t mode_id,int offset,int limit,const char* token);
int* getsids(void);

#endif /* Getbplist_h */

被Getbplist.h引用的inc/util/Basic.h:

#ifndef Basic_h
#define Basic_h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct memory{
    char* response;
    size_t size;
};

size_t cb(char* buffer,size_t size,size_t nitems,void* userdata);

#endif /* Basic_h */

inc/util/Mode.h:

#ifndef Mode_h
#define Mode_h

#include <stdio.h>
#include <stdlib.h>

char* select_mode(uint8_t mode_id);

#endif /* Mode_h */

src/core/network/Getbplist.c:

#include "Getbplist.h"

void get_bplist(int uid,uint8_t mode_id,int offset,int limit,const char* token){
    CURL* eh=curl_easy_init();
    if(eh){
      char* mode=select_mode(mode_id);
      char url;
      char oauth;
      struct curl_slist* list;
      struct memory chunk={0};
      sprintf(url,"https://osu.ppy.sh/api/v2/users/%d/scores/best?include_fails=0&mode=%s&limit=%d&offset=%d",uid,mode,limit,offset);
      sprintf(oauth,"Authorization: Bearer %s",token);
      list=curl_slist_append(NULL,"Content-Type: application/json");
      list=curl_slist_append(list,"Accept: application/json");
      list=curl_slist_append(list,oauth);
      curl_easy_setopt(eh,CURLOPT_URL,url);
      curl_easy_setopt(eh,CURLOPT_HTTPHEADER,list);
      curl_easy_setopt(eh,CURLOPT_WRITEFUNCTION,cb);
      curl_easy_setopt(eh,CURLOPT_WRITEDATA,(void*) &chunk);
      curl_easy_setopt(eh,CURLOPT_TIMEOUT,30);
      curl_easy_perform(eh);
      
      // Write bplist to cache.
      
      FILE* fp;
      fp=fopen("Cache/bplist.json","w+");
      fprintf(fp,"%s",chunk.response);
      fclose(fp);
      
      free(chunk.response);
      curl_easy_cleanup(eh);
    }
    else{
      fprintf(stderr,"Failed to get bplist.\n");
    }
}

int* getsids(void){
    FILE* fp;
    fp=fopen("Cache/bplist.json","r");
    if(fp){
      fseek(fp,0,SEEK_END);
      long fsize=ftell(fp);
      if(fsize==0){
            exit(-1);
      }
      fseek(fp,0,SEEK_SET);
      char ch;
      char* string=malloc(fsize);
      char* ptr=string;
      while(!feof(fp)){
            ch=fgetc(fp);
            * ptr++=ch;
      }
      fclose(fp);
      cJSON* root=cJSON_Parse(string);
      int arraysize=cJSON_GetArraySize(root);
      cJSON* info;
      cJSON* beatmap;
      cJSON* siditem;
      int sid;
      int* array=(int*) malloc(arraysize*sizeof(int));
      for(int i=0;i<arraysize;i++){
            info=cJSON_GetArrayItem(root,i);
            beatmap=cJSON_GetObjectItem(info,"beatmap");
            siditem=cJSON_GetObjectItem(beatmap,"beatmapset_id");
            sid=siditem->valueint;
            array=sid;
      }
      cJSON_Delete(root);
      return array;
    }
    else{
      fprintf(stderr,"Failed to get sids.\n");
      exit(-7);
    }
}

src/core/network/Basic.c:

#include "Basic.h"

size_t cb(char* buffer,size_t size,size_t nitems,void* userdata){
    size_t realsize=size*nitems;
    struct memory *mem=(struct memory*) userdata;
    char* ptr=realloc(mem->response,mem->size+realsize+1);
    if(buffer==NULL){
      fprintf(stderr,"Out of memory.\n");
      exit(2);
    }
    mem->response=ptr;
    memcpy(&(mem->response),buffer,realsize);
    mem->size+=realsize;
    mem->response=0;
    return realsize;
}

src/core/op/Mode.c:

#include "Mode.h"

char* select_mode(uint8_t mode_id){
    char* mode;
    switch(mode_id){
      case 0:
            mode="osu";
            break;
      case 1:
            mode="taiko";
            break;
      case 2:
            mode="fruits";
            break;
      case 3:
            mode="mania";
            break;
      default:
            fprintf(stderr,"Mode error.\n");
            exit(-4);
            break;
    }
    return mode;
}

这里就是一个要注意的地方,因为libcurl的回调是基于TCP的,一次性可写不完,哪怕你说1MB不大吧,嗯,确实不大,但是就是不能一次写完。

事实上一次只能写1KB左右,那么就只能拿个容器接,什么容器呢,就是引入的Basic.h里面的那个memory结构,它就是容器。

回调执行完了之后,继续往下会发现一个事情,就是需要手工free这个chunk,即内存块,因为文件是一整块的,肯定不是什么分散的,所以很明显在free之前这个内存块都是可访问的,而且不需要在回调里面写向什么什么文件写什么数据,这是毫无必要的,因为在回调中写数据,执行一次回调就要写一次数据,那么比如说写1MB的数据,硬盘就被写了1000次,这硬盘可吃不消。那么接下来在free之前直接先把它catch住,直接朝文件里写就是了。

至于为什么要写Mode这个函数,因为0,1,2,3输入起来要比英文单词快,而且方便。

接下来最后一块就是要输出了,下载源方面,官方指定是靠不住了,为什么这么讲,因为这个csm是要靠JS去实现的,我觉得没什么必要,就麻烦一下国内的镜子吧:D。

本来下载是想要写成异步的,即所谓的多线程,但是后来一想,撑死了也就100个文件,慢又能慢到哪里去,于是就直接一个for循环结束了。

代码如下:

inc/util/Downloader.h:

#ifndef Downloader_h
#define Downloader_h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "util/Basic.h"

int compare(const void* p1,const void* p2);
void mapdownloader(int* sids,int offset,int limit);

#endif /* Downloader_h */

src/core/network/Downloader.c:

#include "Downloader.h"

int compare(const void* p1,const void* p2){
    return (*(int*) p1)-(*(int*) p2);
}

void mapdownloader(int* sids,int offset,int limit){
    struct stat st={0};
    if(stat("Mapsets",&st)==-1){
      mkdir("Mapsets",0755);
    }
    //To make life easier, sort the items before removing the duplicated.
    int j=1,count;
    qsort(sids,limit-offset,sizeof(int),compare);
    for(int i=1;i<limit-offset;i++){
      if(sids!=sids){
            sids=sids;
      }
    }
    count=j;
   
    // TODO: curl_multi
    // It's working well now. If needed, will do multi.
   
    CURL* downloader;
   
    for(int i=0;i<count;i++){
      downloader=curl_easy_init();
      if(downloader){
            struct memory chunk={0};
            char url;
            sprintf(url,"https://dl.sayobot.cn/beatmaps/download/full/%d",sids);
            curl_easy_setopt(downloader,CURLOPT_URL,url);
            curl_easy_setopt(downloader,CURLOPT_FOLLOWLOCATION,1);
            curl_easy_setopt(downloader,CURLOPT_WRITEFUNCTION,cb);
            curl_easy_setopt(downloader,CURLOPT_WRITEDATA,(void*) &chunk);
            curl_easy_perform(downloader);
            FILE* fp;
            char fname;
            sprintf(fname,"Mapsets/%d.osz",sids);
            fp=fopen(fname,"wb");
            fwrite(chunk.response,sizeof(char),chunk.size,fp);
            fclose(fp);
            free(chunk.response);
            curl_easy_cleanup(downloader);
      }
    }
}

因为bplist里面会有重复,所以在下载之前需要对sid进行去重,这里选择先排序后去重,qsort()是C标准库函数。

这里这个程序就非常直白,我想做什么就都写进去了。

这就是目的驱动,我想做什么,都得先算计好,然后去写,这样才能写出一个像样的东西。

后期这个项目应该(?)还会更新,今天就写到这里了。

梦泽-MengZe2 发表于 2023-9-22 06:28:43

认真看完了,但是好像没看懂[哔哩_喜极而泣][哔哩_喜极而泣][哔哩_喜极而泣]

Leimsn 发表于 2023-10-27 13:07:25

眼熟的代码[贴吧_滑稽]
页: [1]
查看完整版本: 以目的为驱动的开发实例