comparison src/deezer.c @ 11:e6a594f16403

*: huge refactor the config file has changed drastically, moving to an ini file from that custom format; i *would* have used the win32 functions for those, but they were barely functional, so I decided on using ini.h which is lightweight enough. additionally, I've added Deezer support so album art will be displayed! unfortunately though winhttp is a pain in the ass so if I send a request with any form of unicode chars in it it just returns a "bad request" error. I've tried debugging this but I could never really come up with anything: my hypothesis is that deezer expects their characters in percent-encoded UTF-8, but winhttp is sending them in some other encoding. the config dialog was moved out of config.c (overdue) and many more options are given in the config as well. main.c has been renamed to plugin.c to better differentiate it from... everything else.
author Paper <paper@paper.us.eu.org>
date Thu, 14 Mar 2024 20:25:37 -0400
parents
children dd427b7cc459
comparison
equal deleted inserted replaced
10:42ac054c0231 11:e6a594f16403
1 #include "json.h"
2 #include "utils.h"
3
4 #include <windef.h>
5 #include <minwinbase.h>
6 #include <winhttp.h>
7 #include <winuser.h>
8
9 HINTERNET session = NULL;
10 HINTERNET connection = NULL;
11
12 /* preferably we would use some other API for this, but
13 * meh
14 */
15 static int init_winhttp(void) {
16 session = WinHttpOpen(
17 NULL,
18 WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
19 WINHTTP_NO_PROXY_NAME,
20 WINHTTP_NO_PROXY_BYPASS,
21 0
22 );
23
24 return !!session;
25 }
26
27 static int init_connect(void) {
28 if (!session)
29 return 0;
30
31 connection = WinHttpConnect(
32 session,
33 L"api.deezer.com",
34 /* require HTTPS, we aren't in 2001 anymore */
35 INTERNET_DEFAULT_HTTPS_PORT,
36 0
37 );
38
39 return !!connection;
40 }
41
42 /* do this on exit */
43 void close_open_http_handles(void) {
44 if (session) WinHttpCloseHandle(session);
45 if (connection) WinHttpCloseHandle(connection);
46 }
47
48 /* return MUST be free'd */
49 static LPCWSTR deezer_get_thumbnail_build_query(LPCWSTR restrict artist, LPCWSTR restrict album) {
50 static LPCWSTR begin = L"/search/track?strict=on&q=artist:\"";
51 static LPCWSTR album_query = L" album:\"";
52
53 size_t len = wcslen(begin) + wcslen(artist) + 1 /* quote */;
54 if (album && album[0]) {
55 len += 1 /* space */ + wcslen(album_query) + wcslen(album) + 1 /* quote */;
56 }
57
58 LPWSTR final = calloc(len + 1, sizeof(WCHAR));
59
60 wcscpy(final, begin);
61 wcscat(final, artist);
62 wcscat(final, L"\"");
63 if (album && album[0]) {
64 wcscat(final, album_query);
65 wcscat(final, album);
66 wcscat(final, L"\"");
67 }
68
69 return final;
70 }
71
72 static int deezer_album_object_get_cover(cJSON* restrict album, const char* restrict name, char** restrict url) {
73 if (!url) return 0;
74
75 cJSON* cover = cJSON_GetObjectItemCaseSensitive(album, name);
76 if (cover) {
77 if (*url)
78 free(*url);
79 size_t len = strlen(cover->valuestring);
80 *url = malloc((len + 1) * sizeof(char));
81 (*url)[len] = '\0';
82 strncpy(*url, cover->valuestring, len);
83 return 1;
84 } else return 0;
85 }
86
87
88 static int deezer_get_thumbnail_download_url(LPCWSTR restrict query, char** restrict data, size_t* restrict size) {
89 if (!data || !size)
90 return -1;
91
92 HINTERNET request = WinHttpOpenRequest(
93 connection,
94 L"GET",
95 query,
96 NULL,
97 WINHTTP_NO_REFERER,
98 WINHTTP_DEFAULT_ACCEPT_TYPES,
99 WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_PERCENT
100 );
101 if (!request)
102 return -1;
103
104 BOOL result = WinHttpSendRequest(
105 request,
106 L"Content-Type: application/json; charset=utf-8",
107 0,
108 WINHTTP_NO_REQUEST_DATA,
109 0,
110 0,
111 0
112 );
113 if (!result) {
114 WinHttpCloseHandle(request);
115 return -1;
116 }
117
118 result = WinHttpReceiveResponse(request, NULL);
119 if (!result) {
120 WinHttpCloseHandle(request);
121 return -1;
122 }
123
124 DWORD data_available = 0;
125 DWORD data_downloaded = 0;
126 do {
127 data_available = 0;
128 if (!WinHttpQueryDataAvailable(request, &data_available)) {
129 WinHttpCloseHandle(request);
130 return -1;
131 }
132
133 if (!data_available)
134 break;
135
136 *data = realloc(*data, *size + data_available + 1);
137 (*data)[*size + data_available] = '\0';
138 if (!WinHttpReadData(request, *data + *size, data_available, &data_downloaded)) {
139 WinHttpCloseHandle(request);
140 return -1;
141 }
142 *size += data_downloaded;
143 } while (data_available > 0);
144
145 WinHttpCloseHandle(request);
146 return 0;
147 }
148
149 enum deezer_thumbnail_state {
150 ERROR = 0,
151 ALBUM,
152 ARTIST,
153 };
154
155 static enum deezer_thumbnail_state deezer_get_thumbnail_parse_search_result(cJSON* json, char** restrict cover_url, int get_artist) {
156 cJSON* album = cJSON_GetObjectItemCaseSensitive(json, "album");
157
158 if (cJSON_IsObject(album)) {
159 if (deezer_album_object_get_cover(album, "cover_medium", cover_url)
160 || deezer_album_object_get_cover(album, "cover_large", cover_url)
161 || deezer_album_object_get_cover(album, "cover_small", cover_url)
162 || deezer_album_object_get_cover(album, "cover_xl", cover_url))
163 return ALBUM;
164 } else if (get_artist) {
165 cJSON* artist = cJSON_GetObjectItemCaseSensitive(json, "artist");
166 if (!cJSON_IsObject(artist))
167 return ERROR;
168
169 /* treat artist pictures as a fallback and only retrieve them once */
170 if (deezer_album_object_get_cover(artist, "picture_medium", cover_url)
171 || deezer_album_object_get_cover(artist, "picture_large", cover_url)
172 || deezer_album_object_get_cover(artist, "picture_small", cover_url)
173 || deezer_album_object_get_cover(artist, "picture_xl", cover_url))
174 return ARTIST;
175 }
176
177 return ERROR;
178 }
179
180 static int deezer_get_thumbnail_parse_json(char** restrict cover_url, const char* restrict data, size_t size) {
181 cJSON* json = cJSON_ParseWithLength(data, size);
182 if (!json) {
183 const char* err_ptr = cJSON_GetErrorPtr();
184 if (err_ptr)
185 MessageBoxA(NULL, err_ptr, "wgsdk: Error parsing Deezer JSON!", MB_ICONERROR | MB_OK);
186 cJSON_Delete(json);
187 return -1;
188 }
189
190 cJSON* json_data = cJSON_GetObjectItemCaseSensitive(json, "data");
191 size_t json_data_size;
192 if (!cJSON_IsArray(json_data) || !(json_data_size = cJSON_GetArraySize(json_data)))
193 return -1;
194
195 int have_artist = 0;
196 for (size_t i = 0; i < json_data_size; i++) {
197 cJSON* result = cJSON_GetArrayItem(json_data, i);
198 if (!cJSON_IsObject(result))
199 return -1;
200
201 enum deezer_thumbnail_state state = deezer_get_thumbnail_parse_search_result(result, cover_url, have_artist);
202 switch (state) {
203 case ERROR: return -1;
204 case ARTIST: have_artist = 1; break;
205 case ALBUM: return 0;
206 }
207 }
208
209 cJSON_Delete(json);
210 return !have_artist;
211 }
212
213 char* deezer_get_thumbnail(LPCWSTR restrict artist, LPCWSTR restrict album) {
214 char* response_data = NULL;
215 size_t response_size = 0;
216 LPCWSTR query = NULL;
217 char* cover_url = NULL;
218
219 /* make sure everything is OK */
220 if (!(session || init_winhttp()) || !(connection || init_connect()) || (!artist || !artist[0]))
221 return NULL;
222
223 query = deezer_get_thumbnail_build_query(artist, album);
224 if (!query)
225 return NULL;
226
227 if (deezer_get_thumbnail_download_url(query, &response_data, &response_size))
228 return NULL;
229
230 if (deezer_get_thumbnail_parse_json(&cover_url, response_data, response_size))
231 return NULL;
232
233 return cover_url;
234 }