UID439944性别保密经验 EP铁粒 粒回帖0主题精华在线时间 小时注册时间2022-6-5最后登录1970-1-1
| 本帖最后由 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[114];
- 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[114];
- char oauth[1050];
- 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[i]=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[mem->size]),buffer,realsize);
- mem->size+=realsize;
- mem->response[mem->size]=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[i]!=sids[i-1]){
- sids[j++]=sids[i];
- }
- }
- 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[60];
- sprintf(url,"https://dl.sayobot.cn/beatmaps/download/full/%d",sids[i]);
- 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[20];
- sprintf(fname,"Mapsets/%d.osz",sids[i]);
- 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标准库函数。
这里这个程序就非常直白,我想做什么就都写进去了。
这就是目的驱动,我想做什么,都得先算计好,然后去写,这样才能写出一个像样的东西。
后期这个项目应该(?)还会更新,今天就写到这里了。 |
评分查看全部评分
|