2020-11-21 13:26:03 +00:00
using System ;
2019-01-13 19:24:58 +00:00
using System.Collections.Generic ;
2020-08-06 14:17:45 +00:00
using System.ComponentModel.DataAnnotations ;
2019-01-13 19:24:58 +00:00
using System.Linq ;
using System.Threading ;
2020-08-21 20:01:19 +00:00
using System.Threading.Tasks ;
2020-06-08 18:20:33 +00:00
using Jellyfin.Api.Constants ;
2023-11-09 21:00:29 +00:00
using Jellyfin.Data.Enums ;
2023-11-10 16:17:22 +00:00
using MediaBrowser.Common.Api ;
2019-01-13 19:24:58 +00:00
using MediaBrowser.Controller.Configuration ;
2014-12-22 06:50:29 +00:00
using MediaBrowser.Controller.Entities ;
2013-06-27 19:06:37 +00:00
using MediaBrowser.Controller.Entities.Audio ;
using MediaBrowser.Controller.Entities.TV ;
using MediaBrowser.Controller.Library ;
2014-12-23 03:58:14 +00:00
using MediaBrowser.Controller.LiveTv ;
2014-12-21 05:57:06 +00:00
using MediaBrowser.Controller.Providers ;
2013-06-27 19:06:37 +00:00
using MediaBrowser.Model.Dto ;
2014-12-21 18:58:17 +00:00
using MediaBrowser.Model.Entities ;
2016-10-24 02:45:23 +00:00
using MediaBrowser.Model.Globalization ;
2018-09-12 17:26:21 +00:00
using MediaBrowser.Model.IO ;
2020-06-08 18:20:33 +00:00
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc ;
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
namespace Jellyfin.Api.Controllers ;
/// <summary>
/// Item update controller.
/// </summary>
[Route("")]
[Authorize(Policy = Policies.RequiresElevation)]
public class ItemUpdateController : BaseJellyfinApiController
2013-06-27 19:06:37 +00:00
{
2023-01-31 11:18:10 +00:00
private readonly ILibraryManager _libraryManager ;
private readonly IProviderManager _providerManager ;
private readonly ILocalizationManager _localizationManager ;
private readonly IFileSystem _fileSystem ;
private readonly IServerConfigurationManager _serverConfigurationManager ;
/// <summary>
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public ItemUpdateController (
IFileSystem fileSystem ,
ILibraryManager libraryManager ,
IProviderManager providerManager ,
ILocalizationManager localizationManager ,
IServerConfigurationManager serverConfigurationManager )
{
_libraryManager = libraryManager ;
_providerManager = providerManager ;
_localizationManager = localizationManager ;
_fileSystem = fileSystem ;
_serverConfigurationManager = serverConfigurationManager ;
}
2020-06-08 18:20:33 +00:00
/// <summary>
2023-01-31 11:18:10 +00:00
/// Updates an item.
2020-06-08 18:20:33 +00:00
/// </summary>
2023-01-31 11:18:10 +00:00
/// <param name="itemId">The item id.</param>
/// <param name="request">The new item properties.</param>
/// <response code="204">Item updated.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
[HttpPost("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task < ActionResult > UpdateItem ( [ FromRoute , Required ] Guid itemId , [ FromBody , Required ] BaseItemDto request )
2013-06-27 19:06:37 +00:00
{
2023-01-31 11:18:10 +00:00
var item = _libraryManager . GetItemById ( itemId ) ;
if ( item is null )
{
return NotFound ( ) ;
2014-12-21 05:57:06 +00:00
}
2023-01-31 11:18:10 +00:00
var newLockData = request . LockData ? ? false ;
var isLockedChanged = item . IsLocked ! = newLockData ;
2014-12-21 18:58:17 +00:00
2023-01-31 11:18:10 +00:00
var series = item as Series ;
var displayOrderChanged = series is not null & & ! string . Equals (
series . DisplayOrder ? ? string . Empty ,
request . DisplayOrder ? ? string . Empty ,
StringComparison . OrdinalIgnoreCase ) ;
2013-12-01 06:25:05 +00:00
2023-01-31 11:18:10 +00:00
// Do this first so that metadata savers can pull the updates from the database.
if ( request . People is not null )
{
_libraryManager . UpdatePeople (
item ,
request . People . Select ( x = > new PersonInfo
{
Name = x . Name ,
Role = x . Role ,
Type = x . Type
} ) . ToList ( ) ) ;
}
2018-09-12 17:26:21 +00:00
2022-11-27 14:37:34 +00:00
await UpdateItem ( request , item ) . ConfigureAwait ( false ) ;
2015-06-21 03:35:22 +00:00
2023-01-31 11:18:10 +00:00
item . OnMetadataChanged ( ) ;
2017-06-06 06:13:49 +00:00
2023-01-31 11:18:10 +00:00
await item . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
2017-09-13 18:41:48 +00:00
2023-01-31 11:18:10 +00:00
if ( isLockedChanged & & item . IsFolder )
{
var folder = ( Folder ) item ;
2017-06-06 06:13:49 +00:00
2023-01-31 11:18:10 +00:00
foreach ( var child in folder . GetRecursiveChildren ( ) )
2013-12-01 06:25:05 +00:00
{
2023-01-31 11:18:10 +00:00
child . IsLocked = newLockData ;
await child . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
}
}
2013-12-01 06:25:05 +00:00
2023-01-31 11:18:10 +00:00
if ( displayOrderChanged )
{
_providerManager . QueueRefresh (
series ! . Id ,
new MetadataRefreshOptions ( new DirectoryService ( _fileSystem ) )
2013-12-01 06:25:05 +00:00
{
2023-01-31 11:18:10 +00:00
MetadataRefreshMode = MetadataRefreshMode . FullRefresh ,
ImageRefreshMode = MetadataRefreshMode . FullRefresh ,
ReplaceAllMetadata = true
} ,
RefreshPriority . High ) ;
}
2018-09-12 17:26:21 +00:00
2023-01-31 11:18:10 +00:00
return NoContent ( ) ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
/// <summary>
/// Gets metadata editor info for an item.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <response code="200">Item metadata editor returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
[HttpGet("Items/{itemId}/MetadataEditor")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult < MetadataEditorInfo > GetMetadataEditorInfo ( [ FromRoute , Required ] Guid itemId )
{
var item = _libraryManager . GetItemById ( itemId ) ;
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
var info = new MetadataEditorInfo
{
2022-11-28 12:07:57 +00:00
ParentalRatingOptions = _localizationManager . GetParentalRatings ( ) . ToList ( ) ,
2023-01-31 11:18:10 +00:00
ExternalIdInfos = _providerManager . GetExternalIdInfos ( item ) . ToArray ( ) ,
Countries = _localizationManager . GetCountries ( ) . ToArray ( ) ,
Cultures = _localizationManager . GetCultures ( ) . ToArray ( )
} ;
if ( ! item . IsVirtualItem
& & item is not ICollectionFolder
& & item is not UserView
& & item is not AggregateFolder
& & item is not LiveTvChannel
& & item is not IItemByName
& & item . SourceType = = SourceType . Library )
{
var inheritedContentType = _libraryManager . GetInheritedContentType ( item ) ;
var configuredContentType = _libraryManager . GetConfiguredContentType ( item ) ;
2023-11-09 21:00:29 +00:00
if ( inheritedContentType is null | | configuredContentType is not null )
2020-06-08 18:20:33 +00:00
{
2023-01-31 11:18:10 +00:00
info . ContentTypeOptions = GetContentTypeOptions ( true ) . ToArray ( ) ;
info . ContentType = configuredContentType ;
2020-06-08 18:20:33 +00:00
2023-12-08 22:45:36 +00:00
if ( inheritedContentType is null | | inheritedContentType = = CollectionType . tvshows )
2020-06-08 18:20:33 +00:00
{
2023-01-31 11:18:10 +00:00
info . ContentTypeOptions = info . ContentTypeOptions
. Where ( i = > string . IsNullOrWhiteSpace ( i . Value )
2023-11-09 21:00:29 +00:00
| | string . Equals ( i . Value , "TvShows" , StringComparison . OrdinalIgnoreCase ) )
2023-01-31 11:18:10 +00:00
. ToArray ( ) ;
2020-06-08 18:20:33 +00:00
}
}
}
2023-01-31 11:18:10 +00:00
return info ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
/// <summary>
/// Updates an item's content type.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="contentType">The content type of the item.</param>
/// <response code="204">Item content type updated.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType ( [ FromRoute , Required ] Guid itemId , [ FromQuery ] string? contentType )
{
var item = _libraryManager . GetItemById ( itemId ) ;
if ( item is null )
{
return NotFound ( ) ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
var path = item . ContainingFolderPath ;
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
var types = _serverConfigurationManager . Configuration . ContentTypes
. Where ( i = > ! string . IsNullOrWhiteSpace ( i . Name ) )
. Where ( i = > ! string . Equals ( i . Name , path , StringComparison . OrdinalIgnoreCase ) )
. ToList ( ) ;
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
if ( ! string . IsNullOrWhiteSpace ( contentType ) )
{
types . Add ( new NameValuePair
{
Name = path ,
Value = contentType
} ) ;
2014-07-13 04:55:56 +00:00
}
2023-01-31 11:18:10 +00:00
_serverConfigurationManager . Configuration . ContentTypes = types . ToArray ( ) ;
_serverConfigurationManager . SaveConfiguration ( ) ;
return NoContent ( ) ;
}
2013-06-27 19:06:37 +00:00
2022-11-27 14:37:34 +00:00
private async Task UpdateItem ( BaseItemDto request , BaseItem item )
2023-01-31 11:18:10 +00:00
{
item . Name = request . Name ;
item . ForcedSortName = request . ForcedSortName ;
2016-04-20 05:21:40 +00:00
2023-01-31 11:18:10 +00:00
item . OriginalTitle = string . IsNullOrWhiteSpace ( request . OriginalTitle ) ? null : request . OriginalTitle ;
2013-09-17 02:08:18 +00:00
2023-01-31 11:18:10 +00:00
item . CriticRating = request . CriticRating ;
2013-12-05 16:50:21 +00:00
2023-01-31 11:18:10 +00:00
item . CommunityRating = request . CommunityRating ;
item . IndexNumber = request . IndexNumber ;
item . ParentIndexNumber = request . ParentIndexNumber ;
item . Overview = request . Overview ;
item . Genres = request . Genres ;
2014-04-27 03:42:05 +00:00
2023-01-31 11:18:10 +00:00
if ( item is Episode episode )
{
episode . AirsAfterSeasonNumber = request . AirsAfterSeasonNumber ;
episode . AirsBeforeEpisodeNumber = request . AirsBeforeEpisodeNumber ;
episode . AirsBeforeSeasonNumber = request . AirsBeforeSeasonNumber ;
}
2013-11-24 23:37:38 +00:00
2023-03-10 17:03:11 +00:00
if ( request . Height is not null & & item is LiveTvChannel channel )
{
channel . Height = request . Height . Value ;
}
2023-01-31 11:18:10 +00:00
if ( request . Taglines is not null )
{
item . Tagline = request . Taglines . FirstOrDefault ( ) ;
}
2013-11-24 23:37:38 +00:00
2023-01-31 11:18:10 +00:00
if ( request . Studios is not null )
{
item . Studios = request . Studios . Select ( x = > x . Name ) . ToArray ( ) ;
}
2013-09-03 14:26:17 +00:00
2023-01-31 11:18:10 +00:00
if ( request . DateCreated . HasValue )
{
item . DateCreated = NormalizeDateTime ( request . DateCreated . Value ) ;
}
2013-09-03 14:26:17 +00:00
2023-01-31 11:18:10 +00:00
item . EndDate = request . EndDate . HasValue ? NormalizeDateTime ( request . EndDate . Value ) : null ;
item . PremiereDate = request . PremiereDate . HasValue ? NormalizeDateTime ( request . PremiereDate . Value ) : null ;
item . ProductionYear = request . ProductionYear ;
2022-11-27 14:37:34 +00:00
request . OfficialRating = string . IsNullOrWhiteSpace ( request . OfficialRating ) ? null : request . OfficialRating ;
item . OfficialRating = request . OfficialRating ;
2023-01-31 11:18:10 +00:00
item . CustomRating = request . CustomRating ;
2016-10-21 18:41:49 +00:00
2023-05-14 19:45:46 +00:00
var currentTags = item . Tags ;
var newTags = request . Tags ;
var removedTags = currentTags . Except ( newTags ) . ToList ( ) ;
var addedTags = newTags . Except ( currentTags ) . ToList ( ) ;
item . Tags = newTags ;
2022-11-27 14:37:34 +00:00
if ( item is Series rseries )
{
foreach ( Season season in rseries . Children )
{
season . OfficialRating = request . OfficialRating ;
season . CustomRating = request . CustomRating ;
2023-05-14 19:45:46 +00:00
season . Tags = season . Tags . Concat ( addedTags ) . Except ( removedTags ) . Distinct ( ) . ToArray ( ) ;
2022-11-27 14:37:34 +00:00
season . OnMetadataChanged ( ) ;
await season . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
foreach ( Episode ep in season . Children )
{
ep . OfficialRating = request . OfficialRating ;
ep . CustomRating = request . CustomRating ;
2023-05-14 19:45:46 +00:00
ep . Tags = ep . Tags . Concat ( addedTags ) . Except ( removedTags ) . Distinct ( ) . ToArray ( ) ;
2022-11-27 14:37:34 +00:00
ep . OnMetadataChanged ( ) ;
await ep . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
}
}
}
else if ( item is Season season )
{
foreach ( Episode ep in season . Children )
{
ep . OfficialRating = request . OfficialRating ;
ep . CustomRating = request . CustomRating ;
2023-05-14 19:45:46 +00:00
ep . Tags = ep . Tags . Concat ( addedTags ) . Except ( removedTags ) . Distinct ( ) . ToArray ( ) ;
2022-11-27 14:37:34 +00:00
ep . OnMetadataChanged ( ) ;
await ep . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
}
}
else if ( item is MusicAlbum album )
{
foreach ( BaseItem track in album . Children )
{
track . OfficialRating = request . OfficialRating ;
track . CustomRating = request . CustomRating ;
2023-05-14 19:45:46 +00:00
track . Tags = track . Tags . Concat ( addedTags ) . Except ( removedTags ) . Distinct ( ) . ToArray ( ) ;
2022-11-27 14:37:34 +00:00
track . OnMetadataChanged ( ) ;
await track . UpdateToRepositoryAsync ( ItemUpdateType . MetadataEdit , CancellationToken . None ) . ConfigureAwait ( false ) ;
}
}
2023-01-31 11:18:10 +00:00
if ( request . ProductionLocations is not null )
{
item . ProductionLocations = request . ProductionLocations ;
}
2014-01-03 20:32:27 +00:00
2023-01-31 11:18:10 +00:00
item . PreferredMetadataCountryCode = request . PreferredMetadataCountryCode ;
item . PreferredMetadataLanguage = request . PreferredMetadataLanguage ;
2014-04-27 03:42:05 +00:00
2023-01-31 11:18:10 +00:00
if ( item is IHasDisplayOrder hasDisplayOrder )
{
hasDisplayOrder . DisplayOrder = request . DisplayOrder ;
}
if ( item is IHasAspectRatio hasAspectRatio )
{
hasAspectRatio . AspectRatio = request . AspectRatio ;
}
2014-02-19 05:21:03 +00:00
2023-01-31 11:18:10 +00:00
item . IsLocked = request . LockData ? ? false ;
2014-02-19 05:21:03 +00:00
2023-01-31 11:18:10 +00:00
if ( request . LockedFields is not null )
{
item . LockedFields = request . LockedFields ;
}
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
// Only allow this for series. Runtimes for media comes from ffprobe.
if ( item is Series )
{
item . RunTimeTicks = request . RunTimeTicks ;
}
2013-09-13 18:37:44 +00:00
2023-01-31 11:18:10 +00:00
foreach ( var pair in request . ProviderIds . ToList ( ) )
{
if ( string . IsNullOrEmpty ( pair . Value ) )
2013-06-27 19:06:37 +00:00
{
2023-01-31 11:18:10 +00:00
request . ProviderIds . Remove ( pair . Key ) ;
2013-06-27 19:06:37 +00:00
}
2023-01-31 11:18:10 +00:00
}
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
item . ProviderIds = request . ProviderIds ;
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
if ( item is Video video )
{
video . Video3DFormat = request . Video3DFormat ;
}
2013-09-03 14:26:17 +00:00
2023-01-31 11:18:10 +00:00
if ( request . AlbumArtists is not null )
{
if ( item is IHasAlbumArtist hasAlbumArtists )
2013-06-27 19:06:37 +00:00
{
2023-01-31 11:18:10 +00:00
hasAlbumArtists . AlbumArtists = request
. AlbumArtists
. Select ( i = > i . Name )
. ToArray ( ) ;
2013-06-27 19:06:37 +00:00
}
2023-01-31 11:18:10 +00:00
}
2013-06-27 19:06:37 +00:00
2023-01-31 11:18:10 +00:00
if ( request . ArtistItems is not null )
{
if ( item is IHasArtist hasArtists )
2015-03-13 17:25:28 +00:00
{
2023-01-31 11:18:10 +00:00
hasArtists . Artists = request
. ArtistItems
. Select ( i = > i . Name )
. ToArray ( ) ;
2015-03-13 17:25:28 +00:00
}
2023-01-31 11:18:10 +00:00
}
2015-03-16 01:48:25 +00:00
2023-01-31 11:18:10 +00:00
switch ( item )
{
case Audio song :
song . Album = request . Album ;
break ;
case MusicVideo musicVideo :
musicVideo . Album = request . Album ;
break ;
case Series series :
2020-06-08 18:20:33 +00:00
{
series . Status = GetSeriesStatus ( request ) ;
2013-07-10 20:10:55 +00:00
2022-12-05 14:01:13 +00:00
if ( request . AirDays is not null )
2020-06-08 18:20:33 +00:00
{
series . AirDays = request . AirDays ;
series . AirTime = request . AirTime ;
}
2017-08-13 20:15:07 +00:00
2020-06-08 18:20:33 +00:00
break ;
2017-08-13 20:15:07 +00:00
}
2013-06-27 19:06:37 +00:00
}
2023-01-31 11:18:10 +00:00
}
2017-02-19 03:46:09 +00:00
2023-01-31 11:18:10 +00:00
private SeriesStatus ? GetSeriesStatus ( BaseItemDto item )
{
if ( string . IsNullOrEmpty ( item . Status ) )
2017-02-19 03:46:09 +00:00
{
2023-01-31 11:18:10 +00:00
return null ;
2017-02-19 03:46:09 +00:00
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
return ( SeriesStatus ) Enum . Parse ( typeof ( SeriesStatus ) , item . Status , true ) ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
private DateTime NormalizeDateTime ( DateTime val )
{
return DateTime . SpecifyKind ( val , DateTimeKind . Utc ) ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
private List < NameValuePair > GetContentTypeOptions ( bool isForItem )
{
var list = new List < NameValuePair > ( ) ;
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
if ( isForItem )
{
2020-06-08 18:20:33 +00:00
list . Add ( new NameValuePair
{
2023-01-31 11:18:10 +00:00
Name = "Inherit" ,
Value = string . Empty
2020-06-08 18:20:33 +00:00
} ) ;
2023-01-31 11:18:10 +00:00
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
list . Add ( new NameValuePair
{
Name = "Movies" ,
Value = "movies"
} ) ;
list . Add ( new NameValuePair
{
Name = "Music" ,
Value = "music"
} ) ;
list . Add ( new NameValuePair
{
Name = "Shows" ,
Value = "tvshows"
} ) ;
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
if ( ! isForItem )
{
2020-06-08 18:20:33 +00:00
list . Add ( new NameValuePair
{
2023-01-31 11:18:10 +00:00
Name = "Books" ,
Value = "books"
2020-06-08 18:20:33 +00:00
} ) ;
2023-01-31 11:18:10 +00:00
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
list . Add ( new NameValuePair
{
Name = "HomeVideos" ,
Value = "homevideos"
} ) ;
list . Add ( new NameValuePair
{
Name = "MusicVideos" ,
Value = "musicvideos"
} ) ;
list . Add ( new NameValuePair
{
Name = "Photos" ,
Value = "photos"
} ) ;
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
if ( ! isForItem )
{
list . Add ( new NameValuePair
2020-06-08 18:20:33 +00:00
{
2023-01-31 11:18:10 +00:00
Name = "MixedContent" ,
Value = string . Empty
} ) ;
}
2020-06-08 18:20:33 +00:00
2023-01-31 11:18:10 +00:00
foreach ( var val in list )
{
val . Name = _localizationManager . GetLocalizedString ( val . Name ) ;
2020-06-08 18:20:33 +00:00
}
2023-01-31 11:18:10 +00:00
return list ;
2013-06-27 19:06:37 +00:00
}
}