以目的为驱动的开发实例
本帖最后由 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标准库函数。
这里这个程序就非常直白,我想做什么就都写进去了。
这就是目的驱动,我想做什么,都得先算计好,然后去写,这样才能写出一个像样的东西。
后期这个项目应该(?)还会更新,今天就写到这里了。 认真看完了,但是好像没看懂[哔哩_喜极而泣][哔哩_喜极而泣][哔哩_喜极而泣] 眼熟的代码[贴吧_滑稽]
页: [1]