/* * Copyright 2013 (c) Anna Schumaker. */ #include #include #include #include #include #include #include static Database album_db("album.db"); static Database artist_db("artist.db"); static Database genre_db("genre.db"); static Database track_db("track.db"); static Database library_db("library.db"); struct ImportData { std::string filepath; std::string title; unsigned int track; unsigned int last_day; unsigned int last_month; unsigned int last_year; unsigned int length; unsigned int count; }; /* * library :: Artist: Artist tag information */ library :: AGInfo :: AGInfo() { } library :: AGInfo :: AGInfo(DB_Type type, TagLib :: Tag *tag) : db_type(type) { if (db_type == DB_ARTIST) primary_key = tag->artist().stripWhiteSpace().to8Bit(true); else if (db_type == DB_GENRE) primary_key = tag->genre().stripWhiteSpace().to8Bit(true); else throw -E_INVAL; key_lower = filter :: to_lowercase(primary_key); } library :: AGInfo :: AGInfo(DB_Type type, const std::string &str) : db_type(type) { if ((db_type == DB_ARTIST) || (db_type == DB_GENRE)) { primary_key = str; key_lower = filter :: to_lowercase(primary_key); } else throw -E_INVAL; } void library :: AGInfo :: read(File &f) { primary_key = f.getline(); } void library :: AGInfo :: write(File &f) { f << primary_key; } #ifdef CONFIG_TEST void library :: AGInfo :: print() { if (db_type == DB_ARTIST) :: print("Artist: %s", primary_key.c_str()); else :: print("Genre: %s", primary_key.c_str()); } #endif /* CONFIG_TEST */ /* * library :: Album: Album tag information */ library :: Album :: Album() : name(""), year(0), artist_id(0) { } library :: Album :: Album(TagLib :: Tag *tag, unsigned int artist) : name(tag->album().stripWhiteSpace().to8Bit(true)), year(tag->year()), artist_id(artist) { std::stringstream ss; ss << artist_id << "." << name << "." << year; primary_key = ss.str(); name_lower = filter :: to_lowercase(name); } library :: Album :: Album(const std::string &str, unsigned int yr, unsigned int artist) : name(str), year(yr), artist_id(artist) { std::stringstream ss; ss << artist_id << "." << name << "." << year; primary_key = ss.str(); name_lower = filter :: to_lowercase(name); } void library :: Album :: read(File &f) { f >> artist_id >> year; name = f.getline(); } void library :: Album :: write(File &f) { f << artist_id << " " << year << " " << name; } #ifdef CONFIG_TEST void library :: Album :: print() { :: print("Album: %s (%u) by %s", name.c_str(), year, artist_db[artist_id].primary_key.c_str()); } #endif /* CONFIG_TEST */ /* * library :: Library: Basic information about each directory in the library */ library :: Library :: Library() : root_path(""), size(0), enabled(false) { } library :: Library :: Library(const std::string &path, bool is_enabled) : root_path(path), size(0), enabled(is_enabled) { primary_key = root_path; } void library :: Library :: read(File &f) { f >> enabled; root_path = f.getline(); size = 0; } void library :: Library :: write(File &f) { f << enabled << " " << root_path; } #ifdef CONFIG_TEST void library :: Library :: print() { :: print("%s", root_path.c_str()); if (enabled == true) :: print(" (enabled)"); else :: print(" (disabled)"); :: print(", size = %u", size); } #endif /* CONFIG_TEST */ /* * library :: Track: Track tag information */ library :: Track :: Track() : library_id(0), artist_id(0), album_id(0), genre_id(0) { } library :: Track :: Track(TagLib :: Tag *tag, TagLib :: AudioProperties *audio, unsigned int lib, unsigned int artist, unsigned int album, unsigned int genre, const std :: string &path) : library_id(lib), artist_id(artist), album_id(album), genre_id(genre), track(tag->track()), last_year(0), last_month(0), last_day(0), play_count(0), length(audio->length()), title(tag->title().stripWhiteSpace().to8Bit(true)) { std::stringstream ss; unsigned int minutes, seconds; primary_key = path; filepath = path.substr(library_db[library_id].root_path.size() + 1); title_lower = filter :: to_lowercase(title); minutes = length / 60; seconds = length % 60; ss << minutes << ":"; if (seconds < 10) ss << "0"; ss << seconds; length_str = ss.str(); } library :: Track :: Track(struct ImportData *data, unsigned int lib, unsigned int artist, unsigned int album, unsigned int genre) : library_id(lib), artist_id(artist), album_id(album), genre_id(genre), track(data->track), last_year(data->last_year), last_month(data->last_month), last_day(data->last_day), play_count(data->count), length(data->length), title(data->title) { std::stringstream ss; unsigned int minutes, seconds; primary_key = data->filepath; filepath = primary_key.substr(library_db[library_id].root_path.size() + 1); title_lower = filter :: to_lowercase(title); minutes = length / 60; seconds = length % 60; ss << minutes << ":"; if (seconds < 10) ss << "0"; ss << seconds; length_str = ss.str(); } void library :: Track :: read(File &f) { f >> library_id >> artist_id >> album_id >> genre_id; f >> track >> last_year >> last_month >> last_day; f >> play_count >> length; length_str = f.getline(); title = f.getline(); filepath = f.getline(); library_db[library_id].size++; } void library :: Track :: write(File &f) { f << library_id << " " << artist_id << " " << album_id << " " << genre_id; f << " " << track << " " << last_year << " " << last_month << " " << last_day; f << " " << play_count << " " << length << " " << length_str << std :: endl; f << title << std :: endl; f << filepath; } #ifdef CONFIG_TEST void library :: Track :: print() { :: print("%u. %s by %s from %s (%u)\n", track, title.c_str(), artist_db[artist_id].primary_key.c_str(), album_db[album_id].name.c_str(), album_db[album_id].year); :: print(" Genre: %s, Length: %u (seconds)\n", genre_db[genre_id].primary_key.c_str(), length); :: print(" Play count: %u, last played %u/%u/%u\n", play_count, last_day, last_month, last_year); :: print(" %s", filepath.c_str()); } #endif /* CONFIG_TEST */ /* * Internal library functions */ struct scan_info { unsigned int lib_id; std :: string path; }; static void do_scan_path(struct scan_info &); static void read_tags(unsigned int lib_id, const std :: string &path) { TagLib :: Tag *tag; TagLib :: AudioProperties *audio; TagLib :: FileRef ref(path.c_str(), true, TagLib :: AudioProperties :: Fast); unsigned int artist_id, album_id, genre_id; if (ref.isNull()) { print("ERROR: Could not read tags for file %s", path.c_str()); return; } tag = ref.tag(); audio = ref.audioProperties(); artist_id = artist_db.insert(library :: AGInfo(library :: DB_ARTIST, tag)); album_id = album_db.insert(library :: Album(tag, artist_id)); genre_id = genre_db.insert(library :: AGInfo(library :: DB_GENRE, tag)); track_db.insert(library :: Track(tag, audio, lib_id, artist_id, album_id, genre_id, path)); library_db[lib_id].size++; } static bool process_path(unsigned int lib_id, const std :: string &dir, const std :: string &name) { struct scan_info scan; bool changed = false; std :: string path = dir + "/" + name; if (g_file_test(path.c_str(), G_FILE_TEST_IS_DIR) == true) { scan.lib_id = lib_id; scan.path = path; idle :: schedule (do_scan_path, scan); } else { if (track_db.has_key(path) == false) { read_tags(lib_id, path); changed = true; } } return changed; } static void save_all_dbs() { artist_db.save(); album_db.save(); genre_db.save(); track_db.save(); } static void do_scan_path(struct scan_info &scan) { GDir *dir; const char *name; bool changed = false; dir = g_dir_open(scan.path.c_str(), 0, NULL); if (dir == NULL) return; name = g_dir_read_name(dir); while (name != NULL) { if (process_path(scan.lib_id, scan.path, name)) changed = true; name = g_dir_read_name(dir); } if (changed == true) { save_all_dbs(); get_callbacks()->on_library_update(scan.lib_id, &library_db[scan.lib_id]); } } static void do_validate_library(unsigned int &lib_id) { std :: string path; bool changed = false; if (track_db.size() == 0) return; for (unsigned int i = track_db.first(); i <= track_db.last(); i = track_db.next(i)) { if (track_db[i].library_id != lib_id) continue; path = library_db[lib_id].root_path + "/" + track_db[i].filepath; if (g_file_test(path.c_str(), G_FILE_TEST_EXISTS) == false) { dprint("Removing file: %s\n", path.c_str()); track_db.remove(i); library_db[lib_id].size--; changed = true; } } if (changed == true) get_callbacks()->on_library_update(lib_id, &library_db[lib_id]); } static void do_update_library(unsigned int lib_id) { struct scan_info scan = { lib_id, library_db[lib_id].root_path }; idle :: schedule(do_validate_library, lib_id); idle :: schedule(do_scan_path, scan); } static void do_import_track(File &f, unsigned int lib_id) { struct ImportData data; std::string artist, album, genre; unsigned int artist_id, album_id, genre_id, year, tmp; data.filepath = f.getline(); data.title = f.getline(); artist = f.getline(); album = f.getline(); f.getline(); /* comment */ genre = f.getline(); f.getline(); /* lenstr */ f >> tmp /* id */ >> year >> data.track >> data.count; f >> data.last_day >> data.last_month >> data.last_year >> data.length; f >> tmp >> tmp >>tmp >> tmp; /* bitrate, sample, channels, banned */ f.getline(); /* get rest of line */ artist_id = artist_db.insert(library :: AGInfo(library :: DB_ARTIST, artist)); album_id = album_db.insert(library :: Album(album, year, artist_id)); genre_id = genre_db.insert(library :: AGInfo(library :: DB_GENRE, genre)); track_db.insert(library :: Track(&data, lib_id, artist_id, album_id, genre_id)); library_db[lib_id].size++; } static void do_import_library(std::string &s) { unsigned int id, next_id, size; std::string path; bool enabled; File f(s, FILE_TYPE_LEGACY); print("Importing: %s\n", f.get_filepath()); f.open(OPEN_READ); if (f.get_version() != 2) { print("Version mismatch: %u != 2\n", f.get_version()); return; } path = f.getline(); f >> id >> enabled >> next_id >> size; /* Assign this path a new id */ if (library_db.has_key(path)) { print("Library already contains path: %s, skipping\n", path.c_str()); return; } print("Adding path: %s\n", path.c_str()); id = library_db.insert(library :: Library(path, enabled)); get_callbacks()->on_library_add(id, &library_db[id]); library_db.save(); f.getline(); /* Get rest of line */ for (unsigned int i = 0; i < size; i++) do_import_track(f, id); save_all_dbs(); get_callbacks()->on_library_update(id, &library_db[id]); library :: update_path(id); } /* * API used by the GUI begins here */ void library :: init() { unsigned int i; library_db.load(); for (i = library_db.first(); i < library_db.num_rows(); i = library_db.next(i)) get_callbacks()->on_library_add(i, &library_db[i]); } void library :: add_path(const std::string &dir) { unsigned int id; if (g_file_test(dir.c_str(), G_FILE_TEST_IS_DIR) == false) throw -E_INVAL; id = library_db.insert(library :: Library(dir, true)); library_db.save(); get_callbacks()->on_library_add(id, &library_db[id]); update_path(id); } void library :: del_path(unsigned int id) { library_db.remove(id); library_db.save(); } void library :: update_path(unsigned int id) { if (id > library_db.size()) return; if (library_db[id].valid == false) return; do_update_library(id); } void library :: update_all() { unsigned int i; for (i = library_db.first(); i < library_db.num_rows(); i = library_db.next(i)) update_path(i); } void library :: lookup(unsigned int id, library :: Song *song) { if (id >= track_db.num_rows()) throw -E_EXIST; song->track = &track_db[id]; if (song->track->valid == false) throw -E_EXIST; song->artist = &artist_db[song->track->artist_id]; song->album = &album_db[song->track->album_id]; song->genre = &genre_db[song->track->genre_id]; song->library = &library_db[song->track->library_id]; } library :: Library *library :: lookup_path(unsigned int id) { if (id >= library_db.num_rows()) throw -E_EXIST; if (library_db[id].valid == false) throw -E_EXIST; return &library_db[id]; } void library :: import() { unsigned int i = 0; std::string name; do { std::stringstream ss; ss << i; name = ss.str(); File f(name, FILE_TYPE_LEGACY); if (f.exists() == false) break; idle :: schedule(do_import_library, name); ss.clear(); i++; } while (true); } #ifdef CONFIG_TEST void library :: print_db(DB_Type type) { switch (type) { case DB_ALBUM: album_db.print(); break; case DB_ARTIST: artist_db.print(); break; case DB_GENRE: genre_db.print(); break; case DB_LIBRARY: library_db.print(); break; case DB_TRACK: track_db.print(); } } void library :: reset() { album_db.clear(); artist_db.clear(); genre_db.clear(); library_db.clear(); track_db.clear(); } #endif /* CONFIG_TEST */