Merge pull request #210 from chme/mpddb

Rework mpd command lsinfo
This commit is contained in:
ejurgensen 2016-02-06 11:01:56 +01:00
commit c4c60488de
12 changed files with 2574 additions and 1798 deletions

View File

@ -244,257 +244,6 @@ sqlext_daap_unicode_xcollation(void *notused, int llen, const void *left, int rl
return rpp;
}
/* Taken from "extension-functions.c" by Liam Healy (2010-02-06 15:45:07)
http://www.sqlite.org/contrib/download/extension-functions.c?get=25 */
/* LMH from sqlite3 3.3.13 */
/*
** This table maps from the first byte of a UTF-8 character to the number
** of trailing bytes expected. A value '4' indicates that the table key
** is not a legal first byte for a UTF-8 character.
*/
static const uint8_t xtra_utf8_bytes[256] = {
/* 0xxxxxxx */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 10wwwwww */
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
/* 110yyyyy */
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
/* 1110zzzz */
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
/* 11110yyy */
3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
};
/*
** This table maps from the number of trailing bytes in a UTF-8 character
** to an integer constant that is effectively calculated for each character
** read by a naive implementation of a UTF-8 character reader. The code
** in the READ_UTF8 macro explains things best.
*/
static const int xtra_utf8_bits[] = {
0,
12416, /* (0xC0 << 6) + (0x80) */
925824, /* (0xE0 << 12) + (0x80 << 6) + (0x80) */
63447168 /* (0xF0 << 18) + (0x80 << 12) + (0x80 << 6) + 0x80 */
};
/*
** If a UTF-8 character contains N bytes extra bytes (N bytes follow
** the initial byte so that the total character length is N+1) then
** masking the character with utf8_mask[N] must produce a non-zero
** result. Otherwise, we have an (illegal) overlong encoding.
*/
static const int utf_mask[] = {
0x00000000,
0xffffff80,
0xfffff800,
0xffff0000,
};
/* LMH salvaged from sqlite3 3.3.13 source code src/utf.c */
#define READ_UTF8(zIn, c) { \
int xtra; \
c = *(zIn)++; \
xtra = xtra_utf8_bytes[c]; \
switch( xtra ){ \
case 4: c = (int)0xFFFD; break; \
case 3: c = (c<<6) + *(zIn)++; \
case 2: c = (c<<6) + *(zIn)++; \
case 1: c = (c<<6) + *(zIn)++; \
c -= xtra_utf8_bits[xtra]; \
if( (utf_mask[xtra]&c)==0 \
|| (c&0xFFFFF800)==0xD800 \
|| (c&0xFFFFFFFE)==0xFFFE ){ c = 0xFFFD; } \
} \
}
static int sqlite3ReadUtf8(const unsigned char *z)
{
int c;
READ_UTF8(z, c);
return c;
}
/*
* X is a pointer to the first byte of a UTF-8 character. Increment
* X so that it points to the next character. This only works right
* if X points to a well-formed UTF-8 string.
*/
#define sqliteNextChar(X) while( (0xc0&*++(X))==0x80 ){}
#define sqliteCharVal(X) sqlite3ReadUtf8(X)
/*
* Given a string z1, retutns the (0 based) index of it's first occurence
* in z2 after the first s characters.
* Returns -1 when there isn't a match.
* updates p to point to the character where the match occured.
* This is an auxiliary function.
*/
static int _substr(const char* z1, const char* z2, int s, const char** p)
{
int c = 0;
int rVal = -1;
const char* zt1;
const char* zt2;
int c1, c2;
if ('\0' == *z1)
{
return -1;
}
while ((sqliteCharVal((unsigned char *)z2) != 0) && (c++) < s)
{
sqliteNextChar(z2);
}
c = 0;
while ((sqliteCharVal((unsigned char * )z2)) != 0)
{
zt1 = z1;
zt2 = z2;
do
{
c1 = sqliteCharVal((unsigned char * )zt1);
c2 = sqliteCharVal((unsigned char * )zt2);
sqliteNextChar(zt1);
sqliteNextChar(zt2);
} while (c1 == c2 && c1 != 0 && c2 != 0);
if (c1 == 0)
{
rVal = c;
break;
}
sqliteNextChar(z2);
++c;
}
if (p)
{
*p = z2;
}
return rVal >= 0 ? rVal + s : rVal;
}
/*
* Taken from "extension-functions.c" (function charindexFunc) by Liam Healy (2010-02-06 15:45:07)
* http://www.sqlite.org/contrib/download/extension-functions.c?get=25
*
* Given 2 input strings (s1,s2) and an integer (n) searches from the nth character
* for the string s1. Returns the position where the match occured.
* Characters are counted from 1.
* 0 is returned when no match occurs.
*/
static void sqlext_daap_charindex_xfunc(sqlite3_context *context, int argc, sqlite3_value **argv)
{
const uint8_t *z1; /* s1 string */
uint8_t *z2; /* s2 string */
int s = 0;
int rVal = 0;
//assert(argc == 3 || argc == 2);
if (argc != 2 && argc != 3)
{
sqlite3_result_error(context, "daap_charindex() requires 2 or 3 parameters", -1);
return;
}
if ( SQLITE_NULL == sqlite3_value_type(argv[0]) || SQLITE_NULL == sqlite3_value_type(argv[1]))
{
sqlite3_result_null(context);
return;
}
z1 = sqlite3_value_text(argv[0]);
if (z1 == 0)
return;
z2 = (uint8_t*) sqlite3_value_text(argv[1]);
if (argc == 3)
{
s = sqlite3_value_int(argv[2]) - 1;
if (s < 0)
{
s = 0;
}
}
else
{
s = 0;
}
rVal = _substr((char *) z1, (char *) z2, s, NULL);
sqlite3_result_int(context, rVal + 1);
}
/*
* Taken from "extension-functions.c" (function leftFunc) by Liam Healy (2010-02-06 15:45:07)
* http://www.sqlite.org/contrib/download/extension-functions.c?get=25
*
* Given a string (s) and an integer (n) returns the n leftmost (UTF-8) characters
* if the string has a length<=n or is NULL this function is NOP
*/
static void sqlext_daap_leftstr_xfunc(sqlite3_context *context, int argc, sqlite3_value **argv)
{
int c = 0;
int cc = 0;
int l = 0;
const unsigned char *z; /* input string */
const unsigned char *zt;
unsigned char *rz; /* output string */
//assert( argc==2);
if (argc != 2 && argc != 3)
{
sqlite3_result_error(context, "daap_leftstr() requires 2 parameters", -1);
return;
}
if ( SQLITE_NULL == sqlite3_value_type(argv[0]) || SQLITE_NULL == sqlite3_value_type(argv[1]))
{
sqlite3_result_null(context);
return;
}
z = sqlite3_value_text(argv[0]);
l = sqlite3_value_int(argv[1]);
zt = z;
while ( sqliteCharVal(zt) && c++ < l)
sqliteNextChar(zt);
cc = zt - z;
rz = sqlite3_malloc(zt - z + 1);
if (!rz)
{
sqlite3_result_error_nomem(context);
return;
}
strncpy((char*) rz, (char*) z, zt - z);
*(rz + cc) = '\0';
sqlite3_result_text(context, (char*) rz, -1, SQLITE_TRANSIENT);
sqlite3_free(rz);
}
int
sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi)
{
@ -519,23 +268,5 @@ sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines
return -1;
}
ret = sqlite3_create_function(db, "daap_leftstr", 2, SQLITE_UTF8, NULL, sqlext_daap_leftstr_xfunc, NULL, NULL);
if (ret != SQLITE_OK)
{
if (pzErrMsg)
*pzErrMsg = sqlite3_mprintf("Could not create daap_leftstr function: %s\n", sqlite3_errmsg(db));
return -1;
}
ret = sqlite3_create_function(db, "daap_charindex", 3, SQLITE_UTF8, NULL, sqlext_daap_charindex_xfunc, NULL, NULL);
if (ret != SQLITE_OK)
{
if (pzErrMsg)
*pzErrMsg = sqlite3_mprintf("Could not create daap_charindex function: %s\n", sqlite3_errmsg(db));
return -1;
}
return 0;
}

View File

@ -71,6 +71,7 @@ forked_daapd_LDADD = -lrt \
forked_daapd_SOURCES = main.c \
db.c db.h \
db_upgrade.c db_upgrade.h \
logger.c logger.h \
conffile.c conffile.h \
cache.c cache.h \

1816
src/db.c

File diff suppressed because it is too large Load Diff

View File

@ -61,12 +61,6 @@ enum query_type {
#define ARTWORK_SPOTIFY 6
#define ARTWORK_HTTP 7
enum filelistitem_type {
F_PLAYLIST = 1,
F_DIR = 2,
F_FILE = 3,
};
struct query_params {
/* Query parameters, filled in by caller */
enum query_type type;
@ -188,6 +182,9 @@ struct media_file_info {
char *album_artist_sort;
char *virtual_path;
uint32_t directory_id; /* Id of directory */
uint32_t date_released;
};
#define mfi_offsetof(field) offsetof(struct media_file_info, field)
@ -215,6 +212,7 @@ struct playlist_info {
uint32_t special_id; /* iTunes identifies certain 'special' playlists with special meaning */
char *virtual_path; /* virtual path of underlying playlist */
uint32_t parent_id; /* Id of parent playlist if the playlist is nested */
uint32_t directory_id; /* Id of directory */
};
#define pli_offsetof(field) offsetof(struct playlist_info, field)
@ -233,6 +231,7 @@ struct db_playlist_info {
char *special_id;
char *virtual_path;
char *parent_id;
char *directory_id;
};
#define dbpli_offsetof(field) offsetof(struct db_playlist_info, field)
@ -322,16 +321,12 @@ struct db_media_file_info {
char *composer_sort;
char *album_artist_sort;
char *virtual_path;
char *directory_id;
char *date_released;
};
#define dbmfi_offsetof(field) offsetof(struct db_media_file_info, field)
struct filelist_info {
char *virtual_path;
uint32_t time_modified;
enum filelistitem_type type;
};
struct watch_info {
int wd;
char *path;
@ -353,15 +348,35 @@ struct filecount_info {
uint32_t length;
};
/* Directory ids must be in sync with the ids in Q_DIR* in db.c */
enum directory_ids {
DIR_ROOT = 1,
DIR_FILE = 2,
DIR_HTTP = 3,
DIR_SPOTIFY = 4,
};
struct directory_info {
uint32_t id;
char *virtual_path;
uint32_t db_timestamp;
uint32_t disabled;
uint32_t parent_id;
};
struct directory_enum {
int parent_id;
/* Private enum context, keep out */
sqlite3_stmt *stmt;
};
char *
db_escape_string(const char *str);
void
free_pi(struct pairing_info *pi, int content_only);
void
free_fi(struct filelist_info *fi, int content_only);
void
free_mfi(struct media_file_info *mfi, int content_only);
@ -371,6 +386,9 @@ unicode_fixup_mfi(struct media_file_info *mfi);
void
free_pli(struct playlist_info *pli, int content_only);
void
free_di(struct directory_info *di, int content_only);
/* Maintenance and DB hygiene */
void
db_hook_post_scan(void);
@ -495,6 +513,9 @@ db_file_disable_bymatch(char *path, char *strip, uint32_t cookie);
int
db_file_enable_bycookie(uint32_t cookie, char *path);
int
db_file_update_directoryid(char *path, int dir_id);
/* Playlists */
int
db_pl_get_count(void);
@ -551,12 +572,34 @@ db_groups_clear(void);
int
db_group_persistentid_byid(int id, int64_t *persistentid);
/* Filelist */
/* Directories */
int
db_mpd_start_query_filelist(struct query_params *qp, char *path);
db_directory_id_byvirtualpath(char *virtual_path);
int
db_mpd_query_fetch_filelist(struct query_params *qp, struct filelist_info *fi);
db_directory_enum_start(struct directory_enum *de);
int
db_directory_enum_fetch(struct directory_enum *de, struct directory_info *di);
void
db_directory_enum_end(struct directory_enum *de);
int
db_directory_addorupdate(char *virtual_path, int disabled, int parent_id);
void
db_directory_ping_bymatch(char *path);
void
db_directory_disable_bymatch(char *path, char *strip, uint32_t cookie);
int
db_directory_enable_bycookie(uint32_t cookie, char *path);
int
db_directory_enable_bypath(char *path);
/* Remotes */
int

1563
src/db_upgrade.c Normal file

File diff suppressed because it is too large Load Diff

27
src/db_upgrade.h Normal file
View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2015 Christian Meffert <christian.meffert@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
*/
#ifndef SRC_DB_UPGRADE_H_
#define SRC_DB_UPGRADE_H_
#include <sqlite3.h>
int
db_upgrade(sqlite3 *hdl, int db_ver);
#endif /* SRC_DB_UPGRADE_H_ */

View File

@ -23,6 +23,7 @@
# include <config.h>
#endif
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
@ -112,14 +113,15 @@ struct deferred_pl {
char *path;
time_t mtime;
struct deferred_pl *next;
int directory_id;
};
struct stacked_dir {
char *path;
int parent_id;
struct stacked_dir *next;
};
static int cmd_pipe[2];
static int exit_pipe[2];
static int scan_exit;
@ -197,7 +199,7 @@ nonblock_command(struct filescanner_command *cmd)
}
static int
push_dir(struct stacked_dir **s, char *path)
push_dir(struct stacked_dir **s, char *path, int parent_id)
{
struct stacked_dir *d;
@ -215,28 +217,26 @@ push_dir(struct stacked_dir **s, char *path)
return -1;
}
d->parent_id = parent_id;
d->next = *s;
*s = d;
return 0;
}
static char *
static struct stacked_dir *
pop_dir(struct stacked_dir **s)
{
struct stacked_dir *d;
char *ret;
if (!*s)
return NULL;
d = *s;
*s = d->next;
ret = d->path;
free(d);
return ret;
return d;
}
#ifdef HAVE_REGEX_H
@ -633,7 +633,7 @@ fixup_tags(struct media_file_info *mfi)
void
filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi)
filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi, int dir_id)
{
struct media_file_info *mfi;
char *filename;
@ -765,6 +765,8 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
mfi->virtual_path = strdup(virtual_path);
}
mfi->directory_id = dir_id;
if (mfi->id == 0)
db_file_add(mfi);
else
@ -776,13 +778,13 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
}
static void
process_playlist(char *file, time_t mtime)
process_playlist(char *file, time_t mtime, int dir_id)
{
enum file_type ft;
ft = file_type_get(file);
if (ft == FILE_PLAYLIST)
scan_playlist(file, mtime);
scan_playlist(file, mtime, dir_id);
#ifdef ITUNES
else if (ft == FILE_ITUNES)
scan_itunes_itml(file);
@ -791,7 +793,7 @@ process_playlist(char *file, time_t mtime)
/* Thread: scan */
static void
defer_playlist(char *path, time_t mtime)
defer_playlist(char *path, time_t mtime, int dir_id)
{
struct deferred_pl *pl;
@ -815,6 +817,7 @@ defer_playlist(char *path, time_t mtime)
}
pl->mtime = mtime;
pl->directory_id = dir_id;
pl->next = playlists;
playlists = pl;
@ -831,7 +834,7 @@ process_deferred_playlists(void)
{
playlists = pl->next;
process_playlist(pl->path, pl->mtime);
process_playlist(pl->path, pl->mtime, pl->directory_id);
free(pl->path);
free(pl);
@ -843,7 +846,7 @@ process_deferred_playlists(void)
/* Thread: scan */
static void
process_file(char *file, time_t mtime, off_t size, int type, int flags)
process_file(char *file, time_t mtime, off_t size, int type, int flags, int dir_id)
{
int is_bulkscan;
@ -852,7 +855,7 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
switch (file_type_get(file))
{
case FILE_REGULAR:
filescanner_process_media(file, mtime, size, type, NULL);
filescanner_process_media(file, mtime, size, type, NULL, dir_id);
cache_artwork_ping(file, mtime, !is_bulkscan);
// TODO [artworkcache] If entry in artwork cache exists for no artwork available, delete the entry if media file has embedded artwork
@ -871,14 +874,14 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
case FILE_PLAYLIST:
case FILE_ITUNES:
if (flags & F_SCAN_BULK)
defer_playlist(file, mtime);
defer_playlist(file, mtime, dir_id);
else
process_playlist(file, mtime);
process_playlist(file, mtime, dir_id);
break;
case FILE_SMARTPL:
DPRINTF(E_DBG, L_SCAN, "Smart playlist file: %s\n", file);
scan_smartpl(file, mtime);
scan_smartpl(file, mtime, dir_id);
break;
case FILE_ARTWORK:
@ -948,8 +951,22 @@ check_speciallib(char *path, const char *libtype)
}
/* Thread: scan */
static int
create_virtual_path(char *path, char *virtual_path, int virtual_path_len)
{
int ret;
ret = snprintf(virtual_path, virtual_path_len, "/file:%s", path);
if ((ret < 0) || (ret >= virtual_path_len))
{
DPRINTF(E_LOG, L_SCAN, "Virtual path /file:%s, PATH_MAX exceeded\n", path);
return -1;
}
return 0;
}
static void
process_directory(char *path, int flags)
process_directory(char *path, int flags, int parent_id)
{
DIR *dirp;
struct dirent buf;
@ -962,6 +979,8 @@ process_directory(char *path, int flags)
struct kevent kev;
#endif
int type;
char virtual_path[PATH_MAX];
int dir_id;
int ret;
DPRINTF(E_DBG, L_SCAN, "Processing directory %s (flags = 0x%x)\n", path, flags);
@ -974,6 +993,18 @@ process_directory(char *path, int flags)
return;
}
/* Add/update directories table */
ret = create_virtual_path(path, virtual_path, sizeof(virtual_path));
if (ret < 0)
return;
dir_id = db_directory_addorupdate(virtual_path, 0, parent_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path);
}
/* Check if compilation and/or podcast directory */
type = 0;
if (check_speciallib(path, "compilations"))
@ -1050,15 +1081,15 @@ process_directory(char *path, int flags)
if (S_ISREG(sb.st_mode))
{
if (!(flags & F_SCAN_FAST))
process_file(entry, sb.st_mtime, sb.st_size, F_SCAN_TYPE_FILE | type, flags);
process_file(entry, sb.st_mtime, sb.st_size, F_SCAN_TYPE_FILE | type, flags, dir_id);
}
else if (S_ISFIFO(sb.st_mode))
{
if (!(flags & F_SCAN_FAST))
process_file(entry, sb.st_mtime, sb.st_size, F_SCAN_TYPE_PIPE | type, flags);
process_file(entry, sb.st_mtime, sb.st_size, F_SCAN_TYPE_PIPE | type, flags, dir_id);
}
else if (S_ISDIR(sb.st_mode))
push_dir(&dirstack, entry);
push_dir(&dirstack, entry, dir_id);
else
DPRINTF(E_LOG, L_SCAN, "Skipping %s, not a directory, symlink, pipe nor regular file\n", entry);
}
@ -1116,21 +1147,58 @@ process_directory(char *path, int flags)
}
/* Thread: scan */
static void
process_directories(char *root, int flags)
{
char *path;
process_directory(root, flags);
static int
process_parent_directories(char *path)
{
char *ptr;
int dir_id;
char buf[PATH_MAX];
char virtual_path[PATH_MAX];
int ret;
dir_id = DIR_FILE;
ptr = path + 1;
while (ptr && (ptr = strchr(ptr, '/')))
{
strncpy(buf, path, (ptr - path));
buf[(ptr - path)] = '\0';
ret = create_virtual_path(buf, virtual_path, sizeof(virtual_path));
if (ret < 0)
return 0;
dir_id = db_directory_addorupdate(virtual_path, 0, dir_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path);
return 0;
}
ptr++;
}
return dir_id;
}
static void
process_directories(char *root, int parent_id, int flags)
{
struct stacked_dir *dir;
process_directory(root, flags, parent_id);
if (scan_exit)
return;
while ((path = pop_dir(&dirstack)))
while ((dir = pop_dir(&dirstack)))
{
process_directory(path, flags);
process_directory(dir->path, flags, dir->parent_id);
free(path);
free(dir->path);
free(dir);
if (scan_exit)
return;
@ -1148,6 +1216,7 @@ bulk_scan(int flags)
char *deref;
time_t start;
time_t end;
int parent_id;
int i;
// Set global flag to avoid queued scan requests
@ -1165,6 +1234,8 @@ bulk_scan(int flags)
{
path = cfg_getnstr(lib, "directories", i);
parent_id = process_parent_directories(path);
deref = m_realpath(path);
if (!deref)
{
@ -1173,16 +1244,19 @@ bulk_scan(int flags)
/* Assume dir is mistakenly not mounted, so just disable everything and update timestamps */
db_file_disable_bymatch(path, "", 0);
db_pl_disable_bymatch(path, "", 0);
db_directory_disable_bymatch(path, "", 0);
db_file_ping_bymatch(path, 1);
db_pl_ping_bymatch(path, 1);
db_directory_ping_bymatch(path);
continue;
}
counter = 0;
db_transaction_begin();
process_directories(deref, flags);
process_directories(deref, parent_id, flags);
db_transaction_end();
free(deref);
@ -1306,6 +1380,28 @@ filescanner(void *arg)
pthread_exit(NULL);
}
static int
get_parent_dir_id(const char *path)
{
char *pathcopy;
char *parent_dir;
char virtual_path[PATH_MAX];
int parent_id;
int ret;
pathcopy = strdup(path);
parent_dir = dirname(pathcopy);
ret = create_virtual_path(parent_dir, virtual_path, sizeof(virtual_path));
if (ret == 0)
parent_id = db_directory_id_byvirtualpath(virtual_path);
else
parent_id = 0;
free(pathcopy);
return parent_id;
}
#if defined(__linux__)
static int
@ -1347,6 +1443,7 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
char *s;
int flags = 0;
int ret;
int parent_id;
DPRINTF(E_SPAM, L_SCAN, "Directory event: 0x%x, cookie 0x%x, wd %d\n", ie->mask, ie->cookie, wi->wd);
@ -1354,6 +1451,7 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
{
db_file_disable_bymatch(path, "", 0);
db_pl_disable_bymatch(path, "", 0);
db_directory_disable_bymatch(path, "", 0);
}
if (ie->mask & IN_MOVE_SELF)
@ -1408,6 +1506,7 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
db_watch_mark_bymatch(path, path, ie->cookie);
db_file_disable_bymatch(path, path, ie->cookie);
db_pl_disable_bymatch(path, path, ie->cookie);
db_directory_disable_bymatch(path, path, ie->cookie);
}
if (ie->mask & IN_MOVED_TO)
@ -1417,6 +1516,7 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
db_watch_move_bycookie(ie->cookie, path);
db_file_enable_bycookie(ie->cookie, path);
db_pl_enable_bycookie(ie->cookie, path);
db_directory_enable_bycookie(ie->cookie, path);
/* We'll rescan the directory tree to update playlists */
flags |= F_SCAN_MOVED;
@ -1450,6 +1550,7 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
db_file_disable_bymatch(path, "", 0);
db_pl_disable_bymatch(path, "", 0);
db_directory_disable_bymatch(path, "", 0);
}
else if (ret < 0)
{
@ -1465,7 +1566,8 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
if (ie->mask & IN_CREATE)
{
process_directories(path, flags);
parent_id = get_parent_dir_id(path);
process_directories(path, parent_id, flags);
if (dirstack)
DPRINTF(E_LOG, L_SCAN, "WARNING: unhandled leftover directories\n");
@ -1480,8 +1582,12 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
uint32_t path_hash;
char *deref = NULL;
char *file = path;
char *dir;
char dir_vpath[PATH_MAX];
int type;
int i;
int dir_id;
char *ptr;
int ret;
DPRINTF(E_SPAM, L_SCAN, "File event: 0x%x, cookie 0x%x, wd %d\n", ie->mask, ie->cookie, wi->wd);
@ -1541,7 +1647,28 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
ret = db_file_enable_bycookie(ie->cookie, path);
if (ret <= 0)
if (ret > 0)
{
// If file was successfully enabled, update the directory id
dir = strdup(path);
ptr = strrchr(dir, '/');
dir[(ptr - dir)] = '\0';
ret = create_virtual_path(dir, dir_vpath, sizeof(dir_vpath));
if (ret >= 0)
{
dir_id = db_directory_id_byvirtualpath(dir_vpath);
if (dir_id > 0)
{
ret = db_file_update_directoryid(path, dir_id);
if (ret < 0)
DPRINTF(E_LOG, L_SCAN, "Error updating directory id for file: %s\n", path);
}
}
free(dir);
}
else
{
/* It's not a known media file, so it's either a new file
* or a playlist, known or not.
@ -1636,10 +1763,14 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
if (check_speciallib(path, "audiobooks"))
type |= F_SCAN_TYPE_AUDIOBOOK;
dir_id = get_parent_dir_id(file);
if (S_ISREG(sb.st_mode))
process_file(file, sb.st_mtime, sb.st_size, F_SCAN_TYPE_FILE | type, 0);
{
process_file(file, sb.st_mtime, sb.st_size, F_SCAN_TYPE_FILE | type, 0, dir_id);
}
else if (S_ISFIFO(sb.st_mode))
process_file(file, sb.st_mtime, sb.st_size, F_SCAN_TYPE_PIPE | type, 0);
process_file(file, sb.st_mtime, sb.st_size, F_SCAN_TYPE_PIPE | type, 0, dir_id);
if (deref)
free(deref);
@ -1777,6 +1908,7 @@ kqueue_cb(int fd, short event, void *arg)
int w_len;
int need_rescan;
int ret;
int parent_id;
ts.tv_sec = 0;
ts.tv_nsec = 0;
@ -1839,6 +1971,7 @@ kqueue_cb(int fd, short event, void *arg)
/* Disable files */
db_file_disable_bymatch(wi.path, "", 0);
db_pl_disable_bymatch(wi.path, "", 0);
db_directory_disable_bymatch(wi.path, "", 0);
if (kev.flags & EV_ERROR)
{
@ -1921,17 +2054,21 @@ kqueue_cb(int fd, short event, void *arg)
}
if (need_rescan)
push_dir(&rescan, wi.path);
{
parent_id = get_parent_dir_id(wi.path);
push_dir(&rescan, wi.path, parent_id);
}
}
free(wi.path);
}
while ((path = pop_dir(&rescan)))
while ((d = pop_dir(&rescan)))
{
process_directories(path, 0);
process_directories(d->path, 0, d->parent_id);
free(path);
free(d->path);
free(d);
if (rescan)
DPRINTF(E_LOG, L_SCAN, "WARNING: unhandled leftover directories\n");

View File

@ -19,7 +19,7 @@ void
filescanner_deinit(void);
void
filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi);
filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi, int dir_id);
/* Actual scanners */
int
@ -29,10 +29,10 @@ int
scan_metadata_icy(char *url, struct media_file_info *mfi);
void
scan_playlist(char *file, time_t mtime);
scan_playlist(char *file, time_t mtime, int dir_id);
void
scan_smartpl(char *file, time_t mtime);
scan_smartpl(char *file, time_t mtime, int dir_id);
#ifdef ITUNES
void

View File

@ -74,7 +74,7 @@ extinf_get(char *string, struct media_file_info *mfi, int *extinf)
}
void
scan_playlist(char *file, time_t mtime)
scan_playlist(char *file, time_t mtime, int dir_id)
{
FILE *fp;
struct media_file_info mfi;
@ -172,6 +172,8 @@ scan_playlist(char *file, time_t mtime)
*ptr = '\0';
pli->virtual_path = strdup(virtual_path);
pli->directory_id = dir_id;
ret = db_pl_add(pli, &pl_id);
if (ret < 0)
{
@ -236,7 +238,7 @@ scan_playlist(char *file, time_t mtime)
if (extinf)
DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", mfi.artist, mfi.title);
filescanner_process_media(filename, mtime, 0, F_SCAN_TYPE_URL, &mfi);
filescanner_process_media(filename, mtime, 0, F_SCAN_TYPE_URL, &mfi, DIR_HTTP);
}
/* Regular file, should already be in library */
else

View File

@ -171,7 +171,7 @@ smartpl_parse_file(const char *file, struct playlist_info *pli)
}
void
scan_smartpl(char *file, time_t mtime)
scan_smartpl(char *file, time_t mtime, int dir_id)
{
struct playlist_info *pli;
int pl_id;
@ -203,6 +203,8 @@ scan_smartpl(char *file, time_t mtime)
else
pl_id = pli->id;
pli->directory_id = dir_id;
ret = smartpl_parse_file(file, pli);
if (ret < 0)
{

342
src/mpd.c
View File

@ -1557,13 +1557,13 @@ mpd_queueitem_make(char *path, int recursive)
if (recursive)
{
qp.filter = sqlite3_mprintf("f.virtual_path LIKE '/%q%%'", path);
qp.filter = sqlite3_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q%%'", path);
if (!qp.filter)
DPRINTF(E_DBG, L_PLAYER, "Out of memory\n");
}
else
{
qp.filter = sqlite3_mprintf("f.virtual_path LIKE '/%q'", path);
qp.filter = sqlite3_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q'", path);
if (!qp.filter)
DPRINTF(E_DBG, L_PLAYER, "Out of memory\n");
}
@ -2685,6 +2685,214 @@ mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
return 0;
}
static int
mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int listinfo, char **errmsg)
{
struct directory_info subdir;
struct query_params qp;
struct directory_enum dir_enum;
struct db_playlist_info dbpli;
char modified[32];
uint32_t time_modified;
struct db_media_file_info dbmfi;
int ret;
// Load playlists for dir-id
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_PL;
qp.sort = S_PLAYLIST;
qp.idx_type = I_NONE;
qp.filter = sqlite3_mprintf("(f.directory_id = %d AND (f.type = %d OR f.type = %d))", directory_id, PL_PLAIN, PL_SMART);
ret = db_query_start(&qp);
if (ret < 0)
{
db_query_end(&qp);
ret = asprintf(errmsg, "Could not start query");
if (ret < 0)
DPRINTF(E_LOG, L_MPD, "Out of memory\n");
return ACK_ERROR_UNKNOWN;
}
while (((ret = db_query_fetch_pl(&qp, &dbpli)) == 0) && (dbpli.id))
{
if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0)
{
DPRINTF(E_LOG, L_MPD, "Error converting time modified to uint32_t: %s\n", dbpli.db_timestamp);
return -1;
}
if (listinfo)
{
mpd_time(modified, sizeof(modified), time_modified);
evbuffer_add_printf(evbuf,
"playlist: %s\n"
"Last-Modified: %s\n",
(dbpli.virtual_path + 1),
modified);
}
else
{
evbuffer_add_printf(evbuf,
"playlist: %s\n",
(dbpli.virtual_path + 1));
}
}
db_query_end(&qp);
sqlite3_free(qp.filter);
// Load sub directories for dir-id
memset(&dir_enum, 0, sizeof(struct directory_enum));
dir_enum.parent_id = directory_id;
ret = db_directory_enum_start(&dir_enum);
if (ret < 0)
{
DPRINTF(E_LOG, L_MPD, "Failed to start directory enum for parent_id %d\n", directory_id);
return -1;
}
while ((ret = db_directory_enum_fetch(&dir_enum, &subdir)) == 0 && subdir.id > 0)
{
if (listinfo)
{
evbuffer_add_printf(evbuf,
"directory: %s\n"
"Last-Modified: %s\n",
(subdir.virtual_path + 1),
"2015-12-01 00:00");
}
else
{
evbuffer_add_printf(evbuf,
"directory: %s\n",
(subdir.virtual_path + 1));
}
if (listall)
{
mpd_add_directory(evbuf, subdir.id, listall, listinfo, errmsg);
}
}
db_directory_enum_end(&dir_enum);
// Load files for dir-id
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_ITEMS;
qp.sort = S_ARTIST;
qp.idx_type = I_NONE;
qp.filter = sqlite3_mprintf("(f.directory_id = %d)", directory_id);
ret = db_query_start(&qp);
if (ret < 0)
{
db_query_end(&qp);
ret = asprintf(errmsg, "Could not start query");
if (ret < 0)
DPRINTF(E_LOG, L_MPD, "Out of memory\n");
return ACK_ERROR_UNKNOWN;
}
while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
{
if (listinfo)
{
ret = mpd_add_db_media_file_info(evbuf, &dbmfi);
if (ret < 0)
{
DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id);
}
}
else
{
evbuffer_add_printf(evbuf,
"file: %s\n",
(dbmfi.virtual_path + 1));
}
}
db_query_end(&qp);
return 0;
}
static int
mpd_command_listall(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
{
int dir_id;
char parent[PATH_MAX];
int ret;
if (argc < 2 || strlen(argv[1]) == 0
|| (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1))
{
ret = snprintf(parent, sizeof(parent), "/");
}
else if (strncmp(argv[1], "/", 1) == 0)
{
ret = snprintf(parent, sizeof(parent), "%s/", argv[1]);
}
else
{
ret = snprintf(parent, sizeof(parent), "/%s", argv[1]);
}
if ((ret < 0) || (ret >= sizeof(parent)))
{
DPRINTF(E_INFO, L_MPD, "Parent path exceeds PATH_MAX\n");
return -1;
}
// Load dir-id from db for parent-path
dir_id = db_directory_id_byvirtualpath(parent);
if (dir_id == 0)
{
DPRINTF(E_LOG, L_MPD, "Directory info not found for virtual-path '%s'\n", parent);
return -1;
}
ret = mpd_add_directory(evbuf, dir_id, 1, 0, errmsg);
return ret;
}
static int
mpd_command_listallinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
{
int dir_id;
char parent[PATH_MAX];
int ret;
if (argc < 2 || strlen(argv[1]) == 0
|| (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1))
{
ret = snprintf(parent, sizeof(parent), "/");
}
else if (strncmp(argv[1], "/", 1) == 0)
{
ret = snprintf(parent, sizeof(parent), "%s/", argv[1]);
}
else
{
ret = snprintf(parent, sizeof(parent), "/%s", argv[1]);
}
if ((ret < 0) || (ret >= sizeof(parent)))
{
ret = asprintf(errmsg, "Parent path exceeds PATH_MAX\n");
if (ret < 0)
DPRINTF(E_LOG, L_MPD, "Out of memory\n");
return -1;
}
// Load dir-id from db for parent-path
dir_id = db_directory_id_byvirtualpath(parent);
if (dir_id == 0)
{
ret = asprintf(errmsg, "Directory info not found for virtual-path '%s'\n", parent);
if (ret < 0)
DPRINTF(E_LOG, L_MPD, "Out of memory\n");
return -1;
}
ret = mpd_add_directory(evbuf, dir_id, 1, 1, errmsg);
return ret;
}
/*
* Command handler function for 'lsinfo'
* Lists the contents of the directory given in argv[1].
@ -2692,11 +2900,8 @@ mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
static int
mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
{
struct query_params qp;
int dir_id;
char parent[PATH_MAX];
struct filelist_info *fi;
struct media_file_info *mfi;
char modified[32];
int print_playlists;
int ret;
@ -2711,7 +2916,7 @@ mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
}
else
{
ret = snprintf(parent, sizeof(parent), "/%s/", argv[1]);
ret = snprintf(parent, sizeof(parent), "/%s", argv[1]);
}
if ((ret < 0) || (ret >= sizeof(parent)))
@ -2731,76 +2936,24 @@ mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
print_playlists = 1;
}
fi = (struct filelist_info*)malloc(sizeof(struct filelist_info));
if (!fi)
// Load dir-id from db for parent-path
dir_id = db_directory_id_byvirtualpath(parent);
if (dir_id == 0)
{
DPRINTF(E_LOG, L_MPD, "Out of memory for fi\n");
return ACK_ERROR_UNKNOWN;
DPRINTF(E_LOG, L_MPD, "Directory info not found for virtual-path '%s'\n", parent);
return -1;
}
memset(&qp, 0, sizeof(struct query_params));
ret = mpd_add_directory(evbuf, dir_id, 0, 1, errmsg);
ret = db_mpd_start_query_filelist(&qp, parent);
if (ret < 0)
// If the root directory was passed as argument add the stored playlists to the response
if (ret == 0 && print_playlists)
{
ret = asprintf(errmsg, "Could not start query for path '%s'", argv[1]);
if (ret < 0)
DPRINTF(E_LOG, L_MPD, "Out of memory\n");
free_fi(fi, 0);
return ACK_ERROR_UNKNOWN;
}
while (((ret = db_mpd_query_fetch_filelist(&qp, fi)) == 0) && (fi->virtual_path))
{
if (fi->type == F_DIR)
{
mpd_time(modified, sizeof(modified), fi->time_modified);
evbuffer_add_printf(evbuf,
"directory: %s\n"
"Last-Modified: %s\n",
(fi->virtual_path + 1),
modified);
}
else if (fi->type == F_PLAYLIST)
{
mpd_time(modified, sizeof(modified), fi->time_modified);
evbuffer_add_printf(evbuf,
"playlist: %s\n"
"Last-Modified: %s\n",
(fi->virtual_path + 1),
modified);
}
else if (fi->type == F_FILE)
{
mfi = db_file_fetch_byvirtualpath(fi->virtual_path);
if (mfi)
{
ret = mpd_add_mediainfo(evbuf, mfi, 0, -1);
if (ret < 0)
{
DPRINTF(E_LOG, L_MPD, "Could not add mediainfo for path '%s'\n", fi->virtual_path);
}
free_mfi(mfi, 0);
}
}
}
db_query_end(&qp);
if (fi)
free_fi(fi, 0);
if (print_playlists)
{
// If the root directory was passed as argument add the stored playlists to the response
return mpd_command_listplaylists(evbuf, argc, argv, errmsg);
}
return 0;
return ret;
}
static int
@ -3782,7 +3935,6 @@ static struct command mpd_handlers[] =
.mpdcommand = "list",
.handler = mpd_command_list
},
/*
{
.mpdcommand = "listall",
.handler = mpd_command_listall
@ -3791,6 +3943,7 @@ static struct command mpd_handlers[] =
.mpdcommand = "listallinfo",
.handler = mpd_command_listallinfo
},
/*
{
.mpdcommand = "listfiles",
.handler = mpd_command_listfiles
@ -4588,8 +4741,24 @@ int mpd_init(void)
sin6.sin6_family = AF_INET6;
sin6.sin6_port = htons(port);
saddr = (struct sockaddr *)&sin6;
listener = evconnlistener_new_bind(
evbase_mpd,
mpd_accept_conn_cb,
NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-1,
saddr,
saddr_length);
if (!listener)
{
DPRINTF(E_LOG, L_MPD, "Could not bind to port %d, falling back to IPv4\n", port);
v6enabled = 0;
}
}
else
if (!v6enabled)
{
saddr_length = sizeof(struct sockaddr_in);
memset(&sin, 0, saddr_length);
@ -4597,23 +4766,24 @@ int mpd_init(void)
sin.sin_addr.s_addr = htonl(0);
sin.sin_port = htons(port);
saddr = (struct sockaddr *)&sin;
listener = evconnlistener_new_bind(
evbase_mpd,
mpd_accept_conn_cb,
NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-1,
saddr,
saddr_length);
if (!listener)
{
DPRINTF(E_LOG, L_MPD, "Could not create connection listener for mpd clients on port %d\n", port);
goto connew_fail;
}
}
listener = evconnlistener_new_bind(
evbase_mpd,
mpd_accept_conn_cb,
NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-1,
saddr,
saddr_length);
if (!listener)
{
DPRINTF(E_LOG, L_MPD, "Could not create connection listener for mpd clients on port %d\n", port);
goto connew_fail;
}
evconnlistener_set_error_cb(listener, mpd_accept_error_cb);
http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port");

View File

@ -35,6 +35,7 @@
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/queue.h>
#include <time.h>
#include <pthread.h>
#include <dlfcn.h>
@ -573,6 +574,9 @@ spotify_track_save(int plid, sp_track *track, const char *pltitle, int time_adde
sp_link *link;
char url[1024];
int ret;
int dir_id;
char virtual_path[PATH_MAX];
if (!fptr_sp_track_is_loaded(track))
{
@ -618,7 +622,36 @@ spotify_track_save(int plid, sp_track *track, const char *pltitle, int time_adde
return -1;
}
filescanner_process_media(url, time(NULL), 0, F_SCAN_TYPE_SPOTIFY, &mfi);
ret = snprintf(virtual_path, sizeof(virtual_path), "/spotify:/%s", mfi.artist);
if ((ret < 0) || (ret >= sizeof(virtual_path)))
{
DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s)\n", mfi.artist);
free_mfi(&mfi, 1);
return -1;
}
dir_id = db_directory_addorupdate(virtual_path, 0, DIR_SPOTIFY);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path);
free_mfi(&mfi, 1);
return -1;
}
ret = snprintf(virtual_path, sizeof(virtual_path), "/spotify:/%s/%s", mfi.artist, mfi.album);
if ((ret < 0) || (ret >= sizeof(virtual_path)))
{
DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s/%s)\n", mfi.artist, mfi.album);
free_mfi(&mfi, 1);
return -1;
}
dir_id = db_directory_addorupdate(virtual_path, 0, dir_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path);
free_mfi(&mfi, 1);
return -1;
}
filescanner_process_media(url, time(NULL), 0, F_SCAN_TYPE_SPOTIFY, &mfi, dir_id);
free_mfi(&mfi, 1);
@ -669,7 +702,7 @@ spotify_playlist_save(sp_playlist *pl)
int plid;
int num_tracks;
char virtual_path[PATH_MAX];
int time;
int created;
int ret;
int i;
@ -759,6 +792,7 @@ spotify_playlist_save(sp_playlist *pl)
pli->path = strdup(url);
pli->virtual_path = strdup(virtual_path);
pli->parent_id = g_base_plid;
pli->directory_id = DIR_SPOTIFY;
ret = db_pl_add(pli, &plid);
if ((ret < 0) || (plid < 1))
@ -783,9 +817,9 @@ spotify_playlist_save(sp_playlist *pl)
continue;
}
time = fptr_sp_playlist_track_create_time(pl, i);
created = fptr_sp_playlist_track_create_time(pl, i);
ret = spotify_track_save(plid, track, name, time);
ret = spotify_track_save(plid, track, name, created);
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error saving track %d to playlist '%s' (id %d)\n", i, name, plid);
@ -1383,6 +1417,8 @@ logged_in(sp_session *sess, sp_error error)
DPRINTF(E_LOG, L_SPOTIFY, "Login to Spotify succeeded. Reloading playlists.\n");
db_directory_enable_bypath("/spotify:");
pl = fptr_sp_session_starred_create(sess);
fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL);