1 /*
2  * This file is part of cyanrip.
3  *
4  * cyanrip is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * cyanrip is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with cyanrip; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17  */
18 
19 #include "musicbrainz.h"
20 #include "cyanrip_log.h"
21 
22 #include <musicbrainz5/mb5_c.h>
23 #include <libavutil/crc.h>
24 
25 #define READ_MB(FUNC, MBCTX, DICT, KEY)                                             \
26     do {                                                                            \
27         if (!MBCTX)                                                                 \
28             break;                                                                  \
29         int len = FUNC(MBCTX, NULL, 0) + 1;                                         \
30         char *str = av_mallocz(4*len);                                              \
31         FUNC(MBCTX, str, len);                                                      \
32         if (str[0] == '\0')                                                         \
33             av_free(str);                                                           \
34         else                                                                        \
35             av_dict_set(&DICT, KEY, str, AV_DICT_DONT_STRDUP_VAL | AV_DICT_APPEND); \
36     } while (0)
37 
mb_credit(Mb5ArtistCredit credit,AVDictionary * dict,const char * key)38 static void mb_credit(Mb5ArtistCredit credit, AVDictionary *dict, const char *key)
39 {
40     Mb5NameCreditList namecredit_list = mb5_artistcredit_get_namecreditlist(credit);
41     for (int i = 0; i < mb5_namecredit_list_size(namecredit_list); i++) {
42         Mb5NameCredit namecredit = mb5_namecredit_list_item(namecredit_list, i);
43 
44         if (mb5_namecredit_get_name(namecredit, NULL, 0)) {
45             READ_MB(mb5_namecredit_get_name, namecredit, dict, key);
46         } else {
47             Mb5Artist artist = mb5_namecredit_get_artist(namecredit);
48             if (artist)
49                 READ_MB(mb5_artist_get_name, artist, dict, key);
50         }
51 
52         READ_MB(mb5_namecredit_get_joinphrase, namecredit, dict, key);
53     }
54 }
55 
56 /* This is pretty awful but I blame musicbrainz entirely */
crc_medium(Mb5Medium medium)57 static uint32_t crc_medium(Mb5Medium medium)
58 {
59     uint32_t crc = UINT32_MAX;
60     const AVCRC *crc_tab = av_crc_get_table(AV_CRC_32_IEEE_LE);
61 
62     Mb5TrackList track_list = mb5_medium_get_tracklist(medium);
63     if (!track_list)
64         return 0;
65 
66     for (int i = 0; i < mb5_track_list_size(track_list); i++) {
67         AVDictionary *tmp_dict = NULL;
68         Mb5Track track = mb5_track_list_item(track_list, i);
69         Mb5Recording recording = mb5_track_get_recording(track);
70 
71         READ_MB(mb5_recording_get_id, recording, tmp_dict, "mbid");
72 
73         Mb5ArtistCredit credit;
74         if (recording) {
75             READ_MB(mb5_recording_get_title, recording, tmp_dict, "title");
76             credit = mb5_recording_get_artistcredit(recording);
77         } else {
78             READ_MB(mb5_track_get_title, track, tmp_dict, "title");
79             credit = mb5_track_get_artistcredit(track);
80         }
81         if (credit)
82             mb_credit(credit, tmp_dict, "artist");
83 
84         if (dict_get(tmp_dict, "mbid"))
85             crc = av_crc(crc_tab, crc, dict_get(tmp_dict, "mbid"), strlen(dict_get(tmp_dict, "mbid")));
86         if (dict_get(tmp_dict, "artist"))
87             crc = av_crc(crc_tab, crc, dict_get(tmp_dict, "artist"), strlen(dict_get(tmp_dict, "artist")));
88         if (dict_get(tmp_dict, "title"))
89             crc = av_crc(crc_tab, crc, dict_get(tmp_dict, "title"), strlen(dict_get(tmp_dict, "title")));
90 
91         av_dict_free(&tmp_dict);
92         crc ^= mb5_track_get_length(track);
93         crc ^= i;
94     }
95 
96     return crc;
97 }
98 
mb_tracks(cyanrip_ctx * ctx,Mb5Release release,const char * discid,int discnumber)99 static int mb_tracks(cyanrip_ctx *ctx, Mb5Release release, const char *discid, int discnumber)
100 {
101     /* Set totaldiscs if possible */
102     Mb5MediumList medium_list_extra = NULL;
103     Mb5MediumList medium_full_list = mb5_release_get_mediumlist(release);
104     int num_cds = mb5_medium_list_size(medium_full_list);
105     av_dict_set_int(&ctx->meta, "totaldiscs", num_cds, 0);
106 
107     if (num_cds == 1 && !discnumber)
108         av_dict_set_int(&ctx->meta, "disc", 1, 0);
109 
110     Mb5Medium medium = NULL;
111     if (discnumber) {
112         if (discnumber < 1 || discnumber > num_cds) {
113             cyanrip_log(ctx, 0, "Invalid disc number %i, release only has %i CDs\n", discnumber, num_cds);
114             return 1;
115         }
116         medium = mb5_medium_list_item(medium_full_list, discnumber - 1);
117         if (!medium) {
118             cyanrip_log(ctx, 0, "Got empty medium list.\n");
119             return 1;
120         }
121     } else {
122         medium_list_extra = mb5_release_media_matching_discid(release, discid);
123         if (!medium_list_extra) {
124             cyanrip_log(ctx, 0, "No mediums match DiscID!\n");
125             return 0;
126         }
127 
128         medium = mb5_medium_list_item(medium_list_extra, 0);
129         if (!medium) {
130             cyanrip_log(ctx, 0, "Got empty medium list.\n");
131             mb5_medium_list_delete(medium_list_extra);
132             return 1;
133         }
134 
135         if (num_cds > 1) {
136             uint32_t medium_crc = crc_medium(medium);
137             for (int i = 0; i < num_cds; i++) {
138                 Mb5Medium tmp_medium = mb5_medium_list_item(medium_full_list, i);
139                 if (medium_crc == crc_medium(tmp_medium)) {
140                     av_dict_set_int(&ctx->meta, "disc", i + 1, 0);
141                     break;
142                 }
143             }
144         }
145     }
146 
147     READ_MB(mb5_medium_get_title, medium, ctx->meta, "discname");
148     READ_MB(mb5_medium_get_format, medium, ctx->meta, "format");
149 
150     Mb5TrackList track_list = mb5_medium_get_tracklist(medium);
151     if (!track_list) {
152         cyanrip_log(ctx, 0, "Medium has no track list.\n");
153         if (medium_list_extra)
154             mb5_medium_list_delete(medium_list_extra);
155         return 0;
156     }
157 
158     for (int i = 0; i < mb5_track_list_size(track_list); i++) {
159         if (i >= ctx->nb_cd_tracks)
160             break;
161         Mb5Track track = mb5_track_list_item(track_list, i);
162         Mb5Recording recording = mb5_track_get_recording(track);
163 
164         READ_MB(mb5_recording_get_id, recording, ctx->tracks[i].meta, "mbid");
165 
166         Mb5ArtistCredit credit;
167         if (recording) {
168             READ_MB(mb5_recording_get_title, recording, ctx->tracks[i].meta, "title");
169             credit = mb5_recording_get_artistcredit(recording);
170         } else {
171             READ_MB(mb5_track_get_title, track, ctx->tracks[i].meta, "title");
172             credit = mb5_track_get_artistcredit(track);
173         }
174         if (credit)
175             mb_credit(credit, ctx->tracks[i].meta, "artist");
176     }
177 
178     if (medium_list_extra)
179         mb5_medium_list_delete(medium_list_extra);
180 
181     return 0;
182 }
183 
mb_metadata(cyanrip_ctx * ctx,int manual_metadata_specified,int release_idx,char * release_str,int discnumber)184 static int mb_metadata(cyanrip_ctx *ctx, int manual_metadata_specified, int release_idx, char *release_str, int discnumber)
185 {
186     int ret = 0;
187     const char *ua = "cyanrip/" PROJECT_VERSION_STRING " ( https://github.com/cyanreg/cyanrip )";
188     Mb5Query query = mb5_query_new(ua, NULL, 0);
189     if (!query) {
190         cyanrip_log(ctx, 0, "Could not connect to MusicBrainz.\n");
191         return 1;
192     }
193 
194     char *names[] = { "inc" };
195     char *values[] = { "recordings artist-credits" };
196     const char *discid = dict_get(ctx->meta, "discid");
197     if (!discid) {
198         cyanrip_log(ctx, 0, "Missing DiscID!\n");
199         return 0;
200     }
201 
202     Mb5Metadata metadata = mb5_query_query(query, "discid", discid, 0, 1, names, values);
203     if (!metadata) {
204         tQueryResult res = mb5_query_get_lastresult(query);
205         if (res != eQuery_ResourceNotFound) {
206             int chars = mb5_query_get_lasterrormessage(query, NULL, 0) + 1;
207             char *msg = av_mallocz(chars*sizeof(*msg));
208             mb5_query_get_lasterrormessage(query, msg, chars);
209             cyanrip_log(ctx, 0, "MusicBrainz lookup failed: %s\n", msg);
210             av_freep(&msg);
211         }
212 
213         switch(res) {
214         case eQuery_Timeout:
215         case eQuery_ConnectionError:
216             cyanrip_log(ctx, 0, "Connection failed, try again? Or disable via -N\n");
217             break;
218         case eQuery_AuthenticationError:
219         case eQuery_FetchError:
220         case eQuery_RequestError:
221             cyanrip_log(ctx, 0, "Error fetching/requesting/auth, this shouldn't happen.\n");
222             break;
223         case eQuery_ResourceNotFound:
224             if (manual_metadata_specified) {
225                 cyanrip_log(ctx, 0, "Unable to find metadata for this CD, but "
226                             "metadata has been manually specified, continuing.\n");
227                 cyanrip_log(ctx, 0, "Please help improve the MusicBrainz DB by "
228                             "submitting the disc info to the following URL:\n%s\n", ctx->mb_submission_url);
229                 goto end;
230             } else {
231                 cyanrip_log(ctx, 0, "Unable to find release info for this CD, "
232                             "and metadata hasn't been manually added!\n");
233                 cyanrip_log(ctx, 0, "Please help improve the MusicBrainz DB by "
234                             "submitting the disc info to the following URL:\n%s\n", ctx->mb_submission_url);
235                 cyanrip_log(ctx, 0, "To continue add metadata via -a or -t, or ignore via -N!\n");
236             }
237             break;
238         default:
239             break;
240         }
241 
242         ret = 1;
243         goto end;
244     }
245 
246     Mb5ReleaseList release_list = NULL;
247     Mb5Disc disc = mb5_metadata_get_disc(metadata);
248     if (!disc) {
249         cyanrip_log(ctx, 0, "DiscID not found in MusicBrainz\n");
250         goto end_meta;
251     }
252 
253     release_list = mb5_disc_get_releaselist(disc);
254     if (!release_list) {
255         cyanrip_log(ctx, 0, "DiscID has no associated releases.\n");
256         goto end_meta;
257     }
258 
259     Mb5Release release = NULL;
260     int num_releases = mb5_release_list_size(release_list);
261     if (!num_releases) {
262         cyanrip_log(ctx, 0, "No releases found for DiscID.\n");
263         goto end_meta;
264     } else if (num_releases > 1 && ((release_idx < 0) && !release_str)) {
265         cyanrip_log(ctx, 0, "Multiple releases found in database for DiscID %s:\n", discid);
266         for (int i = 0; i < num_releases; i++) {
267             release = mb5_release_list_item(release_list, i);
268             AVDictionary *tmp_dict = NULL;
269             READ_MB(mb5_release_get_date, release, tmp_dict, "date");
270             READ_MB(mb5_release_get_title, release, tmp_dict, "album");
271             READ_MB(mb5_release_get_id, release, tmp_dict, "id");
272             READ_MB(mb5_release_get_disambiguation, release, tmp_dict, "disambiguation");
273             READ_MB(mb5_release_get_country, release, tmp_dict, "country");
274 
275             Mb5MediumList medium_list = mb5_release_get_mediumlist(release);
276             int num_cds = mb5_medium_list_size(medium_list);
277             if (num_cds > 1)
278                 av_dict_set_int(&tmp_dict, "num_cds", num_cds, 0);
279 
280 #define PROP(key, postamble)                                    \
281     (!!dict_get(tmp_dict, key)) ? " (" : "",                    \
282     (!!dict_get(tmp_dict, key)) ? dict_get(tmp_dict, key) : "", \
283     (!!dict_get(tmp_dict, key)) ? postamble : "",               \
284     (!!dict_get(tmp_dict, key)) ? ")" : ""
285 
286             cyanrip_log(ctx, 0, "    %i (ID: %s): %s" "%s%s%s%s" "%s%s%s%s" "%s%s%s%s" "%s%s%s%s" "%s", i + 1,
287                         dict_get(tmp_dict, "id")    ? dict_get(tmp_dict, "id")    : "unknown id",
288                         dict_get(tmp_dict, "album") ? dict_get(tmp_dict, "album") : "unknown album",
289                         PROP("disambiguation", ""),
290                         PROP("country", ""),
291                         PROP("num_cds", " CDs"),
292                         PROP("date", ""),
293                         "\n");
294 
295 #undef PROP
296 
297             av_dict_free(&tmp_dict);
298         }
299         cyanrip_log(ctx, 0, "\n");
300         cyanrip_log(ctx, 0, "Please specify which release to use by adding the -R argument with an index or ID.\n");
301         ret = 1;
302         goto end_meta;
303     } else if (release_idx >= 0) { /* Release index specified */
304         if ((release_idx < 1) || (release_idx > num_releases)) {
305             cyanrip_log(ctx, 0, "Invalid release index %i specified, only have %i releases!\n", release_idx, num_releases);
306             ret = 1;
307             goto end_meta;
308         }
309         release = mb5_release_list_item(release_list, release_idx - 1);
310     } else if (release_str) { /* Release ID specified */
311         int i = 0;
312         for (; i < num_releases; i++) {
313             release = mb5_release_list_item(release_list, i);
314             AVDictionary *tmp_dict = NULL;
315             READ_MB(mb5_release_get_id, release, tmp_dict, "id");
316             if (dict_get(tmp_dict, "id") && !strcmp(release_str, dict_get(tmp_dict, "id"))) {
317                 av_dict_free(&tmp_dict);
318                 break;
319             }
320             av_dict_free(&tmp_dict);
321         }
322         if (i == num_releases) {
323             cyanrip_log(ctx, 0, "Release ID %s not found in release list for DiscID %s!\n", release_str, discid);
324             ret = 1;
325             goto end_meta;
326         }
327     } else {
328         release = mb5_release_list_item(release_list, 0);
329     }
330 
331     READ_MB(mb5_release_get_id, release, ctx->meta, "release_id");
332     READ_MB(mb5_release_get_disambiguation, release, ctx->meta, "releasecomment");
333     READ_MB(mb5_release_get_date, release, ctx->meta, "date");
334     READ_MB(mb5_release_get_title, release, ctx->meta, "album");
335     READ_MB(mb5_release_get_barcode, release, ctx->meta, "barcode");
336     READ_MB(mb5_release_get_packaging, release, ctx->meta, "packaging");
337     READ_MB(mb5_release_get_country, release, ctx->meta, "country");
338     READ_MB(mb5_release_get_status, release, ctx->meta, "status");
339 
340     /* Label info */
341     Mb5LabelInfoList *labelinfolist = mb5_release_get_labelinfolist(release);
342     if (mb5_labelinfo_list_size(labelinfolist) == 1) {
343         Mb5LabelInfo *labelinfo = mb5_label_list_item(labelinfolist, 0);
344         READ_MB(mb5_labelinfo_get_catalognumber, labelinfo, ctx->meta, "catalog");
345 
346         Mb5Label *label = mb5_labelinfo_get_label(labelinfo);
347         READ_MB(mb5_label_get_name, label, ctx->meta, "label");
348     }
349 
350     Mb5ArtistCredit artistcredit = mb5_release_get_artistcredit(release);
351     if (artistcredit)
352         mb_credit(artistcredit, ctx->meta, "album_artist");
353 
354     cyanrip_log(ctx, 0, "Found MusicBrainz release: %s - %s\n",
355                 dict_get(ctx->meta, "album"), dict_get(ctx->meta, "album_artist"));
356 
357     /* Read track metadata */
358     mb_tracks(ctx, release, discid, discnumber);
359 
360 end_meta:
361     mb5_metadata_delete(metadata); /* This frees _all_ metadata */
362 
363 end:
364     mb5_query_delete(query);
365     return ret;
366 }
367 
crip_fill_metadata(cyanrip_ctx * ctx,int manual_metadata_specified,int release_idx,char * release_str,int discnumber)368 int crip_fill_metadata(cyanrip_ctx *ctx, int manual_metadata_specified,
369                        int release_idx, char *release_str, int discnumber)
370 {
371     /* Get musicbrainz tags */
372     if (!ctx->settings.disable_mb)
373         return mb_metadata(ctx, manual_metadata_specified, release_idx, release_str, discnumber);
374 
375     return 0;
376 }
377