| /*************************************************************************** |
| * __________ __ ___. |
| * Open \______ \ ____ ____ | | _\_ |__ _______ ___ |
| * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / |
| * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < |
| * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ |
| * \/ \/ \/ \/ \/ |
| * $Id$ |
| * |
| * Copyright (C) 2006-2008 Robert Keevil |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License |
| * as published by the Free Software Foundation; either version 2 |
| * of the License, or (at your option) any later version. |
| * |
| * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
| * KIND, either express or implied. |
| * |
| ****************************************************************************/ |
| /* |
| Audioscrobbler spec at: |
| http://www.audioscrobbler.net/wiki/Portable_Player_Logging |
| */ |
| |
| #include <stdio.h> |
| #include <config.h> |
| #include "file.h" |
| #include "logf.h" |
| #include "metadata.h" |
| #include "kernel.h" |
| #include "audio.h" |
| #include "core_alloc.h" |
| #include "settings.h" |
| #include "ata_idle_notify.h" |
| #include "pathfuncs.h" |
| #include "appevents.h" |
| |
| #if CONFIG_RTC |
| #include "time.h" |
| #include "timefuncs.h" |
| #endif |
| |
| #include "scrobbler.h" |
| |
| #define SCROBBLER_VERSION "1.1" |
| |
| /* increment this on any code change that effects output */ |
| #define SCROBBLER_REVISION " $Revision$" |
| |
| #define SCROBBLER_MAX_CACHE 32 |
| /* longest entry I've had is 323, add a safety margin */ |
| #define SCROBBLER_CACHE_LEN 512 |
| |
| static bool scrobbler_initialised = false; |
| static int scrobbler_cache = 0; |
| static int cache_pos = 0; |
| static bool pending = false; |
| #if CONFIG_RTC |
| static time_t timestamp; |
| #define BASE_FILENAME ".scrobbler.log" |
| #define HDR_STR_TIMELESS |
| #define get_timestamp() ((long)timestamp) |
| #define record_timestamp() ((void)(timestamp = mktime(get_time()))) |
| #else /* !CONFIG_RTC */ |
| #define HDR_STR_TIMELESS " Timeless" |
| #define BASE_FILENAME ".scrobbler-timeless.log" |
| #define get_timestamp() (0l) |
| #define record_timestamp() ({}) |
| #endif /* CONFIG_RTC */ |
| |
| static void get_scrobbler_filename(char *path, size_t size) |
| { |
| int used; |
| /* Get location of USB mass storage area */ |
| #ifdef APPLICATION |
| #if (CONFIG_PLATFORM & PLATFORM_MAEMO) |
| used = snprintf(path, size, "/home/user/MyDocs/%s", BASE_FILENAME); |
| #elif (CONFIG_PLATFORM & PLATFORM_ANDROID) |
| used = snprintf(path, size, "/sdcard/%s", BASE_FILENAME); |
| #elif defined (SAMSUNG_YPR0) || defined(DX50) || defined(DX90) |
| used = snprintf(path, size, "%s/%s", HOME_DIR, BASE_FILENAME); |
| #else /* SDL/unknown RaaA build */ |
| used = snprintf(path, size, "%s/%s", ROCKBOX_DIR, BASE_FILENAME); |
| #endif /* (CONFIG_PLATFORM & PLATFORM_MAEMO) */ |
| |
| #else |
| used = snprintf(path, size, "/%s", BASE_FILENAME); |
| #endif |
| |
| if (used >= (int)size) |
| { |
| logf("SCROBBLER: not enough buffer space for log file"); |
| memset(path, 0, size); |
| } |
| } |
| |
| static void write_cache(void) |
| { |
| int i; |
| int fd; |
| |
| char scrobbler_file[MAX_PATH]; |
| get_scrobbler_filename(scrobbler_file, MAX_PATH); |
| |
| /* If the file doesn't exist, create it. |
| Check at each write since file may be deleted at any time */ |
| if(!file_exists(scrobbler_file)) |
| { |
| fd = open(scrobbler_file, O_RDWR | O_CREAT, 0666); |
| if(fd >= 0) |
| { |
| fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n" |
| "#TZ/UNKNOWN\n" "#CLIENT/Rockbox " |
| TARGET_NAME SCROBBLER_REVISION |
| HDR_STR_TIMELESS "\n"); |
| |
| close(fd); |
| } |
| else |
| { |
| logf("SCROBBLER: cannot create log file"); |
| } |
| } |
| |
| /* write the cache entries */ |
| fd = open(scrobbler_file, O_WRONLY | O_APPEND); |
| if(fd >= 0) |
| { |
| logf("SCROBBLER: writing %d entries", cache_pos); |
| /* copy data to temporary storage in case data moves during I/O */ |
| char temp_buf[SCROBBLER_CACHE_LEN]; |
| for ( i=0; i < cache_pos; i++ ) |
| { |
| logf("SCROBBLER: write %d", i); |
| char* scrobbler_buf = core_get_data(scrobbler_cache); |
| ssize_t len = strlcpy(temp_buf, scrobbler_buf+(SCROBBLER_CACHE_LEN*i), |
| sizeof(temp_buf)); |
| if (write(fd, temp_buf, len) != len) |
| break; |
| } |
| close(fd); |
| } |
| else |
| { |
| logf("SCROBBLER: error writing file"); |
| } |
| |
| /* clear even if unsuccessful - don't want to overflow the buffer */ |
| cache_pos = 0; |
| } |
| |
| static void scrobbler_flush_callback(void) |
| { |
| if (scrobbler_initialised && cache_pos) |
| write_cache(); |
| } |
| |
| static void add_to_cache(const struct mp3entry *id) |
| { |
| if ( cache_pos >= SCROBBLER_MAX_CACHE ) |
| write_cache(); |
| |
| char rating = 'S'; /* Skipped */ |
| char* scrobbler_buf = core_get_data(scrobbler_cache); |
| |
| logf("SCROBBLER: add_to_cache[%d]", cache_pos); |
| |
| if (id->elapsed > id->length / 2) |
| rating = 'L'; /* Listened */ |
| |
| char tracknum[11] = { "" }; |
| |
| if (id->tracknum > 0) |
| snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); |
| |
| int ret = snprintf(scrobbler_buf+(SCROBBLER_CACHE_LEN*cache_pos), |
| SCROBBLER_CACHE_LEN, |
| "%s\t%s\t%s\t%s\t%d\t%c\t%ld\t%s\n", |
| id->artist, |
| id->album ?: "", |
| id->title, |
| tracknum, |
| (int)(id->length / 1000), |
| rating, |
| get_timestamp(), |
| id->mb_track_id ?: ""); |
| |
| if ( ret >= SCROBBLER_CACHE_LEN ) |
| { |
| logf("SCROBBLER: entry too long:"); |
| logf("SCROBBLER: %s", id->path); |
| } |
| else |
| { |
| cache_pos++; |
| register_storage_idle_func(scrobbler_flush_callback); |
| } |
| |
| } |
| |
| static void scrobbler_change_event(unsigned short id, void *ev_data) |
| { |
| (void)id; |
| struct mp3entry *id3 = ((struct track_event *)ev_data)->id3; |
| |
| /* check if track was resumed > %50 played |
| check for blank artist or track name */ |
| if (id3->elapsed > id3->length / 2 || !id3->artist || !id3->title) |
| { |
| pending = false; |
| logf("SCROBBLER: skipping file %s", id3->path); |
| } |
| else |
| { |
| logf("SCROBBLER: add pending"); |
| record_timestamp(); |
| pending = true; |
| } |
| } |
| |
| static void scrobbler_finish_event(unsigned short id, void *data) |
| { |
| (void)id; |
| struct track_event *te = (struct track_event *)data; |
| |
| /* add entry using the currently ending track */ |
| if (pending && (te->flags & TEF_CURRENT) |
| #if CONFIG_CODEC == SWCODEC |
| && !(te->flags & TEF_REWIND) |
| #endif |
| ) |
| { |
| pending = false; |
| add_to_cache(te->id3); |
| } |
| } |
| |
| int scrobbler_init(void) |
| { |
| if (scrobbler_initialised) |
| return 1; |
| |
| scrobbler_cache = core_alloc("scrobbler", |
| SCROBBLER_MAX_CACHE*SCROBBLER_CACHE_LEN); |
| |
| if (scrobbler_cache <= 0) |
| { |
| logf("SCROOBLER: OOM"); |
| return -1; |
| } |
| |
| cache_pos = 0; |
| pending = false; |
| |
| scrobbler_initialised = true; |
| |
| add_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); |
| add_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); |
| |
| return 1; |
| } |
| |
| static void scrobbler_flush_cache(void) |
| { |
| /* Add any pending entries to the cache */ |
| if (pending) |
| { |
| pending = false; |
| if (audio_status()) |
| add_to_cache(audio_current_track()); |
| } |
| |
| /* Write the cache to disk if needed */ |
| if (cache_pos) |
| write_cache(); |
| } |
| |
| void scrobbler_shutdown(bool poweroff) |
| { |
| if (!scrobbler_initialised) |
| return; |
| |
| remove_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); |
| remove_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); |
| |
| scrobbler_flush_cache(); |
| |
| if (!poweroff) |
| { |
| /* get rid of the buffer */ |
| core_free(scrobbler_cache); |
| scrobbler_cache = 0; |
| } |
| |
| scrobbler_initialised = false; |
| } |
| |
| bool scrobbler_is_enabled(void) |
| { |
| return scrobbler_initialised; |
| } |