e256c24b20
I don't have user-configurable sorting (yet), but for now I'm sorting by Artist -> Year -> Track #. I also fix a bug where the library wasn't lowercasing artist, album, genere, and track fields when reading from file. Signed-off-by: Anna Schumaker <schumaker.anna@gmail.com>
620 lines
14 KiB
C++
620 lines
14 KiB
C++
/*
|
|
* Copyright 2013 (c) Anna Schumaker.
|
|
*/
|
|
#include <callback.h>
|
|
#include <filter.h>
|
|
#include <idle.h>
|
|
#include <library.h>
|
|
#include <print.h>
|
|
|
|
#include <glib.h>
|
|
#include <sstream>
|
|
|
|
static Database<library :: Album> album_db("album.db");
|
|
static Database<library :: AGInfo> artist_db("artist.db");
|
|
static Database<library :: AGInfo> genre_db("genre.db");
|
|
static Database<library :: Track> track_db("track.db");
|
|
|
|
static Database<library :: Library> 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();
|
|
key_lower = filter :: to_lowercase(primary_key);
|
|
}
|
|
|
|
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();
|
|
name_lower = filter :: to_lowercase(name);
|
|
}
|
|
|
|
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();
|
|
title_lower = filter :: to_lowercase(title);
|
|
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, track_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_id = track_db.insert(library :: Track(tag, audio, lib_id,
|
|
artist_id, album_id, genre_id, path));
|
|
library_db[lib_id].size++;
|
|
get_callbacks()->on_library_track_add(track_id);
|
|
|
|
}
|
|
|
|
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, track_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_id = track_db.insert(library :: Track(&data, lib_id, artist_id,
|
|
album_id, genre_id));
|
|
library_db[lib_id].size++;
|
|
get_callbacks()->on_library_track_add(track_id);
|
|
}
|
|
|
|
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;
|
|
|
|
album_db.load();
|
|
artist_db.load();
|
|
genre_db.load();
|
|
library_db.load();
|
|
track_db.load();
|
|
|
|
for (i = track_db.first(); i < track_db.num_rows(); i = track_db.next(i)) {
|
|
if (library_db[track_db[i].library_id].enabled)
|
|
get_callbacks()->on_library_track_add(i);
|
|
}
|
|
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;
|
|
if (library_db.has_key(dir))
|
|
return;
|
|
|
|
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)
|
|
{
|
|
unsigned int t;
|
|
|
|
for (t = track_db.first(); t < track_db.num_rows(); t = track_db.next(t)) {
|
|
if (track_db[t].library_id == id) {
|
|
get_callbacks()->on_library_track_del(t);
|
|
track_db.remove(t);
|
|
}
|
|
}
|
|
|
|
library_db.remove(id);
|
|
|
|
track_db.save();
|
|
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 :: set_enabled(unsigned int id, bool enabled)
|
|
{
|
|
unsigned int t;
|
|
|
|
library_db[id].enabled = enabled;
|
|
library_db.save();
|
|
|
|
for (t = track_db.first(); t < track_db.num_rows(); t = track_db.next(t)) {
|
|
if (track_db[t].library_id == id) {
|
|
if (enabled)
|
|
get_callbacks()->on_library_track_add(t);
|
|
else
|
|
get_callbacks()->on_library_track_del(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 */
|