owntone-server/src/db.c
2023-10-19 23:49:53 +02:00

7474 lines
183 KiB
C

/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2010 Kai Elwert <elwertk@googlemail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <stdbool.h>
#include <inttypes.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unictype.h>
#include <uninorm.h>
#include <unistr.h>
#include <sys/mman.h>
#include <limits.h>
#include <assert.h>
#include <sqlite3.h>
#include "conffile.h"
#include "logger.h"
#include "cache.h"
#include "listener.h"
#include "library.h"
#include "misc.h"
#include "db.h"
#include "db_init.h"
#include "db_upgrade.h"
#include "rng.h"
// Inotify cookies are uint32_t
#define INOTIFY_FAKE_COOKIE ((int64_t)1 << 32)
// Flags that the field will not be bound to prepared statements, which is relevant if the field has no
// matching column, or if the the column value is set automatically by the db, e.g. by a trigger
#define DB_FLAG_NO_BIND (1 << 0)
// Flags that we will only update column value if we have non-zero value (to avoid zeroing e.g. rating)
#define DB_FLAG_NO_ZERO (1 << 1)
// The two last columns of playlist_info are calculated fields, so all playlist retrieval functions must use this query
#define Q_PL_SELECT "SELECT f.*, COUNT(pi.id), SUM(pi.filepath NOT NULL AND pi.filepath LIKE 'http%%')" \
" FROM playlists f LEFT JOIN playlistitems pi ON (f.id = pi.playlistid)"
enum group_type {
G_ALBUMS = 1,
G_ARTISTS = 2,
};
enum field_type {
DB_TYPE_INT,
DB_TYPE_INT64,
DB_TYPE_STRING,
};
enum fixup_type {
DB_FIXUP_STANDARD = 0,
DB_FIXUP_NO_SANITIZE,
DB_FIXUP_TITLE,
DB_FIXUP_ARTIST,
DB_FIXUP_ALBUM,
DB_FIXUP_ALBUM_ARTIST,
DB_FIXUP_GENRE,
DB_FIXUP_COMPOSER,
DB_FIXUP_TYPE,
DB_FIXUP_CODECTYPE,
DB_FIXUP_MEDIA_KIND,
DB_FIXUP_ITEM_KIND,
DB_FIXUP_TITLE_SORT,
DB_FIXUP_ARTIST_SORT,
DB_FIXUP_ALBUM_SORT,
DB_FIXUP_ALBUM_ARTIST_SORT,
DB_FIXUP_COMPOSER_SORT,
DB_FIXUP_TIME_MODIFIED,
DB_FIXUP_SONGARTISTID,
DB_FIXUP_SONGALBUMID,
};
struct db_unlock {
int proceed;
pthread_cond_t cond;
pthread_mutex_t lck;
};
struct db_statements
{
sqlite3_stmt *files_insert;
sqlite3_stmt *files_update;
sqlite3_stmt *files_ping;
sqlite3_stmt *playlists_insert;
sqlite3_stmt *playlists_update;
sqlite3_stmt *queue_items_insert;
sqlite3_stmt *queue_items_update;
};
struct col_type_map {
char *name;
ssize_t offset;
enum field_type type;
enum fixup_type fixup;
short flag;
};
struct qi_mfi_map
{
ssize_t qi_offset;
ssize_t mfi_offset;
ssize_t dbmfi_offset;
};
struct fixup_ctx
{
const struct col_type_map *map;
size_t map_size;
void *data;
struct media_file_info *mfi;
struct playlist_info *pli;
struct db_queue_item *qi;
};
struct query_clause {
char *where;
char *group;
char *having;
char *order;
char *index;
};
struct browse_clause {
char *select;
char *where;
char *group;
};
/* This list must be kept in sync with
* - the order of the columns in the files table
* - the type and name of the fields in struct media_file_info
*/
static const struct col_type_map mfi_cols_map[] =
{
{ "id", mfi_offsetof(id), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_BIND },
{ "path", mfi_offsetof(path), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "virtual_path", mfi_offsetof(virtual_path), DB_TYPE_STRING },
{ "fname", mfi_offsetof(fname), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "directory_id", mfi_offsetof(directory_id), DB_TYPE_INT },
{ "title", mfi_offsetof(title), DB_TYPE_STRING, DB_FIXUP_TITLE },
{ "artist", mfi_offsetof(artist), DB_TYPE_STRING, DB_FIXUP_ARTIST },
{ "album", mfi_offsetof(album), DB_TYPE_STRING, DB_FIXUP_ALBUM },
{ "album_artist", mfi_offsetof(album_artist), DB_TYPE_STRING, DB_FIXUP_ALBUM_ARTIST },
{ "genre", mfi_offsetof(genre), DB_TYPE_STRING, DB_FIXUP_GENRE },
{ "comment", mfi_offsetof(comment), DB_TYPE_STRING },
{ "type", mfi_offsetof(type), DB_TYPE_STRING, DB_FIXUP_TYPE },
{ "composer", mfi_offsetof(composer), DB_TYPE_STRING, DB_FIXUP_COMPOSER },
{ "orchestra", mfi_offsetof(orchestra), DB_TYPE_STRING },
{ "conductor", mfi_offsetof(conductor), DB_TYPE_STRING },
{ "grouping", mfi_offsetof(grouping), DB_TYPE_STRING },
{ "url", mfi_offsetof(url), DB_TYPE_STRING },
{ "bitrate", mfi_offsetof(bitrate), DB_TYPE_INT },
{ "samplerate", mfi_offsetof(samplerate), DB_TYPE_INT },
{ "song_length", mfi_offsetof(song_length), DB_TYPE_INT },
{ "file_size", mfi_offsetof(file_size), DB_TYPE_INT64 },
{ "year", mfi_offsetof(year), DB_TYPE_INT },
{ "date_released", mfi_offsetof(date_released), DB_TYPE_INT64 },
{ "track", mfi_offsetof(track), DB_TYPE_INT },
{ "total_tracks", mfi_offsetof(total_tracks), DB_TYPE_INT },
{ "disc", mfi_offsetof(disc), DB_TYPE_INT },
{ "total_discs", mfi_offsetof(total_discs), DB_TYPE_INT },
{ "bpm", mfi_offsetof(bpm), DB_TYPE_INT },
{ "compilation", mfi_offsetof(compilation), DB_TYPE_INT },
{ "artwork", mfi_offsetof(artwork), DB_TYPE_INT },
{ "rating", mfi_offsetof(rating), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "play_count", mfi_offsetof(play_count), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "skip_count", mfi_offsetof(skip_count), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "seek", mfi_offsetof(seek), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "data_kind", mfi_offsetof(data_kind), DB_TYPE_INT },
{ "media_kind", mfi_offsetof(media_kind), DB_TYPE_INT, DB_FIXUP_MEDIA_KIND },
{ "item_kind", mfi_offsetof(item_kind), DB_TYPE_INT, DB_FIXUP_ITEM_KIND },
{ "description", mfi_offsetof(description), DB_TYPE_STRING },
{ "db_timestamp", mfi_offsetof(db_timestamp), DB_TYPE_INT },
{ "time_added", mfi_offsetof(time_added), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "time_modified", mfi_offsetof(time_modified), DB_TYPE_INT, DB_FIXUP_TIME_MODIFIED },
{ "time_played", mfi_offsetof(time_played), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "time_skipped", mfi_offsetof(time_skipped), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_ZERO },
{ "disabled", mfi_offsetof(disabled), DB_TYPE_INT64 },
{ "sample_count", mfi_offsetof(sample_count), DB_TYPE_INT64 },
{ "codectype", mfi_offsetof(codectype), DB_TYPE_STRING, DB_FIXUP_CODECTYPE },
{ "idx", mfi_offsetof(idx), DB_TYPE_INT },
{ "has_video", mfi_offsetof(has_video), DB_TYPE_INT },
{ "contentrating", mfi_offsetof(contentrating), DB_TYPE_INT },
{ "bits_per_sample", mfi_offsetof(bits_per_sample), DB_TYPE_INT },
{ "tv_series_name", mfi_offsetof(tv_series_name), DB_TYPE_STRING },
{ "tv_episode_num_str", mfi_offsetof(tv_episode_num_str), DB_TYPE_STRING },
{ "tv_network_name", mfi_offsetof(tv_network_name), DB_TYPE_STRING },
{ "tv_episode_sort", mfi_offsetof(tv_episode_sort), DB_TYPE_INT },
{ "tv_season_num", mfi_offsetof(tv_season_num), DB_TYPE_INT },
{ "songartistid", mfi_offsetof(songartistid), DB_TYPE_INT64, DB_FIXUP_SONGARTISTID },
{ "songalbumid", mfi_offsetof(songalbumid), DB_TYPE_INT64, DB_FIXUP_SONGALBUMID },
{ "title_sort", mfi_offsetof(title_sort), DB_TYPE_STRING, DB_FIXUP_TITLE_SORT },
{ "artist_sort", mfi_offsetof(artist_sort), DB_TYPE_STRING, DB_FIXUP_ARTIST_SORT },
{ "album_sort", mfi_offsetof(album_sort), DB_TYPE_STRING, DB_FIXUP_ALBUM_SORT },
{ "album_artist_sort", mfi_offsetof(album_artist_sort), DB_TYPE_STRING, DB_FIXUP_ALBUM_ARTIST_SORT },
{ "composer_sort", mfi_offsetof(composer_sort), DB_TYPE_STRING, DB_FIXUP_COMPOSER_SORT },
{ "channels", mfi_offsetof(channels), DB_TYPE_INT },
{ "usermark", mfi_offsetof(usermark), DB_TYPE_INT },
{ "scan_kind", mfi_offsetof(scan_kind), DB_TYPE_INT },
{ "lyrics", mfi_offsetof(lyrics), DB_TYPE_STRING },
};
/* This list must be kept in sync with
* - the order of the columns in the playlists table
* - the type and name of the fields in struct playlist_info
*/
static const struct col_type_map pli_cols_map[] =
{
{ "id", pli_offsetof(id), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_BIND },
{ "title", pli_offsetof(title), DB_TYPE_STRING, DB_FIXUP_TITLE },
{ "type", pli_offsetof(type), DB_TYPE_INT },
{ "query", pli_offsetof(query), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "db_timestamp", pli_offsetof(db_timestamp), DB_TYPE_INT },
{ "disabled", pli_offsetof(disabled), DB_TYPE_INT64 },
{ "path", pli_offsetof(path), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "idx", pli_offsetof(index), DB_TYPE_INT },
{ "special_id", pli_offsetof(special_id), DB_TYPE_INT },
{ "virtual_path", pli_offsetof(virtual_path), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "parent_id", pli_offsetof(parent_id), DB_TYPE_INT },
{ "directory_id", pli_offsetof(directory_id), DB_TYPE_INT },
{ "query_order", pli_offsetof(query_order), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "query_limit", pli_offsetof(query_limit), DB_TYPE_INT },
{ "media_kind", pli_offsetof(media_kind), DB_TYPE_INT, DB_FIXUP_MEDIA_KIND },
{ "artwork_url", pli_offsetof(artwork_url), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "scan_kind", pli_offsetof(scan_kind), DB_TYPE_INT },
// Not in the database, but returned via the query's COUNT()/SUM()
{ "items", pli_offsetof(items), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_BIND },
{ "streams", pli_offsetof(streams), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_BIND },
};
/* This list must be kept in sync with
* - the order of the columns in the queue table
* - the type and name of the fields in struct db_queue_item
* - with qi_mfi_map
*/
static const struct col_type_map qi_cols_map[] =
{
{ "id", qi_offsetof(id), DB_TYPE_INT, DB_FIXUP_STANDARD, DB_FLAG_NO_BIND },
{ "file_id", qi_offsetof(file_id), DB_TYPE_INT },
{ "pos", qi_offsetof(pos), DB_TYPE_INT },
{ "shuffle_pos", qi_offsetof(shuffle_pos), DB_TYPE_INT },
{ "data_kind", qi_offsetof(data_kind), DB_TYPE_INT },
{ "media_kind", qi_offsetof(media_kind), DB_TYPE_INT, DB_FIXUP_MEDIA_KIND },
{ "song_length", qi_offsetof(song_length), DB_TYPE_INT },
{ "path", qi_offsetof(path), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "virtual_path", qi_offsetof(virtual_path), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "title", qi_offsetof(title), DB_TYPE_STRING, DB_FIXUP_TITLE },
{ "artist", qi_offsetof(artist), DB_TYPE_STRING, DB_FIXUP_ARTIST },
{ "album_artist", qi_offsetof(album_artist), DB_TYPE_STRING, DB_FIXUP_ALBUM_ARTIST },
{ "album", qi_offsetof(album), DB_TYPE_STRING, DB_FIXUP_ALBUM },
{ "genre", qi_offsetof(genre), DB_TYPE_STRING, DB_FIXUP_GENRE },
{ "songalbumid", qi_offsetof(songalbumid), DB_TYPE_INT64 },
{ "time_modified", qi_offsetof(time_modified), DB_TYPE_INT },
{ "artist_sort", qi_offsetof(artist_sort), DB_TYPE_STRING, DB_FIXUP_ARTIST_SORT },
{ "album_sort", qi_offsetof(album_sort), DB_TYPE_STRING, DB_FIXUP_ALBUM_SORT },
{ "album_artist_sort", qi_offsetof(album_artist_sort), DB_TYPE_STRING, DB_FIXUP_ALBUM_ARTIST_SORT },
{ "year", qi_offsetof(year), DB_TYPE_INT },
{ "track", qi_offsetof(track), DB_TYPE_INT },
{ "disc", qi_offsetof(disc), DB_TYPE_INT },
{ "artwork_url", qi_offsetof(artwork_url), DB_TYPE_STRING, DB_FIXUP_NO_SANITIZE },
{ "queue_version", qi_offsetof(queue_version), DB_TYPE_INT },
{ "composer", qi_offsetof(composer), DB_TYPE_STRING, DB_FIXUP_COMPOSER },
{ "songartistid", qi_offsetof(songartistid), DB_TYPE_INT64 },
{ "type", qi_offsetof(type), DB_TYPE_STRING, DB_FIXUP_CODECTYPE },
{ "bitrate", qi_offsetof(bitrate), DB_TYPE_INT },
{ "samplerate", qi_offsetof(samplerate), DB_TYPE_INT },
{ "channels", qi_offsetof(channels), DB_TYPE_INT },
};
/* This list must be kept in sync with
* - the order of the columns in the files table
* - the name of the fields in struct db_media_file_info
*/
static const ssize_t dbmfi_cols_map[] =
{
dbmfi_offsetof(id),
dbmfi_offsetof(path),
dbmfi_offsetof(virtual_path),
dbmfi_offsetof(fname),
dbmfi_offsetof(directory_id),
dbmfi_offsetof(title),
dbmfi_offsetof(artist),
dbmfi_offsetof(album),
dbmfi_offsetof(album_artist),
dbmfi_offsetof(genre),
dbmfi_offsetof(comment),
dbmfi_offsetof(type),
dbmfi_offsetof(composer),
dbmfi_offsetof(orchestra),
dbmfi_offsetof(conductor),
dbmfi_offsetof(grouping),
dbmfi_offsetof(url),
dbmfi_offsetof(bitrate),
dbmfi_offsetof(samplerate),
dbmfi_offsetof(song_length),
dbmfi_offsetof(file_size),
dbmfi_offsetof(year),
dbmfi_offsetof(date_released),
dbmfi_offsetof(track),
dbmfi_offsetof(total_tracks),
dbmfi_offsetof(disc),
dbmfi_offsetof(total_discs),
dbmfi_offsetof(bpm),
dbmfi_offsetof(compilation),
dbmfi_offsetof(artwork),
dbmfi_offsetof(rating),
dbmfi_offsetof(play_count),
dbmfi_offsetof(skip_count),
dbmfi_offsetof(seek),
dbmfi_offsetof(data_kind),
dbmfi_offsetof(media_kind),
dbmfi_offsetof(item_kind),
dbmfi_offsetof(description),
dbmfi_offsetof(db_timestamp),
dbmfi_offsetof(time_added),
dbmfi_offsetof(time_modified),
dbmfi_offsetof(time_played),
dbmfi_offsetof(time_skipped),
dbmfi_offsetof(disabled),
dbmfi_offsetof(sample_count),
dbmfi_offsetof(codectype),
dbmfi_offsetof(idx),
dbmfi_offsetof(has_video),
dbmfi_offsetof(contentrating),
dbmfi_offsetof(bits_per_sample),
dbmfi_offsetof(tv_series_name),
dbmfi_offsetof(tv_episode_num_str),
dbmfi_offsetof(tv_network_name),
dbmfi_offsetof(tv_episode_sort),
dbmfi_offsetof(tv_season_num),
dbmfi_offsetof(songartistid),
dbmfi_offsetof(songalbumid),
dbmfi_offsetof(title_sort),
dbmfi_offsetof(artist_sort),
dbmfi_offsetof(album_sort),
dbmfi_offsetof(album_artist_sort),
dbmfi_offsetof(composer_sort),
dbmfi_offsetof(channels),
dbmfi_offsetof(usermark),
dbmfi_offsetof(scan_kind),
dbmfi_offsetof(lyrics),
};
/* This list must be kept in sync with
* - the order of the columns in the playlists table
* - the name of the fields in struct playlist_info
*/
static const ssize_t dbpli_cols_map[] =
{
dbpli_offsetof(id),
dbpli_offsetof(title),
dbpli_offsetof(type),
dbpli_offsetof(query),
dbpli_offsetof(db_timestamp),
dbpli_offsetof(disabled),
dbpli_offsetof(path),
dbpli_offsetof(index),
dbpli_offsetof(special_id),
dbpli_offsetof(virtual_path),
dbpli_offsetof(parent_id),
dbpli_offsetof(directory_id),
dbpli_offsetof(query_order),
dbpli_offsetof(query_limit),
dbpli_offsetof(media_kind),
dbpli_offsetof(artwork_url),
dbpli_offsetof(scan_kind),
dbpli_offsetof(items),
dbpli_offsetof(streams),
};
/* This list must be kept in sync with
* - the order of fields in the Q_GROUP_ALBUMS and Q_GROUP_ARTISTS query
* - the name of the fields in struct group_info
*/
static const ssize_t dbgri_cols_map[] =
{
dbgri_offsetof(id),
dbgri_offsetof(persistentid),
dbgri_offsetof(itemname),
dbgri_offsetof(itemname_sort),
dbgri_offsetof(itemcount),
dbgri_offsetof(groupalbumcount),
dbgri_offsetof(songalbumartist),
dbgri_offsetof(songartistid),
dbgri_offsetof(song_length),
dbgri_offsetof(data_kind),
dbgri_offsetof(media_kind),
dbgri_offsetof(year),
dbgri_offsetof(date_released),
dbgri_offsetof(time_added),
dbgri_offsetof(time_played),
dbgri_offsetof(seek),
};
/* This list must be kept in sync with
* - the order of fields in the Q_BROWSE_INFO query
* - the name of the fields in struct db_browse_info
*/
static const ssize_t dbbi_cols_map[] =
{
dbbi_offsetof(itemname),
dbbi_offsetof(itemname_sort),
dbbi_offsetof(track_count),
dbbi_offsetof(album_count),
dbbi_offsetof(artist_count),
dbbi_offsetof(song_length),
dbbi_offsetof(data_kind),
dbbi_offsetof(media_kind),
dbbi_offsetof(year),
dbbi_offsetof(date_released),
dbbi_offsetof(time_added),
dbbi_offsetof(time_played),
dbbi_offsetof(seek),
};
/* This list must be kept in sync with
* - qi_cols_map
*/
static const struct qi_mfi_map qi_mfi_map[] =
{
{ qi_offsetof(id), -1, -1 },
{ qi_offsetof(file_id), mfi_offsetof(id), dbmfi_offsetof(id) },
{ qi_offsetof(pos), -1, -1 },
{ qi_offsetof(shuffle_pos), -1, -1 },
{ qi_offsetof(data_kind), mfi_offsetof(data_kind), dbmfi_offsetof(data_kind) },
{ qi_offsetof(media_kind), mfi_offsetof(media_kind), dbmfi_offsetof(media_kind) },
{ qi_offsetof(song_length), mfi_offsetof(song_length), dbmfi_offsetof(song_length) },
{ qi_offsetof(path), mfi_offsetof(path), dbmfi_offsetof(path) },
{ qi_offsetof(virtual_path), mfi_offsetof(virtual_path), dbmfi_offsetof(virtual_path) },
{ qi_offsetof(title), mfi_offsetof(title), dbmfi_offsetof(title) },
{ qi_offsetof(artist), mfi_offsetof(artist), dbmfi_offsetof(artist) },
{ qi_offsetof(album_artist), mfi_offsetof(album_artist), dbmfi_offsetof(album_artist) },
{ qi_offsetof(album), mfi_offsetof(album), dbmfi_offsetof(album) },
{ qi_offsetof(genre), mfi_offsetof(genre), dbmfi_offsetof(genre) },
{ qi_offsetof(songalbumid), mfi_offsetof(songalbumid), dbmfi_offsetof(songalbumid) },
{ qi_offsetof(time_modified), mfi_offsetof(time_modified), dbmfi_offsetof(time_modified) },
{ qi_offsetof(artist_sort), mfi_offsetof(artist_sort), dbmfi_offsetof(artist_sort) },
{ qi_offsetof(album_sort), mfi_offsetof(album_sort), dbmfi_offsetof(album_sort) },
{ qi_offsetof(album_artist_sort), mfi_offsetof(album_artist_sort), dbmfi_offsetof(album_artist_sort) },
{ qi_offsetof(year), mfi_offsetof(year), dbmfi_offsetof(year) },
{ qi_offsetof(track), mfi_offsetof(track), dbmfi_offsetof(track) },
{ qi_offsetof(disc), mfi_offsetof(disc), dbmfi_offsetof(disc) },
{ qi_offsetof(artwork_url), -1, -1 },
{ qi_offsetof(queue_version), -1, -1 },
{ qi_offsetof(composer), mfi_offsetof(composer), dbmfi_offsetof(composer) },
{ qi_offsetof(songartistid), mfi_offsetof(songartistid), dbmfi_offsetof(songartistid) },
{ qi_offsetof(type), mfi_offsetof(type), dbmfi_offsetof(type) },
{ qi_offsetof(bitrate), mfi_offsetof(bitrate), dbmfi_offsetof(bitrate) },
{ qi_offsetof(samplerate), mfi_offsetof(samplerate), dbmfi_offsetof(samplerate) },
{ qi_offsetof(channels), mfi_offsetof(channels), dbmfi_offsetof(channels) },
};
/* This list must be kept in sync with
* - the order of the columns in the inotify table
* - the name and type of the fields in struct watch_info
*/
static const struct col_type_map wi_cols_map[] =
{
{ "wd", wi_offsetof(wd), DB_TYPE_INT, DB_FLAG_NO_BIND },
{ "cookie", wi_offsetof(cookie), DB_TYPE_INT },
{ "path", wi_offsetof(path), DB_TYPE_STRING },
};
/* Sort clauses, used for ORDER BY */
/* Keep in sync with enum sort_type and indices */
static const char *sort_clause[] =
{
"",
"f.title_sort",
"f.album_sort, f.disc, f.track",
"f.album_artist_sort, f.album_sort, f.disc, f.track",
"f.type, f.parent_id, f.special_id, f.title",
"f.year",
"f.genre",
"f.composer_sort",
"f.disc",
"f.track",
"f.virtual_path COLLATE NOCASE",
"pos",
"shuffle_pos",
"f.date_released DESC, f.title_sort DESC",
};
/* Browse clauses, used for SELECT, WHERE, GROUP BY and for default ORDER BY
* Keep in sync with enum query_type and indices
* Col 1: for SELECT, Col 2: for WHERE, Col 3: for GROUP BY/ORDER BY
*/
static const struct browse_clause browse_clause[] =
{
{ "", "", "" },
{ "f.album_artist, f.album_artist_sort", "f.album_artist", "f.album_artist_sort, f.album_artist" },
{ "f.album, f.album_sort", "f.album", "f.album_sort, f.album" },
{ "f.genre, f.genre", "f.genre", "f.genre" },
{ "f.composer, f.composer_sort", "f.composer", "f.composer_sort, f.composer" },
{ "f.year, f.year", "f.year", "f.year" },
{ "f.disc, f.disc", "f.disc", "f.disc" },
{ "f.track, f.track", "f.track", "f.track" },
{ "f.virtual_path, f.virtual_path", "f.virtual_path", "f.virtual_path" },
{ "f.path, f.path", "f.path", "f.path" },
};
struct enum_label {
int type;
const char *label;
};
/* Keep in sync with enum media_kind */
static const struct enum_label media_kind_labels[] =
{
{ MEDIA_KIND_MUSIC, "music" },
{ MEDIA_KIND_MOVIE, "movie" },
{ MEDIA_KIND_PODCAST, "podcast" },
{ MEDIA_KIND_AUDIOBOOK, "audiobook" },
{ MEDIA_KIND_MUSICVIDEO, "musicvideo" },
{ MEDIA_KIND_TVSHOW, "tvshow" },
};
const char *
db_media_kind_label(enum media_kind media_kind)
{
int i;
for (i = 0; i < ARRAY_SIZE(media_kind_labels); i++)
{
if (media_kind == media_kind_labels[i].type)
return media_kind_labels[i].label;
}
return NULL;
}
enum media_kind
db_media_kind_enum(const char *label)
{
int i;
if (!label)
return 0;
for (i = 0; i < ARRAY_SIZE(media_kind_labels); i++)
{
if (strcmp(label, media_kind_labels[i].label) == 0)
return media_kind_labels[i].type;
}
return 0;
}
/* Keep in sync with enum data_kind */
static char *data_kind_label[] = { "file", "url", "spotify", "pipe" };
const char *
db_data_kind_label(enum data_kind data_kind)
{
if (data_kind < ARRAY_SIZE(data_kind_label))
{
return data_kind_label[data_kind];
}
return NULL;
}
/* Keep in sync with enum scan_kind */
static const struct enum_label scan_kind_labels[] =
{
{ SCAN_KIND_UNKNOWN, "unknown" },
{ SCAN_KIND_FILES, "files" },
{ SCAN_KIND_SPOTIFY, "spotify" },
{ SCAN_KIND_RSS, "rss" },
};
const char *
db_scan_kind_label(enum scan_kind scan_kind)
{
if (scan_kind < ARRAY_SIZE(scan_kind_labels))
{
return scan_kind_labels[scan_kind].label;
}
return NULL;
}
enum scan_kind
db_scan_kind_enum(const char *name)
{
int i;
if (!name)
return 0;
for (i = 0; i < ARRAY_SIZE(scan_kind_labels); i++)
{
if (strcmp(name, scan_kind_labels[i].label) == 0)
return scan_kind_labels[i].type;
}
return 0;
}
/* Keep in sync with enum pl_type */
static char *pl_type_label[] = { "special", "folder", "smart", "plain", "rss" };
const char *
db_pl_type_label(enum pl_type pl_type)
{
if (pl_type < ARRAY_SIZE(pl_type_label))
{
return pl_type_label[pl_type];
}
return NULL;
}
/* Shuffle RNG state */
struct rng_ctx shuffle_rng;
static char *db_path;
static bool db_rating_updates;
static __thread sqlite3 *hdl;
static __thread struct db_statements db_statements;
/* Forward */
static enum group_type
db_group_type_bypersistentid(int64_t persistentid);
static int
db_query_run(char *query, int free, short update_events);
char *
db_escape_string(const char *str)
{
char *escaped;
char *ret;
escaped = sqlite3_mprintf("%q", str);
if (!escaped)
{
DPRINTF(E_LOG, L_DB, "Out of memory for escaped string\n");
return NULL;
}
ret = strdup(escaped);
sqlite3_free(escaped);
return ret;
}
// Basically a wrapper for sqlite3_mprintf()
char *
db_mprintf(const char *fmt, ...)
{
char *query;
char *ret;
va_list va;
va_start(va, fmt);
ret = sqlite3_vmprintf(fmt, va);
if (!ret)
{
DPRINTF(E_FATAL, L_MISC, "Out of memory for db_mprintf\n");
abort();
}
va_end(va);
query = strdup(ret);
sqlite3_free(ret);
return query;
}
int
db_snprintf(char *s, int n, const char *fmt, ...)
{
char *ret;
va_list va;
if (n < 2)
return -1;
// For size check since sqlite3_vsnprintf does not seem to support it
s[n - 2] = '\0';
va_start(va, fmt);
ret = sqlite3_vsnprintf(n, s, fmt, va);
va_end(va);
if (!ret || (s[n - 2] != '\0'))
return -1;
return 0;
}
void
free_pi(struct pairing_info *pi, int content_only)
{
if (!pi)
return;
free(pi->remote_id);
free(pi->name);
free(pi->guid);
if (!content_only)
free(pi);
else
memset(pi, 0, sizeof(struct pairing_info));
}
void
free_mfi(struct media_file_info *mfi, int content_only)
{
if (!mfi)
return;
free(mfi->path);
free(mfi->fname);
free(mfi->title);
free(mfi->artist);
free(mfi->album);
free(mfi->genre);
free(mfi->comment);
free(mfi->type);
free(mfi->composer);
free(mfi->orchestra);
free(mfi->conductor);
free(mfi->grouping);
free(mfi->url);
free(mfi->description);
free(mfi->codectype);
free(mfi->album_artist);
free(mfi->tv_series_name);
free(mfi->tv_episode_num_str);
free(mfi->tv_network_name);
free(mfi->title_sort);
free(mfi->artist_sort);
free(mfi->album_sort);
free(mfi->composer_sort);
free(mfi->album_artist_sort);
free(mfi->virtual_path);
free(mfi->lyrics);
if (!content_only)
free(mfi);
else
memset(mfi, 0, sizeof(struct media_file_info));
}
void
free_pli(struct playlist_info *pli, int content_only)
{
if (!pli)
return;
free(pli->title);
free(pli->query);
free(pli->path);
free(pli->virtual_path);
free(pli->query_order);
free(pli->artwork_url);
if (!content_only)
free(pli);
else
memset(pli, 0, sizeof(struct playlist_info));
}
void
free_di(struct directory_info *di, int content_only)
{
if (!di)
return;
free(di->path);
free(di->virtual_path);
if (!content_only)
free(di);
else
memset(di, 0, sizeof(struct directory_info));
}
void
free_wi(struct watch_info *wi, int content_only)
{
if (!wi)
return;
free(wi->path);
if (!content_only)
free(wi);
else
memset(wi, 0, sizeof(struct watch_info));
}
void
free_query_params(struct query_params *qp, int content_only)
{
if (!qp)
return;
free(qp->filter);
free(qp->having);
free(qp->order);
if (!content_only)
free(qp);
else
memset(qp, 0, sizeof(struct query_params));
}
void
free_queue_item(struct db_queue_item *qi, int content_only)
{
if (!qi)
return;
free(qi->path);
free(qi->virtual_path);
free(qi->title);
free(qi->artist);
free(qi->album_artist);
free(qi->composer);
free(qi->album);
free(qi->genre);
free(qi->artist_sort);
free(qi->album_sort);
free(qi->album_artist_sort);
free(qi->artwork_url);
free(qi->type);
if (!content_only)
free(qi);
else
memset(qi, 0, sizeof(struct db_queue_item));
}
// Utility function for stepping through mfi/pli/qi structs and setting the
// values from some source, which could be another struct. Some examples:
// - src is a dbmfi, dst is a qi, dst_type is DB_TYPE_INT, caller wants to own
// qi so must_strdup is true, dbmfi has strings so parse_integers is true
// - src is a **char, dst is a qi, dst_type is DB_TYPE_STRING, src_offset is 0
// because src is not a struct, caller wants to strdup the string so
// must_strdup is true
static inline void
struct_field_set_str(void *dst, const void *src, bool must_strdup)
{
char *srcstr;
char **dstptr;
srcstr = *(char **)(src);
dstptr = (char **)(dst);
*dstptr = must_strdup ? safe_strdup(srcstr) : srcstr;
}
static inline void
struct_field_set_uint32(void *dst, const void *src, bool parse_integers)
{
char *srcstr;
uint32_t srcu32val;
int ret;
if (parse_integers)
{
srcstr = *(char **)(src);
ret = safe_atou32(srcstr, &srcu32val);
if (ret < 0)
srcu32val = 0;
}
else
srcu32val = *(uint32_t *)(src);
memcpy(dst, &srcu32val, sizeof(srcu32val));
}
static inline void
struct_field_set_int64(void *dst, const void *src, bool parse_integers)
{
char *srcstr;
int64_t srci64val;
int ret;
if (parse_integers)
{
srcstr = *(char **)(src);
ret = safe_atoi64(srcstr, &srci64val);
if (ret < 0)
srci64val = 0;
}
else
srci64val = *(int64_t *)(src);
memcpy(dst, &srci64val, sizeof(srci64val));
}
static void
struct_field_from_field(void *dst_struct, ssize_t dst_offset, enum field_type dst_type, const void *src_struct, ssize_t src_offset, bool must_strdup, bool parse_integers)
{
switch (dst_type)
{
case DB_TYPE_STRING:
struct_field_set_str(dst_struct + dst_offset, src_struct + src_offset, must_strdup);
break;
case DB_TYPE_INT:
struct_field_set_uint32(dst_struct + dst_offset, src_struct + src_offset, parse_integers);
break;
case DB_TYPE_INT64:
struct_field_set_int64(dst_struct + dst_offset, src_struct + src_offset, parse_integers);
break;
}
}
static void
struct_field_from_statement(void *dst_struct, ssize_t dst_offset, enum field_type dst_type, sqlite3_stmt *stmt, int col, bool must_strdup, bool parse_integers)
{
const char *str;
uint32_t u32;
int64_t i64;
switch (dst_type)
{
case DB_TYPE_STRING:
str = (const char *)sqlite3_column_text(stmt, col);
struct_field_set_str(dst_struct + dst_offset, &str, must_strdup);
break;
case DB_TYPE_INT:
u32 = (uint32_t)sqlite3_column_int64(stmt, col); // _int64() because _int() wouldn't be enough for uint32
struct_field_set_uint32(dst_struct + dst_offset, &u32, parse_integers);
break;
case DB_TYPE_INT64:
i64 = sqlite3_column_int64(stmt, col);
struct_field_set_int64(dst_struct + dst_offset, &i64, parse_integers);
break;
}
}
static void
sort_tag_create(char **sort_tag, const char *src_tag)
{
const uint8_t *i_ptr;
const uint8_t *n_ptr;
const uint8_t *number;
uint8_t out[1024];
uint8_t *o_ptr;
int append_number;
ucs4_t puc;
int numlen;
size_t len;
int charlen;
/* Note: include terminating NUL in string length for u8_normalize */
// If the source provides a source tag then we rely on a normalized version of
// that instead of creating our own (see issue #1257). FIXME this produces the
// following bug:
// - Track with no sort tags, assume queue_item->artist is "A", then
// queue_item->artist_sort will be created and set to "A".
// - Update the artist name of the queue item with JSON API to "B".
// - queue_item->artist_sort will still be "A".
if (*sort_tag)
{
DPRINTF(E_DBG, L_DB, "Existing sort tag will be normalized: %s\n", *sort_tag);
o_ptr = u8_normalize(UNINORM_NFD, (uint8_t *)*sort_tag, strlen(*sort_tag) + 1, NULL, &len);
free(*sort_tag);
*sort_tag = (char *)o_ptr;
return;
}
if (!src_tag || ((len = strlen(src_tag)) == 0))
{
*sort_tag = NULL;
return;
}
// Set input pointer past article if present and disregard certain special chars
if ((strncasecmp(src_tag, "a ", 2) == 0) && (len > 2))
i_ptr = (uint8_t *)(src_tag + 2);
else if ((strncasecmp(src_tag, "an ", 3) == 0) && (len > 3))
i_ptr = (uint8_t *)(src_tag + 3);
else if ((strncasecmp(src_tag, "the ", 4) == 0) && (len > 4))
i_ptr = (uint8_t *)(src_tag + 4);
else if (strchr("[('\"", src_tag[0]) && (len > 1))
i_ptr = (uint8_t *)(src_tag + 1);
else
i_ptr = (uint8_t *)src_tag;
// Poor man's natural sort. Makes sure we sort like this: a1, a2, a10, a11, a21, a111
// We do this by padding zeroes to (short) numbers. As an alternative we could have
// made a proper natural sort algorithm in sqlext.c, but we don't, since we don't
// want any risk of hurting response times
memset(&out, 0, sizeof(out));
o_ptr = (uint8_t *)&out;
number = NULL;
append_number = 0;
do
{
n_ptr = u8_next(&puc, i_ptr);
if (uc_is_digit(puc))
{
if (!number) // We have encountered the beginning of a number
number = i_ptr;
append_number = (n_ptr == NULL); // If last char in string append number now
}
else
{
if (number)
append_number = 1; // A number has ended so time to append it
else
{
charlen = u8_strmblen(i_ptr);
if (charlen >= 0)
o_ptr = u8_stpncpy(o_ptr, i_ptr, charlen); // No numbers in sight, just append char
}
}
// Break if less than 100 bytes remain (prevent buffer overflow)
if (sizeof(out) - u8_strlen(out) < 100)
break;
// Break if number is very large (prevent buffer overflow)
if (number && (i_ptr - number > 50))
break;
if (append_number)
{
numlen = i_ptr - number;
if (numlen < 5) // Max pad width
{
u8_strcpy(o_ptr, (uint8_t *)"00000");
o_ptr += (5 - numlen);
}
o_ptr = u8_stpncpy(o_ptr, number, numlen + u8_strmblen(i_ptr));
number = NULL;
append_number = 0;
}
i_ptr = n_ptr;
}
while (n_ptr);
*sort_tag = (char *)u8_normalize(UNINORM_NFD, (uint8_t *)&out, u8_strlen(out) + 1, NULL, &len);
}
static void
fixup_sanitize(char **tag, enum fixup_type fixup, struct fixup_ctx *ctx)
{
char *ret;
if (!tag || !*tag)
return;
switch (fixup)
{
case DB_FIXUP_NO_SANITIZE:
case DB_FIXUP_CODECTYPE:
break; // Don't touch the above
default:
trim(*tag);
// By default we set empty strings to NULL
if (*tag[0] == '\0')
{
free(*tag);
*tag = NULL;
break;
}
ret = unicode_fixup_string(*tag, "ascii");
if (ret != *tag)
{
free(*tag);
*tag = ret;
}
}
}
static void
fixup_defaults(char **tag, enum fixup_type fixup, struct fixup_ctx *ctx)
{
char *ca;
switch(fixup)
{
case DB_FIXUP_SONGARTISTID:
if (ctx->mfi && ctx->mfi->songartistid == 0)
ctx->mfi->songartistid = two_str_hash(ctx->mfi->album_artist, NULL);
break;
case DB_FIXUP_SONGALBUMID:
if (ctx->mfi && ctx->mfi->songalbumid == 0)
ctx->mfi->songalbumid = two_str_hash(ctx->mfi->album_artist, ctx->mfi->album);
break;
case DB_FIXUP_TITLE:
if (*tag)
break;
// fname is left untouched by fixup_sanitize() for obvious reasons, so ensure it is proper UTF-8
if (ctx->mfi && ctx->mfi->fname)
{
*tag = unicode_fixup_string(ctx->mfi->fname, "ascii");
if (*tag == ctx->mfi->fname)
*tag = strdup(ctx->mfi->fname);
}
else if (ctx->pli && ctx->pli->path)
*tag = strdup(ctx->pli->path);
else if (ctx->qi && ctx->qi->path)
*tag = strdup(ctx->qi->path);
else
*tag = strdup(CFG_NAME_UNKNOWN_TITLE);
break;
case DB_FIXUP_ARTIST:
if (*tag)
break;
if (ctx->mfi && ctx->mfi->album_artist)
*tag = strdup(ctx->mfi->album_artist);
else if (ctx->mfi && ctx->mfi->orchestra && ctx->mfi->conductor)
*tag = safe_asprintf("%s - %s", ctx->mfi->orchestra, ctx->mfi->conductor);
else if (ctx->mfi && ctx->mfi->orchestra)
*tag = strdup(ctx->mfi->orchestra);
else if (ctx->mfi && ctx->mfi->conductor)
*tag = strdup(ctx->mfi->conductor);
else if (ctx->mfi && ctx->mfi->tv_series_name)
*tag = strdup(ctx->mfi->tv_series_name);
else
*tag = strdup(CFG_NAME_UNKNOWN_ARTIST);
break;
case DB_FIXUP_ALBUM:
if (*tag)
break;
if (ctx->mfi && ctx->mfi->tv_series_name)
*tag = safe_asprintf("%s, Season %u", ctx->mfi->tv_series_name, ctx->mfi->tv_season_num);
else
*tag = strdup(CFG_NAME_UNKNOWN_ALBUM);
break;
case DB_FIXUP_ALBUM_ARTIST: // Will be set after artist, because artist (must) come first in the col_maps
if (ctx->mfi && ctx->mfi->media_kind == MEDIA_KIND_PODCAST)
{
free(*tag);
*tag = strdup("");
}
if (ctx->mfi && ctx->mfi->compilation && (ca = cfg_getstr(cfg_getsec(cfg, "library"), "compilation_artist")))
{
free(*tag);
*tag = strdup(ca); // If ca is empty string then the artist will not be shown in artist view
}
if (*tag)
break;
if (ctx->mfi && ctx->mfi->artist)
*tag = strdup(ctx->mfi->artist);
else if (ctx->qi && ctx->qi->artist)
*tag = strdup(ctx->qi->artist);
else
*tag = strdup(CFG_NAME_UNKNOWN_ARTIST);
break;
case DB_FIXUP_GENRE:
if (*tag)
break;
*tag = strdup(CFG_NAME_UNKNOWN_GENRE);
break;
case DB_FIXUP_MEDIA_KIND:
if (ctx->mfi && ctx->mfi->tv_series_name)
ctx->mfi->media_kind = MEDIA_KIND_TVSHOW;
else if (ctx->mfi && !ctx->mfi->media_kind)
ctx->mfi->media_kind = MEDIA_KIND_MUSIC;
else if (ctx->pli && !ctx->pli->media_kind)
ctx->pli->media_kind = MEDIA_KIND_MUSIC;
else if (ctx->qi && !ctx->qi->media_kind)
ctx->qi->media_kind = MEDIA_KIND_MUSIC;
break;
case DB_FIXUP_ITEM_KIND:
if (ctx->mfi && !ctx->mfi->item_kind)
ctx->mfi->item_kind = 2; // music
break;
case DB_FIXUP_TIME_MODIFIED:
if (ctx->mfi && ctx->mfi->time_modified == 0)
ctx->mfi->time_modified = ctx->mfi->db_timestamp;
break;
case DB_FIXUP_CODECTYPE:
case DB_FIXUP_TYPE:
// Default to mpeg4 video/audio for unknown file types in an attempt to allow streaming of DRM-afflicted files
if (ctx->mfi && ctx->mfi->codectype && strcmp(ctx->mfi->codectype, "unkn") == 0)
{
if (ctx->mfi->has_video)
{
strcpy(ctx->mfi->codectype, "mp4v");
strcpy(ctx->mfi->type, "m4v");
}
else
{
strcpy(ctx->mfi->codectype, "mp4a");
strcpy(ctx->mfi->type, "m4a");
}
}
break;
default:
break;
}
}
static void
fixup_sort_tags(char **tag, enum fixup_type fixup, struct fixup_ctx *ctx)
{
switch(fixup)
{
case DB_FIXUP_TITLE_SORT:
if (ctx->mfi)
sort_tag_create(tag, ctx->mfi->title);
break;
case DB_FIXUP_ARTIST_SORT:
if (ctx->mfi)
sort_tag_create(tag, ctx->mfi->artist);
else if (ctx->qi)
sort_tag_create(tag, ctx->qi->artist);
break;
case DB_FIXUP_ALBUM_SORT:
if (ctx->mfi)
sort_tag_create(tag, ctx->mfi->album);
else if (ctx->qi)
sort_tag_create(tag, ctx->qi->album);
break;
case DB_FIXUP_ALBUM_ARTIST_SORT:
if (ctx->mfi)
sort_tag_create(tag, ctx->mfi->album_artist);
else if (ctx->qi)
sort_tag_create(tag, ctx->qi->album_artist);
break;
case DB_FIXUP_COMPOSER_SORT:
if (ctx->mfi)
sort_tag_create(tag, ctx->mfi->composer);
break;
default:
break;
}
}
static void
fixup_tags(struct fixup_ctx *ctx)
{
void (*fixup_func[])(char **, enum fixup_type, struct fixup_ctx *) = { fixup_sanitize, fixup_defaults, fixup_sort_tags };
char **tag;
int i;
int j;
for (i = 0; i < ARRAY_SIZE(fixup_func); i++)
{
for (j = 0; j < ctx->map_size; j++)
{
switch (ctx->map[j].type)
{
case DB_TYPE_STRING:
tag = (char **) ((char *)ctx->data + ctx->map[j].offset);
fixup_func[i](tag, ctx->map[j].fixup, ctx);
break;
case DB_TYPE_INT:
case DB_TYPE_INT64:
fixup_func[i](NULL, ctx->map[j].fixup, ctx);
break;
}
}
}
}
static void
fixup_tags_mfi(struct media_file_info *mfi)
{
struct fixup_ctx ctx = { 0 };
ctx.data = mfi;
ctx.mfi = mfi;
ctx.map = mfi_cols_map;
ctx.map_size = ARRAY_SIZE(mfi_cols_map);
fixup_tags(&ctx);
}
static void
fixup_tags_pli(struct playlist_info *pli)
{
struct fixup_ctx ctx = { 0 };
ctx.data = pli;
ctx.pli = pli;
ctx.map = pli_cols_map;
ctx.map_size = ARRAY_SIZE(pli_cols_map);
fixup_tags(&ctx);
}
static void
fixup_tags_qi(struct db_queue_item *qi)
{
struct fixup_ctx ctx = { 0 };
ctx.data = qi;
ctx.qi = qi;
ctx.map = qi_cols_map;
ctx.map_size = ARRAY_SIZE(qi_cols_map);
fixup_tags(&ctx);
}
static int
bind_generic(sqlite3_stmt *stmt, void *data, const struct col_type_map *map, size_t map_size, int id)
{
char **strptr;
char *ptr;
int i;
int n;
for (i = 0, n = 1; i < map_size; i++)
{
if (map[i].flag & DB_FLAG_NO_BIND)
continue;
ptr = data + map[i].offset;
strptr = (char **)(data + map[i].offset);
switch (map[i].type)
{
case DB_TYPE_INT:
sqlite3_bind_int64(stmt, n, *((uint32_t *)ptr)); // Use _int64 because _int is for signed int32
break;
case DB_TYPE_INT64:
sqlite3_bind_int64(stmt, n, *((int64_t *)ptr));
break;
case DB_TYPE_STRING:
sqlite3_bind_text(stmt, n, *strptr, -1, SQLITE_STATIC); // TODO should we use _TRANSIENT?
break;
default:
DPRINTF(E_LOG, L_DB, "BUG: Unknown type %d in column map\n", map[i].type);
return -1;
}
n++;
}
// This binds the final "WHERE id = ?" if it is an update
if (id)
sqlite3_bind_int(stmt, n, id);
return 0;
}
static int
bind_mfi(sqlite3_stmt *stmt, struct media_file_info *mfi)
{
return bind_generic(stmt, mfi, mfi_cols_map, ARRAY_SIZE(mfi_cols_map), mfi->id);
}
static int
bind_pli(sqlite3_stmt *stmt, struct playlist_info *pli)
{
return bind_generic(stmt, pli, pli_cols_map, ARRAY_SIZE(pli_cols_map), pli->id);
}
static int
bind_qi(sqlite3_stmt *stmt, struct db_queue_item *qi)
{
return bind_generic(stmt, qi, qi_cols_map, ARRAY_SIZE(qi_cols_map), qi->id);
}
/* Unlock notification support */
static void
unlock_notify_cb(void **args, int nargs)
{
struct db_unlock *u;
int i;
for (i = 0; i < nargs; i++)
{
u = (struct db_unlock *)args[i];
CHECK_ERR(L_DB, pthread_mutex_lock(&u->lck));
u->proceed = 1;
CHECK_ERR(L_DB, pthread_cond_signal(&u->cond));
CHECK_ERR(L_DB, pthread_mutex_unlock(&u->lck));
}
}
static int
db_wait_unlock(void)
{
struct db_unlock u;
int ret;
u.proceed = 0;
CHECK_ERR(L_DB, mutex_init(&u.lck));
CHECK_ERR(L_DB, pthread_cond_init(&u.cond, NULL));
ret = sqlite3_unlock_notify(hdl, unlock_notify_cb, &u);
if (ret == SQLITE_OK)
{
CHECK_ERR(L_DB, pthread_mutex_lock(&u.lck));
if (!u.proceed)
{
DPRINTF(E_INFO, L_DB, "Waiting for database unlock\n");
CHECK_ERR(L_DB, pthread_cond_wait(&u.cond, &u.lck));
}
CHECK_ERR(L_DB, pthread_mutex_unlock(&u.lck));
}
CHECK_ERR(L_DB, pthread_cond_destroy(&u.cond));
CHECK_ERR(L_DB, pthread_mutex_destroy(&u.lck));
return ret;
}
static int
db_blocking_step(sqlite3_stmt *stmt)
{
int ret;
while ((ret = sqlite3_step(stmt)) == SQLITE_LOCKED)
{
ret = db_wait_unlock();
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Database deadlocked!\n");
break;
}
sqlite3_reset(stmt);
}
return ret;
}
static int
db_blocking_prepare_v2(const char *query, int len, sqlite3_stmt **stmt, const char **end)
{
int ret;
while ((ret = sqlite3_prepare_v2(hdl, query, len, stmt, end)) == SQLITE_LOCKED)
{
ret = db_wait_unlock();
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Database deadlocked!\n");
break;
}
}
return ret;
}
static int
db_statement_run(sqlite3_stmt *stmt, short update_events)
{
int ret;
int changes = 0;
#ifdef HAVE_SQLITE3_EXPANDED_SQL
char *query;
if (logger_severity() >= E_DBG)
{
query = sqlite3_expanded_sql(stmt);
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
sqlite3_free(query);
}
#else
DPRINTF(E_DBG, L_DB, "Running query (prepared statement)\n");
#endif
while ((ret = db_blocking_step(stmt)) == SQLITE_ROW)
; /* EMPTY */
if (ret != SQLITE_DONE)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
}
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
changes = sqlite3_changes(hdl);
if (update_events && changes > 0)
library_update_trigger(update_events);
return (ret == SQLITE_DONE) ? changes : -1;
}
/* Modelled after sqlite3_exec() */
static int
db_exec(const char *query, char **errmsg)
{
sqlite3_stmt *stmt;
int try;
int ret;
*errmsg = NULL;
for (try = 0; try < 5; try++)
{
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
*errmsg = sqlite3_mprintf("prepare failed: %s", sqlite3_errmsg(hdl));
return ret;
}
while ((ret = db_blocking_step(stmt)) == SQLITE_ROW)
; /* EMPTY */
sqlite3_finalize(stmt);
if (ret != SQLITE_SCHEMA)
break;
}
if (ret != SQLITE_DONE)
{
*errmsg = sqlite3_mprintf("step failed: %s", sqlite3_errmsg(hdl));
return ret;
}
return SQLITE_OK;
}
/* Maintenance and DB hygiene */
static void
db_pragma_optimize(void)
{
const char *query = "ANALYZE;";
char *errmsg;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "ANALYZE failed: %s\n", errmsg);
sqlite3_free(errmsg);
}
}
/* Set names of default playlists according to config */
static void
db_set_cfg_names(void)
{
#define Q_TMPL "UPDATE playlists SET title = '%q' WHERE type = %d AND special_id = %d;"
char *cfg_item[6] = { "name_library", "name_music", "name_movies", "name_tvshows", "name_podcasts", "name_audiobooks" };
char special_id[6] = { 0, 6, 4, 5, 1, 7 };
cfg_t *lib;
char *query;
char *title;
char *errmsg;
int ret;
int i;
lib = cfg_getsec(cfg, "library");
for (i = 0; i < (sizeof(cfg_item) / sizeof(cfg_item[0])); i++)
{
title = cfg_getstr(lib, cfg_item[i]);
if (!title)
{
DPRINTF(E_LOG, L_DB, "Internal error, unknown config item '%s'\n", cfg_item[i]);
continue;
}
query = sqlite3_mprintf(Q_TMPL, title, PL_SPECIAL, special_id[i]);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Error setting playlist title, query %s, error: %s\n", query, errmsg);
sqlite3_free(errmsg);
}
else
DPRINTF(E_DBG, L_DB, "Playlist title for config item '%s' set with query '%s'\n", cfg_item[i], query);
sqlite3_free(query);
}
#undef Q_TMPL
}
void
db_hook_post_scan(void)
{
DPRINTF(E_DBG, L_DB, "Running post-scan DB maintenance tasks...\n");
db_pragma_optimize();
DPRINTF(E_DBG, L_DB, "Done with post-scan DB maintenance\n");
}
void
db_purge_cruft(time_t ref)
{
#define Q_TMPL "DELETE FROM directories WHERE id >= %d AND db_timestamp < %" PRIi64 ";"
int i;
int ret;
char *query;
char *queries_tmpl[4] =
{
"DELETE FROM playlistitems WHERE playlistid IN (SELECT p.id FROM playlists p WHERE p.type <> %d AND p.db_timestamp < %" PRIi64 ");",
"DELETE FROM playlistitems WHERE filepath IN (SELECT f.path FROM files f WHERE -1 <> %d AND f.db_timestamp < %" PRIi64 ");",
"DELETE FROM playlists WHERE type <> %d AND db_timestamp < %" PRIi64 ";",
"DELETE FROM files WHERE -1 <> %d AND db_timestamp < %" PRIi64 ";",
};
db_transaction_begin();
for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++)
{
query = sqlite3_mprintf(queries_tmpl[i], PL_SPECIAL, (int64_t)ref);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
db_transaction_end();
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_query_run(query, 1, 0);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
}
query = sqlite3_mprintf(Q_TMPL, DIR_MAX, (int64_t)ref);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
db_transaction_end();
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
db_transaction_end();
#undef Q_TMPL
}
void
db_purge_cruft_bysource(time_t ref, enum scan_kind scan_kind)
{
#define Q_TMPL "DELETE FROM directories WHERE id >= %d AND db_timestamp < %" PRIi64 " AND scan_kind = %d;"
int i;
int ret;
char *query;
char *queries_tmpl[4] =
{
"DELETE FROM playlistitems WHERE playlistid IN (SELECT p.id FROM playlists p WHERE p.type <> %d AND p.db_timestamp < %" PRIi64 " AND scan_kind = %d);",
"DELETE FROM playlistitems WHERE filepath IN (SELECT f.path FROM files f WHERE -1 <> %d AND f.db_timestamp < %" PRIi64 " AND scan_kind = %d);",
"DELETE FROM playlists WHERE type <> %d AND db_timestamp < %" PRIi64 " AND scan_kind = %d;",
"DELETE FROM files WHERE -1 <> %d AND db_timestamp < %" PRIi64 " AND scan_kind = %d;",
};
db_transaction_begin();
for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++)
{
query = sqlite3_mprintf(queries_tmpl[i], PL_SPECIAL, (int64_t)ref, scan_kind);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
db_transaction_end();
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_query_run(query, 1, 0);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
}
query = sqlite3_mprintf(Q_TMPL, DIR_MAX, (int64_t)ref, scan_kind);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
db_transaction_end();
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
db_transaction_end();
#undef Q_TMPL
}
void
db_purge_all(void)
{
#define Q_TMPL_PL "DELETE FROM playlists WHERE type <> %d;"
#define Q_TMPL_DIR "DELETE FROM directories WHERE id >= %d;"
char *queries[4] =
{
"DELETE FROM inotify;",
"DELETE FROM playlistitems;",
"DELETE FROM files;",
"DELETE FROM groups;",
};
char *errmsg;
char *query;
int i;
int ret;
for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++)
{
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", queries[i]);
ret = db_exec(queries[i], &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Purge query %d error: %s\n", i, errmsg);
sqlite3_free(errmsg);
}
else
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
}
// Purge playlists
query = sqlite3_mprintf(Q_TMPL_PL, PL_SPECIAL);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Purge query '%s' error: %s\n", query, errmsg);
sqlite3_free(errmsg);
}
else
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
sqlite3_free(query);
// Purge directories
query = sqlite3_mprintf(Q_TMPL_DIR, DIR_MAX);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Purge query '%s' error: %s\n", query, errmsg);
sqlite3_free(errmsg);
}
else
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
sqlite3_free(query);
#undef Q_TMPL_PL
#undef Q_TMPL_DIR
}
static int
db_get_one_int(const char *query)
{
sqlite3_stmt *stmt;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_INFO, L_DB, "No matching row found for query: %s\n", query);
else
DPRINTF(E_LOG, L_DB, "Could not step: %s (%s)\n", sqlite3_errmsg(hdl), query);
sqlite3_finalize(stmt);
return -1;
}
ret = sqlite3_column_int(stmt, 0);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
return ret;
}
/* Transactions */
void
db_transaction_begin(void)
{
char *query = "BEGIN TRANSACTION;";
char *errmsg;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg);
sqlite3_free(errmsg);
}
}
void
db_transaction_end(void)
{
char *query = "END TRANSACTION;";
char *errmsg;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg);
sqlite3_free(errmsg);
}
}
void
db_transaction_rollback(void)
{
char *query = "ROLLBACK TRANSACTION;";
char *errmsg;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg);
sqlite3_free(errmsg);
}
}
static void
db_free_query_clause(struct query_clause *qc)
{
if (!qc)
return;
sqlite3_free(qc->where);
sqlite3_free(qc->group);
sqlite3_free(qc->having);
sqlite3_free(qc->order);
sqlite3_free(qc->index);
free(qc);
}
static struct query_clause *
db_build_query_clause(struct query_params *qp)
{
struct query_clause *qc;
qc = calloc(1, sizeof(struct query_clause));
if (!qc)
goto error;
if (qp->type & Q_F_BROWSE)
qc->group = sqlite3_mprintf("GROUP BY %s", browse_clause[qp->type & ~Q_F_BROWSE].group);
else if (qp->group)
qc->group = sqlite3_mprintf("GROUP BY %s", qp->group);
else
qc->group = sqlite3_mprintf("");
if (qp->filter && !qp->with_disabled)
qc->where = sqlite3_mprintf("WHERE f.disabled = 0 AND %s", qp->filter);
else if (!qp->with_disabled)
qc->where = sqlite3_mprintf("WHERE f.disabled = 0");
else if (qp->filter)
qc->where = sqlite3_mprintf("WHERE %s", qp->filter);
else
qc->where = sqlite3_mprintf("");
if (qp->having && (qp->type & (Q_GROUP_ALBUMS | Q_GROUP_ARTISTS)))
qc->having = sqlite3_mprintf("HAVING %s", qp->having);
else
qc->having = sqlite3_mprintf("");
if (qp->order)
qc->order = sqlite3_mprintf("ORDER BY %s", qp->order);
else if (qp->sort)
qc->order = sqlite3_mprintf("ORDER BY %s", sort_clause[qp->sort]);
else if (qp->type & Q_F_BROWSE)
qc->order = sqlite3_mprintf("ORDER BY %s", browse_clause[qp->type & ~Q_F_BROWSE].group);
else
qc->order = sqlite3_mprintf("");
switch (qp->idx_type)
{
case I_FIRST:
if (qp->limit)
qc->index = sqlite3_mprintf("LIMIT %d", qp->limit);
else
qc->index = sqlite3_mprintf("");
break;
case I_LAST:
qc->index = sqlite3_mprintf("LIMIT -1 OFFSET %d", qp->results - qp->limit);
break;
case I_SUB:
if (qp->limit)
qc->index = sqlite3_mprintf("LIMIT %d OFFSET %d", qp->limit, qp->offset);
else
qc->index = sqlite3_mprintf("LIMIT -1 OFFSET %d", qp->offset);
break;
case I_NONE:
qc->index = sqlite3_mprintf("");
break;
}
if (!qc->where || !qc->index)
goto error;
return qc;
error:
DPRINTF(E_LOG, L_DB, "Error building query clause\n");
db_free_query_clause(qc);
return NULL;
}
static char *
db_build_query_check(struct query_params *qp, char *count, char *query)
{
if (!count || !query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
goto failed;
}
qp->results = db_get_one_int(count);
if (qp->results < 0)
{
DPRINTF(E_LOG, L_DB, "No results for count\n");
goto failed;
}
sqlite3_free(count);
return query;
failed:
sqlite3_free(count);
sqlite3_free(query);
return NULL;
}
static char *
db_build_query_items(struct query_params *qp, struct query_clause *qc)
{
char *count;
char *query;
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s;", qc->where);
query = sqlite3_mprintf("SELECT f.* FROM files f %s %s %s %s;", qc->where, qc->group, qc->order, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_pls(struct query_params *qp, struct query_clause *qc)
{
char *count;
char *query;
count = sqlite3_mprintf("SELECT COUNT(*) FROM playlists f %s;", qc->where);
query = sqlite3_mprintf(Q_PL_SELECT " %s GROUP BY f.id %s %s;", qc->where, qc->order, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_find_pls(struct query_params *qp, struct query_clause *qc)
{
if (!qp->filter)
{
DPRINTF(E_LOG, L_DB, "Bug! Playlist find called without search criteria\n");
return NULL;
}
// Use qp->filter because qc->where has a f.disabled which is not a column in playlistitems
sqlite3_free(qc->where);
qc->where = sqlite3_mprintf("WHERE f.id IN (SELECT playlistid FROM playlistitems WHERE %s)", qp->filter);
return db_build_query_pls(qp, qc);
}
static char *
db_build_query_plitems_plain(struct query_params *qp, struct query_clause *qc)
{
char *count;
char *query;
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f JOIN playlistitems pi ON f.path = pi.filepath %s AND pi.playlistid = %d;", qc->where, qp->id);
query = sqlite3_mprintf("SELECT f.* FROM files f JOIN playlistitems pi ON f.path = pi.filepath %s AND pi.playlistid = %d ORDER BY pi.id ASC %s;", qc->where, qp->id, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_plitems_smart(struct query_params *qp, struct playlist_info *pli)
{
struct query_clause *qc;
char *count;
char *query;
bool free_orderby = false;
if (pli->query_limit > 0)
{
if (qp->idx_type == I_SUB)
{
if (pli->query_limit > qp->offset + qp->limit)
qp->limit = pli->query_limit;
}
else if (qp->idx_type == I_NONE)
{
qp->idx_type = I_SUB;
qp->limit = pli->query_limit;
qp->offset = 0;
}
else
{
DPRINTF(E_WARN, L_DB, "Cannot append limit from smart playlist '%s' to query\n", pli->path);
}
}
if (pli->query_order)
{
if (!qp->order && qp->sort == S_NONE)
{
qp->order = strdup(pli->query_order);
free_orderby = true;
}
else
DPRINTF(E_WARN, L_DB, "Cannot append order by from smart playlist '%s' to query\n", pli->path);
}
qc = db_build_query_clause(qp);
if (free_orderby)
{
free(qp->order);
qp->order = NULL;
}
if (!qc)
return NULL;
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND %s LIMIT %d;", qc->where, pli->query, pli->query_limit ? pli->query_limit : -1);
query = sqlite3_mprintf("SELECT f.* FROM files f %s AND %s %s %s;", qc->where, pli->query, qc->order, qc->index);
db_free_query_clause(qc);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_plitems(struct query_params *qp, struct query_clause *qc)
{
struct playlist_info *pli;
char *query;
if (qp->id <= 0)
{
DPRINTF(E_LOG, L_DB, "No playlist id specified in playlist items query\n");
return NULL;
}
pli = db_pl_fetch_byid(qp->id);
if (!pli)
return NULL;
switch (pli->type)
{
case PL_SPECIAL:
case PL_SMART:
query = db_build_query_plitems_smart(qp, pli);
break;
case PL_RSS:
case PL_PLAIN:
case PL_FOLDER:
query = db_build_query_plitems_plain(qp, qc);
break;
default:
DPRINTF(E_LOG, L_DB, "Unknown playlist type %d in playlist items query\n", pli->type);
query = NULL;
break;
}
free_pli(pli, 0);
return query;
}
static char *
db_build_query_group_albums(struct query_params *qp, struct query_clause *qc)
{
char *count;
char *query;
count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.songalbumid) FROM files f %s;", qc->where);
query = sqlite3_mprintf("SELECT" \
" g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id) AS track_count," \
" 1 AS album_count, f.album_artist, f.songartistid," \
" SUM(f.song_length) AS song_length, MIN(f.data_kind) AS data_kind, MIN(f.media_kind) AS media_kind," \
" MAX(f.year) AS year, MAX(f.date_released) AS date_released," \
" MAX(f.time_added) AS time_added, MAX(f.time_played) AS time_played, MAX(f.seek) AS seek " \
"FROM files f JOIN groups g ON f.songalbumid = g.persistentid %s " \
"GROUP BY f.songalbumid %s %s %s;", qc->where, qc->having, qc->order, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_group_artists(struct query_params *qp, struct query_clause *qc)
{
char *count;
char *query;
count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.songartistid) FROM files f %s;", qc->where);
query = sqlite3_mprintf("SELECT" \
" g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id) AS track_count," \
" COUNT(DISTINCT f.songalbumid) AS album_count, f.album_artist, f.songartistid," \
" SUM(f.song_length) AS song_length, MIN(f.data_kind) AS data_kind, MIN(f.media_kind) AS media_kind," \
" MAX(f.year) AS year, MAX(f.date_released) AS date_released," \
" MAX(f.time_added) AS time_added, MAX(f.time_played) AS time_played, MAX(f.seek) AS seek " \
"FROM files f JOIN groups g ON f.songartistid = g.persistentid %s " \
"GROUP BY f.songartistid %s %s %s;",
qc->where, qc->having, qc->order, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_group_items(struct query_params *qp, struct query_clause *qc)
{
enum group_type gt;
char *count;
char *query;
gt = db_group_type_bypersistentid(qp->persistentid);
switch (gt)
{
case G_ALBUMS:
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND f.songalbumid = %" PRIi64 ";", qc->where, qp->persistentid);
query = sqlite3_mprintf("SELECT f.* FROM files f %s AND f.songalbumid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index);
break;
case G_ARTISTS:
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND f.songartistid = %" PRIi64 ";", qc->where, qp->persistentid);
query = sqlite3_mprintf("SELECT f.* FROM files f %s AND f.songartistid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index);
break;
default:
DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid);
return NULL;
}
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_group_dirs(struct query_params *qp, struct query_clause *qc)
{
enum group_type gt;
char *count;
char *query;
gt = db_group_type_bypersistentid(qp->persistentid);
switch (gt)
{
case G_ALBUMS:
count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))"
" FROM files f %s AND f.songalbumid = %" PRIi64 ";", qc->where, qp->persistentid);
query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))"
" FROM files f %s AND f.songalbumid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index);
break;
case G_ARTISTS:
count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))"
" FROM files f %s AND f.songartistid = %" PRIi64 ";", qc->where, qp->persistentid);
query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))"
" FROM files f %s AND f.songartistid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index);
break;
default:
DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid);
return NULL;
}
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_browse(struct query_params *qp, struct query_clause *qc)
{
const char *where;
const char *select;
char *count;
char *query;
select = browse_clause[qp->type & ~Q_F_BROWSE].select;
where = browse_clause[qp->type & ~Q_F_BROWSE].where;
count = sqlite3_mprintf("SELECT COUNT(*) FROM (SELECT %s FROM files f %s AND %s != '' %s);", select, qc->where, where, qc->group);
query = sqlite3_mprintf("SELECT %s, COUNT(f.id) AS track_count, COUNT(DISTINCT f.songalbumid) AS album_count, COUNT(DISTINCT f.songartistid) AS artist_count,"
" SUM(f.song_length) AS song_length, MIN(f.data_kind) AS data_kind, MIN(f.media_kind) AS media_kind,"
" MAX(f.year) AS year, MAX(f.date_released) AS date_released,"
" MAX(f.time_added) AS time_added, MAX(f.time_played) AS time_played, MAX(f.seek) AS seek "
"FROM files f %s AND %s != '' %s %s %s;",
select, qc->where, where, qc->group, qc->order, qc->index);
return db_build_query_check(qp, count, query);
}
static char *
db_build_query_count_items(struct query_params *qp, struct query_clause *qc)
{
char *query;
qp->results = 1;
query = sqlite3_mprintf("SELECT COUNT(*), SUM(song_length), COUNT(DISTINCT songartistid), COUNT(DISTINCT songalbumid), SUM(file_size) FROM files f %s;", qc->where);
if (!query)
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return query;
}
int
db_query_start(struct query_params *qp)
{
struct query_clause *qc;
sqlite3_stmt *stmt;
char *query;
int ret;
qp->stmt = NULL;
qp->results = -1;
qc = db_build_query_clause(qp);
if (!qc)
return -1;
switch (qp->type)
{
case Q_ITEMS:
query = db_build_query_items(qp, qc);
break;
case Q_PL:
query = db_build_query_pls(qp, qc);
break;
case Q_FIND_PL:
query = db_build_query_find_pls(qp, qc);
break;
case Q_PLITEMS:
query = db_build_query_plitems(qp, qc);
break;
case Q_GROUP_ALBUMS:
query = db_build_query_group_albums(qp, qc);
break;
case Q_GROUP_ARTISTS:
query = db_build_query_group_artists(qp, qc);
break;
case Q_GROUP_ITEMS:
query = db_build_query_group_items(qp, qc);
break;
case Q_GROUP_DIRS:
query = db_build_query_group_dirs(qp, qc);
break;
case Q_COUNT_ITEMS:
query = db_build_query_count_items(qp, qc);
break;
default:
if (qp->type & Q_F_BROWSE)
query = db_build_query_browse(qp, qc);
else
query = NULL;
}
db_free_query_clause(qc);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Could not create query, unknown type %d\n", qp->type);
return -1;
}
DPRINTF(E_DBG, L_DB, "Starting query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
qp->stmt = stmt;
return 0;
}
void
db_query_end(struct query_params *qp)
{
if (!qp->stmt)
return;
sqlite3_finalize(qp->stmt);
qp->stmt = NULL;
}
/*
* Utility function for running write queries (INSERT, UPDATE, DELETE). If you
* set free to non-zero, the function will free the query. If you set
* library_update to non-zero it means that the update was not just of some
* internal value (like a timestamp), but of something that requires clients
* to update their cache of the library (and of course also of our own cache).
*/
static int
db_query_run(char *query, int free, short update_events)
{
char *errmsg;
int changes = 0;
int ret;
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
/* If the query will be long running we don't want the cache to start regenerating */
cache_daap_suspend();
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
DPRINTF(E_LOG, L_DB, "Error '%s' while runnning '%s'\n", errmsg, query);
else
changes = sqlite3_changes(hdl);
sqlite3_free(errmsg);
if (free)
sqlite3_free(query);
cache_daap_resume();
if (update_events && changes > 0)
library_update_trigger(update_events);
return ((ret != SQLITE_OK) ? -1 : 0);
}
static int
db_query_fetch(void *item, struct query_params *qp, const ssize_t cols_map[], int size)
{
int ncols;
char **strcol;
int i;
int ret;
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Query not started!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
return 1;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ncols = sqlite3_column_count(qp->stmt);
// We allow more cols in db than in map because the db may be a future schema
if (ncols < size)
{
DPRINTF(E_LOG, L_DB, "BUG: database has fewer columns (%d) than column map (%u)\n", ncols, size);
return -1;
}
for (i = 0; i < size; i++)
{
strcol = (char **) ((char *)item + cols_map[i]);
*strcol = (char *)sqlite3_column_text(qp->stmt, i);
}
return 0;
}
int
db_query_fetch_file(struct db_media_file_info *dbmfi, struct query_params *qp)
{
int ret;
memset(dbmfi, 0, sizeof(struct db_media_file_info));
if ((qp->type != Q_ITEMS) && (qp->type != Q_PLITEMS) && (qp->type != Q_GROUP_ITEMS))
{
DPRINTF(E_LOG, L_DB, "Not an items, playlist or group items query!\n");
return -1;
}
ret = db_query_fetch(dbmfi, qp, dbmfi_cols_map, ARRAY_SIZE(dbmfi_cols_map));
if (ret < 0) {
DPRINTF(E_LOG, L_DB, "Failed to fetch db_media_file_info\n");
}
return ret;
}
int
db_query_fetch_pl(struct db_playlist_info *dbpli, struct query_params *qp)
{
int ncols;
char **strcol;
uint32_t nitems;
uint32_t nstreams;
int type;
int i;
int ret;
memset(dbpli, 0, sizeof(struct db_playlist_info));
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Query not started!\n");
return -1;
}
if ((qp->type != Q_PL) && (qp->type != Q_FIND_PL))
{
DPRINTF(E_LOG, L_DB, "Not a playlist query!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
dbpli->id = NULL;
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ncols = sqlite3_column_count(qp->stmt);
// We allow more cols in db than in map because the db may be a future schema
if (ncols < ARRAY_SIZE(dbpli_cols_map))
{
DPRINTF(E_LOG, L_DB, "BUG: database has fewer columns (%d) than dbpli column map (%u)\n", ncols, ARRAY_SIZE(dbpli_cols_map));
return -1;
}
for (i = 0; i < ARRAY_SIZE(dbpli_cols_map); i++)
{
strcol = (char **) ((char *)dbpli + dbpli_cols_map[i]);
*strcol = (char *)sqlite3_column_text(qp->stmt, i);
}
type = sqlite3_column_int(qp->stmt, 2);
if (type == PL_SPECIAL || type == PL_SMART)
{
db_files_get_count(&nitems, &nstreams, dbpli->query);
snprintf(qp->buf1, sizeof(qp->buf1), "%d", (int)nitems);
snprintf(qp->buf2, sizeof(qp->buf2), "%d", (int)nstreams);
dbpli->items = qp->buf1;
dbpli->streams = qp->buf2;
}
return 0;
}
int
db_query_fetch_group(struct db_group_info *dbgri, struct query_params *qp)
{
int ret;
memset(dbgri, 0, sizeof(struct db_group_info));
if ((qp->type != Q_GROUP_ALBUMS) && (qp->type != Q_GROUP_ARTISTS))
{
DPRINTF(E_LOG, L_DB, "Not a groups query!\n");
return -1;
}
ret = db_query_fetch(dbgri, qp, dbgri_cols_map, ARRAY_SIZE(dbgri_cols_map));
if (ret < 0) {
DPRINTF(E_LOG, L_DB, "Failed to fetch db_group_info\n");
}
return ret;
}
int
db_query_fetch_browse(struct db_browse_info *dbbi, struct query_params *qp)
{
int ret;
memset(dbbi, 0, sizeof(struct db_browse_info));
if (!(qp->type & Q_F_BROWSE))
{
DPRINTF(E_LOG, L_DB, "Not a browse query!\n");
return -1;
}
ret = db_query_fetch(dbbi, qp, dbbi_cols_map, ARRAY_SIZE(dbbi_cols_map));
if (ret < 0) {
DPRINTF(E_LOG, L_DB, "Failed to fetch db_browse_info\n");
}
return ret;
}
int
db_query_fetch_count(struct filecount_info *fci, struct query_params *qp)
{
int ret;
memset(fci, 0, sizeof(struct filecount_info));
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Query not started!\n");
return -1;
}
if (qp->type != Q_COUNT_ITEMS)
{
DPRINTF(E_LOG, L_DB, "Not a count query!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results for count query\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
fci->count = sqlite3_column_int(qp->stmt, 0);
fci->length = sqlite3_column_int64(qp->stmt, 1);
fci->artist_count = sqlite3_column_int(qp->stmt, 2);
fci->album_count = sqlite3_column_int(qp->stmt, 3);
fci->file_size = sqlite3_column_int64(qp->stmt, 4);
return 0;
}
int
db_filecount_get(struct filecount_info *fci, struct query_params *qp)
{
int ret;
ret = db_query_start(qp);
if (ret < 0)
{
db_query_end(qp);
return -1;
}
ret = db_query_fetch_count(fci, qp);
if (ret < 0)
{
db_query_end(qp);
return -1;
}
db_query_end(qp);
return 0;
}
int
db_query_fetch_string(char **string, struct query_params *qp)
{
int ret;
*string = NULL;
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Query not started!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
*string = NULL;
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
*string = (char *)sqlite3_column_text(qp->stmt, 0);
return 0;
}
int
db_query_fetch_string_sort(char **string, char **sortstring, struct query_params *qp)
{
int ret;
*string = NULL;
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Query not started!\n");
return -1;
}
if (!(qp->type & Q_F_BROWSE))
{
DPRINTF(E_LOG, L_DB, "Not a browse query!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
*string = NULL;
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
*string = (char *)sqlite3_column_text(qp->stmt, 0);
*sortstring = (char *)sqlite3_column_text(qp->stmt, 1);
return 0;
}
/* Files */
int
db_files_get_count(uint32_t *nitems, uint32_t *nstreams, const char *filter)
{
sqlite3_stmt *stmt = NULL;
char *query = NULL;
int ret;
if (!filter && !nstreams)
query = sqlite3_mprintf("SELECT COUNT(*) FROM files f WHERE f.disabled = 0;");
else if (!filter)
query = sqlite3_mprintf("SELECT COUNT(*), SUM(data_kind = %d) FROM files f WHERE f.disabled = 0;", DATA_KIND_HTTP);
else if (!nstreams)
query = sqlite3_mprintf("SELECT COUNT(*) FROM files f WHERE f.disabled = 0 AND %s;", filter);
else
query = sqlite3_mprintf("SELECT COUNT(*), SUM(data_kind = %d) FROM files f WHERE f.disabled = 0 AND %s;", DATA_KIND_HTTP, filter);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
goto error;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
goto error;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_INFO, L_DB, "No matching row found for query: %s\n", query);
else
DPRINTF(E_LOG, L_DB, "Could not step: %s (%s)\n", sqlite3_errmsg(hdl), query);
goto error;
}
if (nitems)
*nitems = sqlite3_column_int(stmt, 0);
if (nstreams)
*nstreams = sqlite3_column_int(stmt, 1);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_free(query);
sqlite3_finalize(stmt);
return 0;
error:
if (query)
sqlite3_free(query);
if (stmt)
sqlite3_finalize(stmt);
return -1;
}
static void
db_file_inc_playcount_byfilter(const char *filter)
{
#define Q_TMPL "UPDATE files SET play_count = play_count + 1, time_played = %" PRIi64 ", seek = 0 WHERE %s;"
/*
* Rating calculation is taken from from the beets plugin "mpdstats" (see https://beets.readthedocs.io/en/latest/plugins/mpdstats.html)
* and adapted to this servers rating rage (0 to 100).
*
* Rating consist of the stable rating and a rolling rating.
* The stable rating is calculated based on the number was played and skipped:
* stable rating = (play_count + 1.0) / (play_count + skip_count + 2.0) * 100
* The rolling rating is calculated based on the current action (played or skipped):
* rolling rating for played = rating + ((100.0 - rating) / 2.0)
* rolling rating for skipped = rating - (rating / 2.0)
*
* The new rating is a mix of stable and rolling rating (factor 0.75):
* new rating = stable rating * 0.75 + rolling rating * 0.25
*/
#define Q_TMPL_WITH_RATING \
"UPDATE files "\
" SET play_count = play_count + 1, time_played = %" PRIi64 ", seek = 0, "\
" rating = CAST(((play_count + 1.0) / (play_count + skip_count + 2.0) * 100 * 0.75) + ((rating + ((100.0 - rating) / 2.0)) * 0.25) AS INT)" \
" WHERE %s;"
char *query;
int ret;
if (db_rating_updates)
query = sqlite3_mprintf(Q_TMPL_WITH_RATING, (int64_t)time(NULL), filter);
else
query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), filter);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_query_run(query, 1, 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
#undef Q_TMPL_WITH_RATING
}
void
db_file_inc_playcount_byplid(int id, bool only_unplayed)
{
char *filter;
filter = sqlite3_mprintf("path IN (SELECT filepath FROM playlistitems WHERE playlistid = %d) %s",
id, only_unplayed ? "AND play_count = 0" : "");
db_file_inc_playcount_byfilter(filter);
sqlite3_free(filter);
}
void
db_file_inc_playcount_bysongalbumid(int64_t id, bool only_unplayed)
{
char *filter;
filter = sqlite3_mprintf("songalbumid = %" PRIi64 " %s",
id, only_unplayed ? "AND play_count = 0" : "");
db_file_inc_playcount_byfilter(filter);
sqlite3_free(filter);
}
void
db_file_inc_playcount(int id)
{
char *filter;
filter = sqlite3_mprintf("id = %d", id);
db_file_inc_playcount_byfilter(filter);
sqlite3_free(filter);
}
void
db_file_inc_skipcount(int id)
{
#define Q_TMPL "UPDATE files SET skip_count = skip_count + 1, time_skipped = %" PRIi64 " WHERE id = %d;"
// see db_file_inc_playcount for a description of how the rating is calculated
#define Q_TMPL_WITH_RATING \
"UPDATE files "\
" SET skip_count = skip_count + 1, time_skipped = %" PRIi64 ", seek = 0, "\
" rating = CAST(((play_count + 1.0) / (play_count + skip_count + 2.0) * 100 * 0.75) + ((rating - (rating / 2.0)) * 0.25) AS INT)" \
" WHERE id = %d;"
char *query;
int ret;
if (db_rating_updates)
query = sqlite3_mprintf(Q_TMPL_WITH_RATING, (int64_t)time(NULL), id);
else
query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_query_run(query, 1, 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
#undef Q_TMPL_WITH_RATING
}
void
db_file_reset_playskip_count(int id)
{
#define Q_TMPL "UPDATE files SET play_count = 0, skip_count = 0, time_played = 0, time_skipped = 0 WHERE id = %d;"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_query_run(query, 1, 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
}
void
db_file_ping(int id)
{
#define Q_TMPL "UPDATE files SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_file_ping_bypath(const char *path, time_t mtime_max)
{
sqlite3_bind_int64(db_statements.files_ping, 1, (int64_t)time(NULL));
sqlite3_bind_text(db_statements.files_ping, 2, path, -1, SQLITE_STATIC);
sqlite3_bind_int64(db_statements.files_ping, 3, (int64_t)mtime_max);
return db_statement_run(db_statements.files_ping, 0);
}
void
db_file_ping_bymatch(const char *path, int isdir)
{
#define Q_TMPL_DIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';"
#define Q_TMPL_NODIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';"
char *query;
if (isdir)
query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path);
else
query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path);
db_query_run(query, 1, 0);
#undef Q_TMPL_DIR
#undef Q_TMPL_NODIR
}
char *
db_file_path_byid(int id)
{
#define Q_TMPL "SELECT f.path FROM files f WHERE f.id = %d;"
char *query;
sqlite3_stmt *stmt;
char *res;
int ret;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return NULL;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
sqlite3_free(query);
return NULL;
}
res = (char *)sqlite3_column_text(stmt, 0);
if (res)
res = strdup(res);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return res;
#undef Q_TMPL
}
static int
db_file_id_byquery(const char *query)
{
sqlite3_stmt *stmt;
int ret;
if (!query)
return 0;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return 0;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
return 0;
}
ret = sqlite3_column_int(stmt, 0);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
return ret;
}
int
db_file_id_bypath(const char *path)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.path = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_file_id_byfile(const char *filename)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.fname = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, filename);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_file_id_byurl(const char *url)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.url = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, url);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_file_id_by_virtualpath_match(const char *path)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path LIKE '%%%q%%';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
static struct media_file_info *
db_file_fetch_byquery(char *query)
{
struct media_file_info *mfi;
sqlite3_stmt *stmt;
int ncols;
int i;
int ret;
if (!query)
return NULL;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
mfi = calloc(1, sizeof(struct media_file_info));
if (!mfi)
{
DPRINTF(E_LOG, L_DB, "Could not allocate struct media_file_info, out of memory\n");
return NULL;
}
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
free(mfi);
return NULL;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
free(mfi);
return NULL;
}
ncols = sqlite3_column_count(stmt);
// We allow more cols in db than in map because the db may be a future schema
if (ncols < ARRAY_SIZE(mfi_cols_map))
{
DPRINTF(E_LOG, L_DB, "BUG: database has fewer columns (%d) than mfi column map (%u)\n", ncols, ARRAY_SIZE(mfi_cols_map));
sqlite3_finalize(stmt);
free(mfi);
return NULL;
}
for (i = 0; i < ARRAY_SIZE(mfi_cols_map); i++)
{
struct_field_from_statement(mfi, mfi_cols_map[i].offset, mfi_cols_map[i].type, stmt, i, true, false);
}
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
return mfi;
}
struct media_file_info *
db_file_fetch_byid(int id)
{
#define Q_TMPL "SELECT f.* FROM files f WHERE f.id = %d;"
struct media_file_info *mfi;
char *query;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
mfi = db_file_fetch_byquery(query);
sqlite3_free(query);
return mfi;
#undef Q_TMPL
}
struct media_file_info *
db_file_fetch_byvirtualpath(const char *virtual_path)
{
#define Q_TMPL "SELECT f.* FROM files f WHERE f.virtual_path = %Q;"
struct media_file_info *mfi;
char *query;
query = sqlite3_mprintf(Q_TMPL, virtual_path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
mfi = db_file_fetch_byquery(query);
sqlite3_free(query);
return mfi;
#undef Q_TMPL
}
int
db_file_add(struct media_file_info *mfi)
{
int ret;
if (mfi->id != 0)
{
DPRINTF(E_WARN, L_DB, "Trying to add file with non-zero id; use db_file_update()?\n");
return -1;
}
mfi->db_timestamp = (uint64_t)time(NULL);
// We don't do this in fixup_tags_mfi() to avoid affecting db_file_update()
if (mfi->time_added == 0)
mfi->time_added = mfi->db_timestamp;
fixup_tags_mfi(mfi);
ret = bind_mfi(db_statements.files_insert, mfi);
if (ret < 0)
return -1;
ret = db_statement_run(db_statements.files_insert, 0);
if (ret < 0)
return -1;
library_update_trigger(LISTENER_DATABASE);
return 0;
}
int
db_file_update(struct media_file_info *mfi)
{
int ret;
if (mfi->id == 0)
{
DPRINTF(E_WARN, L_DB, "Trying to update file with id 0; use db_file_add()?\n");
return -1;
}
mfi->db_timestamp = (uint64_t)time(NULL);
fixup_tags_mfi(mfi);
ret = bind_mfi(db_statements.files_update, mfi);
if (ret < 0)
return -1;
ret = db_statement_run(db_statements.files_update, 0);
if (ret < 0)
return -1;
library_update_trigger(LISTENER_DATABASE);
return 0;
}
void
db_file_seek_update(int id, uint32_t seek)
{
#define Q_TMPL "UPDATE files SET seek = %d WHERE id = %d;"
char *query;
int ret;
if (id == 0)
return;
query = sqlite3_mprintf(Q_TMPL, seek, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_query_run(query, 1, 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
}
static int
db_file_rating_update(char *query)
{
int ret;
ret = db_query_run(query, 1, 0);
if (ret == 0)
{
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
listener_notify(LISTENER_RATING);
}
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
}
int
db_file_rating_update_byid(uint32_t id, uint32_t rating)
{
#define Q_TMPL "UPDATE files SET rating = %d WHERE id = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL, rating, id);
return db_file_rating_update(query);
#undef Q_TMPL
}
int
db_file_rating_update_byvirtualpath(const char *virtual_path, uint32_t rating)
{
#define Q_TMPL "UPDATE files SET rating = %d WHERE virtual_path = %Q;"
char *query;
query = sqlite3_mprintf(Q_TMPL, rating, virtual_path);
return db_file_rating_update(query);
#undef Q_TMPL
}
int
db_file_usermark_update_byid(uint32_t id, uint32_t usermark)
{
#define Q_TMPL "UPDATE files SET usermark = %d WHERE id = %d;"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, usermark, id);
ret = db_query_run(query, 1, 0);
if (ret == 0)
{
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
listener_notify(LISTENER_UPDATE);
}
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}
void
db_file_delete_bypath(const char *path)
{
#define Q_TMPL "DELETE FROM files WHERE path = '%q';"
char *query;
query = sqlite3_mprintf(Q_TMPL, path);
db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
void
db_file_disable_bypath(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE files SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path = '%q';"
char *query;
int64_t disabled;
int path_striplen;
int vpath_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
vpath_striplen = (strip == STRIP_PATH) ? strlen("/file:") + path_striplen : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, vpath_striplen + 1, disabled, path);
db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
void
db_file_disable_bymatch(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE files SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path LIKE '%q/%%';"
char *query;
int64_t disabled;
int path_striplen;
int vpath_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
vpath_striplen = (strip == STRIP_PATH) ? strlen("/file:") + path_striplen : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, vpath_striplen + 1, disabled, path);
db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
// "path" will be the directory part for directory updates (dir moved) and the
// full path for file updates (file moved). The db will have the filename in
// the path field for the former case (with a "/" prefix), and an empty path
// field for the latter.
int
db_file_enable_bycookie(uint32_t cookie, const char *path, const char *filename)
{
#define Q_TMPL_UPDATE_FNAME "UPDATE files SET path = ('%q' || path), virtual_path = ('/file:%q' || virtual_path), fname = '%q', disabled = 0 WHERE disabled = %" PRIi64 ";"
#define Q_TMPL "UPDATE files SET path = ('%q' || path), virtual_path = ('/file:%q' || virtual_path), disabled = 0 WHERE disabled = %" PRIi64 ";"
char *query;
int ret;
if (filename)
query = sqlite3_mprintf(Q_TMPL_UPDATE_FNAME, path, path, filename, (int64_t)cookie);
else
query = sqlite3_mprintf(Q_TMPL, path, path, (int64_t)cookie);
ret = db_query_run(query, 1, LISTENER_DATABASE);
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL_UPDATE_FNAME
#undef Q_TMPL
}
int
db_file_update_directoryid(const char *path, int dir_id)
{
#define Q_TMPL "UPDATE files SET directory_id = %d WHERE path = %Q;"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, dir_id, path);
ret = db_query_run(query, 1, 0);
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}
/* Playlists */
int
db_pl_get_count(uint32_t *nitems)
{
int ret = db_get_one_int("SELECT COUNT(*) FROM playlists p WHERE p.disabled = 0;");
if (ret < 0)
return -1;
*nitems = (uint32_t)ret;
return 0;
}
void
db_pl_ping(int id)
{
#define Q_TMPL "UPDATE playlists SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
void
db_pl_ping_bymatch(const char *path, int isdir)
{
#define Q_TMPL_DIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';"
#define Q_TMPL_NODIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';"
char *query;
if (isdir)
query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path);
else
query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path);
db_query_run(query, 1, 0);
#undef Q_TMPL_DIR
#undef Q_TMPL_NODIR
}
void
db_pl_ping_items_bymatch(const char *path, int id)
{
#define Q_TMPL "UPDATE files SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE path IN (SELECT filepath FROM playlistitems WHERE filepath LIKE '%q%%' AND playlistid = %d);"
char *query;
query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), path, id);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_pl_id_bypath(const char *path)
{
#define Q_TMPL "SELECT p.id FROM playlists p WHERE p.path = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
ret = db_get_one_int(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
static struct playlist_info *
db_pl_fetch_byquery(const char *query)
{
struct playlist_info *pli;
sqlite3_stmt *stmt;
int ncols;
int i;
int ret;
if (!query)
return NULL;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
pli = calloc(1, sizeof(struct playlist_info));
if (!pli)
{
DPRINTF(E_LOG, L_DB, "Could not allocate struct playlist_info, out of memory\n");
return NULL;
}
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
free(pli);
return NULL;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
free(pli);
return NULL;
}
ncols = sqlite3_column_count(stmt);
if (ncols < ARRAY_SIZE(pli_cols_map))
{
DPRINTF(E_LOG, L_DB, "BUG: database has fewer columns (%d) than pli column map (%u)\n", ncols, ARRAY_SIZE(pli_cols_map));
sqlite3_finalize(stmt);
free(pli);
return NULL;
}
for (i = 0; i < ARRAY_SIZE(pli_cols_map); i++)
{
struct_field_from_statement(pli, pli_cols_map[i].offset, pli_cols_map[i].type, stmt, i, true, false);
}
ret = db_blocking_step(stmt);
sqlite3_finalize(stmt);
if (ret != SQLITE_DONE)
{
DPRINTF(E_WARN, L_DB, "Query had more than a single result!\n");
free_pli(pli, 0);
return NULL;
}
if (pli->type == PL_SPECIAL || pli->type == PL_SMART)
db_files_get_count(&pli->items, &pli->streams, pli->query);
return pli;
}
struct playlist_info *
db_pl_fetch_bypath(const char *path)
{
struct playlist_info *pli;
char *query;
query = sqlite3_mprintf(Q_PL_SELECT " WHERE f.path = '%q' GROUP BY f.id;", path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
pli = db_pl_fetch_byquery(query);
sqlite3_free(query);
return pli;
}
struct playlist_info *
db_pl_fetch_byvirtualpath(const char *virtual_path)
{
struct playlist_info *pli;
char *query;
query = sqlite3_mprintf(Q_PL_SELECT " WHERE f.virtual_path = '%q' GROUP BY f.id;", virtual_path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
pli = db_pl_fetch_byquery(query);
sqlite3_free(query);
return pli;
}
struct playlist_info *
db_pl_fetch_byid(int id)
{
struct playlist_info *pli;
char *query;
query = sqlite3_mprintf(Q_PL_SELECT " WHERE f.id = %d GROUP BY f.id;", id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
pli = db_pl_fetch_byquery(query);
sqlite3_free(query);
return pli;
}
struct playlist_info *
db_pl_fetch_bytitlepath(const char *title, const char *path)
{
struct playlist_info *pli;
char *query;
query = sqlite3_mprintf(Q_PL_SELECT " WHERE f.title = '%q' AND f.path = '%q' GROUP BY f.id;", title, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
pli = db_pl_fetch_byquery(query);
sqlite3_free(query);
return pli;
}
int
db_pl_add(struct playlist_info *pli)
{
int ret;
// If the backend sets 1 it must be preserved, because the backend is still
// scanning and is going to update it later (see filescanner_playlist.c)
if (pli->db_timestamp != 1)
pli->db_timestamp = (uint64_t)time(NULL);
fixup_tags_pli(pli);
ret = bind_pli(db_statements.playlists_insert, pli);
if (ret < 0)
return -1;
ret = db_statement_run(db_statements.playlists_insert, 0);
if (ret < 0)
return -1;
ret = (int)sqlite3_last_insert_rowid(hdl);
if (ret == 0)
{
DPRINTF(E_LOG, L_DB, "Successful playlist insert but no last_insert_rowid!\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Added playlist %s (path %s) as id %d\n", pli->title, pli->path, ret);
return ret;
}
int
db_pl_update(struct playlist_info *pli)
{
int ret;
// If the backend sets 1 it must be preserved, because the backend is still
// scanning and is going to update it later (see filescanner_playlist.c)
if (pli->db_timestamp != 1)
pli->db_timestamp = (uint64_t)time(NULL);
fixup_tags_pli(pli);
ret = bind_pli(db_statements.playlists_update, pli);
if (ret < 0)
return -1;
ret = db_statement_run(db_statements.playlists_update, LISTENER_DATABASE);
if (ret < 0)
return -1;
return pli->id;
}
int
db_pl_add_item_bypath(int plid, const char *path)
{
#define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, '%q');"
char *query;
query = sqlite3_mprintf(Q_TMPL, plid, path);
return db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
int
db_pl_add_item_byid(int plid, int fileid)
{
#define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, (SELECT f.path FROM files f WHERE f.id = %d));"
char *query;
query = sqlite3_mprintf(Q_TMPL, plid, fileid);
return db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
void
db_pl_clear_items(int id)
{
#define Q_TMPL_ITEMS "DELETE FROM playlistitems WHERE playlistid = %d;"
#define Q_TMPL_NESTED "UPDATE playlists SET parent_id = 0 WHERE parent_id = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL_ITEMS, id);
db_query_run(query, 1, 0);
query = sqlite3_mprintf(Q_TMPL_NESTED, id);
db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL_NESTED
#undef Q_TMPL_ITEMS
}
void
db_pl_delete(int id)
{
#define Q_TMPL "DELETE FROM playlists WHERE id = %d;"
#define Q_ORPHAN "SELECT filepath FROM playlistitems WHERE filepath NOT IN (SELECT filepath FROM playlistitems WHERE playlistid <> %d) AND playlistid = %d"
#define Q_FILES "DELETE FROM files WHERE data_kind = %d AND path IN (" Q_ORPHAN ");"
char *query;
int ret;
if (id == 1)
return;
db_transaction_begin();
query = sqlite3_mprintf(Q_TMPL, id);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret < 0)
{
db_transaction_rollback();
return;
}
// Remove orphaned files (http items in files must have been added by the
// playlist. The GROUP BY/count makes sure the files are not referenced by any
// other playlist.
// TODO find a cleaner way of identifying tracks added by a playlist
query = sqlite3_mprintf(Q_FILES, DATA_KIND_HTTP, id, id);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret < 0)
{
db_transaction_rollback();
return;
}
// Clear playlistitems
db_pl_clear_items(id);
db_transaction_end();
#undef Q_FILES
#undef Q_ORPHAN
#undef Q_TMPL
}
void
db_pl_delete_bypath(const char *path)
{
struct query_params qp;
struct db_playlist_info dbpli;
int32_t id;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_PL;
qp.with_disabled = 1;
CHECK_NULL(L_DB, qp.filter = db_mprintf("path = '%q'", path));
ret = db_query_start(&qp);
if (ret < 0)
{
free(qp.filter);
return;
}
while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id))
{
if (safe_atoi32(dbpli.id, &id) != 0)
continue;
db_pl_delete(id);
}
db_query_end(&qp);
free(qp.filter);
}
void
db_pl_disable_bypath(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE playlists SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path = '%q';"
char *query;
int64_t disabled;
int path_striplen;
int vpath_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
vpath_striplen = (strip == STRIP_PATH) ? strlen("/file:") + path_striplen : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, vpath_striplen + 1, disabled, path);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
void
db_pl_disable_bymatch(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE playlists SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path LIKE '%q/%%';"
char *query;
int64_t disabled;
int path_striplen;
int vpath_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
vpath_striplen = (strip == STRIP_PATH) ? strlen("/file:") + path_striplen : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, vpath_striplen + 1, disabled, path);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_pl_enable_bycookie(uint32_t cookie, const char *path)
{
#define Q_TMPL "UPDATE playlists SET path = ('%q' || path), virtual_path = ('/file:%q' || virtual_path), disabled = 0 WHERE disabled = %" PRIi64 ";"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path, path, (int64_t)cookie);
ret = db_query_run(query, 1, 0);
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}
/* Groups */
// Remove album and artist entries in the groups table that are not longer referenced from the files table
int
db_groups_cleanup()
{
#define Q_TMPL_ALBUM "DELETE FROM groups WHERE type = 1 AND NOT persistentid IN (SELECT songalbumid from files WHERE disabled = 0);"
#define Q_TMPL_ARTIST "DELETE FROM groups WHERE type = 2 AND NOT persistentid IN (SELECT songartistid from files WHERE disabled = 0);"
int ret;
db_transaction_begin();
ret = db_query_run(Q_TMPL_ALBUM, 0, LISTENER_DATABASE);
if (ret < 0)
{
db_transaction_rollback();
return -1;
}
DPRINTF(E_DBG, L_DB, "Removed album group-entries: %d\n", sqlite3_changes(hdl));
ret = db_query_run(Q_TMPL_ARTIST, 0, LISTENER_DATABASE);
if (ret < 0)
{
db_transaction_rollback();
return -1;
}
DPRINTF(E_DBG, L_DB, "Removed artist group-entries: %d\n", sqlite3_changes(hdl));
db_transaction_end();
return 0;
#undef Q_TMPL_ALBUM
#undef Q_TMPL_ARTIST
}
static enum group_type
db_group_type_bypersistentid(int64_t persistentid)
{
#define Q_TMPL "SELECT g.type FROM groups g WHERE g.persistentid = %" PRIi64 ";"
char *query;
sqlite3_stmt *stmt;
int ret;
query = sqlite3_mprintf(Q_TMPL, persistentid);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return 0;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
}
ret = sqlite3_column_int(stmt, 0);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_group_persistentid_byid(int id, int64_t *persistentid)
{
#define Q_TMPL "SELECT g.persistentid FROM groups g WHERE g.id = %d;"
char *query;
sqlite3_stmt *stmt;
int ret;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
}
*persistentid = sqlite3_column_int64(stmt, 0);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
/* Directories */
int
db_directory_id_byvirtualpath(const char *virtual_path)
{
#define Q_TMPL "SELECT d.id FROM directories d WHERE d.virtual_path = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, virtual_path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_directory_id_bypath(const char *path)
{
#define Q_TMPL "SELECT d.id FROM directories d WHERE d.path = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_directory_enum_start(struct directory_enum *de)
{
#define Q_TMPL "SELECT * FROM directories WHERE disabled = 0 AND parent_id = %d ORDER BY virtual_path COLLATE NOCASE;"
sqlite3_stmt *stmt;
char *query;
int ret;
de->stmt = NULL;
query = sqlite3_mprintf(Q_TMPL, de->parent_id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
de->stmt = stmt;
return 0;
#undef Q_TMPL
}
int
db_directory_enum_fetch(struct directory_enum *de, struct directory_info *di)
{
int ret;
memset(di, 0, sizeof(struct directory_info));
if (!de->stmt)
{
DPRINTF(E_LOG, L_DB, "Directory enum not started!\n");
return -1;
}
ret = db_blocking_step(de->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of directory enum results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
di->id = sqlite3_column_int(de->stmt, 0);
di->virtual_path = (char *)sqlite3_column_text(de->stmt, 1);
di->db_timestamp = sqlite3_column_int(de->stmt, 2);
di->disabled = sqlite3_column_int64(de->stmt, 3);
di->parent_id = sqlite3_column_int(de->stmt, 4);
di->path = (char *)sqlite3_column_text(de->stmt, 5);
di->scan_kind = sqlite3_column_int(de->stmt, 6);
return 0;
}
void
db_directory_enum_end(struct directory_enum *de)
{
if (!de->stmt)
return;
sqlite3_finalize(de->stmt);
de->stmt = NULL;
}
int
db_directory_add(struct directory_info *di, int *id)
{
#define QADD_TMPL "INSERT INTO directories (virtual_path, db_timestamp, disabled, parent_id, path, scan_kind)" \
" VALUES (TRIM(%Q), %d, %" PRIi64 ", %d, TRIM(%Q), %d);"
char *query;
char *errmsg;
int vp_len = strlen(di->virtual_path);
int ret;
if (vp_len && di->virtual_path[vp_len-1] == ' ')
{
/* Since sqlite removes the trailing space, so these
* directories will be found as new in perpetuity.
*/
DPRINTF(E_LOG, L_DB, "Directory name ends with space: '%s'\n", di->virtual_path);
}
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->path, di->scan_kind);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
sqlite3_free(errmsg);
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
*id = (int)sqlite3_last_insert_rowid(hdl);
if (*id == 0)
{
DPRINTF(E_LOG, L_DB, "Successful insert but no last_insert_rowid!\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Added directory '%s' with id %d\n", di->virtual_path, *id);
return 0;
#undef QADD_TMPL
}
int
db_directory_update(struct directory_info *di)
{
#define QADD_TMPL "UPDATE directories SET virtual_path = TRIM(%Q), db_timestamp = %d, disabled = %" PRIi64 ", parent_id = %d, path = TRIM(%Q), scan_kind = %d" \
" WHERE id = %d;"
char *query;
char *errmsg;
int ret;
/* Add */
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->path, di->scan_kind, di->id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_exec(query, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
sqlite3_free(errmsg);
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
DPRINTF(E_DBG, L_DB, "Updated directory '%s' with id %d\n", di->virtual_path, di->id);
return 0;
#undef QADD_TMPL
}
void
db_directory_ping_bymatch(char *virtual_path)
{
#define Q_TMPL_DIR "UPDATE directories SET db_timestamp = %" PRIi64 " WHERE virtual_path = '%q' OR virtual_path LIKE '%q/%%';"
char *query;
query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), virtual_path, virtual_path);
db_query_run(query, 1, 0);
#undef Q_TMPL_DIR
}
void
db_directory_disable_bymatch(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE directories SET virtual_path = substr(virtual_path, %d)," \
" disabled = %" PRIi64 " WHERE virtual_path = '/file:%q' OR virtual_path LIKE '/file:%q/%%';"
char *query;
int64_t disabled;
int vpath_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
vpath_striplen = (strip == STRIP_PATH) ? strlen("/file:") + strlen(path) : 0;
query = sqlite3_mprintf(Q_TMPL, vpath_striplen + 1, disabled, path, path, path);
db_query_run(query, 1, LISTENER_DATABASE);
#undef Q_TMPL
}
int
db_directory_enable_bycookie(uint32_t cookie, const char *path)
{
#define Q_TMPL "UPDATE directories SET virtual_path = ('/file:%q' || virtual_path)," \
" disabled = 0 WHERE disabled = %" PRIi64 ";"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie);
ret = db_query_run(query, 1, LISTENER_DATABASE);
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}
int
db_directory_enable_bypath(char *path)
{
#define Q_TMPL "UPDATE directories SET disabled = 0 WHERE virtual_path = %Q AND disabled <> 0;"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
ret = db_query_run(query, 1, LISTENER_DATABASE);
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}
/* Remotes */
static int
db_pairing_delete_byremote(char *remote_id)
{
#define Q_TMPL "DELETE FROM pairings WHERE remote = '%q';"
char *query;
query = sqlite3_mprintf(Q_TMPL, remote_id);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_pairing_add(struct pairing_info *pi)
{
#define Q_TMPL "INSERT INTO pairings (remote, name, guid) VALUES ('%q', '%q', '%q');"
char *query;
int ret;
ret = db_pairing_delete_byremote(pi->remote_id);
if (ret < 0)
return ret;
query = sqlite3_mprintf(Q_TMPL, pi->remote_id, pi->name, pi->guid);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_pairing_fetch_byguid(struct pairing_info *pi)
{
#define Q_TMPL "SELECT p.* FROM pairings p WHERE p.guid = '%q';"
char *query;
sqlite3_stmt *stmt;
int ret;
query = sqlite3_mprintf(Q_TMPL, pi->guid);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_INFO, L_DB, "Pairing GUID %s not found\n", pi->guid);
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
}
pi->remote_id = strdup((char *)sqlite3_column_text(stmt, 0));
pi->name = strdup((char *)sqlite3_column_text(stmt, 1));
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
/* Spotify */
void
db_spotify_purge(void)
{
#define Q_TMPL "UPDATE directories SET disabled = %" PRIi64 " WHERE virtual_path = '/spotify:' AND disabled <> %" PRIi64 ";"
char *queries[4] =
{
"DELETE FROM files WHERE path LIKE 'spotify:%%';",
"DELETE FROM playlistitems WHERE filepath LIKE 'spotify:%%';",
"DELETE FROM playlists WHERE path LIKE 'spotify:%%';",
"DELETE FROM directories WHERE virtual_path LIKE '/spotify:/%%';",
};
char *query;
int i;
int ret;
for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++)
{
ret = db_query_run(queries[i], 0, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Processed %d rows\n", sqlite3_changes(hdl));
}
// Disable the spotify directory by setting 'disabled' to INOTIFY_FAKE_COOKIE value
query = sqlite3_mprintf(Q_TMPL, INOTIFY_FAKE_COOKIE, INOTIFY_FAKE_COOKIE);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return;
}
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Disabled spotify directory\n");
#undef Q_TMPL
}
/* Spotify */
void
db_spotify_pl_delete(int id)
{
char *queries_tmpl[2] =
{
"DELETE FROM playlists WHERE id = %d;",
"DELETE FROM playlistitems WHERE playlistid = %d;",
};
char *query;
int i;
int ret;
for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++)
{
query = sqlite3_mprintf(queries_tmpl[i], id);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl));
}
}
/* Spotify */
void
db_spotify_files_delete(void)
{
#define Q_TMPL "DELETE FROM files WHERE path LIKE 'spotify:%%' AND NOT path IN (SELECT filepath FROM playlistitems);"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL);
ret = db_query_run(query, 1, LISTENER_DATABASE);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl));
#undef Q_TMPL
}
/* Admin */
int
db_admin_set(const char *key, const char *value)
{
#define Q_TMPL "INSERT OR REPLACE INTO admin (key, value) VALUES ('%q', '%q');"
char *query;
query = sqlite3_mprintf(Q_TMPL, key, value);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_admin_setint(const char *key, int value)
{
#define Q_TMPL "INSERT OR REPLACE INTO admin (key, value) VALUES ('%q', '%d');"
char *query;
query = sqlite3_mprintf(Q_TMPL, key, value);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_admin_setint64(const char *key, int64_t value)
{
#define Q_TMPL "INSERT OR REPLACE INTO admin (key, value) VALUES ('%q', '%" PRIi64 "');"
char *query;
query = sqlite3_mprintf(Q_TMPL, key, value);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
static int
admin_get(void *value, const char *key, short type)
{
#define Q_TMPL "SELECT value FROM admin a WHERE a.key = '%q';"
char *query;
sqlite3_stmt *stmt;
int ret;
CHECK_NULL(L_DB, query = sqlite3_mprintf(Q_TMPL, key));
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret == SQLITE_DONE)
DPRINTF(E_DBG, L_DB, "No results\n");
else
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
}
struct_field_from_statement(value, 0, type, stmt, 0, true, false);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
int
db_admin_get(char **value, const char *key)
{
return admin_get(value, key, DB_TYPE_STRING);
}
int
db_admin_getint(int *intval, const char *key)
{
return admin_get(intval, key, DB_TYPE_INT);
}
int
db_admin_getint64(int64_t *int64val, const char *key)
{
return admin_get(int64val, key, DB_TYPE_INT64);
}
int
db_admin_delete(const char *key)
{
#define Q_TMPL "DELETE FROM admin WHERE key='%q';"
char *query;
query = sqlite3_mprintf(Q_TMPL, key);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
/* Speakers */
int
db_speaker_save(struct output_device *device)
{
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key) VALUES (%" PRIi64 ", %d, %d, %Q, %Q);"
char *query;
query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_speaker_get(struct output_device *device, uint64_t id)
{
#define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key FROM speakers s WHERE s.id = %" PRIi64 ";"
sqlite3_stmt *stmt;
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
ret = -1;
goto out;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret != SQLITE_DONE)
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
ret = -1;
goto out;
}
device->id = id;
device->selected = sqlite3_column_int(stmt, 0);
device->volume = sqlite3_column_int(stmt, 1);
free(device->name);
device->name = safe_strdup((char *)sqlite3_column_text(stmt, 2));
free(device->auth_key);
device->auth_key = safe_strdup((char *)sqlite3_column_text(stmt, 3));
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
ret = 0;
out:
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
/* Queue */
/*
* Start a new transaction for modifying the queue. Returns the new queue version for the following changes.
* After finishing all queue modifications 'queue_transaction_end' needs to be called.
*/
static int
queue_transaction_begin()
{
int queue_version = 0;
db_transaction_begin();
db_admin_getint(&queue_version, DB_ADMIN_QUEUE_VERSION);
queue_version++;
return queue_version;
}
/*
* If retval == 0, updates the version of the queue in the admin table, commits the transaction
* and notifies listener of LISTENER_QUEUE about the changes.
* If retval < 0, rollsback the transaction.
*
* This function must be called after modifying the queue.
*
* @param retval 'retval' >= 0, if modifying the queue was successful or 'retval' < 0 if an error occurred
* @param queue_version The new queue version, for the pending modifications
*/
static void
queue_transaction_end(int retval, int queue_version)
{
int ret;
if (retval < 0)
goto error;
ret = db_admin_setint(DB_ADMIN_QUEUE_VERSION, queue_version);
if (ret < 0)
goto error;
db_transaction_end();
listener_notify(LISTENER_QUEUE);
return;
error:
db_transaction_rollback();
}
static int
queue_reshuffle(uint32_t item_id, int queue_version);
static int
queue_item_add(struct db_queue_item *qi)
{
int ret;
fixup_tags_qi(qi);
ret = bind_qi(db_statements.queue_items_insert, qi);
if (ret < 0)
return -1;
// Do not update events here, only caller knows if more items will be added
ret = db_statement_run(db_statements.queue_items_insert, 0);
if (ret < 0)
return -1;
ret = (int)sqlite3_last_insert_rowid(hdl);
if (ret == 0)
{
DPRINTF(E_LOG, L_DB, "Successful queue item insert but no last_insert_rowid!\n");
return -1;
}
return ret;
}
static int
queue_item_add_from_file(struct db_media_file_info *dbmfi, int pos, int shuffle_pos, int queue_version)
{
struct db_queue_item qi;
int ret;
// No allocations, just the struct content is copied
db_queue_item_from_dbmfi(&qi, dbmfi);
// No tag fixup, we can't touch the strings in qi, plus must assume dbmfi has proper tags
qi.pos = pos;
qi.shuffle_pos = shuffle_pos;
qi.queue_version = queue_version;
ret = bind_qi(db_statements.queue_items_insert, &qi);
if (ret < 0)
return -1;
// Do not update events here, only caller knows if more items will be added
ret = db_statement_run(db_statements.queue_items_insert, 0);
if (ret < 0)
return -1;
ret = (int)sqlite3_last_insert_rowid(hdl);
if (ret == 0)
{
DPRINTF(E_LOG, L_DB, "Successful queue item insert but no last_insert_rowid!\n");
return -1;
}
return ret;
}
static int
queue_item_update(struct db_queue_item *qi)
{
int ret;
fixup_tags_qi(qi);
ret = bind_qi(db_statements.queue_items_update, qi);
if (ret < 0)
return -1;
// Do not update events here, only caller knows if more items will be updated
ret = db_statement_run(db_statements.queue_items_update, 0);
if (ret < 0)
return -1;
return qi->id;
}
void
db_queue_item_from_mfi(struct db_queue_item *qi, struct media_file_info *mfi)
{
int i;
memset(qi, 0, sizeof(struct db_queue_item));
for (i = 0; i < ARRAY_SIZE(qi_mfi_map); i++)
{
if (qi_mfi_map[i].mfi_offset < 0)
continue;
struct_field_from_field(qi, qi_cols_map[i].offset, qi_cols_map[i].type, mfi, qi_mfi_map[i].mfi_offset, true, false);
}
if (!qi->file_id)
qi->file_id = DB_MEDIA_FILE_NON_PERSISTENT_ID;
}
void
db_queue_item_from_dbmfi(struct db_queue_item *qi, struct db_media_file_info *dbmfi)
{
int i;
memset(qi, 0, sizeof(struct db_queue_item));
for (i = 0; i < ARRAY_SIZE(qi_mfi_map); i++)
{
if (qi_mfi_map[i].dbmfi_offset < 0)
continue;
struct_field_from_field(qi, qi_cols_map[i].offset, qi_cols_map[i].type, dbmfi, qi_mfi_map[i].dbmfi_offset, false, true);
}
if (!qi->file_id)
qi->file_id = DB_MEDIA_FILE_NON_PERSISTENT_ID;
}
int
db_queue_item_update(struct db_queue_item *qi)
{
int queue_version;
int ret;
queue_version = queue_transaction_begin();
qi->queue_version = queue_version;
ret = queue_item_update(qi);
// MPD changes playlist version when metadata changes, also makes LISTENER_QUEUE
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Adds the files matching the given query to the queue after the item with the given item id
*
* The files table is queried with the given parameters and all found files are added after the
* item with the given item id to the "normal" queue. They are appended to end of the shuffled queue
* (assuming that the shuffled queue will get reshuffled after adding new items).
*
* The function returns -1 on failure (e. g. error reading from database) and if the given item id
* does not exist. It wraps all database access in a transaction and performs a rollback if an error
* occurs, leaving the queue in a consistent state.
*
* @param qp Query parameters for the files table
* @param item_id Files are added after item with this id
* @return 0 on success, -1 on failure
*/
int
db_queue_add_by_queryafteritemid(struct query_params *qp, uint32_t item_id)
{
int pos;
int ret;
// Position of the first new item
pos = db_queue_get_pos(item_id, 0);
if (pos < 0)
{
return -1;
}
pos++;
ret = db_queue_add_by_query(qp, 0, 0, pos, NULL, NULL);
return ret;
}
int
db_queue_add_start(struct db_queue_add_info *queue_add_info, int pos)
{
uint32_t queue_count;
int ret;
memset(queue_add_info, 0, sizeof(struct db_queue_add_info));
queue_add_info->queue_version = queue_transaction_begin();
ret = db_queue_get_count(&queue_count);
if (ret < 0)
{
ret = -1;
queue_transaction_end(ret, queue_add_info->queue_version);
return ret;
}
queue_add_info->pos = queue_count;
queue_add_info->shuffle_pos = queue_count;
if (pos >= 0 && pos < queue_count)
queue_add_info->pos = pos;
queue_add_info->start_pos = queue_add_info->pos;
return 0;
}
int
db_queue_add_end(struct db_queue_add_info *queue_add_info, char reshuffle, uint32_t item_id, int ret)
{
char *query;
if (ret < 0)
goto end;
// Update pos for all items from the given position
query = sqlite3_mprintf("UPDATE queue SET pos = pos + %d, queue_version = %d WHERE pos >= %d AND queue_version < %d;",
queue_add_info->count, queue_add_info->queue_version, queue_add_info->start_pos, queue_add_info->queue_version);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end;
// Reshuffle after adding new items
if (reshuffle)
ret = queue_reshuffle(item_id, queue_add_info->queue_version);
end:
queue_transaction_end(ret, queue_add_info->queue_version);
return ret;
}
int
db_queue_add_next(struct db_queue_add_info *queue_add_info, struct db_queue_item *qi)
{
int ret;
qi->pos = queue_add_info->pos;
qi->shuffle_pos = queue_add_info->shuffle_pos;
qi->queue_version = queue_add_info->queue_version;
ret = queue_item_add(qi);
if (ret < 0)
return ret;
queue_add_info->pos++;
queue_add_info->shuffle_pos++;
queue_add_info->count++;
if (queue_add_info->new_item_id == 0)
queue_add_info->new_item_id = ret;
return ret;
}
/*
* Adds the files matching the given query to the queue
*
* The files table is queried with the given parameters and all found files are added to the end of the
* "normal" queue and the shuffled queue.
*
* The function returns -1 on failure (e. g. error reading from database). It wraps all database access
* in a transaction and performs a rollback if an error occurs, leaving the queue in a consistent state.
*
* @param qp Query parameters for the files table
* @param reshuffle If 1 queue will be reshuffled after adding new items
* @param item_id The base item id, all items after this will be reshuffled
* @param position The position in the queue for the new queue item, -1 to add at end of queue
* @param count If not NULL returns the number of items added to the queue
* @param new_item_id If not NULL return the queue item id of the first new queue item
* @return 0 on success, -1 on failure
*/
int
db_queue_add_by_query(struct query_params *qp, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id)
{
struct db_media_file_info dbmfi;
char *query;
int queue_version;
uint32_t queue_count;
int pos;
int shuffle_pos;
bool append_to_queue;
int ret;
if (new_item_id)
*new_item_id = 0;
if (count)
*count = 0;
queue_version = queue_transaction_begin();
ret = db_queue_get_count(&queue_count);
if (ret < 0)
{
ret = -1;
goto end_transaction;
}
ret = db_query_start(qp);
if (ret < 0)
goto end_transaction;
DPRINTF(E_DBG, L_DB, "Player queue query returned %d items\n", qp->results);
if (qp->results == 0)
{
db_query_end(qp);
db_transaction_end();
return 0;
}
append_to_queue = (position < 0 || position > queue_count);
if (append_to_queue)
{
pos = queue_count;
shuffle_pos = queue_count;
}
else
{
pos = position;
shuffle_pos = position;
// Update pos for all items from the given position (make room for the new items in the queue)
query = sqlite3_mprintf("UPDATE queue SET pos = pos + %d, queue_version = %d WHERE pos >= %d;", qp->results, queue_version, pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// and similary update on shuffle_pos
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos + %d, queue_version = %d WHERE shuffle_pos >= %d;", qp->results, queue_version, pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
}
while ((ret = db_query_fetch_file(&dbmfi, qp)) == 0)
{
ret = queue_item_add_from_file(&dbmfi, pos, shuffle_pos, queue_version);
if (ret < 0)
{
DPRINTF(E_DBG, L_DB, "Failed to add song id %s (%s)\n", dbmfi.id, dbmfi.title);
break;
}
DPRINTF(E_DBG, L_DB, "Added (pos=%d shuffle_pos=%d reshuffle=%d req position=%d) song id %s (%s) to queue with item id %d\n", pos, shuffle_pos, reshuffle, position, dbmfi.id, dbmfi.title, ret);
if (new_item_id && *new_item_id == 0)
*new_item_id = ret;
if (count)
(*count)++;
pos++;
shuffle_pos++;
}
if (ret > 0)
ret = 0;
db_query_end(qp);
if (ret < 0)
goto end_transaction;
// Reshuffle after adding new items, if no queue position was specified - this
// case would indicate an 'add next' condition where if shuffling invalidates
// the tracks added to 'next'
if (append_to_queue && reshuffle)
{
ret = queue_reshuffle(item_id, queue_version);
}
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Adds the items of the stored playlist with the given id to the end of the queue
*
* @param plid Id of the stored playlist
* @param reshuffle If 1 queue will be reshuffled after adding new items
* @param item_id The base item id, all items after this will be reshuffled
* @param position The position in the queue for the new queue item, -1 to add at end of queue
* @param count If not NULL returns the number of items added to the queue
* @param new_item_id If not NULL return the queue item id of the first new queue item
* @return 0 on success, -1 on failure
*/
int
db_queue_add_by_playlistid(int plid, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id)
{
struct query_params qp;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.id = plid;
qp.type = Q_PLITEMS;
ret = db_queue_add_by_query(&qp, reshuffle, item_id, position, count, new_item_id);
return ret;
}
/*
* Adds the file with the given id to the queue
*
* @param id Id of the file
* @param reshuffle If 1 queue will be reshuffled after adding new items
* @param item_id The base item id, all items after this will be reshuffled
* @param position The position in the queue for the new queue item, -1 to add at end of queue
* @param count If not NULL returns the number of items added to the queue
* @param new_item_id If not NULL return the queue item id of the first new queue item
* @return 0 on success, -1 on failure
*/
int
db_queue_add_by_fileid(int id, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id)
{
struct query_params qp;
char buf[124];
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_ITEMS;
snprintf(buf, sizeof(buf), "f.id = %" PRIu32, id);
qp.filter = buf;
ret = db_queue_add_by_query(&qp, reshuffle, item_id, position, count, new_item_id);
return ret;
}
static int
queue_enum_start(struct query_params *qp)
{
#define Q_TMPL "SELECT * FROM queue f WHERE %s ORDER BY %s;"
sqlite3_stmt *stmt;
char *query;
const char *orderby;
int ret;
qp->stmt = NULL;
if (qp->sort)
orderby = sort_clause[qp->sort];
else
orderby = sort_clause[S_POS];
if (qp->filter)
query = sqlite3_mprintf(Q_TMPL, qp->filter, orderby);
else
query = sqlite3_mprintf(Q_TMPL, "1=1", orderby);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
qp->stmt = stmt;
return 0;
#undef Q_TMPL
}
static int
queue_enum_fetch(struct query_params *qp, struct db_queue_item *qi, int must_strdup)
{
int ret;
int i;
memset(qi, 0, sizeof(struct db_queue_item));
if (!qp->stmt)
{
DPRINTF(E_LOG, L_DB, "Queue enum not started!\n");
return -1;
}
ret = db_blocking_step(qp->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of queue enum results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
for (i = 0; i < ARRAY_SIZE(qi_cols_map); i++)
{
struct_field_from_statement(qi, qi_cols_map[i].offset, qi_cols_map[i].type, qp->stmt, i, must_strdup, false);
}
return 0;
}
int
db_queue_enum_start(struct query_params *qp)
{
int ret;
db_transaction_begin();
ret = queue_enum_start(qp);
if (ret < 0)
db_transaction_rollback();
return ret;
}
void
db_queue_enum_end(struct query_params *qp)
{
db_query_end(qp);
db_transaction_end();
}
int
db_queue_enum_fetch(struct query_params *qp, struct db_queue_item *qi)
{
return queue_enum_fetch(qp, qi, 0);
}
int
db_queue_get_pos(uint32_t item_id, char shuffle)
{
#define Q_TMPL "SELECT pos FROM queue WHERE id = %d;"
#define Q_TMPL_SHUFFLE "SELECT shuffle_pos FROM queue WHERE id = %d;"
char *query;
int pos;
if (shuffle)
query = sqlite3_mprintf(Q_TMPL_SHUFFLE, item_id);
else
query = sqlite3_mprintf(Q_TMPL, item_id);
pos = db_get_one_int(query);
sqlite3_free(query);
return pos;
#undef Q_TMPL
#undef Q_TMPL_SHUFFLE
}
static int
queue_fetch_byitemid(uint32_t item_id, struct db_queue_item *qi, int with_metadata)
{
struct query_params qp;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.filter = sqlite3_mprintf("id = %d", item_id);
ret = queue_enum_start(&qp);
if (ret < 0)
{
sqlite3_free(qp.filter);
return -1;
}
ret = queue_enum_fetch(&qp, qi, with_metadata);
db_query_end(&qp);
sqlite3_free(qp.filter);
return ret;
}
struct db_queue_item *
db_queue_fetch_byitemid(uint32_t item_id)
{
struct db_queue_item *qi;
int ret;
qi = calloc(1, sizeof(struct db_queue_item));
if (!qi)
{
DPRINTF(E_LOG, L_DB, "Out of memory for queue_item\n");
return NULL;
}
db_transaction_begin();
ret = queue_fetch_byitemid(item_id, qi, 1);
db_transaction_end();
if (ret < 0)
{
free_queue_item(qi, 0);
DPRINTF(E_LOG, L_DB, "Error fetching queue item by item id\n");
return NULL;
}
else if (qi->id == 0)
{
// No item found
free_queue_item(qi, 0);
return NULL;
}
return qi;
}
struct db_queue_item *
db_queue_fetch_byfileid(uint32_t file_id)
{
struct db_queue_item *qi;
struct query_params qp;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qi = calloc(1, sizeof(struct db_queue_item));
if (!qi)
{
DPRINTF(E_LOG, L_DB, "Out of memory for queue_item\n");
return NULL;
}
db_transaction_begin();
qp.filter = sqlite3_mprintf("file_id = %d", file_id);
ret = queue_enum_start(&qp);
if (ret < 0)
{
sqlite3_free(qp.filter);
db_transaction_end();
free_queue_item(qi, 0);
DPRINTF(E_LOG, L_DB, "Error fetching queue item by file id\n");
return NULL;
}
ret = queue_enum_fetch(&qp, qi, 1);
db_query_end(&qp);
sqlite3_free(qp.filter);
db_transaction_end();
if (ret < 0)
{
free_queue_item(qi, 0);
DPRINTF(E_LOG, L_DB, "Error fetching queue item by file id\n");
return NULL;
}
else if (qi->id == 0)
{
// No item found
free_queue_item(qi, 0);
return NULL;
}
return qi;
}
static int
queue_fetch_bypos(uint32_t pos, char shuffle, struct db_queue_item *qi, int with_metadata)
{
struct query_params qp;
int ret;
memset(&qp, 0, sizeof(struct query_params));
if (shuffle)
qp.filter = sqlite3_mprintf("shuffle_pos = %d", pos);
else
qp.filter = sqlite3_mprintf("pos = %d", pos);
ret = queue_enum_start(&qp);
if (ret < 0)
{
sqlite3_free(qp.filter);
return -1;
}
ret = queue_enum_fetch(&qp, qi, with_metadata);
db_query_end(&qp);
sqlite3_free(qp.filter);
return ret;
}
struct db_queue_item *
db_queue_fetch_bypos(uint32_t pos, char shuffle)
{
struct db_queue_item *qi;
int ret;
qi = calloc(1, sizeof(struct db_queue_item));
if (!qi)
{
DPRINTF(E_LOG, L_MAIN, "Out of memory for queue_item\n");
return NULL;
}
db_transaction_begin();
ret = queue_fetch_bypos(pos, shuffle, qi, 1);
db_transaction_end();
if (ret < 0)
{
free_queue_item(qi, 0);
DPRINTF(E_LOG, L_DB, "Error fetching queue item by pos id\n");
return NULL;
}
else if (qi->id == 0)
{
// No item found
free_queue_item(qi, 0);
return NULL;
}
return qi;
}
static int
queue_fetch_byposrelativetoitem(int pos, uint32_t item_id, char shuffle, struct db_queue_item *qi, int with_metadata)
{
int pos_absolute;
int ret;
DPRINTF(E_DBG, L_DB, "Fetch by pos: pos (%d) relative to item with id (%d)\n", pos, item_id);
pos_absolute = db_queue_get_pos(item_id, shuffle);
if (pos_absolute < 0)
{
return -1;
}
DPRINTF(E_DBG, L_DB, "Fetch by pos: item (%d) has absolute pos %d\n", item_id, pos_absolute);
pos_absolute += pos;
ret = queue_fetch_bypos(pos_absolute, shuffle, qi, with_metadata);
if (ret < 0)
DPRINTF(E_LOG, L_DB, "Error fetching item by pos: pos (%d) relative to item with id (%d)\n", pos, item_id);
else
DPRINTF(E_DBG, L_DB, "Fetch by pos: fetched item (id=%d, pos=%d, file-id=%d)\n", qi->id, qi->pos, qi->file_id);
return ret;
}
struct db_queue_item *
db_queue_fetch_byposrelativetoitem(int pos, uint32_t item_id, char shuffle)
{
struct db_queue_item *qi;
int ret;
DPRINTF(E_DBG, L_DB, "Fetch by pos: pos (%d) relative to item with id (%d)\n", pos, item_id);
qi = calloc(1, sizeof(struct db_queue_item));
if (!qi)
{
DPRINTF(E_LOG, L_MAIN, "Out of memory for queue_item\n");
return NULL;
}
db_transaction_begin();
ret = queue_fetch_byposrelativetoitem(pos, item_id, shuffle, qi, 1);
db_transaction_end();
if (ret < 0)
{
free_queue_item(qi, 0);
DPRINTF(E_LOG, L_DB, "Error fetching queue item by pos relative to item id\n");
return NULL;
}
else if (qi->id == 0)
{
// No item found
free_queue_item(qi, 0);
return NULL;
}
DPRINTF(E_DBG, L_DB, "Fetch by pos: fetched item (id=%d, pos=%d, file-id=%d)\n", qi->id, qi->pos, qi->file_id);
return qi;
}
struct db_queue_item *
db_queue_fetch_next(uint32_t item_id, char shuffle)
{
return db_queue_fetch_byposrelativetoitem(1, item_id, shuffle);
}
struct db_queue_item *
db_queue_fetch_prev(uint32_t item_id, char shuffle)
{
return db_queue_fetch_byposrelativetoitem(-1, item_id, shuffle);
}
static int
queue_fix_pos(enum sort_type sort, int queue_version)
{
#define Q_TMPL "UPDATE queue SET %q = %d, queue_version = %d WHERE id = %d and %q <> %d;"
struct query_params qp;
struct db_queue_item queue_item;
char *query;
int pos;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.sort = sort;
ret = queue_enum_start(&qp);
if (ret < 0)
{
return -1;
}
pos = 0;
while ((ret = queue_enum_fetch(&qp, &queue_item, 0)) == 0 && (queue_item.id > 0))
{
if (queue_item.pos != pos)
{
if (sort == S_SHUFFLE_POS)
query = sqlite3_mprintf(Q_TMPL, "shuffle_pos", pos, queue_version, queue_item.id, "shuffle_pos", pos);
else
query = sqlite3_mprintf(Q_TMPL, "pos", pos, queue_version, queue_item.id, "pos", pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Failed to update item with item-id: %d\n", queue_item.id);
break;
}
}
pos++;
}
db_query_end(&qp);
return ret;
#undef Q_TMPL
}
/*
* Remove files that are disabled or non existent in the library and repair ordering of
* the queue (shuffle and normal)
*/
int
db_queue_cleanup()
{
#define Q_TMPL "DELETE FROM queue WHERE NOT file_id IN (SELECT id from files WHERE disabled = 0);"
int queue_version;
int deleted;
int ret;
queue_version = queue_transaction_begin();
ret = db_query_run(Q_TMPL, 0, 0);
if (ret < 0)
goto end_transaction;
deleted = sqlite3_changes(hdl);
if (deleted <= 0)
{
// Nothing to do
db_transaction_end();
return 0;
}
// Update position of normal queue
ret = queue_fix_pos(S_POS, queue_version);
if (ret < 0)
goto end_transaction;
// Update position of shuffle queue
ret = queue_fix_pos(S_SHUFFLE_POS, queue_version);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
#undef Q_TMPL
}
/*
* Removes all items from the queue except the item give by 'keep_item_id' (if 'keep_item_id' > 0).
*
* @param keep_item_id item-id (e. g. the now playing item) to be left in the queue
*/
int
db_queue_clear(uint32_t keep_item_id)
{
int queue_version;
char *query;
int ret;
queue_version = queue_transaction_begin();
query = sqlite3_mprintf("DELETE FROM queue where id <> %d;", keep_item_id);
ret = db_query_run(query, 1, 0);
if (ret == 0 && keep_item_id)
{
query = sqlite3_mprintf("UPDATE queue SET pos = 0, shuffle_pos = 0, queue_version = %d where id = %d;", queue_version, keep_item_id);
ret = db_query_run(query, 1, 0);
}
queue_transaction_end(ret, queue_version);
return ret;
}
static int
queue_delete_item(struct db_queue_item *qi, int queue_version)
{
char *query;
int ret;
// Remove item with the given item_id
query = sqlite3_mprintf("DELETE FROM queue where id = %d;", qi->id);
ret = db_query_run(query, 1, 0);
if (ret < 0)
{
return -1;
}
// Update pos for all items after the item with given item_id
query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1, queue_version = %d WHERE pos > %d;", queue_version, qi->pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
{
return -1;
}
// Update shuffle_pos for all items after the item with given item_id
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1, queue_version = %d WHERE shuffle_pos > %d;", queue_version, qi->shuffle_pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
{
return -1;
}
return 0;
}
int
db_queue_delete_byitemid(uint32_t item_id)
{
int queue_version;
struct db_queue_item queue_item;
int ret;
queue_version = queue_transaction_begin();
ret = queue_fetch_byitemid(item_id, &queue_item, 0);
if (ret < 0)
goto end_transaction;
if (queue_item.id == 0)
{
db_transaction_end();
return 0;
}
ret = queue_delete_item(&queue_item, queue_version);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
int
db_queue_delete_bypos(uint32_t pos, int count)
{
int queue_version;
char *query;
int to_pos;
int ret;
queue_version = queue_transaction_begin();
// Remove item with the given item_id
to_pos = pos + count;
query = sqlite3_mprintf("DELETE FROM queue where pos >= %d AND pos < %d;", pos, to_pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
ret = queue_fix_pos(S_POS, queue_version);
if (ret < 0)
goto end_transaction;
ret = queue_fix_pos(S_SHUFFLE_POS, queue_version);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
int
db_queue_delete_byposrelativetoitem(uint32_t pos, uint32_t item_id, char shuffle)
{
int queue_version;
struct db_queue_item queue_item;
int ret;
queue_version = queue_transaction_begin();
ret = queue_fetch_byposrelativetoitem(pos, item_id, shuffle, &queue_item, 0);
if (ret < 0)
goto end_transaction;
if (queue_item.id == 0)
{
// No item found
db_transaction_end();
return 0;
}
ret = queue_delete_item(&queue_item, queue_version);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Moves the queue item with the given id to the given position (zero-based).
*
* @param item_id Queue item id
* @param pos_to target position in the queue (zero-based)
* @þaram shuffle If 1 move item in the shuffle queue
* @return 0 on success, -1 on failure
*/
int
db_queue_move_byitemid(uint32_t item_id, int pos_to, char shuffle)
{
int queue_version;
char *query;
int pos_from;
int ret;
queue_version = queue_transaction_begin();
// Find item with the given item_id
pos_from = db_queue_get_pos(item_id, shuffle);
if (pos_from < 0)
{
ret = -1;
goto end_transaction;
}
// Update pos for all items after the item with given item_id
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1, queue_version = %d WHERE shuffle_pos > %d;", queue_version, pos_from);
else
query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1, queue_version = %d WHERE pos > %d;", queue_version, pos_from);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update pos for all items from the given pos_to
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos + 1, queue_version = %d WHERE shuffle_pos >= %d;", queue_version, pos_to);
else
query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1, queue_version = %d WHERE pos >= %d;", queue_version, pos_to);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update item with the given item_id
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d, queue_version = %d where id = %d;", pos_to, queue_version, item_id);
else
query = sqlite3_mprintf("UPDATE queue SET pos = %d, queue_version = %d where id = %d;", pos_to, queue_version, item_id);
ret = db_query_run(query, 1, 0);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Moves the queue item at the given position to the given position (zero-based).
*
* @param pos_from Position of the queue item to move
* @param pos_to target position in the queue (zero-based)
* @return 0 on success, -1 on failure
*/
int
db_queue_move_bypos(int pos_from, int pos_to)
{
int queue_version;
struct db_queue_item queue_item;
char *query;
int ret;
queue_version = queue_transaction_begin();
// Find item to move
ret = queue_fetch_bypos(pos_from, 0, &queue_item, 0);
if (ret < 0)
goto end_transaction;
if (queue_item.id == 0)
{
db_transaction_end();
return 0;
}
// Update pos for all items after the item with given position
query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1, queue_version = %d WHERE pos > %d;", queue_version, queue_item.pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update pos for all items from the given pos_to
query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1, queue_version = %d WHERE pos >= %d;", queue_version, pos_to);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update item with the given item_id
query = sqlite3_mprintf("UPDATE queue SET pos = %d, queue_version = %d where id = %d;", pos_to, queue_version, queue_item.id);
ret = db_query_run(query, 1, 0);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Moves the queue item at the given position to the given target position. The positions
* are relavtive to the given base item (item id).
*
* @param from_pos Relative position of the queue item to the base item
* @param to_offset Target position relative to the base item
* @param item_id The base item id (normaly the now playing item)
* @return 0 on success, -1 on failure
*/
int
db_queue_move_byposrelativetoitem(uint32_t from_pos, uint32_t to_offset, uint32_t item_id, char shuffle)
{
int queue_version;
struct db_queue_item queue_item;
char *query;
int pos_move_from;
int pos_move_to;
int ret;
queue_version = queue_transaction_begin();
DPRINTF(E_DBG, L_DB, "Move by pos: from %d offset %d relative to item (%d)\n", from_pos, to_offset, item_id);
// Find item with the given item_id
ret = queue_fetch_byitemid(item_id, &queue_item, 0);
if (ret < 0)
goto end_transaction;
DPRINTF(E_DBG, L_DB, "Move by pos: base item (id=%d, pos=%d, file-id=%d)\n", queue_item.id, queue_item.pos, queue_item.file_id);
if (queue_item.id == 0)
{
db_transaction_end();
return 0;
}
// Calculate the position of the item to move
if (shuffle)
pos_move_from = queue_item.shuffle_pos + from_pos;
else
pos_move_from = queue_item.pos + from_pos;
// Calculate the position where to move the item to
if (shuffle)
pos_move_to = queue_item.shuffle_pos + to_offset;
else
pos_move_to = queue_item.pos + to_offset;
if (pos_move_to < pos_move_from)
{
/*
* Moving an item to a previous position seems to send an offset incremented by one
*/
pos_move_to++;
}
DPRINTF(E_DBG, L_DB, "Move by pos: absolute pos: move from %d to %d\n", pos_move_from, pos_move_to);
// Find item to move
ret = queue_fetch_bypos(pos_move_from, shuffle, &queue_item, 0);
if (ret < 0)
goto end_transaction;
DPRINTF(E_DBG, L_DB, "Move by pos: move item (id=%d, pos=%d, file-id=%d)\n", queue_item.id, queue_item.pos, queue_item.file_id);
if (queue_item.id == 0)
{
db_transaction_end();
return 0;
}
// Update pos for all items after the item with given position
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1, queue_version = %d WHERE shuffle_pos > %d;", queue_version, queue_item.shuffle_pos);
else
query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1, queue_version = %d WHERE pos > %d;", queue_version, queue_item.pos);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update pos for all items from the given pos_to
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos + 1, queue_version = %d WHERE shuffle_pos >= %d;", queue_version, pos_move_to);
else
query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1, queue_version = %d WHERE pos >= %d;", queue_version, pos_move_to);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto end_transaction;
// Update item with the given item_id
if (shuffle)
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d, queue_version = %d where id = %d;", pos_move_to, queue_version, queue_item.id);
else
query = sqlite3_mprintf("UPDATE queue SET pos = %d, queue_version = %d where id = %d;", pos_move_to, queue_version, queue_item.id);
ret = db_query_run(query, 1, 0);
end_transaction:
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Reshuffles the shuffle queue
*
* If the given item_id is 0, the whole shuffle queue is reshuffled, otherwise the
* queue is reshuffled after the item with the given id (excluding this item).
*
* @param item_id The base item, after this item the queue is reshuffled
* @return 0 on success, -1 on failure
*/
static int
queue_reshuffle(uint32_t item_id, int queue_version)
{
char *query;
int pos;
uint32_t count;
struct db_queue_item queue_item;
int *shuffle_pos = NULL;
int len;
int i;
struct query_params qp = { 0 };
int ret;
DPRINTF(E_DBG, L_DB, "Reshuffle queue after item with item-id: %d\n", item_id);
// Reset the shuffled order and mark all items as changed
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = pos, queue_version = %d;", queue_version);
ret = db_query_run(query, 1, 0);
if (ret < 0)
goto error;
pos = 0;
if (item_id > 0)
{
pos = db_queue_get_pos(item_id, 0);
if (pos < 0)
goto error;
pos++; // Do not reshuffle the base item
}
ret = db_queue_get_count(&count);
if (ret < 0)
goto error;
len = count - pos;
DPRINTF(E_DBG, L_DB, "Reshuffle %d items off %" PRIu32 " total items, starting from pos %d\n", len, count, pos);
CHECK_NULL(L_DB, shuffle_pos = malloc(len * sizeof(int)));
for (i = 0; i < len; i++)
{
shuffle_pos[i] = i + pos;
}
rng_shuffle_int(&shuffle_rng, shuffle_pos, len);
qp.filter = sqlite3_mprintf("pos >= %d", pos);
ret = queue_enum_start(&qp);
if (ret < 0)
goto error;
i = 0;
while ((ret = queue_enum_fetch(&qp, &queue_item, 0)) == 0 && (queue_item.id > 0) && (i < len))
{
query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d where id = %d;", shuffle_pos[i], queue_item.id);
ret = db_query_run(query, 1, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Failed to delete item with item-id: %d\n", queue_item.id);
break;
}
i++;
}
db_query_end(&qp);
if (ret < 0)
goto error;
sqlite3_free(qp.filter);
free(shuffle_pos);
return 0;
error:
sqlite3_free(qp.filter);
free(shuffle_pos);
return -1;
}
/*
* Reshuffles the shuffle queue
*
* If the given item_id is 0, the whole shuffle queue is reshuffled, otherwise the
* queue is reshuffled after the item with the given id (excluding this item).
*
* @param item_id The base item, after this item the queue is reshuffled
* @return 0 on success, -1 on failure
*/
int
db_queue_reshuffle(uint32_t item_id)
{
int queue_version;
int ret;
queue_version = queue_transaction_begin();
ret = queue_reshuffle(item_id, queue_version);
queue_transaction_end(ret, queue_version);
return ret;
}
/*
* Increment queue version (triggers queue change event)
*/
int
db_queue_inc_version()
{
int queue_version;
queue_version = queue_transaction_begin();
queue_transaction_end(0, queue_version);
return 0;
}
int
db_queue_get_count(uint32_t *nitems)
{
int ret = db_get_one_int("SELECT COUNT(*) FROM queue;");
if (ret < 0)
return -1;
*nitems = (uint32_t)ret;
return 0;
}
/* Inotify */
int
db_watch_clear(void)
{
return db_query_run("DELETE FROM inotify;", 0, 0);
}
int
db_watch_add(struct watch_info *wi)
{
#define Q_TMPL "INSERT INTO inotify (wd, cookie, path) VALUES (%d, 0, '%q');"
char *query;
query = sqlite3_mprintf(Q_TMPL, wi->wd, wi->path);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_watch_delete_bywd(uint32_t wd)
{
#define Q_TMPL "DELETE FROM inotify WHERE wd = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL, wd);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_watch_delete_bypath(const char *path)
{
#define Q_TMPL "DELETE FROM inotify WHERE path = '%q';"
char *query;
query = sqlite3_mprintf(Q_TMPL, path);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_watch_delete_bymatch(const char *path)
{
#define Q_TMPL "DELETE FROM inotify WHERE path LIKE '%q/%%';"
char *query;
query = sqlite3_mprintf(Q_TMPL, path);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_watch_delete_bycookie(uint32_t cookie)
{
#define Q_TMPL "DELETE FROM inotify WHERE cookie = %" PRIi64 ";"
char *query;
if (cookie == 0)
return -1;
query = sqlite3_mprintf(Q_TMPL, (int64_t)cookie);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
static int
db_watch_get_byquery(struct watch_info *wi, char *query)
{
sqlite3_stmt *stmt;
int64_t cookie;
int ncols;
int i;
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
DPRINTF(E_WARN, L_DB, "Watch not found: '%s'\n", query);
sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
}
ncols = sqlite3_column_count(stmt);
if (ncols < ARRAY_SIZE(wi_cols_map))
{
DPRINTF(E_LOG, L_DB, "BUG: database has fewer columns (%d) than wi column map (%u)\n", ncols, ARRAY_SIZE(wi_cols_map));
sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
}
for (i = 0; i < ARRAY_SIZE(wi_cols_map); i++)
{
struct_field_from_statement(wi, wi_cols_map[i].offset, wi_cols_map[i].type, stmt, i, true, false);
if (wi_cols_map[i].offset == wi_offsetof(cookie))
{
cookie = sqlite3_column_int64(stmt, i);
wi->cookie = (cookie == INOTIFY_FAKE_COOKIE) ? 0 : cookie;
}
}
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
}
int
db_watch_get_bywd(struct watch_info *wi, int wd)
{
#define Q_TMPL "SELECT * FROM inotify WHERE wd = %d;"
char *query;
query = sqlite3_mprintf(Q_TMPL, wd);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
return db_watch_get_byquery(wi, query);
#undef Q_TMPL
}
int
db_watch_get_bypath(struct watch_info *wi, const char *path)
{
#define Q_TMPL "SELECT * FROM inotify WHERE path = '%q';"
char *query;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
return db_watch_get_byquery(wi, query);
#undef Q_TMPL
}
void
db_watch_mark_bypath(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE inotify SET path = substr(path, %d), cookie = %" PRIi64 " WHERE path = '%q';"
char *query;
int64_t disabled;
int path_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, disabled, path);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
void
db_watch_mark_bymatch(const char *path, enum strip_type strip, uint32_t cookie)
{
#define Q_TMPL "UPDATE inotify SET path = substr(path, %d), cookie = %" PRIi64 " WHERE path LIKE '%q/%%';"
char *query;
int64_t disabled;
int path_striplen;
disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE;
path_striplen = (strip == STRIP_PATH) ? strlen(path) : 0;
query = sqlite3_mprintf(Q_TMPL, path_striplen + 1, disabled, path);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
void
db_watch_move_bycookie(uint32_t cookie, const char *path)
{
#define Q_TMPL "UPDATE inotify SET path = '%q' || path, cookie = 0 WHERE cookie = %" PRIi64 ";"
char *query;
if (cookie == 0)
return;
query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie);
db_query_run(query, 1, 0);
#undef Q_TMPL
}
int
db_watch_cookie_known(uint32_t cookie)
{
#define Q_TMPL "SELECT COUNT(*) FROM inotify WHERE cookie = %" PRIi64 ";"
char *query;
int ret;
if (cookie == 0)
return 0;
query = sqlite3_mprintf(Q_TMPL, (int64_t)cookie);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_get_one_int(query);
sqlite3_free(query);
return (ret > 0);
#undef Q_TMPL
}
int
db_watch_enum_start(struct watch_enum *we)
{
#define Q_MATCH_TMPL "SELECT wd FROM inotify WHERE path LIKE '%q/%%';"
#define Q_COOKIE_TMPL "SELECT wd FROM inotify WHERE cookie = %" PRIi64 ";"
sqlite3_stmt *stmt;
char *query;
int ret;
we->stmt = NULL;
if (we->match)
query = sqlite3_mprintf(Q_MATCH_TMPL, we->match);
else if (we->cookie != 0)
query = sqlite3_mprintf(Q_COOKIE_TMPL, we->cookie);
else
{
DPRINTF(E_LOG, L_DB, "Could not start enum, no parameter given\n");
return -1;
}
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1;
}
DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return -1;
}
sqlite3_free(query);
we->stmt = stmt;
return 0;
#undef Q_MATCH_TMPL
#undef Q_COOKIE_TMPL
}
void
db_watch_enum_end(struct watch_enum *we)
{
if (!we->stmt)
return;
sqlite3_finalize(we->stmt);
we->stmt = NULL;
}
int
db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd)
{
int ret;
*wd = 0;
if (!we->stmt)
{
DPRINTF(E_LOG, L_DB, "Watch enum not started!\n");
return -1;
}
ret = db_blocking_step(we->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_INFO, L_DB, "End of watch enum results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
*wd = (uint32_t)sqlite3_column_int(we->stmt, 0);
return 0;
}
#ifdef DB_PROFILE
static int
db_xprofile(unsigned int trace_type, void *notused, void *ptr, void *ptr_data)
{
sqlite3_stmt *pstmt;
int64_t ms = 0;
sqlite3_stmt *stmt;
const char *pquery;
char *query;
int log_level;
int ret;
if (trace_type != SQLITE_TRACE_PROFILE)
return 0;
pstmt = ptr;
pquery = sqlite3_sql(pstmt);
ms = *((int64_t *) ptr_data) / 1000000;
if (ms > 1000)
log_level = E_LOG;
else if (ms > 500)
log_level = E_WARN;
else if (ms > 10)
log_level = E_DBG;
else
log_level = E_SPAM;
if (log_level > logger_severity())
return 0;
DPRINTF(log_level, L_DBPERF, "SQL PROFILE query: %s\n", pquery);
DPRINTF(log_level, L_DBPERF, "SQL PROFILE time: %" PRIi64 " ms\n", ms);
if ((strncmp(pquery, "SELECT", 6) != 0)
&& (strncmp(pquery, "UPDATE", 6) != 0)
&& (strncmp(pquery, "DELETE", 6) != 0))
return 0;
/* Disable profiling callback */
sqlite3_trace_v2(hdl, 0, NULL, NULL);
query = sqlite3_mprintf("EXPLAIN QUERY PLAN %s", pquery);
if (!query)
{
DPRINTF(log_level, L_DBPERF, "Query plan: Out of memory\n");
goto out;
}
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
sqlite3_free(query);
if (ret != SQLITE_OK)
{
DPRINTF(log_level, L_DBPERF, "Query plan: Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
goto out;
}
DPRINTF(log_level, L_DBPERF, "Query plan:\n");
while ((ret = db_blocking_step(stmt)) == SQLITE_ROW)
{
DPRINTF(log_level, L_DBPERF, "(%d,%d,%d) %s\n",
sqlite3_column_int(stmt, 0), sqlite3_column_int(stmt, 1), sqlite3_column_int(stmt, 2),
sqlite3_column_text(stmt, 3));
}
if (ret != SQLITE_DONE)
DPRINTF(log_level, L_DBPERF, "Query plan: Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
out:
/* Reenable profiling callback */
sqlite3_trace_v2(hdl, SQLITE_TRACE_PROFILE, db_xprofile, NULL);
return 0;
}
#endif
static int
db_pragma_get_cache_size()
{
sqlite3_stmt *stmt;
char *query = "PRAGMA cache_size;";
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return 0;
}
ret = db_blocking_step(stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return ret;
}
static int
db_pragma_set_cache_size(int pages)
{
#define Q_TMPL "PRAGMA cache_size=%d;"
sqlite3_stmt *stmt;
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, pages);
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return 0;
}
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
static char *
db_pragma_set_journal_mode(char *mode)
{
#define Q_TMPL "PRAGMA journal_mode=%s;"
sqlite3_stmt *stmt;
char *query;
int ret;
char *new_mode;
query = sqlite3_mprintf(Q_TMPL, mode);
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return NULL;
}
ret = db_blocking_step(stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
sqlite3_free(query);
return NULL;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return NULL;
}
new_mode = (char *) sqlite3_column_text(stmt, 0);
sqlite3_finalize(stmt);
sqlite3_free(query);
return new_mode;
#undef Q_TMPL
}
static int
db_pragma_get_synchronous()
{
sqlite3_stmt *stmt;
char *query = "PRAGMA synchronous;";
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return 0;
}
ret = db_blocking_step(stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return ret;
}
static int
db_pragma_set_synchronous(int synchronous)
{
#define Q_TMPL "PRAGMA synchronous=%d;"
sqlite3_stmt *stmt;
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, synchronous);
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return 0;
}
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
static int
db_pragma_get_mmap_size()
{
sqlite3_stmt *stmt;
char *query = "PRAGMA mmap_size;";
int ret;
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
return 0;
}
ret = db_blocking_step(stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_DBG, L_DB, "End of query results\n");
return 0;
}
else if (ret != SQLITE_ROW)
{
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
return -1;
}
ret = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return ret;
}
static int
db_pragma_set_mmap_size(int mmap_size)
{
#define Q_TMPL "PRAGMA mmap_size=%d;"
sqlite3_stmt *stmt;
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, mmap_size);
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
sqlite3_free(query);
return 0;
}
sqlite3_finalize(stmt);
sqlite3_free(query);
return 0;
#undef Q_TMPL
}
static int
db_open(void)
{
char *errmsg;
int ret;
int cache_size;
char *journal_mode;
int synchronous;
int mmap_size;
if (!db_path)
return -1;
ret = sqlite3_open(db_path, &hdl);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not open '%s': %s\n", db_path, sqlite3_errmsg(hdl));
sqlite3_close(hdl);
return -1;
}
ret = sqlite3_enable_load_extension(hdl, 1);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not enable extension loading\n");
sqlite3_close(hdl);
return -1;
}
ret = sqlite3_load_extension(hdl, PKGLIBDIR "/" PACKAGE_NAME "-sqlext.so", NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not load SQLite extension: %s\n", errmsg);
sqlite3_free(errmsg);
sqlite3_close(hdl);
return -1;
}
ret = sqlite3_enable_load_extension(hdl, 0);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not disable extension loading\n");
sqlite3_close(hdl);
return -1;
}
#ifdef DB_PROFILE
sqlite3_trace_v2(hdl, SQLITE_TRACE_PROFILE, db_xprofile, NULL);
#endif
cache_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_cache_size_library");
if (cache_size > -1)
{
db_pragma_set_cache_size(cache_size);
cache_size = db_pragma_get_cache_size();
DPRINTF(E_DBG, L_DB, "Database cache size in pages: %d\n", cache_size);
}
journal_mode = cfg_getstr(cfg_getsec(cfg, "sqlite"), "pragma_journal_mode");
if (journal_mode)
{
journal_mode = db_pragma_set_journal_mode(journal_mode);
DPRINTF(E_DBG, L_DB, "Database journal mode: %s\n", journal_mode);
}
synchronous = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_synchronous");
if (synchronous > -1)
{
db_pragma_set_synchronous(synchronous);
synchronous = db_pragma_get_synchronous();
DPRINTF(E_DBG, L_DB, "Database synchronous: %d\n", synchronous);
}
mmap_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_mmap_size_library");
if (mmap_size > -1)
{
db_pragma_set_mmap_size(mmap_size);
mmap_size = db_pragma_get_mmap_size();
DPRINTF(E_DBG, L_DB, "Database mmap_size: %d\n", mmap_size);
}
return 0;
}
static sqlite3_stmt *
db_statements_prepare_insert(const struct col_type_map *map, size_t map_size, const char *table)
{
char *query;
char keystr[2048];
char valstr[1024];
sqlite3_stmt *stmt;
int ret;
int i;
memset(keystr, 0, sizeof(keystr));
memset(valstr, 0, sizeof(valstr));
for (i = 0; i < map_size; i++)
{
if (map[i].flag & DB_FLAG_NO_BIND)
continue;
CHECK_ERR(L_DB, safe_snprintf_cat(keystr, sizeof(keystr), "%s, ", map[i].name));
CHECK_ERR(L_DB, safe_snprintf_cat(valstr, sizeof(valstr), "?, "));
}
// Terminate at the ending ", "
*(strrchr(keystr, ',')) = '\0';
*(strrchr(valstr, ',')) = '\0';
CHECK_NULL(L_DB, query = db_mprintf("INSERT INTO %s (%s) VALUES (%s);", table, keystr, valstr));
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "Could not prepare statement '%s': %s\n", query, sqlite3_errmsg(hdl));
free(query);
return NULL;
}
free(query);
return stmt;
}
static sqlite3_stmt *
db_statements_prepare_update(const struct col_type_map *map, size_t map_size, const char *table)
{
char *query;
char keystr[2048];
sqlite3_stmt *stmt;
int ret;
int i;
memset(keystr, 0, sizeof(keystr));
for (i = 0; i < map_size; i++)
{
if (map[i].flag & DB_FLAG_NO_BIND)
continue;
if (map[i].flag & DB_FLAG_NO_ZERO)
CHECK_ERR(L_DB, safe_snprintf_cat(keystr, sizeof(keystr), "%s = daap_no_zero(?, %s), ", map[i].name, map[i].name));
else
CHECK_ERR(L_DB, safe_snprintf_cat(keystr, sizeof(keystr), "%s = ?, ", map[i].name));
}
// Terminate at the ending ", "
*(strrchr(keystr, ',')) = '\0';
CHECK_NULL(L_DB, query = db_mprintf("UPDATE %s SET %s WHERE %s = ?;", table, keystr, map[0].name));
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "Could not prepare statement '%s': %s\n", query, sqlite3_errmsg(hdl));
free(query);
return NULL;
}
free(query);
return stmt;
}
static sqlite3_stmt *
db_statements_prepare_ping(const char *table)
{
char *query;
sqlite3_stmt *stmt;
int ret;
CHECK_NULL(L_DB, query = db_mprintf("UPDATE %s SET db_timestamp = ?, disabled = 0 WHERE path = ? AND db_timestamp >= ?;", table));
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "Could not prepare statement '%s': %s\n", query, sqlite3_errmsg(hdl));
free(query);
return NULL;
}
free(query);
return stmt;
}
static int
db_statements_prepare(void)
{
db_statements.files_insert = db_statements_prepare_insert(mfi_cols_map, ARRAY_SIZE(mfi_cols_map), "files");
db_statements.files_update = db_statements_prepare_update(mfi_cols_map, ARRAY_SIZE(mfi_cols_map), "files");
db_statements.files_ping = db_statements_prepare_ping("files");
db_statements.playlists_insert = db_statements_prepare_insert(pli_cols_map, ARRAY_SIZE(pli_cols_map), "playlists");
db_statements.playlists_update = db_statements_prepare_update(pli_cols_map, ARRAY_SIZE(pli_cols_map), "playlists");
db_statements.queue_items_insert = db_statements_prepare_insert(qi_cols_map, ARRAY_SIZE(qi_cols_map), "queue");
db_statements.queue_items_update = db_statements_prepare_update(qi_cols_map, ARRAY_SIZE(qi_cols_map), "queue");
if ( !db_statements.files_insert || !db_statements.files_update || !db_statements.files_ping
|| !db_statements.playlists_insert || !db_statements.playlists_update
|| !db_statements.queue_items_insert || !db_statements.queue_items_update
)
return -1;
return 0;
}
int
db_backup()
{
int ret;
sqlite3 *backup_hdl;
sqlite3_backup *backup;
const char *backup_path;
char resolved_bp[PATH_MAX];
char resolved_dbp[PATH_MAX];
backup_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_backup_path");
if (!backup_path)
{
DPRINTF(E_LOG, L_DB, "Backup not enabled, 'db_backup_path' is unset\n");
return -2;
}
if (realpath(db_path, resolved_dbp) == NULL || realpath(backup_path, resolved_bp) == NULL)
{
DPRINTF(E_LOG, L_DB, "Failed to resolve real path of db/backup path: %s\n", strerror(errno));
goto error;
}
if (strcmp(resolved_bp, resolved_dbp) == 0)
{
DPRINTF(E_LOG, L_DB, "Backup path same as main db path, ignoring\n");
return -2;
}
DPRINTF(E_INFO, L_DB, "Backup starting...\n");
ret = sqlite3_open(backup_path, &backup_hdl);
if (ret != SQLITE_OK)
{
DPRINTF(E_WARN, L_DB, "Failed to create backup '%s': %s\n", backup_path, sqlite3_errmsg(backup_hdl));
goto error;
}
backup = sqlite3_backup_init(backup_hdl, "main", hdl, "main");
if (!backup)
{
DPRINTF(E_WARN, L_DB, "Failed to initiate backup '%s': %s\n", backup_path, sqlite3_errmsg(backup_hdl));
goto error;
}
ret = sqlite3_backup_step(backup, -1);
sqlite3_backup_finish(backup);
sqlite3_close(backup_hdl);
if (ret == SQLITE_DONE || ret == SQLITE_OK)
DPRINTF(E_INFO, L_DB, "Backup complete to '%s'\n", backup_path);
else
DPRINTF(E_WARN, L_DB, "Failed to complete backup '%s': %s (%d)\n", backup_path, sqlite3_errstr(ret), ret);
return ret;
error:
return -1;
}
int
db_perthread_init(void)
{
int ret;
ret = db_open();
if (ret < 0)
return -1;
ret = db_statements_prepare();
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statements\n");
sqlite3_close(hdl);
return -1;
}
return 0;
}
void
db_perthread_deinit(void)
{
sqlite3_stmt *stmt;
if (!hdl)
return;
/* Tear down anything that's in flight */
while ((stmt = sqlite3_next_stmt(hdl, 0)))
sqlite3_finalize(stmt);
sqlite3_close(hdl);
}
static int
db_check_version(void)
{
#define Q_VACUUM "VACUUM;"
char *errmsg;
int db_ver_major = 0;
int db_ver_minor = 0;
int db_ver;
int vacuum;
int ret;
vacuum = cfg_getbool(cfg_getsec(cfg, "sqlite"), "vacuum");
db_admin_getint(&db_ver_major, DB_ADMIN_SCHEMA_VERSION_MAJOR);
if (!db_ver_major)
db_admin_getint(&db_ver_major, DB_ADMIN_SCHEMA_VERSION); // Pre schema v15.1
if (!db_ver_major)
return 1; // Will create new database
db_admin_getint(&db_ver_minor, DB_ADMIN_SCHEMA_VERSION_MINOR);
db_ver = db_ver_major * 100 + db_ver_minor;
if (db_ver_major < 17)
{
DPRINTF(E_FATAL, L_DB, "Database schema v%d too old, cannot upgrade\n", db_ver_major);
return -1;
}
else if (db_ver_major > SCHEMA_VERSION_MAJOR)
{
DPRINTF(E_FATAL, L_DB, "Database schema v%d is newer than the supported version\n", db_ver_major);
return -1;
}
else if (db_ver < (SCHEMA_VERSION_MAJOR * 100 + SCHEMA_VERSION_MINOR))
{
DPRINTF(E_LOG, L_DB, "Database schema outdated, upgrading schema v%d.%d -> v%d.%d...\n",
db_ver_major, db_ver_minor, SCHEMA_VERSION_MAJOR, SCHEMA_VERSION_MINOR);
ret = sqlite3_exec(hdl, "BEGIN TRANSACTION;", NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "DB error while running 'BEGIN TRANSACTION': %s\n", errmsg);
sqlite3_free(errmsg);
return -1;
}
// Will drop indices and triggers
ret = db_upgrade(hdl, db_ver);
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Database upgrade errored out, rolling back changes ...\n");
ret = sqlite3_exec(hdl, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "DB error while running 'ROLLBACK TRANSACTION': %s\n", errmsg);
sqlite3_free(errmsg);
}
return -1;
}
ret = db_init_indices(hdl);
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Database upgrade errored out, rolling back changes ...\n");
ret = sqlite3_exec(hdl, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "DB error while running 'ROLLBACK TRANSACTION': %s\n", errmsg);
sqlite3_free(errmsg);
}
return -1;
}
ret = db_init_triggers(hdl);
if (ret < 0)
{
DPRINTF(E_LOG, L_DB, "Database upgrade errored out, rolling back changes ...\n");
ret = sqlite3_exec(hdl, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "DB error while running 'ROLLBACK TRANSACTION': %s\n", errmsg);
sqlite3_free(errmsg);
}
return -1;
}
ret = sqlite3_exec(hdl, "COMMIT TRANSACTION;", NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "DB error while running 'COMMIT TRANSACTION': %s\n", errmsg);
sqlite3_free(errmsg);
return -1;
}
DPRINTF(E_LOG, L_DB, "Upgrading schema to v%d.%d completed\n", SCHEMA_VERSION_MAJOR, SCHEMA_VERSION_MINOR);
vacuum = 1;
}
else if (db_ver_minor > SCHEMA_VERSION_MINOR)
{
DPRINTF(E_LOG, L_DB, "Future (but compatible) database version detected (v%d.%d)\n", db_ver_major, db_ver_minor);
}
if (vacuum)
{
DPRINTF(E_LOG, L_DB, "Now vacuuming database, this may take some time...\n");
ret = sqlite3_exec(hdl, Q_VACUUM, NULL, NULL, &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not VACUUM database: %s\n", errmsg);
sqlite3_free(errmsg);
return -1;
}
}
return 0;
#undef Q_VACUUM
}
int
db_init(void)
{
uint32_t files;
uint32_t pls;
int ret;
int i;
static_assert(ARRAY_SIZE(dbmfi_cols_map) == ARRAY_SIZE(mfi_cols_map), "mfi column maps are not in sync");
static_assert(ARRAY_SIZE(dbpli_cols_map) == ARRAY_SIZE(pli_cols_map), "pli column maps are not in sync");
static_assert(ARRAY_SIZE(qi_cols_map) == ARRAY_SIZE(qi_mfi_map), "queue_item column maps are not in sync");
for (i = 0; i < ARRAY_SIZE(qi_cols_map); i++)
{
assert(qi_cols_map[i].offset == qi_mfi_map[i].qi_offset);
}
db_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_path");
db_rating_updates = cfg_getbool(cfg_getsec(cfg, "library"), "rating_updates");
DPRINTF(E_LOG, L_DB, "Configured to use database file '%s'\n", db_path);
ret = sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "Could not switch SQLite3 to multithread mode\n");
DPRINTF(E_FATAL, L_DB, "Check that SQLite3 has been configured for thread-safe operations\n");
return -1;
}
ret = sqlite3_enable_shared_cache(1);
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "Could not enable SQLite3 shared-cache mode\n");
return -1;
}
ret = sqlite3_initialize();
if (ret != SQLITE_OK)
{
DPRINTF(E_FATAL, L_DB, "SQLite3 failed to initialize\n");
return -1;
}
ret = db_open();
if (ret < 0)
{
DPRINTF(E_FATAL, L_DB, "Could not open database\n");
return -1;
}
ret = db_check_version();
if (ret < 0)
{
DPRINTF(E_FATAL, L_DB, "Database version check errored out, incompatible database\n");
db_perthread_deinit();
return -1;
}
else if (ret > 0)
{
DPRINTF(E_LOG, L_DB, "Could not check database version, trying DB init\n");
ret = db_init_tables(hdl);
if (ret < 0)
{
DPRINTF(E_FATAL, L_DB, "Could not create tables\n");
db_perthread_deinit();
return -1;
}
}
db_set_cfg_names();
CHECK_ERR(L_DB, db_files_get_count(&files, NULL, NULL));
CHECK_ERR(L_DB, db_pl_get_count(&pls));
db_admin_setint64(DB_ADMIN_START_TIME, (int64_t) time(NULL));
db_perthread_deinit();
DPRINTF(E_LOG, L_DB, "Database OK with %" PRIu32 " active files and %" PRIu32 " active playlists\n", files, pls);
rng_init(&shuffle_rng);
return 0;
}
void
db_deinit(void)
{
sqlite3_shutdown();
}