Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
vlc-2-2
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Redmine
Redmine
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
videolan
vlc-2-2
Commits
db662f04
Commit
db662f04
authored
Oct 18, 2010
by
Srikanth Raju
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Base Media Library Module files
parent
c83d4a8d
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
1743 additions
and
0 deletions
+1743
-0
include/vlc_interface.h
include/vlc_interface.h
+12
-0
modules/media_library/.gitignore
modules/media_library/.gitignore
+1
-0
modules/media_library/sql_media_library.c
modules/media_library/sql_media_library.c
+1388
-0
modules/media_library/sql_media_library.h
modules/media_library/sql_media_library.h
+342
-0
No files found.
include/vlc_interface.h
View file @
db662f04
...
@@ -168,6 +168,18 @@ typedef enum vlc_dialog {
...
@@ -168,6 +168,18 @@ typedef enum vlc_dialog {
/* Useful text messages shared by interfaces */
/* Useful text messages shared by interfaces */
#define INTF_ABOUT_MSG LICENSE_MSG
#define INTF_ABOUT_MSG LICENSE_MSG
#define EXTENSIONS_AUDIO_CSV "a52", "aac", "ac3", "ape", "dts", "flac", "it", \
"m4a", "m4p", "mka", "mlp", "mod", "mp1", "mp2", "mp3",\
"oga", "ogg", "oma", "s3m", "spx" \
"wav", "wma", "wv", "xm"
#define EXTENSIONS_VIDEO_CSV "asf", "avi", "divx", "dv", "flv", "gxf", "iso", \
"m1v", "m2v", "m2t", "m2ts", "m4v", "mkv", "mov",\
"mp2", "mp4", "mpeg", "mpeg1", \
"mpeg2", "mpeg4", "mpg", "mts", "mxf", "nuv", \
"ogg", "ogm", "ogv", "ogx", "ps", \
"rec", "rm", "rmvb", "ts", "vob", "wmv"
#define EXTENSIONS_AUDIO \
#define EXTENSIONS_AUDIO \
"*.a52;" \
"*.a52;" \
"*.aac;" \
"*.aac;" \
...
...
modules/media_library/.gitignore
0 → 100644
View file @
db662f04
Makefile.am
modules/media_library/sql_media_library.c
0 → 100644
View file @
db662f04
/*****************************************************************************
* sql_media_library.c: SQL-based media library
*****************************************************************************
* Copyright (C) 2008-2010 the VideoLAN Team and AUTHORS
* $Id$
*
* Authors: Antoine Lejeune <phytos@videolan.org>
* Jean-Philippe André <jpeg@videolan.org>
* Rémi Duraffort <ivoire@videolan.org>
* Adrien Maglo <magsoft@videolan.org>
* Srikanth Raju <srikiraju at gmail dot com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#include "sql_media_library.h"
static
const
char
*
ppsz_AudioExtensions
[]
=
{
EXTENSIONS_AUDIO_CSV
,
NULL
};
static
const
char
*
ppsz_VideoExtensions
[]
=
{
EXTENSIONS_VIDEO_CSV
,
NULL
};
#define MEDIA_LIBRARY_PATH_TEXT N_( "Filename of the SQLite database" )
#define MEDIA_LIBRARY_PATH_LONGTEXT N_( "Path to the file containing " \
"the SQLite database" )
#define IGNORE_TEXT N_( "Ignored extensions in the media library" )
#define IGNORE_LONGTEXT N_( "Files with these extensions will not be added to"\
" the media library when scanning directories." )
#define RECURSIVE_TEXT N_( "Subdirectory recursive scanning" )
#define RECURSIVE_LONGTEXT N_( "When scanning a directory, scan also all its"\
" subdirectories." )
/*****************************************************************************
* Static functions
*****************************************************************************/
/* Module entry point and exit point */
static
int
load
(
vlc_object_t
*
);
static
void
unload
(
vlc_object_t
*
);
static
int
CreateInputItemFromMedia
(
media_library_t
*
p_ml
,
input_item_t
**
pp_item
,
ml_media_t
*
p_media
);
struct
ml_table_elt
{
int
column_id
;
char
column_name
[];
};
static
const
struct
ml_table_elt
ml_table_map
[]
=
{
{
ML_ALBUM_COVER
,
"album_cover"
},
{
ML_ALBUM_ID
,
"album_id"
},
{
ML_ALBUM
,
"album_title"
},
{
ML_COMMENT
,
"comment"
},
{
ML_COVER
,
"cover"
},
{
ML_DIRECTORY
,
"directory_id"
},
{
ML_DIRECTORY
,
"directory_id"
},
{
ML_DISC_NUMBER
,
"disc"
},
{
ML_DURATION
,
"duration"
},
{
ML_EXTRA
,
"extra"
},
{
ML_FILESIZE
,
"filesize"
},
{
ML_FIRST_PLAYED
,
"first_played"
},
{
ML_GENRE
,
"genre"
},
{
ML_ID
,
"id"
},
{
ML_IMPORT_TIME
,
"import_time"
},
{
ML_LANGUAGE
,
"language"
},
{
ML_LAST_PLAYED
,
"last_played"
},
{
ML_LAST_SKIPPED
,
"last_skipped"
},
{
ML_ORIGINAL_TITLE
,
"original_title"
},
{
ML_PEOPLE_ID
,
"people_id"
},
{
ML_PEOPLE
,
"people_name"
},
{
ML_PEOPLE_ROLE
,
"people_role"
},
{
ML_PLAYED_COUNT
,
"played_count"
},
{
ML_PREVIEW
,
"preview"
},
{
ML_SCORE
,
"score"
},
{
ML_SKIPPED_COUNT
,
"skipped_count"
},
{
ML_TITLE
,
"title"
},
{
ML_TRACK_NUMBER
,
"track"
},
{
ML_TYPE
,
"type"
},
{
ML_URI
,
"uri"
},
{
ML_VOTE
,
"vote"
},
{
ML_YEAR
,
"year"
}
};
/*****************************************************************************
* Module description
*****************************************************************************/
vlc_module_begin
()
set_shortname
(
"Media Library"
)
set_description
(
_
(
"Media Library based on a SQL based database"
)
)
set_capability
(
"media-library"
,
1
)
set_callbacks
(
load
,
unload
)
set_category
(
CAT_ADVANCED
)
set_subcategory
(
SUBCAT_ADVANCED_MISC
)
add_string
(
"ml-filename"
,
"vlc-media-library.db"
,
NULL
,
MEDIA_LIBRARY_PATH_TEXT
,
MEDIA_LIBRARY_PATH_LONGTEXT
,
false
)
add_string
(
"ml-username"
,
""
,
NULL
,
N_
(
"Username for the database"
),
N_
(
"Username for the database"
),
false
)
add_string
(
"ml-password"
,
""
,
NULL
,
N_
(
"Password for the database"
),
N_
(
"Password for the database"
),
false
)
add_integer
(
"ml-port"
,
0
,
NULL
,
N_
(
"Port for the database"
),
N_
(
"Port for the database"
),
false
)
add_bool
(
"ml-recursive-scan"
,
true
,
NULL
,
RECURSIVE_TEXT
,
RECURSIVE_LONGTEXT
,
false
)
add_bool
(
"ml-auto-add"
,
true
,
NULL
,
N_
(
"Auto add new medias"
),
N_
(
"Automatically add new medias to ML"
),
false
)
vlc_module_end
()
/**
* @brief Load module
* @param obj Parent object
*/
static
int
load
(
vlc_object_t
*
obj
)
{
msg_Dbg
(
obj
,
"loading media library module"
);
media_library_t
*
p_ml
=
(
media_library_t
*
)
obj
;
p_ml
->
p_sys
=
(
media_library_sys_t
*
)
calloc
(
1
,
sizeof
(
media_library_sys_t
)
);
if
(
!
p_ml
->
p_sys
)
return
VLC_ENOMEM
;
p_ml
->
functions
.
pf_Find
=
FindVa
;
p_ml
->
functions
.
pf_FindAdv
=
FindAdv
;
p_ml
->
functions
.
pf_Control
=
Control
;
p_ml
->
functions
.
pf_InputItemFromMedia
=
GetInputItemFromMedia
;
p_ml
->
functions
.
pf_Update
=
Update
;
p_ml
->
functions
.
pf_Delete
=
Delete
;
p_ml
->
functions
.
pf_GetMedia
=
GetMedia
;
vlc_mutex_init
(
&
p_ml
->
p_sys
->
lock
);
/* Initialise Sql module */
InitDatabase
(
p_ml
);
/* Initialise the media pool */
ARRAY_INIT
(
p_ml
->
p_sys
->
mediapool
);
vlc_mutex_init
(
&
p_ml
->
p_sys
->
pool_mutex
);
/* Create variables system */
var_Create
(
p_ml
,
"media-added"
,
VLC_VAR_INTEGER
);
var_Create
(
p_ml
,
"media-deleted"
,
VLC_VAR_INTEGER
);
var_Create
(
p_ml
,
"media-meta-change"
,
VLC_VAR_INTEGER
);
/* Launching the directory monitoring thread */
monitoring_thread_t
*
p_mon
=
vlc_object_create
(
p_ml
,
sizeof
(
monitoring_thread_t
)
);
if
(
!
p_mon
)
{
vlc_mutex_destroy
(
&
p_ml
->
p_sys
->
lock
);
sql_Destroy
(
p_ml
->
p_sys
->
p_sql
);
free
(
p_ml
->
p_sys
);
return
VLC_ENOMEM
;
}
p_ml
->
p_sys
->
p_mon
=
p_mon
;
p_mon
->
p_ml
=
p_ml
;
if
(
vlc_clone
(
&
p_mon
->
thread
,
RunMonitoringThread
,
p_mon
,
VLC_THREAD_PRIORITY_LOW
)
)
{
msg_Err
(
p_ml
,
"cannot spawn the media library monitoring thread"
);
vlc_mutex_destroy
(
&
p_ml
->
p_sys
->
lock
);
sql_Destroy
(
p_ml
->
p_sys
->
p_sql
);
free
(
p_ml
->
p_sys
);
vlc_object_release
(
p_mon
);
return
VLC_EGENERIC
;
}
/* Starting the watching system (starts a thread) */
watch_Init
(
p_ml
);
msg_Dbg
(
p_ml
,
"Media library module loaded successfully"
);
return
VLC_SUCCESS
;
}
/**
* @brief Unload module
*
* @param obj the media library object
* @return Nothing
*/
static
void
unload
(
vlc_object_t
*
obj
)
{
media_library_t
*
p_ml
=
(
media_library_t
*
)
obj
;
/* Stopping the watching system */
watch_Close
(
p_ml
);
/* Stop the monitoring thread */
vlc_cancel
(
p_ml
->
p_sys
->
p_mon
->
thread
);
vlc_join
(
p_ml
->
p_sys
->
p_mon
->
thread
,
NULL
);
vlc_object_release
(
p_ml
->
p_sys
->
p_mon
);
/* Destroy the variable */
var_Destroy
(
p_ml
,
"media-meta-change"
);
var_Destroy
(
p_ml
,
"media-deleted"
);
var_Destroy
(
p_ml
,
"media-added"
);
/* Empty the media pool */
ml_media_t
*
item
;
FOREACH_ARRAY
(
item
,
p_ml
->
p_sys
->
mediapool
)
ml_gc_decref
(
item
);
FOREACH_END
()
vlc_mutex_destroy
(
&
p_ml
->
p_sys
->
pool_mutex
);
sql_Destroy
(
p_ml
->
p_sys
->
p_sql
);
vlc_mutex_destroy
(
&
p_ml
->
p_sys
->
lock
);
free
(
p_ml
->
p_sys
);
}
/**
* @brief Get results of an SQL-Query on the database (please : free the result)
*
* @param p_ml the media library object
* @param ppp_res char *** in which to store the table of results (allocated)
* @param pi_rows resulting row number in table
* @param pi_cols resulting column number in table
* @param psz_fmt query command with printf-like format enabled
* @param va_args format the command
* @return VLC_SUCCESS or a VLC error code
*/
int
Query
(
media_library_t
*
p_ml
,
char
***
ppp_res
,
int
*
pi_rows
,
int
*
pi_cols
,
const
char
*
psz_fmt
,
...
)
{
va_list
argp
;
va_start
(
argp
,
psz_fmt
);
int
i_ret
=
QueryVa
(
p_ml
,
ppp_res
,
pi_rows
,
pi_cols
,
psz_fmt
,
argp
);
va_end
(
argp
);
return
i_ret
;
}
/**
* @brief Get results of an SQL-Query on the database (please : free the result)
*
* @param p_ml the media library object
* @param ppp_res char *** in which to store the table of results (allocated)
* @param pi_rows resulting row number in table
* @param pi_cols resulting column number in table
* @param psz_fmt query command with printf-like format enabled
* @param va_args format the command
* @return VLC_SUCCESS or a VLC error code
*/
int
QueryVa
(
media_library_t
*
p_ml
,
char
***
ppp_res
,
int
*
pi_rows
,
int
*
pi_cols
,
const
char
*
psz_fmt
,
va_list
argp
)
{
assert
(
p_ml
);
if
(
!
ppp_res
||
!
psz_fmt
)
return
VLC_EGENERIC
;
char
*
psz_query
=
sql_VPrintf
(
p_ml
->
p_sys
->
p_sql
,
psz_fmt
,
argp
);
if
(
!
psz_query
)
return
VLC_ENOMEM
;
int
i_ret
=
sql_Query
(
p_ml
->
p_sys
->
p_sql
,
psz_query
,
ppp_res
,
pi_rows
,
pi_cols
);
free
(
psz_query
);
return
i_ret
;
}
/**
* @brief Do a SQL-query without any data coming back
*
* @param p_ml the media library object
* @param psz_fmt query command with printf-like format enabled
* @param va_args format the command
* @return VLC_SUCCESS or a VLC error code
*/
int
QuerySimple
(
media_library_t
*
p_ml
,
const
char
*
psz_fmt
,
...
)
{
va_list
argp
;
va_start
(
argp
,
psz_fmt
);
int
i_ret
=
QuerySimpleVa
(
p_ml
,
psz_fmt
,
argp
);
va_end
(
argp
);
return
i_ret
;
}
/**
* @brief Do a SQL-query without any data coming back
*
* @param p_ml the media library object
* @param psz_fmt query command with printf-like format enabled
* @param argp format the command
* @return VLC_SUCCESS or a VLC error code
*/
int
QuerySimpleVa
(
media_library_t
*
p_ml
,
const
char
*
psz_fmt
,
va_list
argp
)
{
assert
(
p_ml
);
int
i_ret
=
VLC_SUCCESS
;
int
i_rows
,
i_cols
;
char
**
pp_results
=
NULL
;
i_ret
=
QueryVa
(
p_ml
,
&
pp_results
,
&
i_rows
,
&
i_cols
,
psz_fmt
,
argp
);
FreeSQLResult
(
p_ml
,
pp_results
);
va_end
(
argp
);
return
i_ret
;
}
/**
* @brief Transforms a string to a ml_result_t, with given type and id (as psz)
*
* @param res the result of the function
* @param psz string to transform into a result
* @param psz_id id as a string
* @param result_type type of the result
* @return ID or a VLC error code
*/
int
StringToResult
(
ml_result_t
*
p_result
,
const
char
*
psz
,
const
char
*
psz_id
,
ml_result_type_e
result_type
)
{
memset
(
&
p_result
->
value
,
0
,
sizeof
(
p_result
->
value
)
);
p_result
->
id
=
psz_id
?
atoi
(
psz_id
)
:
0
;
p_result
->
type
=
result_type
;
switch
(
result_type
)
{
case
ML_TYPE_INT
:
p_result
->
value
.
i
=
psz
?
atoi
(
psz
)
:
0
;
break
;
case
ML_TYPE_TIME
:
p_result
->
value
.
time
=
psz
?
(
mtime_t
)
atoi
(
psz
)
:
(
mtime_t
)
0LL
;
break
;
case
ML_TYPE_PSZ
:
p_result
->
value
.
psz
=
psz
?
strdup
(
psz
)
:
NULL
;
break
;
case
ML_TYPE_MEDIA
:
default:
/* This is an error */
return
VLC_EGENERIC
;
}
return
p_result
->
id
;
}
/**
* @brief fills an ml_result_array_t with result of an SQL query
*
* @param p_ml the media library object
* @param p_media ml_result_array_t object to fill
* @param pp_results result of sql query
* @param i_rows row number
* @param i_cols column number
* @param result_type type of the result
* @return VLC_SUCCESS or a VLC error code
**/
int
SQLToResultArray
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_result_array
,
char
**
pp_results
,
int
i_rows
,
int
i_cols
,
ml_result_type_e
result_type
)
{
assert
(
p_ml
);
if
(
!
p_result_array
)
return
VLC_EGENERIC
;
if
(
i_cols
==
0
)
/* No result */
return
VLC_SUCCESS
;
if
(
i_cols
<
0
)
{
msg_Err
(
p_ml
,
"negative number of columns in result ?"
);
return
VLC_EGENERIC
;
}
if
(
i_cols
==
1
)
{
for
(
int
i
=
1
;
i
<=
i_rows
;
i
++
)
{
ml_result_t
*
res
=
(
ml_result_t
*
)
calloc
(
1
,
sizeof
(
ml_result_t
)
);
if
(
!
res
)
return
VLC_ENOMEM
;
StringToResult
(
res
,
pp_results
[
i
],
NULL
,
result_type
);
vlc_array_append
(
p_result_array
,
res
);
}
}
/* FIXME?: Assuming all double column results are id - result pairs */
else
if
(
(
i_cols
==
2
)
)
{
for
(
int
i
=
1
;
i
<=
i_rows
;
i
++
)
{
ml_result_t
*
res
=
(
ml_result_t
*
)
calloc
(
1
,
sizeof
(
ml_result_t
)
);
if
(
!
res
)
return
VLC_ENOMEM
;
StringToResult
(
res
,
pp_results
[
i
*
2
+
1
],
pp_results
[
i
*
2
],
result_type
);
vlc_array_append
(
p_result_array
,
res
);
}
}
else
if
(
result_type
==
ML_TYPE_MEDIA
)
{
return
SQLToMediaArray
(
p_ml
,
p_result_array
,
pp_results
,
i_rows
,
i_cols
);
}
else
{
msg_Err
(
p_ml
,
"unable to convert SQL result to a ml_result_t array"
);
return
VLC_EGENERIC
;
}
return
VLC_SUCCESS
;
}
/**
* @brief fills a vlc_array_t with results of an SQL query
* medias in ml_result_t
*
* @param p_ml the media library object
* @param p_array array to fill with ml_media_t elements (might be initialized)
* @param pp_results result of sql query
* @param i_rows row number
* @param i_cols column number
* @return VLC_SUCCESS or a VLC error code
* Warning: this returns VLC_EGENERIC if i_rows == 0 (empty result)
**/
int
SQLToMediaArray
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_result_array
,
char
**
pp_results
,
int
i_rows
,
int
i_cols
)
{
int
i_ret
=
VLC_SUCCESS
;
assert
(
p_ml
);
#define res( i, j ) ( pp_results[ i * i_cols + j ] )
#define atoinull( a ) ( (a) ? atoi( a ) : 0 )
#define strdupnull( a ) ( (a) ? strdup( a ) : NULL )
if
(
i_rows
==
0
)
return
VLC_EGENERIC
;
if
(
!
p_result_array
||
!
pp_results
||
i_rows
<
0
||
i_cols
<=
0
)
{
msg_Warn
(
p_ml
,
"bad arguments (%s:%d)"
,
__FILE__
,
__LINE__
);
return
VLC_EGENERIC
;
}
vlc_array_t
*
p_intermediate_array
=
vlc_array_new
();
/* Analyze first row */
int
*
indexes
=
(
int
*
)
calloc
(
i_cols
+
1
,
sizeof
(
int
)
);
if
(
!
indexes
)
return
VLC_ENOMEM
;
const
int
count
=
sizeof
(
ml_table_map
)
/
sizeof
(
struct
ml_table_elt
);
for
(
int
col
=
0
;
col
<
i_cols
;
col
++
)
{
//binary search
int
low
=
0
,
high
=
count
-
1
;
int
answer
=
-
1
;
while
(
low
<=
high
)
{
int
mid
=
(
low
+
high
)
/
2
;
char
*
mid_val
=
ml_table_map
[
mid
].
column_name
;
int
cmp
=
strcmp
(
mid_val
,
res
(
0
,
col
)
);
if
(
cmp
>
0
)
low
=
mid
+
1
;
else
if
(
cmp
<
0
)
high
=
mid
-
1
;
else
{
answer
=
mid
;
break
;
}
}
if
(
answer
==
-
1
)
msg_Warn
(
p_ml
,
"unknown column: %s"
,
res
(
0
,
col
)
);
else
indexes
[
col
]
=
ml_table_map
[
answer
].
column_id
;
}
/* Read rows 1 to i_rows */
ml_media_t
*
p_media
=
NULL
;
ml_result_t
*
p_result
=
NULL
;
for
(
int
row
=
1
;
(
row
<=
i_rows
)
&&
(
i_ret
==
VLC_SUCCESS
);
row
++
)
{
p_media
=
media_New
(
p_ml
,
0
,
ML_MEDIA
,
false
);
if
(
!
p_media
)
{
free
(
indexes
);
return
VLC_ENOMEM
;
}
p_result
=
(
ml_result_t
*
)
calloc
(
1
,
sizeof
(
ml_result_t
)
);
if
(
!
p_result
)
{
ml_gc_decref
(
p_media
);
free
(
indexes
);
return
VLC_ENOMEM
;
}
char
*
psz_append_pname
=
NULL
;
char
*
psz_append_prole
=
NULL
;
int
i_append_pid
=
0
;
#define SWITCH_INT( key, value ) case key: \
p_media-> value = atoinull( res( row, col ) );
#define SWITCH_PSZ( key, value ) case key: \
p_media-> value = strdupnull( res( row, col ) );
ml_LockMedia
(
p_media
);
for
(
int
col
=
0
;
(
col
<
i_cols
)
&&
(
i_ret
==
VLC_SUCCESS
);
col
++
)
{
switch
(
indexes
[
col
]
)
{
SWITCH_INT
(
ML_ALBUM_ID
,
i_album_id
);
SWITCH_PSZ
(
ML_ALBUM
,
psz_album
);
SWITCH_PSZ
(
ML_COMMENT
,
psz_comment
);
SWITCH_INT
(
ML_DISC_NUMBER
,
i_disc_number
);
SWITCH_INT
(
ML_DURATION
,
i_duration
);
SWITCH_PSZ
(
ML_EXTRA
,
psz_extra
);
SWITCH_INT
(
ML_FILESIZE
,
i_filesize
);
SWITCH_INT
(
ML_FIRST_PLAYED
,
i_first_played
);
SWITCH_PSZ
(
ML_GENRE
,
psz_genre
);
SWITCH_INT
(
ML_IMPORT_TIME
,
i_import_time
);
SWITCH_PSZ
(
ML_LANGUAGE
,
psz_language
);
SWITCH_INT
(
ML_LAST_PLAYED
,
i_last_played
);
SWITCH_INT
(
ML_LAST_SKIPPED
,
i_last_skipped
);
SWITCH_PSZ
(
ML_ORIGINAL_TITLE
,
psz_orig_title
);
SWITCH_INT
(
ML_PLAYED_COUNT
,
i_played_count
);
SWITCH_PSZ
(
ML_PREVIEW
,
psz_preview
);
SWITCH_INT
(
ML_SCORE
,
i_score
);
SWITCH_INT
(
ML_SKIPPED_COUNT
,
i_skipped_count
);
SWITCH_PSZ
(
ML_TITLE
,
psz_title
);
SWITCH_INT
(
ML_TRACK_NUMBER
,
i_track_number
);
SWITCH_INT
(
ML_TYPE
,
i_type
);
SWITCH_INT
(
ML_VOTE
,
i_vote
);
SWITCH_INT
(
ML_YEAR
,
i_year
);
case
ML_ALBUM_COVER
:
/* See ML_COVER */
// Discard attachment://
if
(
!
p_media
->
psz_cover
||
!*
p_media
->
psz_cover
||
!
strncmp
(
p_media
->
psz_cover
,
"attachment://"
,
13
)
)
{
free
(
p_media
->
psz_cover
);
p_media
->
psz_cover
=
strdupnull
(
res
(
row
,
col
)
);
}
break
;
case
ML_PEOPLE
:
psz_append_pname
=
strdupnull
(
res
(
row
,
col
)
);
break
;
case
ML_PEOPLE_ID
:
i_append_pid
=
atoinull
(
res
(
row
,
col
)
);
break
;
case
ML_PEOPLE_ROLE
:
psz_append_prole
=
strdupnull
(
res
(
row
,
col
)
);
break
;
case
ML_COVER
:
/* See ML_ALBUM_COVER */
if
(
!
p_media
->
psz_cover
||
!*
p_media
->
psz_cover
||
!
strncmp
(
p_media
->
psz_cover
,
"attachment://"
,
13
)
)
{
free
(
p_media
->
psz_cover
);
p_media
->
psz_cover
=
strdupnull
(
res
(
row
,
col
)
);
}
break
;
case
ML_ID
:
p_media
->
i_id
=
atoinull
(
res
(
row
,
col
)
);
if
(
p_media
->
i_id
<=
0
)
msg_Warn
(
p_ml
,
"entry with id null or inferior to zero"
);
break
;
case
ML_URI
:
p_media
->
psz_uri
=
strdupnull
(
res
(
row
,
col
)
);
if
(
!
p_media
->
psz_uri
)
msg_Warn
(
p_ml
,
"entry without uri"
);
break
;
case
ML_DIRECTORY
:
break
;
// The column directory_id is'nt part of the media model
default:
msg_Warn
(
p_ml
,
"unknown element, row %d column %d (of %d) - %s - %s"
,
row
,
col
,
i_cols
,
res
(
0
,
col
),
res
(
row
,
col
)
);
break
;
}
}
#undef SWITCH_INT
#undef SWITCH_PSZ
int
i_appendrow
;
ml_result_t
*
p_append
=
NULL
;
for
(
i_appendrow
=
0
;
i_appendrow
<
vlc_array_count
(
p_intermediate_array
);
i_appendrow
++
)
{
p_append
=
(
ml_result_t
*
)
vlc_array_item_at_index
(
p_intermediate_array
,
i_appendrow
);
if
(
p_append
->
id
==
p_media
->
i_id
)
break
;
}
if
(
i_appendrow
==
vlc_array_count
(
p_intermediate_array
)
)
{
p_result
->
id
=
p_media
->
i_id
;
p_result
->
type
=
ML_TYPE_MEDIA
;
p_result
->
value
.
p_media
=
p_media
;
if
(
psz_append_pname
&&
i_append_pid
&&
psz_append_prole
)
ml_CreateAppendPersonAdv
(
&
(
p_result
->
value
.
p_media
->
p_people
),
psz_append_prole
,
psz_append_pname
,
i_append_pid
);
vlc_array_append
(
p_intermediate_array
,
p_result
);
ml_UnlockMedia
(
p_media
);
}
else
/* This is a repeat row and the people need to be put together */
{
ml_LockMedia
(
p_append
->
value
.
p_media
);
if
(
psz_append_pname
&&
i_append_pid
&&
psz_append_prole
)
ml_CreateAppendPersonAdv
(
&
(
p_append
->
value
.
p_media
->
p_people
),
psz_append_prole
,
psz_append_pname
,
i_append_pid
);
ml_UnlockMedia
(
p_append
->
value
.
p_media
);
ml_UnlockMedia
(
p_media
);
ml_gc_decref
(
p_media
);
}
FREENULL
(
psz_append_prole
);
FREENULL
(
psz_append_pname
);
i_append_pid
=
0
;
}
p_media
=
NULL
;
free
(
indexes
);
/* Now check if these medias are already on the pool, and sync */
for
(
int
i
=
0
;
i
<
vlc_array_count
(
p_intermediate_array
);
i
++
)
{
p_result
=
(
ml_result_t
*
)
vlc_array_item_at_index
(
p_intermediate_array
,
i
);
p_media
=
p_result
->
value
.
p_media
;
ml_media_t
*
p_poolmedia
=
pool_GetMedia
(
p_ml
,
p_result
->
id
);
/* TODO: Pool_syncMedia might be cleaner? */
p_result
=
(
ml_result_t
*
)
calloc
(
1
,
sizeof
(
ml_result_t
*
)
);
if
(
!
p_result
)
goto
quit_sqlmediaarray
;
if
(
p_poolmedia
)
{
/* TODO: This might cause some weird stuff to occur w/ GC? */
ml_CopyMedia
(
p_poolmedia
,
p_media
);
p_result
->
id
=
p_poolmedia
->
i_id
;
p_result
->
type
=
ML_TYPE_MEDIA
;
p_result
->
value
.
p_media
=
p_poolmedia
;
vlc_array_append
(
p_result_array
,
p_result
);
}
else
{
i_ret
=
pool_InsertMedia
(
p_ml
,
p_media
,
false
);
if
(
i_ret
==
VLC_SUCCESS
)
{
ml_gc_incref
(
p_media
);
p_result
->
id
=
p_media
->
i_id
;
p_result
->
type
=
ML_TYPE_MEDIA
;
p_result
->
value
.
p_media
=
p_media
;
vlc_array_append
(
p_result_array
,
p_result
);
}
}
}
#undef strdupnull
#undef atoinull
#undef res
quit_sqlmediaarray:
for
(
int
k
=
0
;
k
<
vlc_array_count
(
p_intermediate_array
);
k
++
)
{
ml_result_t
*
temp
=
((
ml_result_t
*
)
vlc_array_item_at_index
(
p_intermediate_array
,
k
));
ml_FreeResult
(
temp
);
}
vlc_array_destroy
(
p_intermediate_array
);
return
i_ret
;
}
/**
* @brief Returns (unique) ID of media with specified URI
*
* @param p_ml the media library object
* @param psz_uri URI to look for
* @return i_id: (first) ID found, VLC_EGENERIC in case of error
* NOTE: Normally, there should not be more than one ID with one URI
*/
int
GetMediaIdOfURI
(
media_library_t
*
p_ml
,
const
char
*
psz_uri
)
{
int
i_ret
=
VLC_EGENERIC
;
vlc_array_t
*
p_array
=
vlc_array_new
();
i_ret
=
Find
(
p_ml
,
p_array
,
ML_ID
,
ML_URI
,
psz_uri
,
ML_LIMIT
,
1
,
ML_END
);
if
(
(
i_ret
==
VLC_SUCCESS
)
&&
(
vlc_array_count
(
p_array
)
>
0
)
&&
vlc_array_item_at_index
(
p_array
,
0
)
)
{
i_ret
=
(
(
ml_result_t
*
)
vlc_array_item_at_index
(
p_array
,
0
)
)
->
value
.
i
;
}
else
{
i_ret
=
VLC_EGENERIC
;
}
vlc_array_destroy
(
p_array
);
return
i_ret
;
}
/**
* @brief Control function for media library
*
* @param p_ml Media library handle
* @param i_query query type
* @param args query arguments
* @return VLC_SUCCESS if ok
*/
int
Control
(
media_library_t
*
p_ml
,
int
i_query
,
va_list
args
)
{
switch
(
i_query
)
{
case
ML_ADD_INPUT_ITEM
:
{
input_item_t
*
p_item
=
(
input_item_t
*
)
va_arg
(
args
,
input_item_t
*
);
return
AddInputItem
(
p_ml
,
p_item
);
}
case
ML_ADD_PLAYLIST_ITEM
:
{
playlist_item_t
*
p_item
=
(
playlist_item_t
*
)
va_arg
(
args
,
playlist_item_t
*
);
return
AddPlaylistItem
(
p_ml
,
p_item
);
}
case
ML_ADD_MONITORED
:
{
char
*
psz_dir
=
(
char
*
)
va_arg
(
args
,
char
*
);
return
AddDirToMonitor
(
p_ml
,
psz_dir
);
}
case
ML_GET_MONITORED
:
{
vlc_array_t
*
p_array
=
(
vlc_array_t
*
)
va_arg
(
args
,
vlc_array_t
*
);
return
ListMonitoredDirs
(
p_ml
,
p_array
);
}
case
ML_DEL_MONITORED
:
{
char
*
psz_dir
=
(
char
*
)
va_arg
(
args
,
char
*
);
return
RemoveDirToMonitor
(
p_ml
,
psz_dir
);
}
default:
return
VLC_EGENERIC
;
}
}
/**
* @brief Create a new (empty) database. The database might be initialized
*
* @param p_ml This ML
* @return VLC_SUCCESS or VLC_EGENERIC
* @note This function is transactional
*/
int
CreateEmptyDatabase
(
media_library_t
*
p_ml
)
{
assert
(
p_ml
);
int
i_ret
=
VLC_SUCCESS
;
msg_Dbg
(
p_ml
,
"creating a new (empty) database"
);
Begin
(
p_ml
);
/* Albums */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE album ( "
"id INTEGER PRIMARY KEY,"
"album_artist_id INTEGER,"
"title VARCHAR(1024),"
"cover VARCHAR(1024) )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Add "unknown" entry to albums */
i_ret
=
QuerySimple
(
p_ml
,
"INSERT INTO album ( id, title, cover, album_artist_id ) "
"VALUES ( 0, 'Unknown', '', 0 )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Main media table */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE media ( "
"id INTEGER PRIMARY KEY,"
"timestamp INTEGER,"
/* File timestamp */
"uri VARCHAR(1024),"
"type INTEGER,"
"title VARCHAR(1024),"
"original_title VARCHAR(1024),"
"album_id INTEGER,"
"cover VARCHAR(1024),"
"preview VARCHAR(1024),"
/* Video preview */
"track INTEGER,"
/* Track number */
"disc INTEGER,"
/* Disc number */
"year INTEGER,"
"genre VARCHAR(1024),"
"vote INTEGER,"
/* Rating/Stars */
"score INTEGER,"
/* ML score/rating */
"comment VARCHAR(1024),"
/* Comment */
"filesize INTEGER,"
/* Dates and times */
"duration INTEGER,"
/* Length of media */
"played_count INTEGER,"
"last_played DATE,"
"first_played DATE,"
"import_time DATE,"
"skipped_count INTEGER,"
"last_skipped DATE,"
"directory_id INTEGER,"
"CONSTRAINT associated_album FOREIGN KEY(album_id) "
"REFERENCES album(id) ON DELETE SET DEFAULT ON UPDATE RESTRICT)"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* People */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE people ( "
"id INTEGER PRIMARY KEY,"
"name VARCHAR(1024) ,"
"role VARCHAR(1024) )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Media to people */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE media_to_people ( "
"media_id INTEGER, "
"people_id INTEGER, "
"PRIMARY KEY( media_id, people_id ), "
"CONSTRAINT associated_people FOREIGN KEY(people_id) "
"REFERENCES people(id) ON DELETE SET DEFAULT ON UPDATE RESTRICT, "
"CONSTRAINT associated_media FOREIGN KEY(media_id) "
"REFERENCES media(id) ON DELETE CASCADE ON UPDATE RESTRICT )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Add "unknown" entry to people */
i_ret
=
QuerySimple
(
p_ml
,
"INSERT INTO people ( id, name, role ) "
"VALUES ( 0, 'Unknown', NULL )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* recursive is set to 1 if the directory is added to the database
by recursion and 0 if not */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE directories ( "
"id INTEGER PRIMARY KEY,"
"uri VARCHAR(1024),"
"timestamp INTEGER,"
"recursive INTEGER )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Create information table
* This table should have one row and the version number is the version
* of the database
* Other information may be stored here at later stages */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE information ( "
"version INTEGER PRIMARY KEY )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Insert current DB version */
i_ret
=
QuerySimple
(
p_ml
,
"INSERT INTO information ( version ) "
"VALUES ( %d )"
,
ML_DBVERSION
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Text data: song lyrics or subtitles */
i_ret
=
QuerySimple
(
p_ml
,
"CREATE TABLE extra ( "
"id INTEGER PRIMARY KEY,"
"extra TEXT,"
"language VARCHAR(256),"
"bitrate INTEGER,"
"samplerate INTEGER,"
"bpm INTEGER )"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
/* Emulating foreign keys with triggers */
/* Warning: Lots of SQL */
if
(
!
strcmp
(
module_get_name
(
p_ml
->
p_sys
->
p_sql
->
p_module
,
false
),
"SQLite"
)
)
{
i_ret
=
QuerySimple
(
p_ml
,
"
\n
CREATE TRIGGER genfkey1_insert_referencing BEFORE INSERT ON
\"
media
\"
WHEN
\n
"
" new.
\"
album_id
\"
IS NOT NULL AND NOT EXISTS (SELECT 1 FROM
\"
album
\"
WHERE new.
\"
album_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey1_insert_referencing failed. Cannot insert album_id into media. Album did not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey1_update_referencing BEFORE
\n
"
" UPDATE OF album_id ON
\"
media
\"
WHEN
\n
"
" new.
\"
album_id
\"
IS NOT NULL AND
\n
"
" NOT EXISTS (SELECT 1 FROM
\"
album
\"
WHERE new.
\"
album_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey1_update_referencing failed. Cannot update album_id in media. Album did not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey1_delete_referenced BEFORE DELETE ON
\"
album
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media
\"
WHERE old.
\"
id
\"
==
\"
album_id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey1_delete_referenced failed. Cannot delete album, media still exist');
\n
"
"END;
\n
"
"
\n
"
"
\n
"
"CREATE TRIGGER genfkey1_update_referenced AFTER
\n
"
" UPDATE OF id ON
\"
album
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media
\"
WHERE old.
\"
id
\"
==
\"
album_id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey1_update_referenced failed. Cannot change album id in album, media still exist');
\n
"
"END;
\n
"
"
\n
"
"
\n
"
"CREATE TRIGGER genfkey2_insert_referencing BEFORE INSERT ON
\"
media_to_people
\"
WHEN
\n
"
" new.
\"
media_id
\"
IS NOT NULL AND NOT EXISTS (SELECT 1 FROM
\"
media
\"
WHERE new.
\"
media_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey2_insert_referencing failed. Cannot insert into media_to_people, that media does not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey2_update_referencing BEFORE
\n
"
" UPDATE OF media_id ON
\"
media_to_people
\"
WHEN
\n
"
" new.
\"
media_id
\"
IS NOT NULL AND
\n
"
" NOT EXISTS (SELECT 1 FROM
\"
media
\"
WHERE new.
\"
media_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey2_update_referencing failed. Cannot update media_to_people, that media does not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey2_delete_referenced BEFORE DELETE ON
\"
media
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media_to_people
\"
WHERE old.
\"
id
\"
==
\"
media_id
\"
)
\n
"
"BEGIN
\n
"
" DELETE FROM
\"
media_to_people
\"
WHERE
\"
media_id
\"
= old.
\"
id
\"
;
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey2_update_referenced AFTER
\n
"
" UPDATE OF id ON
\"
media
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media_to_people
\"
WHERE old.
\"
id
\"
==
\"
media_id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey2_update_referenced failed. Cannot update media id, refs still exist in media_to_people');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey3_insert_referencing BEFORE INSERT ON
\"
media_to_people
\"
WHEN
\n
"
" new.
\"
people_id
\"
IS NOT NULL AND NOT EXISTS (SELECT 1 FROM
\"
people
\"
WHERE new.
\"
people_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey3_insert_referencing failed. Cannot insert into media_to_people, people does not exist');
\n
"
"END;
\n
"
"CREATE TRIGGER genfkey3_update_referencing BEFORE
\n
"
" UPDATE OF people_id ON
\"
media_to_people
\"
WHEN
\n
"
" new.
\"
people_id
\"
IS NOT NULL AND
\n
"
" NOT EXISTS (SELECT 1 FROM
\"
people
\"
WHERE new.
\"
people_id
\"
==
\"
id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey3_update_referencing failed. Cannot update media_to_people, people does not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey3_delete_referenced BEFORE DELETE ON
\"
people
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media_to_people
\"
WHERE old.
\"
id
\"
==
\"
people_id
\"
)
\n
"
"BEGIN
\n
"
" UPDATE media_to_people SET people_id = 0 WHERE people_id == old.
\"
id
\"
;
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER genfkey3_update_referenced AFTER
\n
"
" UPDATE OF id ON
\"
people
\"
WHEN
\n
"
" EXISTS (SELECT 1 FROM
\"
media_to_people
\"
WHERE old.
\"
id
\"
==
\"
people_id
\"
)
\n
"
"BEGIN
\n
"
" SELECT RAISE(ABORT, 'constraint genfkey3_update_referenced failed. Cannot update people_id, people does not exist');
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER keep_people_clean AFTER
\n
"
" DELETE ON
\"
media_to_people
\"\n
"
" WHEN NOT EXISTS( SELECT 1 from
\"
media_to_people
\"
WHERE old.
\"
people_id
\"
==
\"
people_id
\"
)
\n
"
"BEGIN
\n
"
" DELETE FROM people WHERE people.id = old.
\"
people_id
\"
AND people.id != 0;
\n
"
"END;
\n
"
"
\n
"
"CREATE TRIGGER keep_album_clean AFTER
\n
"
" DELETE ON
\"
media
\"\n
"
" WHEN NOT EXISTS( SELECT 1 FROM
\"
media
\"
WHERE old.
\"
album_id
\"
==
\"
album_id
\"
)
\n
"
"BEGIN
\n
"
" DELETE FROM album WHERE album.id = old.
\"
album_id
\"
AND album.id != 0;
\n
"
"END;"
);
if
(
i_ret
!=
VLC_SUCCESS
)
goto
quit_createemptydatabase
;
}
quit_createemptydatabase:
if
(
i_ret
==
VLC_SUCCESS
)
Commit
(
p_ml
);
else
Rollback
(
p_ml
);
return
VLC_SUCCESS
;
}
/**
* @brief Initiates database (create the database and the tables if needed)
*
* @param p_ml This ML
* @return VLC_SUCCESS or an error code
*/
int
InitDatabase
(
media_library_t
*
p_ml
)
{
assert
(
p_ml
);
msg_Dbg
(
p_ml
,
"initializing database"
);
/* Select database name */
char
*
psz_dbhost
=
NULL
,
*
psz_user
=
NULL
,
*
psz_pass
=
NULL
;
int
i_port
=
0
;
psz_dbhost
=
config_GetPsz
(
p_ml
,
"ml-filename"
);
psz_user
=
config_GetPsz
(
p_ml
,
"ml-username"
);
psz_pass
=
config_GetPsz
(
p_ml
,
"ml-password"
);
i_port
=
config_GetInt
(
p_ml
,
"ml-port"
);
/* Let's consider that a filename with a DIR_SEP is a full URL */
if
(
strchr
(
psz_dbhost
,
DIR_SEP_CHAR
)
==
NULL
)
{
char
*
psz_datadir
=
config_GetUserDir
(
VLC_DATA_DIR
);
char
*
psz_tmp
=
psz_dbhost
;
if
(
asprintf
(
&
psz_dbhost
,
"%s"
DIR_SEP
"%s"
,
psz_datadir
,
psz_tmp
)
==
-
1
)
{
free
(
psz_datadir
);
free
(
psz_tmp
);
return
VLC_ENOMEM
;
}
free
(
psz_datadir
);
free
(
psz_tmp
);
}
p_ml
->
p_sys
->
p_sql
=
sql_Create
(
p_ml
,
NULL
,
psz_dbhost
,
i_port
,
psz_user
,
psz_pass
);
if
(
!
p_ml
->
p_sys
->
p_sql
)
{
vlc_mutex_destroy
(
&
p_ml
->
p_sys
->
lock
);
free
(
p_ml
->
p_sys
);
return
VLC_EGENERIC
;
}
/* Let's check if tables exist */
int
i_version
=
GetDatabaseVersion
(
p_ml
);
if
(
i_version
<=
0
)
CreateEmptyDatabase
(
p_ml
);
else
if
(
i_version
!=
ML_DBVERSION
)
return
VLC_EGENERIC
;
/**
* The below code ensures that correct code is written
* when database versions are changed
*/
#if ML_DBVERSION != 1
#error "ML versioning code needs to be updated. Is this done correctly?"
#endif
msg_Dbg
(
p_ml
,
"ML initialized"
);
return
VLC_SUCCESS
;
}
/**
* @brief Gets the current version number from the database
*
* @param p_ml media library object
* @return version number of the current db. <= 0 on error.
*/
int
GetDatabaseVersion
(
media_library_t
*
p_ml
)
{
int
i_rows
,
i_cols
;
char
**
pp_results
;
int
i_return
;
i_return
=
Query
(
p_ml
,
&
pp_results
,
&
i_rows
,
&
i_cols
,
"SELECT version FROM information ORDER BY version DESC LIMIT 1"
);
if
(
i_return
!=
VLC_SUCCESS
)
i_return
=
-
1
;
else
i_return
=
atoi
(
pp_results
[
1
]
);
FreeSQLResult
(
p_ml
,
pp_results
);
return
i_return
;
}
/**
* @brief Object constructor for ml_media_t
* @param p_ml The media library object
* @param id If 0, this item isn't in database. If non zero, it is and
* it will be a singleton
* @param select Type of object
* @param reload Whether to reload from database
*/
ml_media_t
*
GetMedia
(
media_library_t
*
p_ml
,
int
id
,
ml_select_e
select
,
bool
reload
)
{
assert
(
id
>
0
);
assert
(
select
==
ML_MEDIA
||
select
==
ML_MEDIA_SPARSE
);
int
i_ret
=
VLC_SUCCESS
;
ml_media_t
*
p_media
=
NULL
;
if
(
!
reload
)
{
p_media
=
pool_GetMedia
(
p_ml
,
id
);
if
(
!
p_media
)
reload
=
true
;
else
{
ml_LockMedia
(
p_media
);
if
(
p_media
->
b_sparse
==
true
&&
select
==
ML_MEDIA
)
reload
=
true
;
/* Utilise ML_MEDIA_EXTRA load? TODO */
ml_UnlockMedia
(
p_media
);
ml_gc_incref
(
p_media
);
}
}
else
{
vlc_array_t
*
p_array
=
vlc_array_new
();
i_ret
=
ml_Find
(
p_ml
,
p_array
,
select
,
ML_ID
,
id
);
assert
(
vlc_array_count
(
p_array
)
==
1
);
if
(
(
i_ret
==
VLC_SUCCESS
)
&&
(
vlc_array_count
(
p_array
)
>
0
)
&&
vlc_array_item_at_index
(
p_array
,
0
)
)
{
p_media
=
((
ml_result_t
*
)
vlc_array_item_at_index
(
p_array
,
0
))
->
value
.
p_media
;
ml_gc_incref
(
p_media
);
ml_FreeResult
(
vlc_array_item_at_index
(
p_array
,
0
)
);
}
vlc_array_destroy
(
p_array
);
if
(
select
==
ML_MEDIA
)
p_media
->
b_sparse
=
false
;
else
p_media
->
b_sparse
=
true
;
}
return
p_media
;
}
/**
* @brief Create an input item from media (given its ID)
*
* @param p_ml This media_library_t object
* @param i_media Media ID
* @return input_item_t* created
*
* @note This is a public function (pf_InputItemFromMedia)
* The input_item will have a refcount at 2 (1 for the ML, 1 for you)
*/
input_item_t
*
GetInputItemFromMedia
(
media_library_t
*
p_ml
,
int
i_media
)
{
input_item_t
*
p_item
=
NULL
;
p_item
=
watch_get_itemOfMediaId
(
p_ml
,
i_media
);
if
(
!
p_item
)
{
ml_media_t
*
p_media
=
media_New
(
p_ml
,
i_media
,
ML_MEDIA
,
true
);
if
(
p_media
==
NULL
)
return
NULL
;
CreateInputItemFromMedia
(
p_ml
,
&
p_item
,
p_media
);
watch_add_Item
(
p_ml
,
p_item
,
p_media
);
ml_gc_decref
(
p_media
);
}
return
p_item
;
}
/**
* @brief Copy an input_item_t to a ml_media_t
* @param p_media Destination
* @param p_item Source
* @note Media ID will not be set! This function is threadsafe. Leaves
* unsyncable items alone
*/
void
CopyInputItemToMedia
(
ml_media_t
*
p_media
,
input_item_t
*
p_item
)
{
ml_LockMedia
(
p_media
);
#if 0
// unused meta :
input_item_GetCopyright( item )
input_item_GetRating( item ) /* TODO */
input_item_GetGetting( item )
input_item_GetNowPlaying( item )
input_item_GetTrackID( item )
input_item_GetSetting( item )
#endif
p_media
->
psz_title
=
input_item_GetTitle
(
p_item
);
p_media
->
psz_uri
=
input_item_GetURL
(
p_item
);
if
(
!
p_media
->
psz_uri
)
p_media
->
psz_uri
=
strdup
(
p_item
->
psz_uri
);
p_media
->
psz_album
=
input_item_GetAlbum
(
p_item
);
p_media
->
psz_cover
=
input_item_GetArtURL
(
p_item
);
p_media
->
psz_genre
=
input_item_GetGenre
(
p_item
);
p_media
->
psz_language
=
input_item_GetLanguage
(
p_item
);
p_media
->
psz_comment
=
input_item_GetDescription
(
p_item
);
char
*
psz_track
=
input_item_GetTrackNum
(
p_item
);
p_media
->
i_track_number
=
psz_track
?
atoi
(
psz_track
)
:
0
;
free
(
psz_track
);
char
*
psz_date
=
input_item_GetDate
(
p_item
);
p_media
->
i_year
=
psz_date
?
atoi
(
psz_date
)
:
0
;
free
(
psz_date
);
p_media
->
i_duration
=
p_item
->
i_duration
;
/* People */
char
*
psz_tmp
=
input_item_GetArtist
(
p_item
);
if
(
psz_tmp
)
ml_CreateAppendPersonAdv
(
&
p_media
->
p_people
,
ML_PERSON_ARTIST
,
psz_tmp
,
0
);
free
(
psz_tmp
);
psz_tmp
=
input_item_GetPublisher
(
p_item
);
if
(
psz_tmp
)
ml_CreateAppendPersonAdv
(
&
p_media
->
p_people
,
ML_PERSON_PUBLISHER
,
psz_tmp
,
0
);
free
(
psz_tmp
);
psz_tmp
=
input_item_GetEncodedBy
(
p_item
);
if
(
psz_tmp
)
ml_CreateAppendPersonAdv
(
&
p_media
->
p_people
,
ML_PERSON_ENCODER
,
psz_tmp
,
0
);
free
(
psz_tmp
);
/* Determine input type: audio, video, stream */
/* First read input type */
switch
(
p_item
->
i_type
)
{
case
ITEM_TYPE_FILE
:
p_media
->
i_type
|=
0
;
break
;
case
ITEM_TYPE_DISC
:
case
ITEM_TYPE_CARD
:
p_media
->
i_type
|=
ML_REMOVABLE
;
break
;
case
ITEM_TYPE_CDDA
:
case
ITEM_TYPE_NET
:
p_media
->
i_type
|=
ML_STREAM
;
break
;
case
ITEM_TYPE_PLAYLIST
:
case
ITEM_TYPE_NODE
:
case
ITEM_TYPE_DIRECTORY
:
p_media
->
i_type
|=
ML_NODE
;
break
;
case
ITEM_TYPE_NUMBER
:
case
ITEM_TYPE_UNKNOWN
:
default:
p_media
->
i_type
|=
ML_UNKNOWN
;
break
;
}
/* Then try to guess if this is a video or not */
/* Check file extension, and guess if this is a video or an audio media
Note: this test is not very good, but it's OK for normal files */
char
*
psz_ext
=
strrchr
(
p_item
->
psz_uri
,
'.'
);
if
(
psz_ext
&&
strlen
(
psz_ext
)
<
5
)
{
bool
b_ok
=
false
;
psz_ext
++
;
for
(
unsigned
i
=
0
;
ppsz_AudioExtensions
[
i
];
i
++
)
{
if
(
strcasecmp
(
psz_ext
,
ppsz_AudioExtensions
[
i
]
)
==
0
)
{
p_media
->
i_type
|=
ML_AUDIO
;
b_ok
=
true
;
break
;
}
}
if
(
!
b_ok
)
{
for
(
unsigned
i
=
0
;
ppsz_VideoExtensions
[
i
];
i
++
)
{
if
(
strcasecmp
(
psz_ext
,
ppsz_VideoExtensions
[
i
]
)
==
0
)
{
p_media
->
i_type
|=
ML_VIDEO
;
break
;
}
}
}
}
ml_UnlockMedia
(
p_media
);
}
/**
* @brief Copy a ml_media_t to an input_item_t
* @param p_item Destination
* @param p_media Source
*/
void
CopyMediaToInputItem
(
input_item_t
*
p_item
,
ml_media_t
*
p_media
)
{
ml_LockMedia
(
p_media
);
if
(
p_media
->
psz_title
&&
*
p_media
->
psz_title
)
input_item_SetTitle
(
p_item
,
p_media
->
psz_title
);
if
(
p_media
->
psz_uri
&&
*
p_media
->
psz_uri
)
input_item_SetURL
(
p_item
,
p_media
->
psz_uri
);
if
(
p_media
->
psz_album
&&
*
p_media
->
psz_album
)
input_item_SetAlbum
(
p_item
,
p_media
->
psz_album
);
if
(
p_media
->
psz_cover
&&
*
p_media
->
psz_cover
)
input_item_SetArtURL
(
p_item
,
p_media
->
psz_cover
);
if
(
p_media
->
psz_genre
&&
*
p_media
->
psz_genre
)
input_item_SetGenre
(
p_item
,
p_media
->
psz_genre
);
if
(
p_media
->
psz_language
&&
*
p_media
->
psz_language
)
input_item_SetLanguage
(
p_item
,
p_media
->
psz_language
);
if
(
p_media
->
psz_comment
&&
*
p_media
->
psz_comment
)
input_item_SetDescription
(
p_item
,
p_media
->
psz_comment
);
if
(
p_media
->
i_track_number
)
{
char
*
psz_track
;
if
(
asprintf
(
&
psz_track
,
"%d"
,
p_media
->
i_track_number
)
!=
-
1
)
input_item_SetTrackNum
(
p_item
,
psz_track
);
free
(
psz_track
);
}
if
(
p_media
->
i_year
)
{
char
*
psz_date
;
if
(
asprintf
(
&
psz_date
,
"%d"
,
p_media
->
i_year
)
!=
-
1
)
input_item_SetDate
(
p_item
,
psz_date
);
free
(
psz_date
);
}
p_item
->
i_duration
=
p_media
->
i_duration
;
ml_person_t
*
person
=
p_media
->
p_people
;
while
(
person
)
{
if
(
!
strcmp
(
person
->
psz_role
,
ML_PERSON_ARTIST
)
)
input_item_SetArtist
(
p_item
,
person
->
psz_name
);
else
if
(
!
strcmp
(
person
->
psz_role
,
ML_PERSON_PUBLISHER
)
)
input_item_SetPublisher
(
p_item
,
person
->
psz_name
);
else
if
(
!
strcmp
(
person
->
psz_role
,
ML_PERSON_ENCODER
)
)
input_item_SetEncodedBy
(
p_item
,
person
->
psz_name
);
person
=
person
->
p_next
;
}
ml_UnlockMedia
(
p_media
);
}
/**
* @brief Copy a ml_media_t to an input_item_t
* @param p_ml The Media Library object
* @param pp_item A pointer to a new input_item (return value)
* @param p_media The media to copy as an input item
* @note This function is threadsafe
*/
static
int
CreateInputItemFromMedia
(
media_library_t
*
p_ml
,
input_item_t
**
pp_item
,
ml_media_t
*
p_media
)
{
playlist_t
*
p_pl
=
pl_Get
(
p_ml
);
*
pp_item
=
input_item_New
(
VLC_OBJECT
(
p_pl
),
p_media
->
psz_uri
,
p_media
->
psz_title
);
/* ITEM_TYPE_FILE ); */
if
(
!*
pp_item
)
return
VLC_EGENERIC
;
CopyMediaToInputItem
(
*
pp_item
,
p_media
);
return
VLC_SUCCESS
;
}
/**
* @brief Find the media_id associated to an input item
* @param p_ml This
* @param p_item Input item to look for
* @return Media ID or <= 0 if not found
*/
int
GetMediaIdOfInputItem
(
media_library_t
*
p_ml
,
input_item_t
*
p_item
)
{
int
i_media_id
=
watch_get_mediaIdOfItem
(
p_ml
,
p_item
);
if
(
i_media_id
<=
0
)
{
i_media_id
=
GetMediaIdOfURI
(
p_ml
,
p_item
->
psz_uri
);
}
return
i_media_id
;
}
modules/media_library/sql_media_library.h
0 → 100644
View file @
db662f04
/*****************************************************************************
* sql_media_library.h : Media Library Interface
*****************************************************************************
* Copyright (C) 2008-2010 the VideoLAN team and AUTHORS
* $Id$
*
* Authors: Antoine Lejeune <phytos@videolan.org>
* Jean-Philippe André <jpeg@videolan.org>
* Rémi Duraffort <ivoire@videolan.org>
* Adrien Maglo <magsoft@videolan.org>
* Srikanth Raju <srikiraju at gmail dot com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifndef SQL_MEDIA_LIBRARY_H
#define SQL_MEDIA_LIBRARY_H
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <vlc_common.h>
#include <vlc/vlc.h>
#include <vlc_sql.h>
#include <vlc_media_library.h>
#include <vlc_playlist.h>
#include <vlc_input.h>
#include <vlc_arrays.h>
#include <vlc_charset.h>
#include <vlc_plugin.h>
#include <vlc_interface.h>
#include <vlc_modules.h>
#include <stdarg.h>
#include <assert.h>
#include <errno.h>
#include "item_list.h"
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif
/*****************************************************************************
* Static parameters
*****************************************************************************/
#define THREAD_SLEEP_DELAY 2
/* Time between two calls to item_list_loop */
#define MONITORING_DELAY 30
/* Media library updates interval */
#define ITEM_LOOP_UPDATE 1
/* An item is updated after 1 loop */
#define ITEM_LOOP_MAX_AGE 10
/* An item is deleted after 10 loops */
#define ML_DBVERSION 1
/* The current version of the database */
#define ML_MEDIAPOOL_HASH_LENGTH 100
/* The length of the media pool hash */
/*****************************************************************************
* Structures and types definitions
*****************************************************************************/
typedef
struct
monitoring_thread_t
monitoring_thread_t
;
typedef
struct
ml_poolobject_t
ml_poolobject_t
;
struct
ml_poolobject_t
{
ml_media_t
*
p_media
;
ml_poolobject_t
*
p_next
;
};
struct
media_library_sys_t
{
/* Lock on the ML object */
vlc_mutex_t
lock
;
/* SQL object */
sql_t
*
p_sql
;
/* Monitoring thread */
monitoring_thread_t
*
p_mon
;
/* Watch thread */
watch_thread_t
*
p_watch
;
/* Holds all medias */
DECL_ARRAY
(
ml_media_t
*
)
mediapool
;
ml_poolobject_t
*
p_mediapool
[
ML_MEDIAPOOL_HASH_LENGTH
];
vlc_mutex_t
pool_mutex
;
/* Info on update/collection rebuilding */
bool
b_updating
;
bool
b_rebuilding
;
};
/* Directory Monitoring thread */
struct
monitoring_thread_t
{
VLC_COMMON_MEMBERS
;
vlc_cond_t
wait
;
vlc_mutex_t
lock
;
vlc_thread_t
thread
;
media_library_t
*
p_ml
;
};
/* Media status Watching thread */
struct
watch_thread_t
{
VLC_COMMON_MEMBERS
;
media_library_t
*
p_ml
;
vlc_cond_t
cond
;
vlc_mutex_t
lock
;
/* Input items watched */
struct
item_list_t
*
p_hlist
[
ML_ITEMLIST_HASH_LENGTH
];
vlc_mutex_t
list_mutex
;
/* List of items to check */
input_item_t
**
item_append_queue
;
vlc_mutex_t
item_append_queue_lock
;
int
item_append_queue_count
;
};
/*****************************************************************************
* Function headers
*****************************************************************************/
/* General functions */
int
CreateEmptyDatabase
(
media_library_t
*
p_ml
);
int
InitDatabase
(
media_library_t
*
p_ml
);
/* Module Control */
int
Control
(
media_library_t
*
p_ml
,
int
i_query
,
va_list
args
);
/* Add functions */
int
AddMedia
(
media_library_t
*
p_ml
,
ml_media_t
*
p_media
);
int
AddAlbum
(
media_library_t
*
p_ml
,
const
char
*
psz_title
,
const
char
*
psz_cover
,
const
int
i_album_artist
);
int
AddPeople
(
media_library_t
*
p_ml
,
const
char
*
psz_name
,
const
char
*
psz_role
);
int
AddPlaylistItem
(
media_library_t
*
p_ml
,
playlist_item_t
*
p_playlist_item
);
int
AddInputItem
(
media_library_t
*
p_ml
,
input_item_t
*
p_input
);
/* Create and Copy functions */
ml_media_t
*
GetMedia
(
media_library_t
*
p_ml
,
int
id
,
ml_select_e
select
,
bool
reload
);
input_item_t
*
GetInputItemFromMedia
(
media_library_t
*
p_ml
,
int
i_media
);
void
CopyInputItemToMedia
(
ml_media_t
*
p_media
,
input_item_t
*
p_item
);
void
CopyMediaToInputItem
(
input_item_t
*
p_item
,
ml_media_t
*
p_media
);
/* Get functions */
int
GetDatabaseVersion
(
media_library_t
*
p_ml
);
int
GetMediaIdOfInputItem
(
media_library_t
*
p_ml
,
input_item_t
*
p_item
);
int
GetMediaIdOfURI
(
media_library_t
*
p_ml
,
const
char
*
psz_uri
);
/* Search in the database */
int
BuildSelectVa
(
media_library_t
*
p_ml
,
char
**
ppsz_query
,
ml_result_type_e
*
p_result_type
,
va_list
criterias
);
int
BuildSelect
(
media_library_t
*
p_ml
,
char
**
ppsz_query
,
ml_result_type_e
*
p_result_type
,
const
char
*
psz_selected_type_lvalue
,
ml_select_e
selected_type
,
ml_ftree_t
*
tree
);
int
Find
(
media_library_t
*
p_ml
,
vlc_array_t
*
results
,
...
);
int
FindVa
(
media_library_t
*
p_ml
,
vlc_array_t
*
results
,
va_list
criterias
);
int
FindAdv
(
media_library_t
*
p_ml
,
vlc_array_t
*
results
,
ml_select_e
selected_type
,
const
char
*
psz_lvalue
,
ml_ftree_t
*
tree
);
/* Update the database */
int
Update
(
media_library_t
*
p_ml
,
ml_select_e
selected_type
,
const
char
*
psz_lvalue
,
ml_ftree_t
*
where
,
vlc_array_t
*
changes
);
int
BuildUpdate
(
media_library_t
*
p_ml
,
char
**
ppsz_query
,
char
**
ppsz_id_query
,
const
char
*
psz_lvalue
,
ml_select_e
selected_type
,
ml_ftree_t
*
where
,
vlc_array_t
*
changes
);
int
UpdateMedia
(
media_library_t
*
p_ml
,
ml_media_t
*
p_media
);
int
SetArtCover
(
media_library_t
*
p_ml
,
int
i_album_id
,
const
char
*
psz_cover
);
/* Delete medias in the database */
int
Delete
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_array
);
/* Do some query on the database */
int
QuerySimple
(
media_library_t
*
p_ml
,
const
char
*
psz_fmt
,
...
);
int
Query
(
media_library_t
*
p_ml
,
char
***
ppp_res
,
int
*
pi_rows
,
int
*
pi_cols
,
const
char
*
psz_fmt
,
...
);
int
QueryVa
(
media_library_t
*
p_ml
,
char
***
ppp_res
,
int
*
pi_rows
,
int
*
pi_cols
,
const
char
*
psz_fmt
,
va_list
args
);
int
QuerySimpleVa
(
media_library_t
*
p_ml
,
const
char
*
psz_fmt
,
va_list
argp
);
/* Convert SQL results to ML results */
int
StringToResult
(
ml_result_t
*
res
,
const
char
*
psz
,
const
char
*
psz_id
,
ml_result_type_e
result_type
);
int
SQLToMediaArray
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_result_array
,
char
**
pp_results
,
int
i_rows
,
int
i_cols
);
int
SQLToResultArray
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_result_array
,
char
**
pp_results
,
int
i_rows
,
int
i_cols
,
ml_result_type_e
result_type
);
/* Database locking functions */
/**
* @brief Begin a transaction
* @param p_ml The Media Library object
* @return VLC_SUCCESS and VLC_EGENERIC
* @note This creates a SHARED lock in SQLITE. All queries made between
* a Begin and Commit/Rollback will be transactional.
*/
static
inline
int
Begin
(
media_library_t
*
p_ml
)
{
return
sql_BeginTransaction
(
p_ml
->
p_sys
->
p_sql
);
}
/**
* @brief Commits the transaction
* @param p_ml The Media Library object
*/
static
inline
void
Commit
(
media_library_t
*
p_ml
)
{
sql_CommitTransaction
(
p_ml
->
p_sys
->
p_sql
);
}
/**
* @brief Rollback the transaction
* @param p_ml The Media Library Object
*/
static
inline
void
Rollback
(
media_library_t
*
p_ml
)
{
sql_RollbackTransaction
(
p_ml
->
p_sys
->
p_sql
);
}
/****************************************************************************
* Scanning/monitoring functions
*****************************************************************************/
void
*
RunMonitoringThread
(
void
*
p_mon
);
int
AddDirToMonitor
(
media_library_t
*
p_ml
,
const
char
*
psz_dir
);
int
ListMonitoredDirs
(
media_library_t
*
p_ml
,
vlc_array_t
*
p_array
);
int
RemoveDirToMonitor
(
media_library_t
*
p_ml
,
const
char
*
psz_dir
);
/*****************************************************************************
* Media pool functions
*****************************************************************************/
ml_media_t
*
pool_GetMedia
(
media_library_t
*
p_ml
,
int
media_id
);
int
pool_InsertMedia
(
media_library_t
*
p_ml
,
ml_media_t
*
media
,
bool
locked
);
void
pool_GC
(
media_library_t
*
p_ml
);
/*****************************************************************************
* Items watching system
*****************************************************************************/
/* Watching thread */
#define watch_add_Item( a, b, c ) __watch_add_Item( a, b, c, false )
int
watch_Init
(
media_library_t
*
p_ml
);
void
watch_Close
(
media_library_t
*
p_ml
);
int
__watch_add_Item
(
media_library_t
*
p_ml
,
input_item_t
*
p_item
,
ml_media_t
*
p_media
,
bool
locked
);
#define watch_del_Item( a, b ) __watch_del_Item( a, b, false )
int
__watch_del_Item
(
media_library_t
*
p_ml
,
input_item_t
*
p_item
,
bool
locked
);
int
watch_del_MediaById
(
media_library_t
*
p_ml
,
int
i_media_id
);
input_item_t
*
watch_get_itemOfMediaId
(
media_library_t
*
p_ml
,
int
i_media_id
);
ml_media_t
*
watch_get_mediaOfMediaId
(
media_library_t
*
p_ml
,
int
i_media_id
);
int
watch_get_mediaIdOfItem
(
media_library_t
*
p_ml
,
input_item_t
*
p_item
);
void
watch_Force_Update
(
media_library_t
*
p_ml
);
/*****************************************************************************
* Free result of ml_Query
*****************************************************************************/
static
inline
void
FreeSQLResult
(
media_library_t
*
p_ml
,
char
**
ppsz_result
)
{
if
(
ppsz_result
)
{
sql_Free
(
p_ml
->
p_sys
->
p_sql
,
ppsz_result
);
}
}
#endif
/* SQL_MEDIA_LIBRARY_H */
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment