Workout Library Part 2 of 3

Add trainDB sqlite database of workouts and videos
found during a library search. A bit ghetto but
works well enough.

Part 3 will need to;
- update traintool to list contents from db
- allow drag-n-drop of files into db (and worry
  about copy to library vs add path).
This commit is contained in:
Mark Liversedge
2012-12-21 16:27:49 +00:00
parent 2d73924f25
commit 2756a678c7
5 changed files with 695 additions and 3 deletions

View File

@@ -19,6 +19,7 @@
#include "Library.h"
#include "Settings.h"
#include "LibraryParser.h"
#include "TrainDB.h"
#include <QVBoxLayout>
#include <QHeaderView>
#include <QLabel>
@@ -374,15 +375,23 @@ LibrarySearchDialog::removeDirectory()
void
LibrarySearchDialog::updateDB()
{
// update the database of workouts and videos
//XXX update db here...
trainDB->connection().transaction();
// workouts
foreach(QString ergFile, workoutsFound) {
int mode;
ErgFile file(ergFile, mode, mainWindow);
if (file.isValid()) {
//XXX qDebug()<<"ergfile:"<<QFileInfo(ergFile).fileName()<<file.Duration<<file.TSS<<file.IF;
trainDB->importWorkout(ergFile, &file);
}
}
// videos
foreach(QString video, videosFound) {
trainDB->importVideo(video);
}
trainDB->connection().commit();
}
//

612
src/TrainDB.cpp Normal file
View File

@@ -0,0 +1,612 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* 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., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "TrainDB.h"
#include "ErgFile.h"
// DB Schema Version - YOU MUST UPDATE THIS IF THE TRAIN DB SCHEMA CHANGES
// Revision History
// Rev Date Who What Changed
// 01 21 Dec 2012 Mark Liversedge Initial Build
static int TrainDBSchemaVersion = 2;
TrainDB *trainDB;
TrainDB::TrainDB(QDir home) : home(home)
{
// we live above the rider directory
initDatabase(home);
}
void TrainDB::closeConnection()
{
dbconn.close();
}
TrainDB::~TrainDB()
{
closeConnection();
}
void
TrainDB::initDatabase(QDir home)
{
if(dbconn.isOpen()) return;
// get a connection
db = QSqlDatabase::addDatabase("QSQLITE", "1");
db.setDatabaseName(home.absolutePath() + "/trainDB");
dbconn = db.database("1");
if (!dbconn.isOpen()) {
QMessageBox::critical(0, qApp->translate("TrainDB","Cannot open database"),
qApp->translate("TrainDB","Unable to establish a database connection.\n"
"This feature requires SQLite support. Please read "
"the Qt SQL driver documentation for information how "
"to build it.\n\n"
"Click Cancel to exit."), QMessageBox::Cancel);
} else {
// create database - does nothing if its already there
createDatabase();
}
}
bool TrainDB::createVideoTable()
{
QSqlQuery query(dbconn);
bool rc;
bool createTables = true;
// does the table exist?
rc = query.exec("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
if (rc) {
while (query.next()) {
QString table = query.value(0).toString();
if (table == "videos") {
createTables = false;
break;
}
}
}
// we need to create it!
if (rc && createTables) {
QString createVideoTable = "create table videos (filename varchar primary key,"
"timestamp integer,"
"length integer);";
rc = query.exec(createVideoTable);
// add row to version database
query.exec("DELETE FROM version where table_name = \"videos\"");
// insert into table
query.prepare("INSERT INTO version (table_name, schema_version, creation_date) values (?,?,?);");
query.addBindValue("videos");
query.addBindValue(TrainDBSchemaVersion);
query.addBindValue(QDateTime::currentDateTime().toTime_t());
rc = query.exec();
}
return rc;
}
bool TrainDB::createWorkoutTable()
{
QSqlQuery query(dbconn);
bool rc;
bool createTables = true;
// does the table exist?
rc = query.exec("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
if (rc) {
while (query.next()) {
QString table = query.value(0).toString();
if (table == "workouts") {
createTables = false;
break;
}
}
}
// we need to create it!
if (rc && createTables) {
QString createMetricTable = "create table workouts (filename varchar primary key,"
"timestamp integer,"
"description varchar,"
"source varchar,"
"ftp integer,"
"length integer,"
"coggan_tss integer,"
"coggan_if integer,"
"elevation integer,"
"grade double );";
rc = query.exec(createMetricTable);
// add row to version database
query.exec("DELETE FROM version where table_name = \"workouts\"");
// insert into table
query.prepare("INSERT INTO version (table_name, schema_version, creation_date) values (?,?,?);");
query.addBindValue("workouts");
query.addBindValue(TrainDBSchemaVersion);
query.addBindValue(QDateTime::currentDateTime().toTime_t());
rc = query.exec();
}
return rc;
}
bool TrainDB::dropVideoTable()
{
QSqlQuery query("DROP TABLE videos", dbconn);
bool rc = query.exec();
return rc;
}
bool TrainDB::dropWorkoutTable()
{
QSqlQuery query("DROP TABLE workouts", dbconn);
bool rc = query.exec();
return rc;
}
bool TrainDB::createDatabase()
{
// check schema version and if missing recreate database
checkDBVersion();
// Workouts
createWorkoutTable();
createVideoTable();
return true;
}
void TrainDB::checkDBVersion()
{
// can we get a version number?
QSqlQuery query("SELECT table_name, schema_version, creation_date, metadata_crc from version;", dbconn);
bool rc = query.exec();
if (!rc) {
// we couldn't read the version table properly
// it must be out of date!!
QSqlQuery dropM("DROP TABLE version", dbconn);
dropM.exec();
// recreate version table and add one entry
QSqlQuery version("CREATE TABLE version ( table_name varchar primary key, schema_version integer, creation_date date );", dbconn);
version.exec();
// wipe away whatever (if anything is there)
dropWorkoutTable();
createWorkoutTable();
dropVideoTable();
createVideoTable();
return;
}
// ok we checked out ok, so lets adjust db schema to reflect
// tne current version / crc
bool dropWorkout = false;
bool dropVideo = false;
while (query.next()) {
QString table_name = query.value(0).toString();
int currentversion = query.value(1).toInt();
if (table_name == "workouts" && currentversion != TrainDBSchemaVersion) dropWorkout = true;
if (table_name == "videos" && currentversion != TrainDBSchemaVersion) dropVideo = true;
}
query.finish();
// "workouts" table, is it up-to-date?
if (dropWorkout) dropWorkoutTable();
if (dropVideo) dropVideoTable();
}
int TrainDB::getDBVersion()
{
int schema_version = -1;
// can we get a version number?
QSqlQuery query("SELECT schema_version from version;", dbconn);
bool rc = query.exec();
if (rc) {
while (query.next()) {
if (query.value(0).toInt() > schema_version)
schema_version = query.value(0).toInt();
}
}
query.finish();
return schema_version;
}
/*----------------------------------------------------------------------
* CRUD routines
*----------------------------------------------------------------------*/
bool TrainDB::importWorkout(QString pathname, ErgFile *ergFile)
{
QSqlQuery query(dbconn);
QDateTime timestamp = QDateTime::currentDateTime();
// zap the current row - if there is one
query.prepare("DELETE FROM workouts WHERE filename = ?;");
query.addBindValue(pathname);
query.exec();
// construct an insert statement
QString insertStatement = "insert into workouts ( filename, "
"timestamp,"
"description,"
"source,"
"ftp,"
"length,"
"coggan_tss,"
"coggan_if,"
"elevation,"
"grade ) values ( ?,?,?,?,?,?,?,?,?,? );";
query.prepare(insertStatement);
// filename, timestamp, ride date
query.addBindValue(pathname);
query.addBindValue(timestamp);
query.addBindValue(ergFile->Name);
query.addBindValue(ergFile->Source);
query.addBindValue(ergFile->Ftp);
query.addBindValue((int)ergFile->Duration);
query.addBindValue(ergFile->TSS);
query.addBindValue(ergFile->IF);
query.addBindValue(ergFile->ELE);
query.addBindValue(ergFile->GRADE);
// go do it!
bool rc = query.exec();
return rc;
}
bool TrainDB::importVideo(QString pathname)
{
QSqlQuery query(dbconn);
QDateTime timestamp = QDateTime::currentDateTime();
// zap the current row - if there is one
query.prepare("DELETE FROM videos WHERE filename = ?;");
query.addBindValue(pathname);
query.exec();
// construct an insert statement
QString insertStatement = "insert into videos ( filename ) values ( ? );";
query.prepare(insertStatement);
// filename, timestamp, ride date
query.addBindValue(pathname);
// go do it!
bool rc = query.exec();
return rc;
}
#if 0
// XXX WILL UPDATE THESE FOR WORKOUT/VIDEO SHORTLY
bool
TrainDB::deleteRide(QString name)
{
QSqlQuery query(dbconn);
query.prepare("DELETE FROM metrics WHERE filename = ?;");
query.addBindValue(name);
return query.exec();
}
QList<QDateTime> TrainDB::getAllDates()
{
QSqlQuery query("SELECT ride_date from metrics ORDER BY ride_date;", dbconn);
QList<QDateTime> dates;
query.exec();
while(query.next())
{
QDateTime date = query.value(0).toDateTime();
dates << date;
}
return dates;
}
bool
TrainDB::getRide(QString filename, SummaryMetrics &summaryMetrics, QColor&color)
{
// lookup a ride by filename returning true/false if found
bool found = false;
// construct the select statement
QString selectStatement = "SELECT filename, identifier, ride_date, color";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
selectStatement += " FROM metrics where filename = :name;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":start", filename);
query.exec();
while(query.next())
{
found = true;
// filename and date
summaryMetrics.setFileName(query.value(0).toString());
summaryMetrics.setId(query.value(1).toString());
summaryMetrics.setRideDate(query.value(2).toDateTime());
color = QColor(query.value(3).toString());
// the values
int i=0;
for (; i<factory.metricCount(); i++)
summaryMetrics.setForSymbol(factory.metricName(i), query.value(i+4).toDouble());
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+4).toDouble());
i++;
} else if (!main->specialFields.isMetric(field.name) && field.type < 3) {
QString underscored = field.name;
// ignore texts for now XXX todo if want metadata from Summary Metrics
summaryMetrics.setText(underscored.replace("_"," "), query.value(i+4).toString());
i++;
}
}
}
return found;
}
QList<SummaryMetrics> TrainDB::getAllMetricsFor(QDateTime start, QDateTime end)
{
QList<SummaryMetrics> metrics;
// null date range fetches all, but not currently used by application code
// since it relies too heavily on the results of the QDateTime constructor
if (start == QDateTime()) start = QDateTime::currentDateTime().addYears(-10);
if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10);
// construct the select statement
QString selectStatement = "SELECT filename, identifier, ride_date";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
selectStatement += " FROM metrics where DATE(ride_date) >=DATE(:start) AND DATE(ride_date) <=DATE(:end) "
" ORDER BY ride_date;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":start", start.date());
query.bindValue(":end", end.date());
query.exec();
while(query.next())
{
SummaryMetrics summaryMetrics;
// filename and date
summaryMetrics.setFileName(query.value(0).toString());
summaryMetrics.setId(query.value(1).toString());
summaryMetrics.setRideDate(query.value(2).toDateTime());
// the values
int i=0;
for (; i<factory.metricCount(); i++)
summaryMetrics.setForSymbol(factory.metricName(i), query.value(i+3).toDouble());
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+3).toDouble());
i++;
} else if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
QString underscored = field.name;
// ignore texts for now XXX todo if want metadata from Summary Metrics
summaryMetrics.setText(underscored.replace("_"," "), query.value(i+3).toString());
i++;
}
}
metrics << summaryMetrics;
}
return metrics;
}
SummaryMetrics TrainDB::getRideMetrics(QString filename)
{
SummaryMetrics summaryMetrics;
// construct the select statement
QString selectStatement = "SELECT filename, identifier";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
selectStatement += " FROM metrics where filename == :filename ;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":filename", filename);
query.exec();
while(query.next())
{
// filename and date
summaryMetrics.setFileName(query.value(0).toString());
summaryMetrics.setId(query.value(1).toString());
// the values
int i=0;
for (; i<factory.metricCount(); i++)
summaryMetrics.setForSymbol(factory.metricName(i), query.value(i+2).toDouble());
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!main->specialFields.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+2).toDouble());
i++;
} else if (!main->specialFields.isMetric(field.name) && (field.type < 3 || field.type == 7)) {
// ignore texts for now XXX todo if want metadata from Summary Metrics
QString underscored = field.name;
summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString());
i++;
}
}
}
return summaryMetrics;
}
/*----------------------------------------------------------------------
* CRUD routines for Measures table
*----------------------------------------------------------------------*/
bool TrainDB::importMeasure(SummaryMetrics *summaryMetrics)
{
QSqlQuery query(dbconn);
// construct an insert statement
QString insertStatement = "insert into measures (timestamp, measure_date";
// And all the metadata texts
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 3 || field.type == 7) {
insertStatement += QString(", Z%1 ").arg(msp.makeTechName(field.name));
}
}
// And all the metadata metrics
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type == 3 || field.type == 4) {
insertStatement += QString(", Z%1 ").arg(msp.makeTechName(field.name));
}
}
insertStatement += " ) values (?,?"; // timestamp, measure_date
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 5 || field.type == 7) {
insertStatement += ",?";
}
}
insertStatement += ")";
query.prepare(insertStatement);
// timestamp and date
query.addBindValue(summaryMetrics->getDateTime().toTime_t());
query.addBindValue(summaryMetrics->getDateTime().date());
// And all the text measures
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type < 3 || field.type == 7) {
query.addBindValue(summaryMetrics->getText(field.name, ""));
}
}
// And all the numeric measures
foreach(FieldDefinition field, mfieldDefinitions) {
if (field.type == 3 || field.type == 4) {
query.addBindValue(summaryMetrics->getText(field.name, "nan").toDouble());
}
}
// go do it!
bool rc = query.exec();
//if(!rc) qDebug() << query.lastError();
return rc;
}
QList<SummaryMetrics> TrainDB::getAllMeasuresFor(QDateTime start, QDateTime end)
{
QList<FieldDefinition> fieldDefinitions;
QList<KeywordDefinition> keywordDefinitions; //NOTE: not used in measures.xml
QString colorfield;
// check we have one and use built in if not there
QString filename = main->home.absolutePath()+"/measures.xml";
if (!QFile(filename).exists()) filename = ":/xml/measures.xml";
RideMetadata::readXML(filename, keywordDefinitions, fieldDefinitions, colorfield);
QList<SummaryMetrics> measures;
// null date range fetches all, but not currently used by application code
// since it relies too heavily on the results of the QDateTime constructor
if (start == QDateTime()) start = QDateTime::currentDateTime().addYears(-10);
if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10);
// construct the select statement
QString selectStatement = "SELECT timestamp, measure_date";
foreach(FieldDefinition field, fieldDefinitions) {
if (!main->specialFields.isMetric(field.name) && (field.type < 5 || field.type == 7)) {
selectStatement += QString(", Z%1 ").arg(main->specialFields.makeTechName(field.name));
}
}
selectStatement += " FROM measures where DATE(measure_date) >=DATE(:start) AND DATE(measure_date) <=DATE(:end) "
" ORDER BY measure_date;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":start", start.date());
query.bindValue(":end", end.date());
query.exec();
while(query.next())
{
SummaryMetrics add;
// filename and date
add.setDateTime(query.value(1).toDateTime());
// the values
int i=2;
foreach(FieldDefinition field, fieldDefinitions) {
if (field.type == 3 || field.type == 4) {
add.setText(field.name, query.value(i).toString());
i++;
} else if (field.type < 3 || field.type == 7) {
add.setText(field.name, query.value(i).toString());
i++;
}
}
measures << add;
}
return measures;
}
#endif

65
src/TrainDB.h Normal file
View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* 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., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_TrainDB_h
#define _GC_TrainDB_h 1
#include "GoldenCheetah.h"
#include <QMessageBox>
#include <QDir>
#include <QHash>
#include <QtSql>
class ErgFile;
class TrainDB
{
public:
// get connection name
QSqlDatabase connection() { return dbconn; }
// check the db structure is up to date
void checkDBVersion();
// get schema version
int getDBVersion();
// create and drop connections
TrainDB(QDir home);
~TrainDB();
bool importWorkout(QString pathname, ErgFile *ergFile);
bool importVideo(QString pathname); //XXX simple for now
private:
QDir home;
QSqlDatabase db;
QSqlDatabase dbconn;
void initDatabase(QDir home);
bool createDatabase();
void closeConnection();
bool createWorkoutTable();
bool dropWorkoutTable();
bool createVideoTable();
bool dropVideoTable();
};
extern TrainDB *trainDB;
#endif

View File

@@ -22,6 +22,7 @@
#include "ChooseCyclistDialog.h"
#include "MainWindow.h"
#include "Settings.h"
#include "TrainDB.h"
#ifdef Q_OS_X11
#include <X11/Xlib.h>
@@ -114,6 +115,9 @@ main(int argc, char *argv[])
// Initialize global registry once the translator is installed
GcWindowRegistry::initialize();
// initialise the trainDB
trainDB = new TrainDB(home);
QStringList args( app.arguments() );
QVariant lastOpened;

View File

@@ -380,6 +380,7 @@ HEADERS += \
TimeUtils.h \
ToolsDialog.h \
ToolsRhoEstimator.h \
TrainDB.h \
TrainTool.h \
TreeMapWindow.h \
TreeMapPlot.h \
@@ -575,6 +576,7 @@ SOURCES += \
TimeUtils.cpp \
ToolsDialog.cpp \
ToolsRhoEstimator.cpp \
TrainDB.cpp \
TrainTool.cpp \
TreeMapWindow.cpp \
TreeMapPlot.cpp \