/* -*- linux-c -*- Copyright (C) 2004 Tom Szilagyi 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. $Id: cdda.c 973 2008-01-01 11:16:27Z peterszilagyi $ */ #include #ifdef HAVE_CDDA #include #include #include #include #include #include #ifndef _WIN32 #include #endif /* !_WIN32 */ #ifdef HAVE_CDDB #define _TMP_HAVE_CDDB 1 #undef HAVE_CDDB #endif /* HAVE_CDDB */ #include #include #include #ifdef HAVE_CDDB #undef HAVE_CDDB #endif /* HAVE_CDDB */ #ifdef _TMP_HAVE_CDDB #define HAVE_CDDB 1 #undef _TMP_HAVE_CDDB #endif /* _TMP_HAVE_CDDB */ #include "common.h" #include "utils_gui.h" #include "options.h" #include "music_browser.h" #include "playlist.h" #include "gui_main.h" #include "rb.h" #include "i18n.h" #include "cdda.h" #include "store_cdda.h" extern options_t options; extern GdkPixbuf * icon_store; extern GdkPixbuf * icon_record; extern GdkPixbuf * icon_track; extern GdkPixbuf * icon_cdda; extern GdkPixbuf * icon_cdda_disc; extern GdkPixbuf * icon_cdda_nodisc; extern GtkTreeStore * music_store; extern GtkWidget * browser_window; typedef struct { int event_type; char * device_path; } cdda_notify_t; #define CDDA_NOTIFY_RB_SIZE (16*sizeof(cdda_notify_t)) #define CDDA_TIMEOUT_PERIOD 500 #define CDDA_EVENT_NEW_DRIVE 1 #define CDDA_EVENT_REMOVED_DRIVE 2 #define CDDA_EVENT_CHANGED_DRIVE 3 cdda_drive_t cdda_drives[CDDA_DRIVES_MAX]; AQUALUNG_THREAD_DECLARE(cdda_scanner_id) AQUALUNG_MUTEX_DECLARE(cdda_mutex) int cdda_scanner_working; guint cdda_timeout_tag; rb_t * cdda_notify_rb; static void cdda_log_handler(cdio_log_level_t level, const char message[]) { switch(level) { case CDIO_LOG_DEBUG: case CDIO_LOG_INFO: case CDIO_LOG_WARN: return; default: printf("cdio %d: %s\n", level, message); } } int cdda_get_first_free_slot(int * n) { int i; for (i = 0; i < CDDA_DRIVES_MAX; i++) { if (cdda_drives[i].device_path[0] == '\0') { *n = i; return 0; } } return -1; } cdda_drive_t * cdda_get_drive(int n) { if (n >= 0 && n < CDDA_DRIVES_MAX) { return &cdda_drives[n]; } else { return NULL; } } int cdda_get_n(char * device_path) { int i; for (i = 0; i < CDDA_DRIVES_MAX; i++) { if (!strcmp(device_path, cdda_drives[i].device_path)) return i; } return -1; } cdda_drive_t * cdda_get_drive_by_device_path(char * device_path) { int i; for (i = 0; i < CDDA_DRIVES_MAX; i++) { if (!strcmp(device_path, cdda_drives[i].device_path)) return &cdda_drives[i]; } return NULL; } cdda_drive_t * cdda_get_drive_by_spec_device_path(char * device_path) { if (strstr(device_path, "CDDA_DRIVE ") == device_path) { return cdda_get_drive_by_device_path(device_path + 11); } return NULL; } /* value should point to a buffer of size at least CDDA_MAXLEN */ /* ret 0 if OK, -1 if readlink error */ #ifndef _WIN32 int symlink_deref_absolute(char * symlink, char * path) { int ret = readlink(symlink, path, CDDA_MAXLEN-1); if (ret < 0) return -1; path[ret] = '\0'; if (!g_path_is_absolute(path)) { char tmp[CDDA_MAXLEN]; char * dir = g_path_get_dirname(symlink); snprintf(tmp, CDDA_MAXLEN-1, "%s/%s", dir, path); strcpy(path, tmp); g_free(dir); } return 0; } #endif /* !_WIN32 */ /* return 1 if drives[i] is a symlink to a drive path * also present in the drives[] string array, or the * second, third, ... of symlinks pointing to the same * physical device path. Return 0 else. */ int cdda_skip_extra_symlink(char ** drives, int i) { #ifndef _WIN32 int j; char path[CDDA_MAXLEN]; if (!g_file_test(drives[i], G_FILE_TEST_IS_SYMLINK)) return 0; if ((symlink_deref_absolute(drives[i], path)) != 0) return 1; /* have the destination of the symlink in our device list */ for (j = 0; drives[j] != NULL; j++) { if (i == j) continue; if (strcmp(drives[j], path) == 0) { return 1; } } /* have another symlink to the same destination with a smaller index */ for (j = 0; (j < i) && (drives[j] != NULL); j++) { char path2[CDDA_MAXLEN]; if (!g_file_test(drives[j], G_FILE_TEST_IS_SYMLINK)) continue; if ((symlink_deref_absolute(drives[j], path2)) != 0) continue; if (strcmp(path, path2) == 0) return 1; } return 0; #else return 0; #endif /* !_WIN32 */ } unsigned long calc_cdda_hash(cdda_disc_t * disc) { unsigned long result = 0; unsigned long tmp; unsigned long discid; int i; if (disc->n_tracks == 0) { return 0; } for (i = 0; i < disc->n_tracks; i++) { tmp = (disc->toc[i] + 150) / 75; do { result += tmp % 10; tmp /= 10; } while (tmp != 0); } discid = (result % 0xff) << 24 | (disc->toc[disc->n_tracks] / 75) << 8 | disc->n_tracks; return discid; } int cdda_hash_matches(char * filename, unsigned long hash) { char device_path[CDDA_MAXLEN]; unsigned long fhash; unsigned int track; if (sscanf(filename, "CDDA %s %lX %u", device_path, &fhash, &track) < 3) { return 0; } return fhash == hash; } /* return 0 if OK, -1 if drive is apparently not available */ int cdda_scan_drive(char * device_path, cdda_drive_t * cdda_drive) { track_t tracks; track_t first_track; track_t last_track; track_t skipped = 0; int i, n = 0; cdrom_drive_t * d; if (cdda_drive->cdio == NULL) { cdda_drive->cdio = cdio_open(device_path, DRIVER_DEVICE); if (!cdda_drive->cdio) { return -1; } strncpy(cdda_drive->device_path, device_path, CDDA_MAXLEN-1); } cdda_drive->disc.n_tracks = 0; tracks = cdio_get_num_tracks(cdda_drive->cdio); if (tracks < 1 || tracks >= 100) { cdda_drive->disc.hash_prev = cdda_drive->disc.hash; cdda_drive->disc.hash = 0L; return 0; } d = cdio_cddap_identify_cdio(cdda_drive->cdio, 0, NULL); if (d == NULL) { return -1; } first_track = cdio_get_first_track_num(cdda_drive->cdio); last_track = cdio_get_last_track_num(cdda_drive->cdio); for (i = first_track; i <= last_track; i++) { if (!cdio_cddap_track_audiop(d, i)) { skipped = i; last_track = i-1; break; } } cdio_cddap_close_no_free_cdio(d); if (last_track < first_track) { return 0; } cdda_drive->disc.n_tracks = last_track - first_track + 1; for (i = first_track; i <= last_track; i++) { lsn_t lsn = cdio_get_track_lsn(cdda_drive->cdio, i); if (lsn != CDIO_INVALID_LSN) { cdda_drive->disc.toc[n++] = lsn; } } if (skipped == 0) { cdda_drive->disc.toc[n] = cdio_get_track_lsn(cdda_drive->cdio, CDIO_CDROM_LEADOUT_TRACK); } else { cdda_drive->disc.toc[n] = cdio_get_track_lsn(cdda_drive->cdio, skipped); if ((cdda_drive->disc.toc[n] - cdda_drive->disc.toc[n-1]) >= 11400) { /* compensate end of track with multisession offset */ cdda_drive->disc.toc[n] -= 11400; } } cdda_drive->disc.hash_prev = cdda_drive->disc.hash; cdda_drive->disc.hash = calc_cdda_hash(&cdda_drive->disc); if (cdda_drive->disc.hash != cdda_drive->disc.hash_prev) { strncpy(cdda_drive->disc.artist_name, _("Unknown Artist"), MAXLEN-1); strncpy(cdda_drive->disc.record_name, _("Unknown Record"), MAXLEN-1); cdda_drive->swap_bytes = -1; /* unknown */ } return 0; } void cdda_send_event(int event_type, char * drive) { cdda_notify_t notify; /* If there is no space for the message, it is dropped. * This may happen if GUI gets blocked (unable to run cdda_timeout_callback() * for several seconds), however it is not anticipated to happen very often. * In such case these events are not very much of use, anyway. */ if (rb_write_space(cdda_notify_rb) >= sizeof(cdda_notify_t)) { notify.event_type = event_type; notify.device_path = strdup(drive); rb_write(cdda_notify_rb, (char *)¬ify, sizeof(cdda_notify_t)); } } void cdda_scan_all_drives(void) { char ** drives = NULL; char touched[CDDA_DRIVES_MAX]; int i; for (i = 0; i < CDDA_DRIVES_MAX; i++) { if (cdda_drives[i].cdio != NULL) { cdda_drives[i].media_changed = options.cdda_force_drive_rescan ? 1 : cdio_get_media_changed(cdda_drives[i].cdio); if (cdda_drives[i].media_changed) { cdio_destroy(cdda_drives[i].cdio); cdda_drives[i].cdio = NULL; } } } drives = cdio_get_devices(DRIVER_DEVICE); if (!drives) return; for (i = 0; i < CDDA_DRIVES_MAX; i++) touched[i] = 0; for (i = 0; (drives[i] != NULL) && (i < CDDA_DRIVES_MAX); i++) { int n; /* see if drives[i] is already known to us... */ cdda_drive_t * d = cdda_get_drive_by_device_path(drives[i]); if (d != NULL) { /* yes */ n = cdda_get_n(drives[i]); touched[n] = 1; if (cdda_drives[n].media_changed) { cdda_scan_drive(drives[i], cdda_get_drive(n)); if ((cdda_drives[n].disc.hash == 0L) || (cdda_drives[n].disc.hash != cdda_drives[n].disc.hash_prev)) { /* EVENT refresh disc data */ cdda_send_event(CDDA_EVENT_CHANGED_DRIVE, drives[i]); } } } else { /* no, scan the drive */ if (cdda_skip_extra_symlink(drives, i)) continue; if (cdda_get_first_free_slot(&n) < 0) { printf("cdda.c: error: too many CD drives\n"); return; } if (cdda_scan_drive(drives[i], cdda_get_drive(n)) >= 0) { touched[n] = 1; /* EVENT newly discovered drive */ cdda_send_event(CDDA_EVENT_NEW_DRIVE, drives[i]); } } } cdio_free_device_list(drives); /* remove all drives that were not touched (those that disappeared) */ for (i = 0; i < CDDA_DRIVES_MAX; i++) { if ((cdda_drives[i].device_path[0] != '\0') && (touched[i] == 0)) { /* EVENT removed drive */ cdda_send_event(CDDA_EVENT_REMOVED_DRIVE, cdda_drives[i].device_path); cdio_destroy(cdda_drives[i].cdio); memset(cdda_drives + i, 0, sizeof(cdda_drive_t)); } } } void * cdda_scanner(void * arg) { int i = 0; cdio_log_set_handler(cdda_log_handler); AQUALUNG_MUTEX_LOCK(cdda_mutex) cdda_scan_all_drives(); AQUALUNG_MUTEX_UNLOCK(cdda_mutex) while (cdda_scanner_working) { struct timespec req_time; struct timespec rem_time; req_time.tv_sec = 0; req_time.tv_nsec = 100000000; nanosleep(&req_time, &rem_time); if (i == 50) { /* scan every 5 seconds */ AQUALUNG_MUTEX_LOCK(cdda_mutex) cdda_scan_all_drives(); AQUALUNG_MUTEX_UNLOCK(cdda_mutex) i = 0; } ++i; } AQUALUNG_MUTEX_LOCK(cdda_mutex) for (i = 0; i < CDDA_DRIVES_MAX; i++) { if (cdda_drives[i].cdio != NULL) { cdio_destroy(cdda_drives[i].cdio); memset(cdda_drives + i, 0, sizeof(cdda_drive_t)); } } AQUALUNG_MUTEX_UNLOCK(cdda_mutex) return NULL; } gint cdda_timeout_callback(gpointer data) { cdda_notify_t notify; while (rb_read_space(cdda_notify_rb) >= sizeof(cdda_notify_t)) { rb_read(cdda_notify_rb, (char *)¬ify, sizeof(cdda_notify_t)); switch (notify.event_type) { case CDDA_EVENT_NEW_DRIVE: AQUALUNG_MUTEX_LOCK(cdda_mutex) insert_cdda_drive_node(notify.device_path); AQUALUNG_MUTEX_UNLOCK(cdda_mutex) free(notify.device_path); break; case CDDA_EVENT_CHANGED_DRIVE: AQUALUNG_MUTEX_LOCK(cdda_mutex) refresh_cdda_drive_node(notify.device_path); AQUALUNG_MUTEX_UNLOCK(cdda_mutex) free(notify.device_path); break; case CDDA_EVENT_REMOVED_DRIVE: AQUALUNG_MUTEX_LOCK(cdda_mutex) remove_cdda_drive_node(notify.device_path); AQUALUNG_MUTEX_UNLOCK(cdda_mutex) free(notify.device_path); break; } } return TRUE; } void cdda_timeout_start(void) { cdda_timeout_tag = aqualung_timeout_add(CDDA_TIMEOUT_PERIOD, cdda_timeout_callback, NULL); } void cdda_timeout_stop(void) { g_source_remove(cdda_timeout_tag); } void cdda_scanner_start(void) { if (cdda_scanner_working) { return; } cdda_notify_rb = rb_create(CDDA_NOTIFY_RB_SIZE); #ifdef _WIN32 cdda_mutex = g_mutex_new(); #endif /* _WIN32 */ cdda_timeout_start(); cdda_scanner_working = 1; AQUALUNG_THREAD_CREATE(cdda_scanner_id, NULL, cdda_scanner, NULL) } void cdda_scanner_stop(void) { cdda_scanner_working = 0; AQUALUNG_THREAD_JOIN(cdda_scanner_id) cdda_timeout_callback(NULL); /* cleanup any leftover messages */ cdda_timeout_stop(); #ifdef _WIN32 g_mutex_free(cdda_mutex); #endif /* _WIN32 */ rb_free(cdda_notify_rb); } /* For some odd reason, drive names on Win32 begin with "\\.\" so eg. D: has a * device_path of "\\.\D:" This function gets rid of that. Returns a pointer to * the same buffer that is passed in (no mem allocated). */ char * cdda_displayed_device_path(char * device_path) { #ifdef _WIN32 if (strstr(device_path, "\\\\.\\") == device_path) { return device_path + 4; } else { return device_path; } #else return device_path; #endif /* _WIN32 */ } /* Returns 0 on success, 1 on failure */ int update_track_data_from_cdtext(cdda_drive_t * drive, GtkTreeIter * iter_drive) { CdIo_t * cdio; cdtext_t * cdtext; GtkTreeIter iter; int i; int ret = 0; cdio = cdio_open(drive->device_path, DRIVER_UNKNOWN); cdtext = cdio_get_cdtext(cdio, 0); if (cdtext == NULL) { ret = 1; } else { char tmp[MAXLEN]; tmp[0] = '\0'; if (cdtext->field[CDTEXT_PERFORMER] != NULL && *(cdtext->field[CDTEXT_PERFORMER]) != '\0') { strncat(tmp, cdtext->field[CDTEXT_PERFORMER], MAXLEN-1); strncpy(drive->disc.artist_name, cdtext->field[CDTEXT_PERFORMER], MAXLEN-1); } else { ret = 1; } strncat(tmp, ": ", MAXLEN - strlen(tmp) - 1); if (cdtext->field[CDTEXT_TITLE] != NULL && *(cdtext->field[CDTEXT_TITLE]) != '\0') { strncat(tmp, cdtext->field[CDTEXT_TITLE], MAXLEN - strlen(tmp) - 1); strncpy(drive->disc.record_name, cdtext->field[CDTEXT_TITLE], MAXLEN-1); } else { ret = 1; } if (ret == 0) { gtk_tree_store_set(music_store, iter_drive, MS_COL_NAME, tmp, -1); } } i = 0; while (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(music_store), &iter, iter_drive, i++)) { cdtext = cdio_get_cdtext(cdio, i); if (cdtext == NULL) { ret = 1; continue; } if (cdtext->field[CDTEXT_TITLE] != NULL && *(cdtext->field[CDTEXT_TITLE]) != '\0') { gtk_tree_store_set(music_store, &iter, MS_COL_NAME, cdtext->field[CDTEXT_TITLE], -1); } else { ret = 1; } } cdio_destroy(cdio); return ret; } void update_track_data(cdda_drive_t * drive, GtkTreeIter iter_drive) { int i; for (i = 0; i < drive->disc.n_tracks; i++) { GtkTreeIter iter_track; char title[MAXLEN]; char path[CDDA_MAXLEN]; char sort[16]; cdda_track_t * data; snprintf(title, MAXLEN-1, "Track %d", i+1); snprintf(path, CDDA_MAXLEN-1, "CDDA %s %lX %d", drive->device_path, drive->disc.hash, i+1); snprintf(sort, 15, "%02d", i+1); if ((data = (cdda_track_t *)malloc(sizeof(cdda_track_t))) == NULL) { fprintf(stderr, "update_track_data: malloc error\n"); return; } data->path = strdup(path); data->duration = (drive->disc.toc[i+1] - drive->disc.toc[i]) / 75.0; gtk_tree_store_append(music_store, &iter_track, &iter_drive); gtk_tree_store_set(music_store, &iter_track, MS_COL_NAME, title, MS_COL_SORT, sort, MS_COL_DATA, data, -1); if (options.enable_ms_tree_icons) { gtk_tree_store_set(music_store, &iter_track, MS_COL_ICON, icon_track, -1); } } if (update_track_data_from_cdtext(drive, &iter_drive) != 0) { #ifdef HAVE_CDDB cdda_record_auto_query_cddb(&iter_drive); #else if (options.cdda_add_to_playlist) { cdda_add_to_playlist(&iter_drive, drive->disc.hash); } #endif /* HAVE_CDDB */ } else if (options.cdda_add_to_playlist) { cdda_add_to_playlist(&iter_drive, drive->disc.hash); } } void insert_cdda_drive_node(char * device_path) { GtkTreeIter iter_cdda; GtkTreeIter iter_drive; cdda_drive_t * drive = cdda_get_drive_by_device_path(device_path); char str_title[MAXLEN]; char str_sort[16]; if (drive == NULL) { return; } if (drive->disc.n_tracks > 0) { snprintf(str_title, MAXLEN-1, "%s [%s]", _("Unknown disc"), cdda_displayed_device_path(device_path)); } else { snprintf(str_title, MAXLEN-1, "%s [%s]", _("No disc"), cdda_displayed_device_path(device_path)); } snprintf(str_sort, 15, "%d", cdda_get_n(device_path)); gtk_tree_model_get_iter_first(GTK_TREE_MODEL(music_store), &iter_cdda); gtk_tree_store_append(music_store, &iter_drive, &iter_cdda); gtk_tree_store_set(music_store, &iter_drive, MS_COL_NAME, str_title, MS_COL_SORT, str_sort, MS_COL_DATA, drive, -1); if (options.enable_ms_tree_icons) { if (drive->disc.n_tracks > 0) { gtk_tree_store_set(music_store, &iter_drive, MS_COL_ICON, icon_cdda_disc, -1); } else { gtk_tree_store_set(music_store, &iter_drive, MS_COL_ICON, icon_cdda_nodisc, -1); } } if (drive->disc.n_tracks > 0) { update_track_data(drive, iter_drive); } music_store_selection_changed(STORE_TYPE_CDDA); } void remove_cdda_drive_node(char * device_path) { GtkTreeIter iter_cdda; GtkTreeIter iter_drive; cdda_drive_t * drive; int i, found; gtk_tree_model_get_iter_first(GTK_TREE_MODEL(music_store), &iter_cdda); i = found = 0; while (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(music_store), &iter_drive, &iter_cdda, i++)) { gtk_tree_model_get(GTK_TREE_MODEL(music_store), &iter_drive, MS_COL_DATA, &drive, -1); if (strcmp(drive->device_path, device_path) == 0) { found = 1; break; } } if (found) { store_cdda_remove_record(&iter_drive); } music_store_selection_changed(STORE_TYPE_CDDA); } void refresh_cdda_drive_node(char * device_path) { GtkTreeIter iter_cdda; GtkTreeIter iter_drive; GtkTreeIter iter_tmp; cdda_drive_t * drive; char str_title[MAXLEN]; int i, found; gtk_tree_model_get_iter_first(GTK_TREE_MODEL(music_store), &iter_cdda); i = found = 0; while (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(music_store), &iter_drive, &iter_cdda, i++)) { gtk_tree_model_get(GTK_TREE_MODEL(music_store), &iter_drive, MS_COL_DATA, &drive, -1); if (strcmp(drive->device_path, device_path) == 0) { found = 1; break; } } if (!found) { return; } if (drive->disc.n_tracks > 0) { snprintf(str_title, MAXLEN-1, "%s [%s]", _("Unknown disc"), cdda_displayed_device_path(device_path)); } else { snprintf(str_title, MAXLEN-1, "%s [%s]", _("No disc"), cdda_displayed_device_path(device_path)); } gtk_tree_store_set(music_store, &iter_drive, MS_COL_NAME, str_title, -1); if (gtk_tree_model_iter_n_children(GTK_TREE_MODEL(music_store), &iter_drive) > 0) { if (options.cdda_remove_from_playlist) { cdda_remove_from_playlist(drive); } while (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(music_store), &iter_tmp, &iter_drive, 0)) { store_cdda_remove_track(&iter_tmp); } } if (options.enable_ms_tree_icons) { if (drive->disc.n_tracks > 0) { gtk_tree_store_set(music_store, &iter_drive, MS_COL_ICON, icon_cdda_disc, -1); } else { gtk_tree_store_set(music_store, &iter_drive, MS_COL_ICON, icon_cdda_nodisc, -1); } } if (drive->disc.n_tracks > 0) { update_track_data(drive, iter_drive); } music_store_selection_changed(STORE_TYPE_CDDA); } void cdda_shutdown(void) { if (options.cdda_remove_from_playlist) { GtkTreeIter iter_cdda; GtkTreeIter iter_drive; cdda_drive_t * drive; int i = 0; gtk_tree_model_get_iter_first(GTK_TREE_MODEL(music_store), &iter_cdda); while (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(music_store), &iter_drive, &iter_cdda, i++)) { gtk_tree_model_get(GTK_TREE_MODEL(music_store), &iter_drive, MS_COL_DATA, &drive, -1); if (gtk_tree_model_iter_n_children(GTK_TREE_MODEL(music_store), &iter_drive) > 0) { cdda_remove_from_playlist(drive); } } } cdda_scanner_stop(); } #endif /* HAVE_CDDA */ // vim: shiftwidth=8:tabstop=8:softtabstop=8 :