JJ_MKVMaker/lib/core/remote_thumbs.dart

160 lines
5.3 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;
/// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 (WebDAV · SFTP · SMB · FTP) 의 그림 · 동영상 썸네일.
/// - 받는 때: 설정 'wifi' (처음 값) 면 데이터 요금제가 아닌 망에서만, 'always' 면 늘, 'off' 면 받지 않음
/// - 서버에 부담이 가지 않게 동시에 [parallel] 개까지 (처음 2). 줄 세운 것 중 화면에서 사라진 줄은 시작하지 않는다
/// - 만든 썸네일은 [dir] 에 두고, 모두 [maxBytes] 를 넘으면 오래 쓰지 않은 것부터 지운다. [clear] 로 모두 지움
/// - 만들기 ([make]) 는 부르는 쪽이 준다 (앱: FFmpeg 가 이 기기 안 스트리밍 다리에서 앞 · 뒤 일부만 읽음)
class RemoteThumbs {
final Directory dir;
/// 캐시 상한 (바이트)
int Function() maxBytes;
/// 동시에 받는 수
final int parallel;
/// 지금 받아도 되는지 (설정 · 망)
final Future<bool> Function() allowed;
/// [path] 의 썸네일을 [out] (jpg) 에 만든다 - 만들었으면 true
final Future<bool> Function(String path, String out) make;
RemoteThumbs({required this.dir, required this.maxBytes, required this.allowed, required this.make, this.parallel = 2});
final _done = <String, File?>{};
final _waiting = Queue<_Job>();
final _jobs = <String, _Job>{};
int _running = 0;
/// 시험: 지금까지 [make] 를 부른 경로 · 동시에 돈 가장 큰 수
final made = <String>[];
int peak = 0;
static String keyOf(String path, int size, DateTime modified) => '$path|$size|${modified.millisecondsSinceEpoch}';
/// 이미 만든 것 (없으면 null - 만들기는 [request])
File? cached(String key) => _done[key];
/// [path] 의 기억을 잊는다 (보관함에 넣은 파일 - 관리자 지시 10/21. 파일은 removeThumbCaches 가 지움)
void forget(String path) => _done.removeWhere((k, _) => k.startsWith('$path|'));
/// 썸네일을 달라고 줄 세운다. [wanted] 가 시작할 때 false 면 (화면에서 사라진 줄) 건너뛴다.
/// 끝나면 [onDone] (못 만들었으면 null). 받지 않는 망이면 줄 세우지 않고 null
Future<File?> request(String path, String key, {required bool Function() wanted}) async {
if (_done.containsKey(key)) return _done[key];
final out = File(p.join(dir.path, '${md5.convert(utf8.encode(key))}.jpg'));
if (await out.exists() && await out.length() > 0) {
unawaited(out.setLastModified(DateTime.now()).catchError((_) {}));
return _done[key] = out;
}
final old = _jobs[key];
if (old != null) {
old.wanted.add(wanted);
return old.result.future;
}
if (!await allowed()) return null;
final job = _Job(path, key, out)..wanted.add(wanted);
_jobs[key] = job;
_waiting.add(job);
_pump();
return job.result.future;
}
void _pump() {
while (_running < parallel && _waiting.isNotEmpty) {
final job = _waiting.removeFirst();
// 화면에서 사라진 줄이면 시작하지 않는다 (다시 보이면 다시 달라고 한다)
if (!job.wanted.any((w) => w())) {
_jobs.remove(job.key);
job.result.complete(null);
continue;
}
_running++;
if (_running > peak) peak = _running;
unawaited(_run(job));
}
}
Future<void> _run(_Job job) async {
File? got;
try {
if (await allowed()) {
await dir.create(recursive: true);
made.add(job.path);
if (await make(job.path, job.out.path) && await job.out.exists() && await job.out.length() > 0) {
got = job.out;
_done[job.key] = got;
await _trim();
}
}
} catch (_) {
got = null;
} finally {
_running--;
_jobs.remove(job.key);
job.result.complete(got);
_pump();
}
}
/// 캐시의 크기 (바이트)
Future<int> size() async {
var n = 0;
if (!await dir.exists()) return 0;
await for (final f in dir.list()) {
if (f is File) n += await f.length().catchError((_) => 0);
}
return n;
}
/// 상한을 넘으면 오래 쓰지 않은 것 (바뀐 때가 옛것) 부터 지워 상한의 90% 아래로
Future<void> _trim() async {
final files = <(File, int, DateTime)>[];
var total = 0;
await for (final f in dir.list()) {
if (f is! File) continue;
final st = await f.stat();
files.add((f, st.size, st.modified));
total += st.size;
}
final cap = maxBytes();
if (total <= cap) return;
files.sort((a, b) => a.$3.compareTo(b.$3));
for (final (f, size, _) in files) {
if (total <= cap * 0.9) break;
try {
await f.delete();
total -= size;
_done.removeWhere((_, v) => v?.path == f.path);
} catch (_) {}
}
}
/// 모두 지운다 (환경 설정 [캐시 지우기])
Future<void> clear() async {
_done.clear();
if (await dir.exists()) {
await for (final f in dir.list()) {
try {
await f.delete(recursive: true);
} catch (_) {}
}
}
}
}
class _Job {
final String path, key;
final File out;
final wanted = <bool Function()>[];
final result = Completer<File?>();
_Job(this.path, this.key, this.out);
}