2014-09-30 04:47:30 +00:00
using MediaBrowser.Common.Configuration ;
2013-02-21 04:37:50 +00:00
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Persistence ;
2013-02-21 20:26:35 +00:00
using MediaBrowser.Model.Logging ;
2013-02-21 01:33:05 +00:00
using System ;
2014-09-30 04:47:30 +00:00
using System.Collections.Generic ;
2013-06-18 09:43:07 +00:00
using System.Data ;
2016-06-04 00:15:14 +00:00
using System.Globalization ;
2013-02-21 01:33:05 +00:00
using System.IO ;
2016-06-04 00:15:14 +00:00
using System.Text ;
2013-02-21 01:33:05 +00:00
using System.Threading ;
using System.Threading.Tasks ;
2013-06-17 20:35:43 +00:00
namespace MediaBrowser.Server.Implementations.Persistence
2013-02-21 01:33:05 +00:00
{
2015-05-02 22:59:01 +00:00
public class SqliteUserDataRepository : BaseSqliteRepository , IUserDataRepository
2013-02-21 01:33:05 +00:00
{
2016-06-12 23:33:11 +00:00
private IDbConnection _connection ;
2016-06-11 15:55:05 +00:00
public SqliteUserDataRepository ( ILogManager logManager , IApplicationPaths appPaths , IDbConnector connector ) : base ( logManager , connector )
2015-05-02 22:59:01 +00:00
{
2016-06-11 17:26:35 +00:00
DbFilePath = Path . Combine ( appPaths . DataPath , "userdata_v2.db" ) ;
2015-05-02 22:59:01 +00:00
}
2013-09-27 15:23:27 +00:00
2016-06-12 23:33:11 +00:00
protected override bool EnableConnectionPooling
{
get { return false ; }
}
2013-02-21 01:33:05 +00:00
/// <summary>
/// Gets the name of the repository
/// </summary>
/// <value>The name.</value>
public string Name
{
get
{
2013-06-18 19:16:27 +00:00
return "SQLite" ;
2013-02-21 01:33:05 +00:00
}
}
2016-06-15 18:56:37 +00:00
protected override async Task < IDbConnection > CreateConnection ( bool isReadOnly = false )
{
var connection = await DbConnector . Connect ( DbFilePath , false , false , 10000 ) . ConfigureAwait ( false ) ;
connection . RunQueries ( new [ ]
{
"pragma temp_store = memory"
} , Logger ) ;
return connection ;
}
2013-02-21 01:33:05 +00:00
/// <summary>
/// Opens the connection to the database
/// </summary>
/// <returns>Task.</returns>
2016-06-25 05:16:54 +00:00
public async Task Initialize ( IDbConnection connection , SemaphoreSlim writeLock )
2013-02-21 01:33:05 +00:00
{
2016-06-25 05:16:54 +00:00
WriteLock . Dispose ( ) ;
WriteLock = writeLock ;
2016-06-24 06:32:51 +00:00
_connection = connection ;
2016-06-12 23:33:11 +00:00
string [ ] queries = {
2013-06-18 09:43:07 +00:00
2016-06-24 06:32:51 +00:00
"create table if not exists UserDataDb.userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)" ,
2013-09-27 15:23:27 +00:00
2016-06-24 06:32:51 +00:00
"drop index if exists UserDataDb.idx_userdata" ,
"drop index if exists UserDataDb.idx_userdata1" ,
"drop index if exists UserDataDb.idx_userdata2" ,
"drop index if exists UserDataDb.userdataindex1" ,
"create unique index if not exists UserDataDb.userdataindex on userdata (key, userId)" ,
"create index if not exists UserDataDb.userdataindex2 on userdata (key, userId, played)" ,
2016-06-24 16:51:13 +00:00
"create index if not exists UserDataDb.userdataindex3 on userdata (key, userId, playbackPositionTicks)" ,
"create index if not exists UserDataDb.userdataindex4 on userdata (key, userId, isFavorite)" ,
2016-06-12 23:33:11 +00:00
//pragmas
"pragma temp_store = memory" ,
"pragma shrink_memory"
2013-06-18 09:43:07 +00:00
} ;
2016-06-12 23:33:11 +00:00
_connection . RunQueries ( queries , Logger ) ;
2016-02-20 23:06:57 +00:00
2016-06-12 23:33:11 +00:00
_connection . AddColumn ( Logger , "userdata" , "AudioStreamIndex" , "int" ) ;
_connection . AddColumn ( Logger , "userdata" , "SubtitleStreamIndex" , "int" ) ;
2013-02-21 01:33:05 +00:00
}
/// <summary>
2013-04-02 19:25:16 +00:00
/// Saves the user data.
2013-02-21 01:33:05 +00:00
/// </summary>
2013-04-02 19:25:16 +00:00
/// <param name="userId">The user id.</param>
2013-04-13 18:02:30 +00:00
/// <param name="key">The key.</param>
2013-04-02 19:25:16 +00:00
/// <param name="userData">The user data.</param>
2013-02-21 01:33:05 +00:00
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2013-04-13 18:02:30 +00:00
/// <exception cref="System.ArgumentNullException">userData
2013-04-02 19:25:16 +00:00
/// or
/// cancellationToken
/// or
/// userId
/// or
2013-04-13 18:02:30 +00:00
/// userDataId</exception>
2013-10-02 16:58:30 +00:00
public Task SaveUserData ( Guid userId , string key , UserItemData userData , CancellationToken cancellationToken )
2013-02-21 01:33:05 +00:00
{
2013-04-02 19:25:16 +00:00
if ( userData = = null )
2013-02-21 01:33:05 +00:00
{
2013-04-02 19:25:16 +00:00
throw new ArgumentNullException ( "userData" ) ;
2013-02-21 01:33:05 +00:00
}
2013-04-02 19:25:16 +00:00
if ( userId = = Guid . Empty )
{
throw new ArgumentNullException ( "userId" ) ;
}
2013-04-13 18:02:30 +00:00
if ( string . IsNullOrEmpty ( key ) )
2013-04-02 19:25:16 +00:00
{
2013-04-13 18:02:30 +00:00
throw new ArgumentNullException ( "key" ) ;
2013-04-02 19:25:16 +00:00
}
2013-10-02 16:58:30 +00:00
return PersistUserData ( userId , key , userData , cancellationToken ) ;
2013-04-19 23:43:00 +00:00
}
2014-09-20 21:57:16 +00:00
public Task SaveAllUserData ( Guid userId , IEnumerable < UserItemData > userData , CancellationToken cancellationToken )
{
if ( userData = = null )
{
throw new ArgumentNullException ( "userData" ) ;
}
if ( userId = = Guid . Empty )
{
throw new ArgumentNullException ( "userId" ) ;
}
return PersistAllUserData ( userId , userData , cancellationToken ) ;
}
2013-04-13 18:02:30 +00:00
/// <summary>
/// Persists the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task PersistUserData ( Guid userId , string key , UserItemData userData , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2016-06-12 23:33:11 +00:00
await WriteLock . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2013-06-18 09:43:07 +00:00
2016-06-12 23:33:11 +00:00
IDbTransaction transaction = null ;
2016-06-11 17:26:35 +00:00
2016-06-12 23:33:11 +00:00
try
{
transaction = _connection . BeginTransaction ( ) ;
2013-06-18 09:43:07 +00:00
2016-06-12 23:33:11 +00:00
using ( var cmd = _connection . CreateCommand ( ) )
2013-06-18 09:43:07 +00:00
{
2016-06-12 23:33:11 +00:00
cmd . CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)" ;
2016-06-11 17:26:35 +00:00
2016-06-12 23:33:11 +00:00
cmd . Parameters . Add ( cmd , "@key" , DbType . String ) . Value = key ;
cmd . Parameters . Add ( cmd , "@userId" , DbType . Guid ) . Value = userId ;
cmd . Parameters . Add ( cmd , "@rating" , DbType . Double ) . Value = userData . Rating ;
cmd . Parameters . Add ( cmd , "@played" , DbType . Boolean ) . Value = userData . Played ;
cmd . Parameters . Add ( cmd , "@playCount" , DbType . Int32 ) . Value = userData . PlayCount ;
cmd . Parameters . Add ( cmd , "@isFavorite" , DbType . Boolean ) . Value = userData . IsFavorite ;
cmd . Parameters . Add ( cmd , "@playbackPositionTicks" , DbType . Int64 ) . Value = userData . PlaybackPositionTicks ;
cmd . Parameters . Add ( cmd , "@lastPlayedDate" , DbType . DateTime ) . Value = userData . LastPlayedDate ;
cmd . Parameters . Add ( cmd , "@AudioStreamIndex" , DbType . Int32 ) . Value = userData . AudioStreamIndex ;
cmd . Parameters . Add ( cmd , "@SubtitleStreamIndex" , DbType . Int32 ) . Value = userData . SubtitleStreamIndex ;
cmd . Transaction = transaction ;
cmd . ExecuteNonQuery ( ) ;
2013-06-18 09:43:07 +00:00
}
2016-06-12 23:33:11 +00:00
transaction . Commit ( ) ;
}
catch ( OperationCanceledException )
{
if ( transaction ! = null )
2016-06-11 17:26:35 +00:00
{
2016-06-12 23:33:11 +00:00
transaction . Rollback ( ) ;
}
2013-06-18 09:43:07 +00:00
2016-06-12 23:33:11 +00:00
throw ;
}
catch ( Exception e )
{
Logger . ErrorException ( "Failed to save user data:" , e ) ;
2016-06-11 17:26:35 +00:00
2016-06-12 23:33:11 +00:00
if ( transaction ! = null )
{
transaction . Rollback ( ) ;
2016-06-11 17:26:35 +00:00
}
2016-06-12 23:33:11 +00:00
throw ;
}
finally
{
if ( transaction ! = null )
2013-06-18 09:43:07 +00:00
{
2016-06-12 23:33:11 +00:00
transaction . Dispose ( ) ;
2013-06-18 09:43:07 +00:00
}
2016-06-12 23:33:11 +00:00
WriteLock . Release ( ) ;
2013-05-25 23:52:41 +00:00
}
2013-02-21 01:33:05 +00:00
}
2014-09-20 21:57:16 +00:00
/// <summary>
/// Persist all user data for the specified user
/// </summary>
/// <param name="userId"></param>
/// <param name="userData"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task PersistAllUserData ( Guid userId , IEnumerable < UserItemData > userData , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2016-06-12 23:33:11 +00:00
await WriteLock . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
IDbTransaction transaction = null ;
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
try
{
transaction = _connection . BeginTransaction ( ) ;
2014-09-30 04:47:30 +00:00
2016-06-12 23:33:11 +00:00
foreach ( var userItemData in userData )
2014-09-20 21:57:16 +00:00
{
2016-06-12 23:33:11 +00:00
using ( var cmd = _connection . CreateCommand ( ) )
2016-06-11 17:26:35 +00:00
{
2016-06-12 23:33:11 +00:00
cmd . CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)" ;
cmd . Parameters . Add ( cmd , "@key" , DbType . String ) . Value = userItemData . Key ;
cmd . Parameters . Add ( cmd , "@userId" , DbType . Guid ) . Value = userId ;
cmd . Parameters . Add ( cmd , "@rating" , DbType . Double ) . Value = userItemData . Rating ;
cmd . Parameters . Add ( cmd , "@played" , DbType . Boolean ) . Value = userItemData . Played ;
cmd . Parameters . Add ( cmd , "@playCount" , DbType . Int32 ) . Value = userItemData . PlayCount ;
cmd . Parameters . Add ( cmd , "@isFavorite" , DbType . Boolean ) . Value = userItemData . IsFavorite ;
cmd . Parameters . Add ( cmd , "@playbackPositionTicks" , DbType . Int64 ) . Value = userItemData . PlaybackPositionTicks ;
cmd . Parameters . Add ( cmd , "@lastPlayedDate" , DbType . DateTime ) . Value = userItemData . LastPlayedDate ;
cmd . Parameters . Add ( cmd , "@AudioStreamIndex" , DbType . Int32 ) . Value = userItemData . AudioStreamIndex ;
cmd . Parameters . Add ( cmd , "@SubtitleStreamIndex" , DbType . Int32 ) . Value = userItemData . SubtitleStreamIndex ;
cmd . Transaction = transaction ;
cmd . ExecuteNonQuery ( ) ;
2016-06-11 17:26:35 +00:00
}
2016-06-12 23:33:11 +00:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-09-20 21:57:16 +00:00
}
2016-06-12 23:33:11 +00:00
transaction . Commit ( ) ;
}
catch ( OperationCanceledException )
{
if ( transaction ! = null )
2016-06-11 17:26:35 +00:00
{
2016-06-12 23:33:11 +00:00
transaction . Rollback ( ) ;
}
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
throw ;
}
catch ( Exception e )
{
Logger . ErrorException ( "Failed to save user data:" , e ) ;
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
if ( transaction ! = null )
{
transaction . Rollback ( ) ;
2014-09-20 21:57:16 +00:00
}
2016-06-12 23:33:11 +00:00
throw ;
}
finally
{
if ( transaction ! = null )
2014-09-20 21:57:16 +00:00
{
2016-06-12 23:33:11 +00:00
transaction . Dispose ( ) ;
2014-09-20 21:57:16 +00:00
}
2016-06-12 23:33:11 +00:00
WriteLock . Release ( ) ;
2014-09-20 21:57:16 +00:00
}
}
2013-02-21 01:33:05 +00:00
/// <summary>
2013-04-02 19:25:16 +00:00
/// Gets the user data.
2013-02-21 01:33:05 +00:00
/// </summary>
2013-04-02 19:25:16 +00:00
/// <param name="userId">The user id.</param>
2013-04-13 18:02:30 +00:00
/// <param name="key">The key.</param>
2013-04-02 19:25:16 +00:00
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="System.ArgumentNullException">
/// userId
/// or
2013-04-13 18:02:30 +00:00
/// key
2013-04-02 19:25:16 +00:00
/// </exception>
2013-06-17 20:35:43 +00:00
public UserItemData GetUserData ( Guid userId , string key )
2013-02-21 01:33:05 +00:00
{
2013-04-02 19:25:16 +00:00
if ( userId = = Guid . Empty )
2013-02-21 01:33:05 +00:00
{
2013-04-02 19:25:16 +00:00
throw new ArgumentNullException ( "userId" ) ;
}
2013-04-13 18:02:30 +00:00
if ( string . IsNullOrEmpty ( key ) )
2013-04-02 19:25:16 +00:00
{
2013-04-13 18:02:30 +00:00
throw new ArgumentNullException ( "key" ) ;
2013-02-21 01:33:05 +00:00
}
2013-05-09 20:29:50 +00:00
2016-06-12 23:33:11 +00:00
using ( var cmd = _connection . CreateCommand ( ) )
2013-05-08 20:58:52 +00:00
{
2016-06-12 23:33:11 +00:00
cmd . CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key = @key and userId=@userId" ;
2013-06-18 09:43:07 +00:00
2016-06-12 23:33:11 +00:00
cmd . Parameters . Add ( cmd , "@key" , DbType . String ) . Value = key ;
cmd . Parameters . Add ( cmd , "@userId" , DbType . Guid ) . Value = userId ;
2013-06-18 09:43:07 +00:00
2016-06-12 23:33:11 +00:00
using ( var reader = cmd . ExecuteReader ( CommandBehavior . SequentialAccess | CommandBehavior . SingleResult | CommandBehavior . SingleRow ) )
{
if ( reader . Read ( ) )
2013-06-18 09:43:07 +00:00
{
2016-06-12 23:33:11 +00:00
return ReadRow ( reader ) ;
2014-09-20 21:57:16 +00:00
}
2016-06-11 17:26:35 +00:00
}
2016-06-12 23:33:11 +00:00
return null ;
2014-09-20 21:57:16 +00:00
}
}
2013-09-27 15:23:27 +00:00
2016-06-04 00:15:14 +00:00
public UserItemData GetUserData ( Guid userId , List < string > keys )
{
if ( userId = = Guid . Empty )
{
throw new ArgumentNullException ( "userId" ) ;
}
if ( keys = = null )
{
throw new ArgumentNullException ( "keys" ) ;
}
2016-06-12 23:33:11 +00:00
using ( var cmd = _connection . CreateCommand ( ) )
2016-06-04 00:15:14 +00:00
{
2016-06-12 23:33:11 +00:00
var index = 0 ;
2016-06-24 15:12:03 +00:00
var userdataKeys = new List < string > ( ) ;
2016-06-12 23:33:11 +00:00
var builder = new StringBuilder ( ) ;
foreach ( var key in keys )
2016-06-04 00:15:14 +00:00
{
2016-06-12 23:33:11 +00:00
var paramName = "@Key" + index ;
2016-06-24 15:12:03 +00:00
userdataKeys . Add ( "Key =" + paramName ) ;
2016-06-12 23:33:11 +00:00
cmd . Parameters . Add ( cmd , paramName , DbType . String ) . Value = key ;
builder . Append ( " WHEN Key=" + paramName + " THEN " + index ) ;
index + + ;
2016-06-24 15:12:03 +00:00
break ;
2016-06-12 23:33:11 +00:00
}
2016-06-04 00:15:14 +00:00
2016-06-24 15:12:03 +00:00
var keyText = string . Join ( " OR " , userdataKeys . ToArray ( ) ) ;
2016-06-04 00:15:14 +00:00
2016-06-12 23:33:11 +00:00
cmd . CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId AND (" + keyText + ") " ;
2016-06-04 00:15:14 +00:00
2016-06-12 23:33:11 +00:00
cmd . CommandText + = " ORDER BY (Case " + builder + " Else " + keys . Count . ToString ( CultureInfo . InvariantCulture ) + " End )" ;
cmd . CommandText + = " LIMIT 1" ;
2016-06-04 00:15:14 +00:00
2016-06-12 23:33:11 +00:00
cmd . Parameters . Add ( cmd , "@userId" , DbType . Guid ) . Value = userId ;
2016-06-04 00:15:14 +00:00
2016-06-12 23:33:11 +00:00
using ( var reader = cmd . ExecuteReader ( CommandBehavior . SequentialAccess | CommandBehavior . SingleResult | CommandBehavior . SingleRow ) )
{
if ( reader . Read ( ) )
2016-06-04 00:15:14 +00:00
{
2016-06-12 23:33:11 +00:00
return ReadRow ( reader ) ;
2016-06-04 00:15:14 +00:00
}
2016-06-11 17:26:35 +00:00
}
2016-06-12 23:33:11 +00:00
return null ;
2016-06-04 00:15:14 +00:00
}
}
2014-09-20 21:57:16 +00:00
/// <summary>
/// Return all user-data associated with the given user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public IEnumerable < UserItemData > GetAllUserData ( Guid userId )
{
if ( userId = = Guid . Empty )
{
throw new ArgumentNullException ( "userId" ) ;
}
2016-06-12 23:33:11 +00:00
using ( var cmd = _connection . CreateCommand ( ) )
2014-09-20 21:57:16 +00:00
{
2016-06-12 23:33:11 +00:00
cmd . CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId" ;
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
cmd . Parameters . Add ( cmd , "@userId" , DbType . Guid ) . Value = userId ;
2014-09-20 21:57:16 +00:00
2016-06-12 23:33:11 +00:00
using ( var reader = cmd . ExecuteReader ( CommandBehavior . SequentialAccess | CommandBehavior . SingleResult ) )
{
while ( reader . Read ( ) )
2014-09-20 21:57:16 +00:00
{
2016-06-12 23:33:11 +00:00
yield return ReadRow ( reader ) ;
2013-06-18 09:43:07 +00:00
}
}
2013-05-08 20:58:52 +00:00
}
2014-09-20 21:57:16 +00:00
}
/// <summary>
/// Read a row from the specified reader into the provided userData object
/// </summary>
/// <param name="reader"></param>
2014-09-30 04:47:30 +00:00
private UserItemData ReadRow ( IDataReader reader )
2014-09-20 21:57:16 +00:00
{
2014-09-30 04:47:30 +00:00
var userData = new UserItemData ( ) ;
userData . Key = reader . GetString ( 0 ) ;
userData . UserId = reader . GetGuid ( 1 ) ;
if ( ! reader . IsDBNull ( 2 ) )
2014-09-20 21:57:16 +00:00
{
2014-09-30 04:47:30 +00:00
userData . Rating = reader . GetDouble ( 2 ) ;
2014-09-20 21:57:16 +00:00
}
2014-09-30 04:47:30 +00:00
userData . Played = reader . GetBoolean ( 3 ) ;
userData . PlayCount = reader . GetInt32 ( 4 ) ;
userData . IsFavorite = reader . GetBoolean ( 5 ) ;
userData . PlaybackPositionTicks = reader . GetInt64 ( 6 ) ;
2014-09-20 21:57:16 +00:00
2014-09-30 04:47:30 +00:00
if ( ! reader . IsDBNull ( 7 ) )
2014-09-20 21:57:16 +00:00
{
2014-09-30 04:47:30 +00:00
userData . LastPlayedDate = reader . GetDateTime ( 7 ) . ToUniversalTime ( ) ;
2014-09-20 21:57:16 +00:00
}
2014-09-30 04:47:30 +00:00
2016-02-20 23:06:57 +00:00
if ( ! reader . IsDBNull ( 8 ) )
{
userData . AudioStreamIndex = reader . GetInt32 ( 8 ) ;
}
if ( ! reader . IsDBNull ( 9 ) )
{
userData . SubtitleStreamIndex = reader . GetInt32 ( 9 ) ;
}
2014-09-30 04:47:30 +00:00
return userData ;
2013-02-21 01:33:05 +00:00
}
2016-06-12 23:33:11 +00:00
2016-06-25 05:16:54 +00:00
protected override void Dispose ( bool dispose )
2016-06-12 23:33:11 +00:00
{
2016-06-25 05:16:54 +00:00
// handled by library database
}
2016-06-12 23:33:11 +00:00
2016-06-25 05:16:54 +00:00
protected override void CloseConnection ( )
{
// handled by library database
2016-06-12 23:33:11 +00:00
}
2013-02-21 01:33:05 +00:00
}
2013-05-09 20:29:50 +00:00
}