Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Jean-Francois Dockes
upmpdcli
Commits
a825f1ad
Commit
a825f1ad
authored
Nov 20, 2016
by
Jean-Francois Dockes
Browse files
Improve streaming services searches
parent
66a200ae
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
src/execmd.cpp
View file @
a825f1ad
...
...
@@ -888,7 +888,7 @@ again:
int
n
=
con
->
getline
(
buf
,
BS
,
timeosecs
);
if
(
n
<
0
)
{
if
(
con
->
timedout
())
{
LOGDEB
(
"ExecCmd::getline:
timeout
\n
"
);
LOGDEB
0
(
"ExecCmd::getline:
select timeout, report and retry
\n
"
);
if
(
m
->
m_advise
)
{
m
->
m_advise
->
newData
(
0
);
}
...
...
src/mediaserver/cdplugins/.#trace
0 → 120000
View file @
a825f1ad
dockes@y.dockes.com.11624:1473896341
\ No newline at end of file
src/mediaserver/cdplugins/gmusic/gmusic-app.py
View file @
a825f1ad
...
...
@@ -303,6 +303,8 @@ def search(a):
objid
=
a
[
'objid'
]
field
=
a
[
'field'
]
if
'field'
in
a
else
None
value
=
a
[
'value'
]
objkind
=
a
[
'objkind'
]
if
'objkind'
in
a
else
None
if
re
.
match
(
'0\$gmusic\$'
,
objid
)
is
None
:
raise
Exception
(
"bad objid [%s]"
%
objid
)
xbmcplugin
.
objid
=
objid
...
...
@@ -310,19 +312,19 @@ def search(a):
searchresults
=
session
.
search
(
value
)
if
field
and
fiel
d
not
in
[
'artist'
,
'album'
,
'playlist'
,
'track'
]:
msgproc
.
log
(
'Unknown
fiel
d
\'
%s
\'
'
%
fiel
d
)
fiel
d
=
None
if
fiel
d
is
None
or
fiel
d
==
'artist'
:
if
objkind
and
objkin
d
not
in
[
'artist'
,
'album'
,
'playlist'
,
'track'
]:
msgproc
.
log
(
'Unknown
objkin
d
\'
%s
\'
'
%
objkin
d
)
objkin
d
=
None
if
objkin
d
is
None
or
objkin
d
==
'artist'
:
view
(
searchresults
.
artists
,
urls_from_id
(
artist_view
,
searchresults
.
artists
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'album'
:
if
objkin
d
is
None
or
objkin
d
==
'album'
:
view
(
searchresults
.
albums
,
urls_from_id
(
album_view
,
searchresults
.
albums
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'playlist'
:
if
objkin
d
is
None
or
objkin
d
==
'playlist'
:
view
(
searchresults
.
playlists
,
urls_from_id
(
playlist_view
,
searchresults
.
playlists
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'track'
:
if
objkin
d
is
None
or
objkin
d
==
'track'
:
track_list
(
searchresults
.
tracks
)
#msgproc.log("%s" % xbmcplugin.entries)
...
...
src/mediaserver/cdplugins/plgwithslave.cxx
View file @
a825f1ad
...
...
@@ -92,11 +92,11 @@ public:
static
struct
MHD_Daemon
*
mhd
;
// Microhttpd connection handler. We re-build the complete url + query
// string (&trackid=value), use this to retrieve a
Tidal URL, and
// redirect to it (HTTP). A previous version
handled rtmp streams, and
// had to read them. Look up the history if
you need the code again
// (the apparition of RTMP streams was
apparently linked to the use of
// a different API key).
// string (&trackid=value), use this to retrieve a
service URL
//
(tidal/qobuz...), and
redirect to it (HTTP). A previous version
//
handled rtmp streams, and
had to read them. Look up the history if
//
you need the code again
(the apparition of RTMP streams was
//
apparently linked to the use of
a different API key).
static
int
answer_to_connection
(
void
*
cls
,
struct
MHD_Connection
*
connection
,
const
char
*
url
,
const
char
*
method
,
const
char
*
version
,
...
...
@@ -483,7 +483,7 @@ int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
vector
<
UpSong
>&
entries
,
const
vector
<
string
>&
sortcrits
)
{
LOGDEB
1
(
"PlgWithSlave::search
\n
"
);
LOGDEB
(
"PlgWithSlave::search
: ["
<<
searchstr
<<
"]
\n
"
);
entries
.
clear
();
if
(
!
m
->
maybeStartCmd
())
{
return
errorEntries
(
ctid
,
entries
);
...
...
@@ -513,11 +513,26 @@ int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
string
slavefield
;
string
value
;
string
classfilter
;
string
objkind
;
for
(
unsigned
int
i
=
0
;
i
<
vs
.
size
()
-
2
;
i
+=
4
)
{
const
string
&
upnpproperty
=
vs
[
i
];
LOGDEB
(
"PlgWithSlave::search:clause: "
<<
vs
[
i
]
<<
" "
<<
vs
[
i
+
1
]
<<
" "
<<
vs
[
i
+
2
]
<<
endl
);
if
(
!
upnpproperty
.
compare
(
"upnp:artist"
)
||
if
(
!
upnpproperty
.
compare
(
"upnp:class"
))
{
// This defines -what- we are looking for (track/album/artist)
const
string
&
what
(
vs
[
i
+
2
]);
if
(
beginswith
(
what
,
"object.item"
))
{
objkind
=
"track"
;
}
else
if
(
beginswith
(
what
,
"object.container.person"
))
{
objkind
=
"artist"
;
}
else
if
(
beginswith
(
what
,
"object.container.musicAlbum"
)
||
beginswith
(
what
,
"object.container.album"
))
{
objkind
=
"album"
;
}
else
if
(
beginswith
(
what
,
"object.container.playlistContainer"
)
||
beginswith
(
what
,
"object.container.playlist"
))
{
objkind
=
"playlist"
;
}
}
else
if
(
!
upnpproperty
.
compare
(
"upnp:artist"
)
||
!
upnpproperty
.
compare
(
"dc:author"
))
{
slavefield
=
"artist"
;
value
=
vs
[
i
+
2
];
...
...
@@ -541,7 +556,7 @@ int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
// In cache ?
SearchCacheEntry
*
cep
;
string
cachekey
(
m_name
+
":"
+
slavefield
+
":"
+
value
);
string
cachekey
(
m_name
+
":"
+
objkind
+
":"
+
slavefield
+
":"
+
value
);
if
((
cep
=
o_scache
.
get
(
cachekey
))
!=
nullptr
)
{
int
total
=
resultFromCacheEntry
(
classfilter
,
stidx
,
cnt
,
*
cep
,
entries
);
delete
cep
;
...
...
@@ -552,6 +567,7 @@ int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
unordered_map
<
string
,
string
>
res
;
if
(
!
m
->
cmd
.
callproc
(
"search"
,
{
{
"objid"
,
ctid
},
{
"objkind"
,
objkind
},
{
"field"
,
slavefield
},
{
"value"
,
value
}
},
res
))
{
LOGERR
(
"PlgWithSlave::search: slave failure
\n
"
);
...
...
src/mediaserver/cdplugins/qobuz/qobuz-app.py
View file @
a825f1ad
...
...
@@ -312,6 +312,8 @@ def search(a):
objid
=
a
[
'objid'
]
field
=
a
[
'field'
]
if
'field'
in
a
else
None
value
=
a
[
'value'
]
objkind
=
a
[
'objkind'
]
if
'objkind'
in
a
else
None
if
re
.
match
(
'0\$qobuz\$'
,
objid
)
is
None
:
raise
Exception
(
"bad objid [%s]"
%
objid
)
xbmcplugin
.
objid
=
objid
...
...
@@ -321,21 +323,24 @@ def search(a):
msgproc
.
log
(
'Unknown field
\'
%s
\'
'
%
field
)
field
=
'track'
if
objkind
and
objkind
not
in
[
'artist'
,
'album'
,
'playlist'
,
'track'
]:
msgproc
.
log
(
'Unknown objkind
\'
%s
\'
'
%
objkind
)
objkind
=
'track'
# type may be 'tracks', 'albums', 'artists' or 'playlists'
q
field
=
fiel
d
+
"s"
if
field
else
None
searchresults
=
session
.
search
(
value
,
q
fiel
d
)
q
kind
=
objkin
d
+
"s"
if
field
else
None
searchresults
=
session
.
search
(
value
,
q
kin
d
)
if
fiel
d
is
None
or
fiel
d
==
'artist'
:
if
objkin
d
is
None
or
objkin
d
==
'artist'
:
view
(
searchresults
.
artists
,
urls_from_id
(
artist_view
,
searchresults
.
artists
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'album'
:
if
objkin
d
is
None
or
objkin
d
==
'album'
:
view
(
searchresults
.
albums
,
urls_from_id
(
album_view
,
searchresults
.
albums
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'playlist'
:
if
objkin
d
is
None
or
objkin
d
==
'playlist'
:
view
(
searchresults
.
playlists
,
urls_from_id
(
playlist_view
,
searchresults
.
playlists
),
end
=
False
)
if
field
is
None
or
field
==
'track'
:
if
objkind
is
None
or
objkind
==
'track'
:
track_list
(
searchresults
.
tracks
)
#msgproc.log("%s" % xbmcplugin.entries)
...
...
src/mediaserver/cdplugins/tidal/tidal-app.py
View file @
a825f1ad
...
...
@@ -380,28 +380,29 @@ def search(a):
msgproc
.
log
(
"search: [%s]"
%
a
)
objid
=
a
[
'objid'
]
field
=
a
[
'field'
]
if
'field'
in
a
else
None
objkind
=
a
[
'objkind'
]
if
'objkind'
in
a
else
None
value
=
a
[
'value'
]
if
re
.
match
(
'0\$tidal\$'
,
objid
)
is
None
:
raise
Exception
(
"bad objid [%s]"
%
objid
)
xbmcplugin
.
objid
=
objid
maybelogin
()
# field is mandatory and maybe 'artist', 'album', 'playlist', 'track'
# But quite frequently, our caller does not set it. So need to run multiple
# searches
if
field
is
None
or
field
==
'artist'
:
# objkind is mandatory and maybe 'artist', 'album', 'playlist', 'track'
# If our caller does not set it, we run multiple searches
if
objkind
is
None
or
objkind
==
'artist'
:
searchresults
=
session
.
search
(
'artist'
,
value
)
view
(
searchresults
.
artists
,
urls_from_id
(
artist_view
,
searchresults
.
artists
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'album'
:
if
objkin
d
is
None
or
objkin
d
==
'album'
:
searchresults
=
session
.
search
(
'album'
,
value
)
view
(
searchresults
.
albums
,
urls_from_id
(
album_view
,
searchresults
.
albums
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'playlist'
:
if
objkin
d
is
None
or
objkin
d
==
'playlist'
:
searchresults
=
session
.
search
(
'playlist'
,
value
)
view
(
searchresults
.
playlists
,
urls_from_id
(
playlist_view
,
searchresults
.
playlists
),
end
=
False
)
if
fiel
d
is
None
or
fiel
d
==
'track'
:
if
objkin
d
is
None
or
objkin
d
==
'track'
:
searchresults
=
session
.
search
(
'track'
,
value
)
track_list
(
searchresults
.
tracks
)
#msgproc.log("%s" % xbmcplugin.entries)
...
...
src/smallut.cpp
View file @
a825f1ad
/* Copyright (C) 2004-2016 J.F.Dockes
* 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.
/* Copyright (C) 2006-2016 J.F.Dockes
*
*
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
f
or
more details
.
* This
library is free software; you can redistribute it and/or
*
modify it under the terms of the GNU Lesser General Public
*
License as published by the Free Software Foundation; ei
the
r
*
version 2.1 of the
License
,
or
(at your option) any later version
.
*
* 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.
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#include
<stdio.h>
#include
<stdlib.h>
...
...
@@ -184,6 +185,14 @@ int stringuppercmp(const string& s1, const string& s2)
}
}
bool
beginswith
(
const
std
::
string
&
big
,
const
std
::
string
&
small
)
{
if
(
big
.
compare
(
0
,
small
.
size
(),
small
))
{
return
false
;
}
return
true
;
}
// Compare charset names, removing the more common spelling variations
bool
samecharset
(
const
string
&
cs1
,
const
string
&
cs2
)
{
...
...
src/smallut.cpp.orig
0 → 100644
View file @
a825f1ad
This diff is collapsed.
Click to expand it.
src/smallut.h
View file @
a825f1ad
/* Copyright (C) 2004-2016 J.F.Dockes
* 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.
/* Copyright (C) 2006-2016 J.F.Dockes
*
*
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
f
or
more details
.
* This
library is free software; you can redistribute it and/or
*
modify it under the terms of the GNU Lesser General Public
*
License as published by the Free Software Foundation; ei
the
r
*
version 2.1 of the
License
,
or
(at your option) any later version
.
*
* 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.
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#ifndef _SMALLUT_H_INCLUDED_
#define _SMALLUT_H_INCLUDED_
...
...
@@ -65,6 +66,7 @@ extern void stringtolower(std::string& io);
extern
std
::
string
stringtolower
(
const
std
::
string
&
io
);
extern
void
stringtoupper
(
std
::
string
&
io
);
extern
std
::
string
stringtoupper
(
const
std
::
string
&
io
);
extern
bool
beginswith
(
const
std
::
string
&
big
,
const
std
::
string
&
small
);
// Is one string the end part of the other ?
extern
int
stringisuffcmp
(
const
std
::
string
&
s1
,
const
std
::
string
&
s2
);
...
...
src/smallut.h.orig
0 → 100644
View file @
a825f1ad
/* Copyright (C) 2006-2016 J.F.Dockes
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#ifndef _SMALLUT_H_INCLUDED_
#define _SMALLUT_H_INCLUDED_
#include <sys/types.h>
#include <string>
#include <vector>
#include <map>
#include <set>
// Miscellaneous mostly string-oriented small utilities
// Note that none of the following code knows about utf-8.
// Call this before going multithread.
void smallut_init_mt();
#ifndef SMALLUT_DISABLE_MACROS
#ifndef MIN
#define MIN(A,B) (((A)<(B)) ? (A) : (B))
#endif
#ifndef MAX
#define MAX(A,B) (((A)>(B)) ? (A) : (B))
#endif
#ifndef deleteZ
#define deleteZ(X) {delete X;X = 0;}
#endif
#endif /* SMALLUT_DISABLE_MACROS */
// Case-insensitive compare. ASCII ONLY !
extern int stringicmp(const std::string& s1, const std::string& s2);
// For find_if etc.
struct StringIcmpPred {
StringIcmpPred(const std::string& s1)
: m_s1(s1) {
}
bool operator()(const std::string& s2) {
return stringicmp(m_s1, s2) == 0;
}
const std::string& m_s1;
};
extern int stringlowercmp(const std::string& alreadylower,
const std::string& s2);
extern int stringuppercmp(const std::string& alreadyupper,
const std::string& s2);
extern void stringtolower(std::string& io);
extern std::string stringtolower(const std::string& io);
extern void stringtoupper(std::string& io);
extern std::string stringtoupper(const std::string& io);
// Is one string the end part of the other ?
extern int stringisuffcmp(const std::string& s1, const std::string& s2);
// Divine language from locale
extern std::string localelang();
// Divine 8bit charset from language
extern std::string langtocode(const std::string& lang);
// Compare charset names, removing the more common spelling variations
extern bool samecharset(const std::string& cs1, const std::string& cs2);
// Parse date interval specifier into pair of y,m,d dates. The format
// for the time interval is based on a subset of iso 8601 with
// the addition of open intervals, and removal of all time indications.
// 'P' is the Period indicator, it's followed by a length in
// years/months/days (or any subset thereof)
// Dates: YYYY-MM-DD YYYY-MM YYYY
// Periods: P[nY][nM][nD] where n is an integer value.
// At least one of YMD must be specified
// The separator for the interval is /. Interval examples
// YYYY/ (from YYYY) YYYY-MM-DD/P3Y (3 years after date) etc.
// This returns a pair of y,m,d dates.
struct DateInterval {
int y1;
int m1;
int d1;
int y2;
int m2;
int d2;
};
extern bool parsedateinterval(const std::string& s, DateInterval *di);
extern int monthdays(int mon, int year);
/**
* Parse input string into list of strings.
*
* Token delimiter is " \t\n" except inside dquotes. dquote inside
* dquotes can be escaped with \ etc...
* Input is handled a byte at a time, things will work as long as
* space tab etc. have the ascii values and can't appear as part of a
* multibyte char. utf-8 ok but so are the iso-8859-x and surely
* others. addseps do have to be single-bytes
*/
template <class T> bool stringToStrings(const std::string& s, T& tokens,
const std::string& addseps = "");
/**
* Inverse operation:
*/
template <class T> void stringsToString(const T& tokens, std::string& s);
template <class T> std::string stringsToString(const T& tokens);
/**
* Strings to CSV string. tokens containing the separator are quoted (")
* " inside tokens is escaped as "" ([word "quote"] =>["word ""quote"""]
*/
template <class T> void stringsToCSV(const T& tokens, std::string& s,
char sep = ',');
/**
* Split input string. No handling of quoting
*/
extern void stringToTokens(const std::string& s,
std::vector<std::string>& tokens,
const std::string& delims = " \t",
bool skipinit = true);
/** Convert string to boolean */
extern bool stringToBool(const std::string& s);
/** Remove instances of characters belonging to set (default {space,
tab}) at beginning and end of input string */
extern void trimstring(std::string& s, const char *ws = " \t");
/** Escape things like < or & by turning them into entities */
extern std::string escapeHtml(const std::string& in);
/** Double-quote and escape to produce C source code string (prog generation) */
extern std::string makeCString(const std::string& in);
/** Replace some chars with spaces (ie: newline chars). */
extern std::string neutchars(const std::string& str, const std::string& chars);
extern void neutchars(const std::string& str, std::string& out,
const std::string& chars);
/** Turn string into something that won't be expanded by a shell. In practise
* quote with double-quotes and escape $`\ */
extern std::string escapeShell(const std::string& str);
/** Truncate a string to a given maxlength, avoiding cutting off midword
* if reasonably possible. */
extern std::string truncate_to_word(const std::string& input,
std::string::size_type maxlen);
void ulltodecstr(unsigned long long val, std::string& buf);
void lltodecstr(long long val, std::string& buf);
std::string lltodecstr(long long val);
std::string ulltodecstr(unsigned long long val);
/** Convert byte count into unit (KB/MB...) appropriate for display */
std::string displayableBytes(off_t size);
/** Break big string into lines */
std::string breakIntoLines(const std::string& in, unsigned int ll = 100,
unsigned int maxlines = 50);
/** Small utility to substitute printf-like percents cmds in a string */
bool pcSubst(const std::string& in, std::string& out,
const std::map<char, std::string>& subs);
/** Substitute printf-like percents and also %(key) */
bool pcSubst(const std::string& in, std::string& out,
const std::map<std::string, std::string>& subs);
/** Append system error message */
void catstrerror(std::string *reason, const char *what, int _errno);
/** Portable timegm. MS C has _mkgmtime, but there is a bug in Gminw which
* makes it inaccessible */
struct tm;
time_t portable_timegm(struct tm *tm);
inline void leftzeropad(std::string& s, unsigned len)
{
if (s.length() && s.length() < len) {
s = s.insert(0, len - s.length(), '0');
}
}
// A class to solve platorm/compiler issues for simple regex
// matches. Uses the appropriate native lib under the hood.
// This always uses extended regexp syntax.
class SimpleRegexp {
public:
enum Flags {SRE_NONE = 0, SRE_ICASE = 1, SRE_NOSUB = 2};
/// @param nmatch must be >= the number of parenthesed subexp in exp
SimpleRegexp(const std::string& exp, int flags, int nmatch = 0);
~SimpleRegexp();
/// Match input against exp, return true if matches
bool simpleMatch(const std::string& val) const;
/// After simpleMatch success, get nth submatch, 0 is the whole
/// match, 1 first parentheses, etc.
std::string getMatch(const std::string& val, int matchidx) const;
/// Calls simpleMatch()
bool operator() (const std::string& val) const;
/// Check after construction
bool ok() const;
class Internal;
private:
Internal *m;
};
/// Utilities for printing names for defined values (Ex: O_RDONLY->"O_RDONLY")
/// Entries for the descriptive table
struct CharFlags {
unsigned int value; // Flag or value
const char *yesname;// String to print if flag set or equal
const char *noname; // String to print if flag not set (unused for values)
};
/// Helper macro for the common case where we want to print the
/// flag/value defined name
#define CHARFLAGENTRY(NM) {NM, #NM}
/// Translate a bitfield into string description
extern std::string flagsToString(const std::vector<CharFlags>&,
unsigned int flags);
/// Translate a value into a name
extern std::string valToString(const std::vector<CharFlags>&, unsigned int val);
/// Reverse operation: translate string into bitfield
extern unsigned int
stringToFlags(const std::vector<CharFlags>&, const std::string& input,
const char *sep = "|");
#endif /* _SMALLUT_H_INCLUDED_ */
Write
Preview
Supports
Markdown
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