summaryrefslogtreecommitdiff
path: root/apps/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'apps/plugins')
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES1
-rw-r--r--apps/plugins/lastfm_scrobbler.c584
3 files changed, 586 insertions, 0 deletions
diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES
index 89aba0e32f..0ef3fe81a4 100644
--- a/apps/plugins/CATEGORIES
+++ b/apps/plugins/CATEGORIES
@@ -49,6 +49,7 @@ jpeg,viewers
49keybox,apps 49keybox,apps
50keyremap,apps 50keyremap,apps
51lamp,apps 51lamp,apps
52lastfm_scrobbler,apps
52logo,demos 53logo,demos
53lrcplayer,apps 54lrcplayer,apps
54lua,viewers 55lua,viewers
diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES
index d2f3c39d54..e3a10c9d15 100644
--- a/apps/plugins/SOURCES
+++ b/apps/plugins/SOURCES
@@ -12,6 +12,7 @@ dict.c
12jackpot.c 12jackpot.c
13keybox.c 13keybox.c
14keyremap.c 14keyremap.c
15lastfm_scrobbler.c
15logo.c 16logo.c
16lrcplayer.c 17lrcplayer.c
17mosaique.c 18mosaique.c
diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c
new file mode 100644
index 0000000000..db75427895
--- /dev/null
+++ b/apps/plugins/lastfm_scrobbler.c
@@ -0,0 +1,584 @@
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2006-2008 Robert Keevil
11 * Converted to Plugin
12 * Copyright (C) 2022 William Wilgus
13 *
14 * This program is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU General Public License
16 * as published by the Free Software Foundation; either version 2
17 * of the License, or (at your option) any later version.
18 *
19 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20 * KIND, either express or implied.
21 *
22 ****************************************************************************/
23/* Scrobbler Plugin
24Audioscrobbler spec at:
25http://www.audioscrobbler.net/wiki/Portable_Player_Logging
26*/
27
28#include "plugin.h"
29
30#ifdef ROCKBOX_HAS_LOGF
31#define logf rb->logf
32#else
33#define logf(...) do { } while(0)
34#endif
35
36
37/****************** constants ******************/
38#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF)
39#define EV_OTHINSTANCE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE)
40#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01)
41#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02)
42#define EV_TRACKFINISH MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x03)
43
44#define SCROBBLER_VERSION "1.1"
45
46/* increment this on any code change that effects output */
47#define SCROBBLER_REVISION " $Revision$"
48
49#define SCROBBLER_MAX_CACHE 32
50/* longest entry I've had is 323, add a safety margin */
51#define SCROBBLER_CACHE_LEN 512
52
53#define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMMP #MUSICBRAINZ_TRACKID\n"
54
55#if CONFIG_RTC
56static time_t timestamp;
57#define BASE_FILENAME HOME_DIR "/.scrobbler.log"
58#define HDR_STR_TIMELESS
59#define get_timestamp() ((long)timestamp)
60#define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time())))
61#else /* !CONFIG_RTC */
62#define HDR_STR_TIMELESS " Timeless"
63#define BASE_FILENAME ".scrobbler-timeless.log"
64#define get_timestamp() (0l)
65#define record_timestamp() ({})
66#endif /* CONFIG_RTC */
67
68#define THREAD_STACK_SIZE 4*DEFAULT_STACK_SIZE
69
70#if (CONFIG_KEYPAD == IRIVER_H100_PAD) || \
71 (CONFIG_KEYPAD == IRIVER_H300_PAD)
72#define SCROBBLE_OFF BUTTON_OFF
73#define SCROBBLE_OFF_TXT "STOP"
74#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
75 (CONFIG_KEYPAD == IPOD_3G_PAD) || \
76 (CONFIG_KEYPAD == IPOD_1G2G_PAD)
77#define SCROBBLE_OFF BUTTON_MENU
78#define SCROBBLE_OFF_TXT "MENU"
79#elif CONFIG_KEYPAD == IAUDIO_X5M5_PAD || \
80 CONFIG_KEYPAD == AGPTEK_ROCKER_PAD
81#define SCROBBLE_OFF BUTTON_POWER
82#define SCROBBLE_OFF_TXT "POWER"
83#elif (CONFIG_KEYPAD == SANSA_E200_PAD) || \
84 (CONFIG_KEYPAD == SANSA_C200_PAD) || \
85 (CONFIG_KEYPAD == SANSA_CLIP_PAD) || \
86 (CONFIG_KEYPAD == SANSA_M200_PAD)
87#define SCROBBLE_OFF BUTTON_POWER
88#define SCROBBLE_OFF_TXT "POWER"
89#elif (CONFIG_KEYPAD == SANSA_FUZE_PAD)
90#define SCROBBLE_OFF BUTTON_HOME
91#define SCROBBLE_OFF_TXT "HOME"
92#elif (CONFIG_KEYPAD == IRIVER_H10_PAD || \
93 CONFIG_KEYPAD == CREATIVE_ZENXFI3_PAD || \
94 CONFIG_KEYPAD == SONY_NWZ_PAD || \
95 CONFIG_KEYPAD == XDUOO_X3_PAD || \
96 CONFIG_KEYPAD == IHIFI_770_PAD || \
97 CONFIG_KEYPAD == IHIFI_800_PAD || \
98 CONFIG_KEYPAD == XDUOO_X3II_PAD || \
99 CONFIG_KEYPAD == XDUOO_X20_PAD || \
100 CONFIG_KEYPAD == FIIO_M3K_LINUX_PAD || \
101 CONFIG_KEYPAD == EROSQ_PAD)
102#define SCROBBLE_OFF BUTTON_POWER
103#define SCROBBLE_OFF_TXT "POWER"
104#elif CONFIG_KEYPAD == GIGABEAT_PAD
105#define SCROBBLE_OFF BUTTON_POWER
106#define SCROBBLE_OFF_TXT "POWER"
107#elif CONFIG_KEYPAD == GIGABEAT_S_PAD \
108 || CONFIG_KEYPAD == SAMSUNG_YPR0_PAD \
109 || CONFIG_KEYPAD == CREATIVE_ZEN_PAD
110#define SCROBBLE_OFF BUTTON_BACK
111#define SCROBBLE_OFF_TXT "BACK"
112#elif CONFIG_KEYPAD == MROBE500_PAD
113#define SCROBBLE_OFF BUTTON_POWER
114#define SCROBBLE_OFF_TXT "POWER"
115#elif CONFIG_KEYPAD == MROBE100_PAD
116#define SCROBBLE_OFF BUTTON_POWER
117#define SCROBBLE_OFF_TXT "POWER"
118#elif CONFIG_KEYPAD == IAUDIO_M3_PAD
119#define SCROBBLE_OFF BUTTON_REC
120#define BATTERY_RC_OFF BUTTON_RC_REC
121#define SCROBBLE_OFF_TXT "REC"
122#elif CONFIG_KEYPAD == COWON_D2_PAD
123#define SCROBBLE_OFF BUTTON_POWER
124#define SCROBBLE_OFF_TXT "POWER"
125#elif CONFIG_KEYPAD == CREATIVEZVM_PAD
126#define SCROBBLE_OFF BUTTON_BACK
127#define SCROBBLE_OFF_TXT "BACK"
128#elif CONFIG_KEYPAD == PHILIPS_HDD1630_PAD
129#define SCROBBLE_OFF BUTTON_POWER
130#define SCROBBLE_OFF_TXT "POWER"
131#elif CONFIG_KEYPAD == PHILIPS_HDD6330_PAD
132#define SCROBBLE_OFF BUTTON_POWER
133#define SCROBBLE_OFF_TXT "POWER"
134#elif CONFIG_KEYPAD == PHILIPS_SA9200_PAD
135#define SCROBBLE_OFF BUTTON_POWER
136#define SCROBBLE_OFF_TXT "POWER"
137#elif CONFIG_KEYPAD == ONDAVX747_PAD
138#define SCROBBLE_OFF BUTTON_POWER
139#define SCROBBLE_OFF_TXT "POWER"
140#elif CONFIG_KEYPAD == ONDAVX777_PAD
141#define SCROBBLE_OFF BUTTON_POWER
142#define SCROBBLE_OFF_TXT "POWER"
143#elif (CONFIG_KEYPAD == SAMSUNG_YH820_PAD) || \
144 (CONFIG_KEYPAD == SAMSUNG_YH92X_PAD)
145#define SCROBBLE_OFF BUTTON_RIGHT
146#define SCROBBLE_OFF_TXT "RIGHT"
147#elif CONFIG_KEYPAD == PBELL_VIBE500_PAD
148#define SCROBBLE_OFF BUTTON_REC
149#define SCROBBLE_OFF_TXT "REC"
150#elif CONFIG_KEYPAD == MPIO_HD200_PAD
151#define SCROBBLE_OFF BUTTON_REC
152#define SCROBBLE_OFF_TXT "REC"
153#elif CONFIG_KEYPAD == MPIO_HD300_PAD
154#define SCROBBLE_OFF BUTTON_REC
155#define SCROBBLE_OFF_TXT "REC"
156#elif CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD
157#define SCROBBLE_OFF BUTTON_POWER
158#define SCROBBLE_OFF_TXT "POWER"
159#elif CONFIG_KEYPAD == SANSA_CONNECT_PAD
160#define SCROBBLE_OFF BUTTON_POWER
161#define SCROBBLE_OFF_TXT "POWER"
162#elif (CONFIG_KEYPAD == HM60X_PAD) || (CONFIG_KEYPAD == HM801_PAD)
163#define SCROBBLE_OFF BUTTON_POWER
164#define SCROBBLE_OFF_TXT "POWER"
165#elif CONFIG_KEYPAD == DX50_PAD
166#define SCROBBLE_OFF BUTTON_POWER_LONG
167#define SCROBBLE_OFF_TXT "Power Long"
168#elif CONFIG_KEYPAD == CREATIVE_ZENXFI2_PAD
169#define SCROBBLE_OFF BUTTON_POWER
170#define SCROBBLE_OFF_TXT "Power"
171#elif CONFIG_KEYPAD == FIIO_M3K_PAD
172#define SCROBBLE_OFF BUTTON_POWER
173#define SCROBBLE_OFF_TXT "Power"
174#elif CONFIG_KEYPAD == SHANLING_Q1_PAD
175/* use touchscreen */
176#else
177#error "No keymap defined!"
178#endif
179#if defined(HAVE_TOUCHSCREEN)
180#ifndef SCROBBLE_OFF
181#define SCROBBLE_OFF BUTTON_TOPLEFT
182#endif
183#ifndef SCROBBLE_OFF_TXT
184#define SCROBBLE_OFF_TXT "TOPLEFT"
185#endif
186#endif
187/****************** prototypes ******************/
188int plugin_main(const void* parameter); /* main loop */
189enum plugin_status plugin_start(const void* parameter); /* entry */
190
191/****************** globals ******************/
192unsigned char **language_strings; /* for use with str() macro; must be init */
193/* communication to the worker thread */
194static struct
195{
196 bool exiting; /* signal to the thread that we want to exit */
197 unsigned int id; /* worker thread id */
198 struct event_queue queue; /* thread event queue */
199 long stack[THREAD_STACK_SIZE / sizeof(long)];
200} gThread;
201
202static struct
203{
204 char *buf;
205 int pos;
206 size_t size;
207 bool pending;
208 bool force_flush;
209} gCache;
210
211/****************** helper fuctions ******************/
212
213int scrobbler_init(void)
214{
215 gCache.buf = rb->plugin_get_buffer(&gCache.size);
216
217 size_t reqsz = SCROBBLER_MAX_CACHE*SCROBBLER_CACHE_LEN;
218 gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz);
219
220 if (gCache.size < reqsz)
221 {
222 logf("SCROBBLER: OOM , %ld < req:%ld", gCache.size, reqsz);
223 return -1;
224 }
225
226 gCache.pos = 0;
227 gCache.pending = false;
228 gCache.force_flush = true;
229 logf("Scrobbler Initialized");
230 return 1;
231}
232
233static void get_scrobbler_filename(char *path, size_t size)
234{
235 int used;
236
237 used = rb->snprintf(path, size, "/%s", BASE_FILENAME);
238
239 if (used >= (int)size)
240 {
241 logf("%s: not enough buffer space for log file", __func__);
242 rb->memset(path, 0, size);
243 }
244}
245
246static void scrobbler_write_cache(void)
247{
248 int i;
249 int fd;
250 logf("%s", __func__);
251 char scrobbler_file[MAX_PATH];
252 get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file));
253
254 /* If the file doesn't exist, create it.
255 Check at each write since file may be deleted at any time */
256 if(!rb->file_exists(scrobbler_file))
257 {
258 fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666);
259 if(fd >= 0)
260 {
261 rb->fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n"
262 "#TZ/UNKNOWN\n" "#CLIENT/Rockbox "
263 TARGET_NAME SCROBBLER_REVISION
264 HDR_STR_TIMELESS "\n");
265 rb->fdprintf(fd, ITEM_HDR);
266
267 rb->close(fd);
268 }
269 else
270 {
271 logf("SCROBBLER: cannot create log file (%s)", scrobbler_file);
272 }
273 }
274
275 /* write the cache entries */
276 fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND);
277 if(fd >= 0)
278 {
279 logf("SCROBBLER: writing %d entries", gCache.pos);
280 /* copy data to temporary storage in case data moves during I/O */
281 char temp_buf[SCROBBLER_CACHE_LEN];
282 for ( i=0; i < gCache.pos; i++ )
283 {
284 logf("SCROBBLER: write %d", i);
285 char* scrobbler_buf = gCache.buf;
286 ssize_t len = rb->strlcpy(temp_buf, scrobbler_buf+(SCROBBLER_CACHE_LEN*i),
287 sizeof(temp_buf));
288 if (rb->write(fd, temp_buf, len) != len)
289 break;
290 }
291 rb->close(fd);
292 }
293 else
294 {
295 logf("SCROBBLER: error writing file");
296 }
297
298 /* clear even if unsuccessful - don't want to overflow the buffer */
299 gCache.pos = 0;
300}
301
302static void scrobbler_flush_callback(void)
303{
304 (void) gCache.force_flush;
305 if(gCache.pos <= 0)
306 return;
307#if (CONFIG_STORAGE & STORAGE_ATA)
308 else
309#else
310 if ((gCache.pos >= SCROBBLER_MAX_CACHE / 2) || gCache.force_flush == true)
311#endif
312 {
313 gCache.force_flush = false;
314 logf("%s", __func__);
315 scrobbler_write_cache();
316 }
317}
318
319static void scrobbler_add_to_cache(const struct mp3entry *id)
320{
321 if ( gCache.pos >= SCROBBLER_MAX_CACHE )
322 scrobbler_write_cache();
323
324 char rating = 'S'; /* Skipped */
325 char* scrobbler_buf = gCache.buf;
326
327 logf("SCROBBLER: add_to_cache[%d]", gCache.pos);
328
329 if (id->elapsed > id->length / 2)
330 rating = 'L'; /* Listened */
331
332 char tracknum[11] = { "" };
333
334 if (id->tracknum > 0)
335 rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum);
336
337 int ret = rb->snprintf(&scrobbler_buf[(SCROBBLER_CACHE_LEN*gCache.pos)],
338 SCROBBLER_CACHE_LEN,
339 "%s\t%s\t%s\t%s\t%d\t%c\t%ld\t%s\n",
340 id->artist,
341 id->album ?: "",
342 id->title,
343 tracknum,
344 (int)(id->length / 1000),
345 rating,
346 get_timestamp(),
347 id->mb_track_id ?id->mb_track_id: "");
348
349 if ( ret >= SCROBBLER_CACHE_LEN )
350 {
351 logf("SCROBBLER: entry too long:");
352 logf("SCROBBLER: %s", id->path);
353 }
354 else
355 {
356 logf("Added %s", scrobbler_buf);
357 gCache.pos++;
358 rb->register_storage_idle_func(scrobbler_flush_callback);
359 }
360
361}
362
363static void scrobbler_flush_cache(void)
364{
365 logf("%s", __func__);
366 /* Add any pending entries to the cache */
367 if (gCache.pending)
368 {
369 gCache.pending = false;
370 if (rb->audio_status())
371 scrobbler_add_to_cache(rb->audio_current_track());
372 }
373
374 /* Write the cache to disk if needed */
375 if (gCache.pos > 0)
376 {
377 scrobbler_write_cache();
378 }
379}
380
381static void scrobbler_change_event(unsigned short id, void *ev_data)
382{
383 (void)id;
384 logf("%s", __func__);
385 struct mp3entry *id3 = ((struct track_event *)ev_data)->id3;
386
387 /* check if track was resumed > %50 played ( likely got saved )
388 check for blank artist or track name */
389 if ((id3->elapsed > id3->length / 2) || !id3->artist || !id3->title)
390 {
391 gCache.pending = false;
392 logf("SCROBBLER: skipping file %s", id3->path);
393 }
394 else
395 {
396 logf("SCROBBLER: add pending %s",id3->path);
397 record_timestamp();
398 gCache.pending = true;
399 }
400}
401#ifdef ROCKBOX_HAS_LOGF
402static const char* track_event_info(struct track_event* te)
403{
404
405 static const char *strflags[] = {"TEF_NONE", "TEF_CURRENT",
406 "TEF_AUTOSKIP", "TEF_CUR|ASKIP",
407 "TEF_REWIND", "TEF_CUR|REW",
408 "TEF_ASKIP|REW", "TEF_CUR|ASKIP|REW"};
409/*TEF_NONE = 0x0, no flags are set
410* TEF_CURRENT = 0x1, event is for the current track
411* TEF_AUTO_SKIP = 0x2, event is sent in context of auto skip
412* TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the
413 position before the seek back to 0
414*/
415 logf("flag %d", te->flags);
416 return strflags[te->flags&0x7];
417}
418
419#endif
420static void scrobbler_finish_event(unsigned short id, void *ev_data)
421{
422 (void)id;
423 struct track_event *te = ((struct track_event *)ev_data);
424 struct mp3entry *id3 = te->id3;
425 logf("%s %s %s", __func__, gCache.pending?"True":"False", track_event_info(te));
426 /* add entry using the currently ending track */
427 if (gCache.pending && (te->flags & TEF_CURRENT) && !(te->flags & TEF_REWIND))
428 {
429 gCache.pending = false;
430
431 if (id3->elapsed*2 >= id3->length)
432 scrobbler_add_to_cache(te->id3);
433 else
434 {
435 logf("%s Discarding < 50%% played", __func__);
436 }
437 }
438
439
440}
441
442/****************** main thread + helper ******************/
443void thread(void)
444{
445 bool in_usb = false;
446
447 struct queue_event ev;
448 while (!gThread.exiting)
449 {
450 rb->queue_wait(&gThread.queue, &ev);
451
452 switch (ev.id)
453 {
454 case SYS_USB_CONNECTED:
455 scrobbler_flush_cache();
456 rb->usb_acknowledge(SYS_USB_CONNECTED_ACK);
457 in_usb = true;
458 break;
459 case SYS_USB_DISCONNECTED:
460 in_usb = false;
461 /*fall through*/
462 case EV_STARTUP:
463 rb->beep_play(1500, 100, 1000);
464 break;
465 case SYS_POWEROFF:
466 gCache.force_flush = true;
467 /*fall through*/
468 case EV_EXIT:
469 rb->unregister_storage_idle_func(scrobbler_flush_callback, !in_usb);
470 return;
471 case EV_OTHINSTANCE:
472 scrobbler_flush_cache();
473 rb->splashf(HZ * 2, "%s Cache Flushed", str(LANG_AUDIOSCROBBLER));
474 break;
475 default:
476 logf("default %ld", ev.id);
477 break;
478 }
479 }
480}
481
482void thread_create(void)
483{
484 /* put the thread's queue in the bcast list */
485 rb->queue_init(&gThread.queue, true);
486 gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack),
487 0, "Last.Fm_TSR"
488 IF_PRIO(, PRIORITY_BACKGROUND)
489 IF_COP(, CPU));
490 rb->queue_post(&gThread.queue, EV_STARTUP, 0);
491 rb->yield();
492}
493
494void thread_quit(void)
495{
496 if (!gThread.exiting) {
497 rb->queue_post(&gThread.queue, EV_EXIT, 0);
498 rb->thread_wait(gThread.id);
499 /* we don't want any more events */
500 rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event);
501 rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event);
502 /* remove the thread's queue from the broadcast list */
503 rb->queue_delete(&gThread.queue);
504 gThread.exiting = true;
505 }
506}
507
508/* callback to end the TSR plugin, called before a new one gets loaded */
509static bool exit_tsr(bool reenter)
510{
511 logf("%s", __func__);
512 bool is_exit = false;
513 int button;
514 if (reenter)
515 {
516 logf(" reenter other instance ");
517 rb->queue_post(&gThread.queue, EV_OTHINSTANCE, 0);
518 return false; /* dont let it start again */
519 }
520 rb->lcd_clear_display();
521 rb->lcd_puts_scroll(0, 0, "Scrobbler is currently running.");
522 rb->lcd_puts_scroll(0, 1, "Press " SCROBBLE_OFF_TXT " to exit");
523 rb->lcd_puts_scroll(0, 2, "Anything else will resume");
524
525 rb->lcd_update();
526 rb->button_clear_queue();
527 while (1)
528 {
529 button = rb->button_get(true);
530 if (IS_SYSEVENT(button))
531 continue;
532 if (button == SCROBBLE_OFF)
533 {
534 rb->queue_post(&gThread.queue, EV_EXIT, 0);
535 rb->thread_wait(gThread.id);
536 /* remove the thread's queue from the broadcast list */
537 rb->queue_delete(&gThread.queue);
538 is_exit = true;
539 }
540 else is_exit = false;
541
542 break;
543 }
544 FOR_NB_SCREENS(idx)
545 rb->screens[idx]->scroll_stop();
546
547 if (is_exit)
548 thread_quit();
549
550 return is_exit;
551}
552
553/****************** main ******************/
554
555int plugin_main(const void* parameter)
556{
557 (void)parameter;
558
559 rb->memset(&gThread, 0, sizeof(gThread));
560 rb->splashf(HZ / 2, "%s Started",str(LANG_AUDIOSCROBBLER));
561 logf("%s: %s Started", __func__, str(LANG_AUDIOSCROBBLER));
562
563 rb->plugin_tsr(exit_tsr); /* stay resident */
564
565 rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event);
566 rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event);
567 thread_create();
568
569 return 0;
570}
571
572/***************** Plugin Entry Point *****************/
573
574enum plugin_status plugin_start(const void* parameter)
575{
576 /* now go ahead and have fun! */
577 if (rb->usb_inserted() == true)
578 return PLUGIN_USB_CONNECTED;
579 language_strings = rb->language_strings;
580 if (scrobbler_init() < 0)
581 return PLUGIN_ERROR;
582 int ret = plugin_main(parameter);
583 return (ret==0) ? PLUGIN_OK : PLUGIN_ERROR;
584}