Merge commit 'andrunko/master'
[lms] / lightmediascanner / src / plugins / id3 / id3.c
1 /**
2  * Copyright (C) 2008 by INdT
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program 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
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  *
18  * @author Andre Moreira Magalhaes <andre.magalhaes@openbossa.org>
19  */
20
21 /**
22  * @brief
23  *
24  * id3 file parser.
25  */
26
27 #ifdef HAVE_CONFIG_H
28 #include "config.h"
29 #endif
30
31 #define _GNU_SOURCE
32 #define _XOPEN_SOURCE 600
33 #include <lightmediascanner_plugin.h>
34 #include <lightmediascanner_db.h>
35 #include <lightmediascanner_charset_conv.h>
36 #include <sys/types.h>
37 #include <sys/stat.h>
38 #include <fcntl.h>
39 #include <stdio.h>
40 #include <stdlib.h>
41 #include <string.h>
42 #include <unistd.h>
43 #include <ctype.h>
44
45 #define ID3V2_HEADER_SIZE       10
46 #define ID3V2_FOOTER_SIZE       10
47 #define ID3V2_FRAME_HEADER_SIZE 10
48
49 #define ID3V1_NUM_GENRES 148
50
51 #define ID3_NUM_ENCODINGS 5
52
53 enum ID3Encodings {
54     ID3_ENCODING_LATIN1 = 0,
55     ID3_ENCODING_UTF16,
56     ID3_ENCODING_UTF16BE,
57     ID3_ENCODING_UTF8,
58     ID3_ENCODING_UTF16LE
59 };
60
61 static const char *id3v1_genres[ID3V1_NUM_GENRES] = {
62     "Blues",
63     "Classic Rock",
64     "Country",
65     "Dance",
66     "Disco",
67     "Funk",
68     "Grunge",
69     "Hip-Hop",
70     "Jazz",
71     "Metal",
72     "New Age",
73     "Oldies",
74     "Other",
75     "Pop",
76     "R&B",
77     "Rap",
78     "Reggae",
79     "Rock",
80     "Techno",
81     "Industrial",
82     "Alternative",
83     "Ska",
84     "Death Metal",
85     "Pranks",
86     "Soundtrack",
87     "Euro-Techno",
88     "Ambient",
89     "Trip-Hop",
90     "Vocal",
91     "Jazz+Funk",
92     "Fusion",
93     "Trance",
94     "Classical",
95     "Instrumental",
96     "Acid",
97     "House",
98     "Game",
99     "Sound Clip",
100     "Gospel",
101     "Noise",
102     "Alternative Rock",
103     "Bass",
104     "Soul",
105     "Punk",
106     "Space",
107     "Meditative",
108     "Instrumental Pop",
109     "Instrumental Rock",
110     "Ethnic",
111     "Gothic",
112     "Darkwave",
113     "Techno-Industrial",
114     "Electronic",
115     "Pop-Folk",
116     "Eurodance",
117     "Dream",
118     "Southern Rock",
119     "Comedy",
120     "Cult",
121     "Gangsta",
122     "Top 40",
123     "Christian Rap",
124     "Pop/Funk",
125     "Jungle",
126     "Native American",
127     "Cabaret",
128     "New Wave",
129     "Psychedelic",
130     "Rave",
131     "Showtunes",
132     "Trailer",
133     "Lo-Fi",
134     "Tribal",
135     "Acid Punk",
136     "Acid Jazz",
137     "Polka",
138     "Retro",
139     "Musical",
140     "Rock & Roll",
141     "Hard Rock",
142     "Folk",
143     "Folk/Rock",
144     "National Folk",
145     "Swing",
146     "Fusion",
147     "Bebob",
148     "Latin",
149     "Revival",
150     "Celtic",
151     "Bluegrass",
152     "Avantgarde",
153     "Gothic Rock",
154     "Progressive Rock",
155     "Psychedelic Rock",
156     "Symphonic Rock",
157     "Slow Rock",
158     "Big Band",
159     "Chorus",
160     "Easy Listening",
161     "Acoustic",
162     "Humour",
163     "Speech",
164     "Chanson",
165     "Opera",
166     "Chamber Music",
167     "Sonata",
168     "Symphony",
169     "Booty Bass",
170     "Primus",
171     "Porn Groove",
172     "Satire",
173     "Slow Jam",
174     "Club",
175     "Tango",
176     "Samba",
177     "Folklore",
178     "Ballad",
179     "Power Ballad",
180     "Rhythmic Soul",
181     "Freestyle",
182     "Duet",
183     "Punk Rock",
184     "Drum Solo",
185     "A Cappella",
186     "Euro-House",
187     "Dance Hall",
188     "Goa",
189     "Drum & Bass",
190     "Club-House",
191     "Hardcore",
192     "Terror",
193     "Indie",
194     "BritPop",
195     "Negerpunk",
196     "Polsk Punk",
197     "Beat",
198     "Christian Gangsta Rap",
199     "Heavy Metal",
200     "Black Metal",
201     "Crossover",
202     "Contemporary Christian",
203     "Christian Rock",
204     "Merengue",
205     "Salsa",
206     "Thrash Metal",
207     "Anime",
208     "Jpop",
209     "Synthpop"
210 };
211
212 struct id3v2_frame_header {
213     char frame_id[4];
214     unsigned int frame_size;
215     int compression;
216     int data_length_indicator;
217 };
218
219 struct id3v1_tag {
220     char title[30];
221     char artist[30];
222     char album[30];
223     char year[4];
224     char comments[30];
225     char genre;
226 } __attribute__((packed));
227
228 struct plugin {
229     struct lms_plugin plugin;
230     lms_db_audio_t *audio_db;
231     lms_charset_conv_t *cs_convs[ID3_NUM_ENCODINGS];
232 };
233
234 static const char _name[] = "id3";
235 static const struct lms_string_size _exts[] = {
236     LMS_STATIC_STRING_SIZE(".mp3"),
237     LMS_STATIC_STRING_SIZE(".aac")
238 };
239
240 static unsigned int
241 _to_uint(const char *data, int data_size)
242 {
243     unsigned int sum = 0;
244     unsigned int last, i;
245
246     last = data_size > 4 ? 3 : data_size - 1;
247
248     for (i = 0; i <= last; i++)
249         sum |= ((unsigned char) data[i]) << ((last - i) * 8);
250
251     return sum;
252 }
253
254 static inline int
255 _is_id3v2_second_synch_byte(unsigned char byte)
256 {
257     if (byte == 0xff)
258         return 0;
259     if ((byte & 0xE0) == 0xE0)
260         return 1;
261     return 0;
262 }
263
264 static long
265 _find_id3v2(int fd)
266 {
267     long buffer_offset = 0;
268     char buffer[3], *p;
269     int buffer_size = sizeof(buffer);
270     const char pattern[] = "ID3";
271     ssize_t nread;
272
273     /* These variables are used to keep track of a partial match that happens at
274      * the end of a buffer. */
275     int previous_partial_match = -1;
276     int previous_partial_synch_match = 0;
277     int first_synch_byte;
278
279     /* Start the search at the beginning of the file. */
280     lseek(fd, 0, SEEK_SET);
281
282     if ((nread = read(fd, &buffer, buffer_size)) != buffer_size)
283         return -1;
284
285     /* check if pattern is in the beggining of the file */
286     if (memcmp(buffer, pattern, 3) == 0)
287         return 0;
288
289     /* This loop is the crux of the find method.  There are three cases that we
290      * want to account for:
291      * (1) The previously searched buffer contained a partial match of the search
292      * pattern and we want to see if the next one starts with the remainder of
293      * that pattern.
294      *
295      * (2) The search pattern is wholly contained within the current buffer.
296      *
297      * (3) The current buffer ends with a partial match of the pattern.  We will
298      * note this for use in the next iteration, where we will check for the rest
299      * of the pattern. */
300     while (1) {
301         /* (1) previous partial match */
302         if (previous_partial_synch_match && _is_id3v2_second_synch_byte(buffer[0]))
303             return -1;
304
305         if (previous_partial_match >= 0 && previous_partial_match < buffer_size) {
306             const int pattern_offset = buffer_size - previous_partial_match;
307
308             if (memcmp(buffer, pattern + pattern_offset, 3 - pattern_offset) == 0)
309                 return buffer_offset - buffer_size + previous_partial_match;
310         }
311
312         /* (2) pattern contained in current buffer */
313         p = buffer;
314         while ((p = memchr(p, 'I', buffer_size))) {
315             if (memcmp(p, pattern, 3) == 0)
316                 return buffer_offset + (p - buffer);
317             p += 1;
318         }
319
320         p = memchr(buffer, 255, buffer_size);
321         if (p)
322             first_synch_byte = p - buffer;
323         else
324             first_synch_byte = -1;
325
326         /* Here we have to loop because there could be several of the first
327          * (11111111) byte, and we want to check all such instances until we find
328          * a full match (11111111 111) or hit the end of the buffer. */
329         while (first_synch_byte >= 0) {
330             /* if this *is not* at the end of the buffer */
331             if (first_synch_byte < buffer_size - 1) {
332                 if(_is_id3v2_second_synch_byte(buffer[first_synch_byte + 1]))
333                     /* We've found the frame synch pattern. */
334                     return -1;
335                 else
336                     /* We found 11111111 at the end of the current buffer indicating a
337                      * partial match of the synch pattern.  The find() below should
338                      * return -1 and break out of the loop. */
339                     previous_partial_synch_match = 1;
340             }
341
342             /* Check in the rest of the buffer. */
343             p = memchr(p + 1, 255, buffer_size);
344             if (p)
345                 first_synch_byte = p - buffer;
346             else
347                 first_synch_byte = -1;
348         }
349
350         /* (3) partial match */
351         if (buffer[nread - 1] == pattern[1])
352             previous_partial_match = nread - 1;
353         else if (memcmp(&buffer[nread - 2], pattern, 2) == 0)
354             previous_partial_match = nread - 2;
355         buffer_offset += buffer_size;
356
357         if ((nread = read(fd, &buffer, sizeof(buffer))) == -1)
358             return -1;
359     }
360
361     return -1;
362 }
363
364 static unsigned int
365 _get_id3v2_frame_header_size(unsigned int version)
366 {
367     switch (version) {
368     case 0:
369     case 1:
370     case 2:
371         return 6;
372     case 3:
373     case 4:
374     default:
375         return 10;
376     }
377 }
378
379 static void
380 _parse_id3v2_frame_header(char *data, unsigned int version, struct id3v2_frame_header *fh)
381 {
382     switch (version) {
383     case 0:
384     case 1:
385     case 2:
386         memcpy(fh->frame_id, data, 3);
387         fh->frame_id[3] = 0;
388         fh->frame_size = _to_uint(data + 3, 3);
389         fh->compression = 0;
390         fh->data_length_indicator = 0;
391         break;
392     case 3:
393         memcpy(fh->frame_id, data, 4);
394         fh->frame_size = _to_uint(data + 4, 4);
395         fh->compression = data[9] & 0x40;
396         fh->data_length_indicator = 0;
397         break;
398     case 4:
399     default:
400         memcpy(fh->frame_id, data, 4);
401         fh->frame_size = _to_uint(data + 4, 4);
402         fh->compression = data[9] & 0x4;
403         fh->data_length_indicator = data[9] & 0x1;
404         break;
405     }
406 }
407
408 static inline void
409 _get_id3v2_frame_info(const char *frame_data, unsigned int frame_size, struct lms_string_size *s, lms_charset_conv_t *cs_conv)
410 {
411     s->str = malloc(sizeof(char) * (frame_size + 1));
412     memcpy(s->str, frame_data, frame_size);
413     s->str[frame_size] = '\0';
414     s->len = frame_size;
415     if (cs_conv)
416         lms_charset_conv(cs_conv, &s->str, &s->len);
417 }
418
419 static void
420 _parse_id3v2_frame(struct id3v2_frame_header *fh, const char *frame_data, struct lms_audio_info *info, lms_charset_conv_t **cs_convs)
421 {
422     lms_charset_conv_t *cs_conv = NULL;
423     unsigned int text_encoding, frame_size;
424
425 #if 0
426     fprintf(stderr, "text encoding = %d\n", frame_data[0]);
427 #endif
428
429     /* Latin1  = 0
430      * UTF16   = 1
431      * UTF16BE = 2
432      * UTF8    = 3
433      * UTF16LE = 4
434      */
435     text_encoding = frame_data[0];
436     if (text_encoding >= 0 && text_encoding < ID3_NUM_ENCODINGS)
437         cs_conv = cs_convs[text_encoding];
438
439     /* skip first byte - text encoding */
440     frame_data += 1;
441     frame_size = fh->frame_size - 1;
442
443     if (memcmp(fh->frame_id, "TIT2", 4) == 0)
444         _get_id3v2_frame_info(frame_data, frame_size, &info->title, cs_conv);
445     else if (memcmp(fh->frame_id, "TPE1", 4) == 0)
446         _get_id3v2_frame_info(frame_data, frame_size, &info->artist, cs_conv);
447     else if (memcmp(fh->frame_id, "TALB", 4) == 0)
448         _get_id3v2_frame_info(frame_data, frame_size, &info->album, cs_conv);
449     else if (memcmp(fh->frame_id, "TCON", 4) == 0) {
450         /* TODO handle multiple genres */
451         int i, is_number;
452
453         _get_id3v2_frame_info(frame_data, frame_size, &info->genre, cs_conv);
454
455         is_number = 1;
456         for (i = 0; i < info->genre.len; ++i) {
457             if (!isdigit(info->genre.str[i]))
458                 is_number = 0;
459         }
460
461         if ((is_number) &&
462             (info->genre.str) &&
463             (info->genre.len > 0)) {
464             int genre = atoi(info->genre.str);
465             if (genre >= 0 && genre < ID3V1_NUM_GENRES) {
466                 free(info->genre.str);
467                 info->genre.str = strdup(id3v1_genres[(int) genre]);
468                 info->genre.len = strlen(info->genre.str);
469             }
470             else {
471                 /* ignore other genres */
472                 free(info->genre.str);
473                 info->genre.str = NULL;
474                 info->genre.len = 0;
475             }
476         }
477     }
478     else if (memcmp(fh->frame_id, "TRCK", 4) == 0) {
479         struct lms_string_size trackno;
480         _get_id3v2_frame_info(frame_data, frame_size, &trackno, cs_conv);
481         info->trackno = atoi(trackno.str);
482         free(trackno.str);
483     }
484 }
485
486 static int
487 _parse_id3v2(int fd, long id3v2_offset, struct lms_audio_info *info, lms_charset_conv_t **cs_convs)
488 {
489     char header_data[10], frame_header_data[10];
490     unsigned int tag_size, major_version, frame_data_pos, frame_data_length, frame_header_size;
491     int extended_header, footer_present;
492     struct id3v2_frame_header fh;
493
494     lseek(fd, id3v2_offset, SEEK_SET);
495
496     /* parse header */
497     if (read(fd, header_data, ID3V2_HEADER_SIZE) != ID3V2_HEADER_SIZE)
498         return -1;
499
500     tag_size = _to_uint(header_data + 6, 4);
501     if (tag_size == 0)
502         return -1;
503
504     /* parse frames */
505     major_version = header_data[3];
506
507     frame_data_pos = 0;
508     frame_data_length = tag_size;
509
510     /* check for extended header */
511     extended_header = header_data[5] & 0x20; /* bit 6 */
512     if (extended_header) {
513         /* skip extended header */
514         unsigned int extended_header_size;
515         char extended_header_data[4];
516
517         if (read(fd, extended_header_data, 4) != 4)
518             return -1;
519         extended_header_size = _to_uint(extended_header_data, 4);
520         lseek(fd, extended_header_size - 4, SEEK_CUR);
521         frame_data_pos += extended_header_size;
522         frame_data_length -= extended_header_size;
523     }
524
525     footer_present = header_data[5] & 0x8;   /* bit 4 */
526     if (footer_present && frame_data_length > ID3V2_FOOTER_SIZE)
527         frame_data_length -= ID3V2_FOOTER_SIZE;
528
529     frame_header_size = _get_id3v2_frame_header_size(major_version);
530     while (frame_data_pos < frame_data_length - frame_header_size) {
531         if (read(fd, frame_header_data, ID3V2_FRAME_HEADER_SIZE) != ID3V2_FRAME_HEADER_SIZE)
532             return -1;
533
534         if (frame_header_data[0] == 0)
535             break;
536
537         _parse_id3v2_frame_header(frame_header_data, major_version, &fh);
538
539         if (!fh.compression &&
540             fh.frame_id[0] == 'T' &&
541             memcmp(fh.frame_id, "TXXX", 4) != 0) {
542             char *frame_data;
543
544             if (fh.data_length_indicator)
545                 lseek(fd, 4, SEEK_CUR);
546
547             frame_data = malloc(sizeof(char) * fh.frame_size);
548             if (read(fd, frame_data, fh.frame_size) != fh.frame_size) {
549                 free(frame_data);
550                 return -1;
551             }
552
553             _parse_id3v2_frame(&fh, frame_data, info, cs_convs);
554             free(frame_data);
555         }
556         else {
557             if (fh.data_length_indicator)
558                 lseek(fd, fh.frame_size + 4, SEEK_CUR);
559             else
560                 lseek(fd, fh.frame_size, SEEK_CUR);
561         }
562
563         frame_data_pos += fh.frame_size + frame_header_size;
564     }
565
566     return 0;
567 }
568
569 static int
570 _parse_id3v1(int fd, struct lms_audio_info *info, lms_charset_conv_t *cs_conv)
571 {
572     struct id3v1_tag tag;
573     if (read(fd, &tag, sizeof(struct id3v1_tag)) == -1)
574         return -1;
575
576     info->title.str = strndup(tag.title, 30);
577     info->title.len = strlen(info->title.str);
578     lms_charset_conv(cs_conv, &info->title.str, &info->title.len);
579     info->artist.str = strndup(tag.artist, 30);
580     info->artist.len = strlen(info->artist.str);
581     lms_charset_conv(cs_conv, &info->artist.str, &info->artist.len);
582     info->album.str = strndup(tag.album, 30);
583     info->album.len = strlen(info->album.str);
584     lms_charset_conv(cs_conv, &info->album.str, &info->album.len);
585     if ((unsigned char) tag.genre < ID3V1_NUM_GENRES) {
586         info->genre.str = strdup(id3v1_genres[(unsigned char) tag.genre]);
587         info->genre.len = strlen(info->genre.str);
588     }
589     if (tag.comments[28] == 0 && tag.comments[29] != 0)
590         info->trackno = (unsigned char) tag.comments[29];
591
592     return 0;
593 }
594
595 static void *
596 _match(struct plugin *p, const char *path, int len, int base)
597 {
598     int i;
599
600     i = lms_which_extension(path, len, _exts, LMS_ARRAY_SIZE(_exts));
601     if (i < 0)
602       return NULL;
603     else
604       return (void*)(i + 1);
605 }
606
607 static int
608 _parse(struct plugin *plugin, struct lms_context *ctxt, const struct lms_file_info *finfo, void *match)
609 {
610     struct lms_audio_info info = {0, {0}, {0}, {0}, {0}, 0, 0, 0};
611     int r, fd;
612     long id3v2_offset;
613
614     fd = open(finfo->path, O_RDONLY);
615     if (fd < 0) {
616         perror("open");
617         return -1;
618     }
619
620     id3v2_offset = _find_id3v2(fd);
621     if (id3v2_offset >= 0) {
622 #if 0
623         fprintf(stderr, "id3v2 tag found in file %s with offset %ld\n",
624                 finfo->path, id3v2_offset);
625 #endif
626         if (_parse_id3v2(fd, id3v2_offset, &info, plugin->cs_convs) != 0) {
627             r = -2;
628             goto done;
629         }
630     }
631     else {
632         char tag[3];
633 #if 0
634         fprintf(stderr, "id3v2 tag not found in file %s. trying id3v1\n", finfo->path);
635 #endif
636         /* check for id3v1 tag */
637         if (lseek(fd, -128, SEEK_END) == -1) {
638             r = -3;
639             goto done;
640         }
641
642         if (read(fd, &tag, 3) == -1) {
643             r = -4;
644             goto done;
645         }
646
647         if (memcmp(tag, "TAG", 3) == 0) {
648 #if 0
649             fprintf(stderr, "id3v1 tag found in file %s\n", finfo->path);
650 #endif
651             if (_parse_id3v1(fd, &info, ctxt->cs_conv) != 0) {
652                 r = -5;
653                 goto done;
654             }
655         }
656     }
657
658     lms_string_size_strip_and_free(&info.title);
659     lms_string_size_strip_and_free(&info.artist);
660     lms_string_size_strip_and_free(&info.album);
661     lms_string_size_strip_and_free(&info.genre);
662
663     if (!info.title.str) {
664         int ext_idx;
665         ext_idx = ((int)match) - 1;
666         info.title.len = finfo->path_len - finfo->base - _exts[ext_idx].len;
667         info.title.str = malloc((info.title.len + 1) * sizeof(char));
668         memcpy(info.title.str, finfo->path + finfo->base, info.title.len);
669         info.title.str[info.title.len] = '\0';
670         lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
671     }
672
673 #if 0
674     fprintf(stderr, "file %s info\n", finfo->path);
675     fprintf(stderr, "\ttitle='%s'\n", info.title.str);
676     fprintf(stderr, "\tartist='%s'\n", info.artist.str);
677     fprintf(stderr, "\talbum='%s'\n", info.album.str);
678     fprintf(stderr, "\tgenre='%s'\n", info.genre.str);
679     fprintf(stderr, "\ttrack number='%d'\n", info.trackno);
680 #endif
681
682     info.id = finfo->id;
683     r = lms_db_audio_add(plugin->audio_db, &info);
684
685   done:
686     close(fd);
687
688     if (info.title.str)
689         free(info.title.str);
690     if (info.artist.str)
691         free(info.artist.str);
692     if (info.album.str)
693         free(info.album.str);
694     if (info.genre.str)
695         free(info.genre.str);
696
697     return r;
698 }
699
700 static int
701 _setup(struct plugin *plugin, struct lms_context *ctxt)
702 {
703     int i;
704     const char *id3v2_encodings[ID3_NUM_ENCODINGS] = {
705         "Latin1",
706         "UTF-16",
707         "UTF-16BE",
708         NULL, /* UTF-8 */
709         "UTF-16LE",
710     };
711
712     plugin->audio_db = lms_db_audio_new(ctxt->db);
713     if (!plugin->audio_db)
714         return -1;
715
716     for (i = 0; i < ID3_NUM_ENCODINGS; ++i) {
717         /* do not create charset conv for UTF-8 encoding */
718         if (i == ID3_ENCODING_UTF8) {
719             plugin->cs_convs[i] = NULL;
720             continue;
721         }
722         plugin->cs_convs[i] = lms_charset_conv_new_full(0, 0);
723         if (!plugin->cs_convs[i])
724             return -1;
725         lms_charset_conv_add(plugin->cs_convs[i], id3v2_encodings[i]);
726     }
727
728     return 0;
729 }
730
731 static int
732 _start(struct plugin *plugin, struct lms_context *ctxt)
733 {
734     return lms_db_audio_start(plugin->audio_db);
735 }
736
737 static int
738 _finish(struct plugin *plugin, struct lms_context *ctxt)
739 {
740     int i;
741
742     if (plugin->audio_db)
743         lms_db_audio_free(plugin->audio_db);
744
745     for (i = 0; i < ID3_NUM_ENCODINGS; ++i) {
746         if (plugin->cs_convs[i])
747             lms_charset_conv_free(plugin->cs_convs[i]);
748     }
749
750     return 0;
751 }
752
753 static int
754 _close(struct plugin *plugin)
755 {
756     free(plugin);
757     return 0;
758 }
759
760 API struct lms_plugin *
761 lms_plugin_open(void)
762 {
763     struct plugin *plugin;
764
765     plugin = (struct plugin *)malloc(sizeof(*plugin));
766     plugin->plugin.name = _name;
767     plugin->plugin.match = (lms_plugin_match_fn_t)_match;
768     plugin->plugin.parse = (lms_plugin_parse_fn_t)_parse;
769     plugin->plugin.close = (lms_plugin_close_fn_t)_close;
770     plugin->plugin.setup = (lms_plugin_setup_fn_t)_setup;
771     plugin->plugin.start = (lms_plugin_start_fn_t)_start;
772     plugin->plugin.finish = (lms_plugin_finish_fn_t)_finish;
773
774     return (struct lms_plugin *)plugin;
775 }