// OpenTween - Client of Twitter
// Copyright (c) 2013 kim_upsilon (@kim_upsilon)
// All rights reserved.
//
// This file is part of OpenTween.
//
// 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 3 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, see , or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using OpenTween.Connection;
namespace OpenTween
{
public class ImageCache : IDisposable
{
///
/// キャッシュとして URL と取得した画像を対に保持する辞書
///
internal LRUCacheDictionary> InnerDictionary;
///
/// 非同期タスクをキャンセルするためのトークンのもと
///
private CancellationTokenSource cancelTokenSource;
///
/// innerDictionary の排他制御のためのロックオブジェクト
///
private readonly object lockObject = new object();
///
/// オブジェクトが破棄された否か
///
private bool disposed = false;
public ImageCache()
{
this.InnerDictionary = new LRUCacheDictionary>(trimLimit: 300, autoTrimCount: 100);
this.InnerDictionary.CacheRemoved += (s, e) =>
{
// まだ参照されている場合もあるのでDisposeはファイナライザ任せ
this.CacheRemoveCount++;
};
this.cancelTokenSource = new CancellationTokenSource();
}
///
/// 保持しているキャッシュの件数
///
public long CacheCount
=> this.InnerDictionary.Count;
///
/// 破棄されたキャッシュの件数
///
public int CacheRemoveCount { get; private set; }
///
/// 指定された URL にある画像を非同期に取得するメソッド
///
/// 取得先の URL
/// キャッシュを使用せずに取得する場合は true
/// 非同期に画像を取得するタスク
public Task DownloadImageAsync(string address, bool force = false)
{
var cancelToken = this.cancelTokenSource.Token;
return Task.Run(() =>
{
lock (this.lockObject)
{
this.InnerDictionary.TryGetValue(address, out var cachedImageTask);
if (cachedImageTask != null)
{
if (force)
this.InnerDictionary.Remove(address);
else
return cachedImageTask;
}
cancelToken.ThrowIfCancellationRequested();
var imageTask = this.FetchImageAsync(address, cancelToken);
this.InnerDictionary[address] = imageTask;
return imageTask;
}
},
cancelToken);
}
private async Task FetchImageAsync(string uri, CancellationToken cancelToken)
{
using var response = await Networking.Http.GetAsync(uri, cancelToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using var imageStream = await response.Content.ReadAsStreamAsync()
.ConfigureAwait(false);
return await MemoryImage.CopyFromStreamAsync(imageStream)
.ConfigureAwait(false);
}
public MemoryImage? TryGetFromCache(string address)
{
lock (this.lockObject)
{
if (!this.InnerDictionary.TryGetValue(address, out var imageTask) ||
imageTask.Status != TaskStatus.RanToCompletion)
return null;
return imageTask.Result;
}
}
public void CancelAsync()
{
lock (this.lockObject)
{
var oldTokenSource = this.cancelTokenSource;
this.cancelTokenSource = new CancellationTokenSource();
oldTokenSource.Cancel();
oldTokenSource.Dispose();
}
}
protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
this.CancelAsync();
lock (this.lockObject)
{
foreach (var (_, task) in this.InnerDictionary)
{
if (task.Status == TaskStatus.RanToCompletion)
task.Result?.Dispose();
}
this.InnerDictionary.Clear();
this.cancelTokenSource.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
~ImageCache()
=> this.Dispose(false);
}
}