|
|
|
|
@@ -190,7 +190,11 @@ public final class VideoCache {
|
|
|
|
|
public static void preload(String url) {
|
|
|
|
|
if (url == null || url.isEmpty()) return;
|
|
|
|
|
if (!(url.startsWith("http://") || url.startsWith("https://"))) return;
|
|
|
|
|
if (READY.containsKey(url)) {
|
|
|
|
|
// Use lookup() (which also verifies the file exists on disk) instead of
|
|
|
|
|
// READY.containsKey — defends against READY containing a stale entry whose
|
|
|
|
|
// backing file was removed by clearAll() / a user-side file delete. Without this,
|
|
|
|
|
// a stale key would silently block a re-preload.
|
|
|
|
|
if (lookup(url) != null) {
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: already cached {}", VideoPlayerMod.MOD_ID, url);
|
|
|
|
|
notifyChat("[videopreload] 이미 캐시됨: " + url, ChatFormatting.GRAY);
|
|
|
|
|
return;
|
|
|
|
|
@@ -229,6 +233,33 @@ public final class VideoCache {
|
|
|
|
|
|
|
|
|
|
// -- internals -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Publish {@code path} into {@link #READY} for {@code url}, but only if the cache
|
|
|
|
|
* epoch hasn't moved since {@code startEpoch} was captured. Wraps the
|
|
|
|
|
* pre-check / put / post-check / rollback dance so every publish site uses the
|
|
|
|
|
* same race-free pattern instead of re-implementing it.
|
|
|
|
|
*
|
|
|
|
|
* <p>Race coverage: if {@link #clearAll()} runs at any point between the
|
|
|
|
|
* pre-check and the post-check, the post-check sees the epoch bump and we
|
|
|
|
|
* roll back — {@link Map#remove(Object, Object)} is a compareAndRemove so we
|
|
|
|
|
* don't clobber a legitimate concurrent put under the same key, and we delete
|
|
|
|
|
* the on-disk file because clearAll's directory scan may have missed it (the
|
|
|
|
|
* file may not have existed yet when clearAll ran).
|
|
|
|
|
*
|
|
|
|
|
* @return {@code true} if the entry is now published; {@code false} if the
|
|
|
|
|
* caller's work was cancelled and {@code path} has been cleaned up.
|
|
|
|
|
*/
|
|
|
|
|
private static boolean publishIfNotCancelled(String url, Path path, long startEpoch) {
|
|
|
|
|
if (CACHE_EPOCH.get() != startEpoch) return false;
|
|
|
|
|
READY.put(url, path);
|
|
|
|
|
if (CACHE_EPOCH.get() != startEpoch) {
|
|
|
|
|
READY.remove(url, path);
|
|
|
|
|
try { Files.deleteIfExists(path); } catch (Throwable ignored) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void download(String url, long startEpoch) {
|
|
|
|
|
Path cacheDir = cacheDir();
|
|
|
|
|
Path partPath = null;
|
|
|
|
|
@@ -255,8 +286,11 @@ public final class VideoCache {
|
|
|
|
|
// Resume-friendly: if the file's already on disk from an earlier session, just
|
|
|
|
|
// index it without re-downloading.
|
|
|
|
|
if (Files.exists(finalPath) && Files.size(finalPath) > 0) {
|
|
|
|
|
if (CACHE_EPOCH.get() != startEpoch) return;
|
|
|
|
|
READY.put(url, finalPath);
|
|
|
|
|
if (!publishIfNotCancelled(url, finalPath, startEpoch)) {
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: reindex cancelled (clearAll ran) — {}",
|
|
|
|
|
VideoPlayerMod.MOD_ID, url);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: indexed existing cache {} -> {}",
|
|
|
|
|
VideoPlayerMod.MOD_ID, url, finalPath.getFileName());
|
|
|
|
|
notifyChat("[videopreload] 기존 캐시 사용: " + url, ChatFormatting.GREEN);
|
|
|
|
|
@@ -352,21 +386,16 @@ public final class VideoCache {
|
|
|
|
|
Files.move(partPath, finalPath, StandardCopyOption.REPLACE_EXISTING,
|
|
|
|
|
StandardCopyOption.ATOMIC_MOVE);
|
|
|
|
|
|
|
|
|
|
// Post-move cancellation check. clearAll() may have run between the pre-move
|
|
|
|
|
// check above and the move itself — in that case clearAll's directory scan
|
|
|
|
|
// either missed our file (it didn't exist yet) or saw the .part and skipped /
|
|
|
|
|
// failed to delete it. Either way, finalPath now exists on disk but the
|
|
|
|
|
// user-visible cache state is "cleared", so we must delete the file and skip
|
|
|
|
|
// both the READY.put and the "완료" chat. Without this check, a clear right at
|
|
|
|
|
// this window leaves a resurrected file in READY and a stale "완료" message.
|
|
|
|
|
if (CACHE_EPOCH.get() != startEpoch) {
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: cancelled after move (clearAll ran) — {}",
|
|
|
|
|
// Race-safe publish: this covers the window between Files.move and READY.put
|
|
|
|
|
// (clearAll deletes the freshly-moved file then we resurrect it in READY) AND
|
|
|
|
|
// the window between READY.put and the "완료" notification (clearAll wipes
|
|
|
|
|
// READY then we re-publish + emit stale completion chat). publishIfNotCancelled
|
|
|
|
|
// rolls back both READY and the on-disk file if the epoch moved at any time.
|
|
|
|
|
if (!publishIfNotCancelled(url, finalPath, startEpoch)) {
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: cancelled at publish (clearAll ran) — {}",
|
|
|
|
|
VideoPlayerMod.MOD_ID, url);
|
|
|
|
|
try { Files.deleteIfExists(finalPath); } catch (Throwable ignored) {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
READY.put(url, finalPath);
|
|
|
|
|
VideoPlayerMod.LOG.info("[{}] preload: cached {} ({} bytes) -> {}",
|
|
|
|
|
VideoPlayerMod.MOD_ID, url, total, finalPath.getFileName());
|
|
|
|
|
long mb = Math.max(1, total / (1024 * 1024));
|
|
|
|
|
|