317 lines
8.3 KiB
JavaScript
317 lines
8.3 KiB
JavaScript
import hashApiClient from "../network/clients/beatmaps/api-hash";
|
|
import keyApiClient from "../network/clients/beatmaps/api-key";
|
|
import { PRIORITY } from "../network/queues/http-queue";
|
|
import log from "../utils/logger";
|
|
import { SsrHttpNotFoundError, SsrNetworkError } from "../network/errors";
|
|
import songsBeatMapsRepository from "../db/repository/songs-beatmaps";
|
|
import cacheRepository from "../db/repository/cache";
|
|
import { addToDate, dateFromString, HOUR } from "../utils/date";
|
|
import { capitalize, opt } from "../utils/js";
|
|
|
|
const BM_SUSPENSION_KEY = "bmSuspension";
|
|
const BM_NOT_FOUND_KEY = "bm404";
|
|
const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1;
|
|
|
|
const INVALID_NOTES_COUNT_FIXES = {
|
|
e738b38b594861745bfb0473c66ca5cca15072ff: [
|
|
{ type: "Standard", diff: "ExpertPlus", notes: 942 },
|
|
],
|
|
};
|
|
|
|
export default () => {
|
|
const cacheSongInfo = async (songInfo, originalHash) => {
|
|
if (!songInfo) return null;
|
|
|
|
const hash =
|
|
originalHash && originalHash.length ? originalHash : songInfo.hash;
|
|
|
|
if (!hash || !songInfo.key) return null;
|
|
|
|
songInfo.hash = hash.toLowerCase();
|
|
songInfo.key = songInfo.key.toLowerCase();
|
|
|
|
delete songInfo.description;
|
|
|
|
await songsBeatMapsRepository().set(songInfo);
|
|
|
|
return songInfo;
|
|
};
|
|
|
|
const isSuspended = (bsSuspension) =>
|
|
!!bsSuspension &&
|
|
bsSuspension.activeTo > new Date() &&
|
|
bsSuspension.started > addToDate(-24 * HOUR);
|
|
const getCurrentSuspension = async () =>
|
|
cacheRepository().get(BM_SUSPENSION_KEY);
|
|
const prolongSuspension = async (bsSuspension) => {
|
|
const current = new Date();
|
|
|
|
const suspension = isSuspended(bsSuspension)
|
|
? bsSuspension
|
|
: { started: current, activeTo: new Date(), count: 0 };
|
|
|
|
suspension.activeTo = addToDate(
|
|
Math.pow(2, suspension.count) * HOUR,
|
|
suspension.activeTo,
|
|
);
|
|
suspension.count++;
|
|
|
|
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
|
};
|
|
|
|
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
|
const set404Hashes = async (hashes) =>
|
|
cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
|
const setHashNotFound = async (hash) => {
|
|
let songs404 = await get404Hashes();
|
|
if (!songs404) songs404 = {};
|
|
|
|
const item = songs404[hash]
|
|
? songs404[hash]
|
|
: { firstTry: new Date(), recentTry: null, count: 0 };
|
|
|
|
if (
|
|
!item.recentTry ||
|
|
addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) <
|
|
new Date()
|
|
) {
|
|
item.recentTry = new Date();
|
|
item.count++;
|
|
|
|
songs404[hash] = item;
|
|
|
|
await set404Hashes(songs404);
|
|
}
|
|
};
|
|
const isHashUnavailable = async (hash) => {
|
|
const songs404 = await get404Hashes();
|
|
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
|
};
|
|
|
|
const fixInvalidNotesCount = (hash, songInfo) => {
|
|
if (!hash) return songInfo;
|
|
|
|
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
|
songInfo.versions.forEach((si) => {
|
|
if (!si?.diffs) return;
|
|
|
|
si.diffs.forEach((d) => {
|
|
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(
|
|
(f) => f.type === d?.characteristic && f.diff === d?.difficulty,
|
|
);
|
|
if (!newNotesCnt) return;
|
|
|
|
d.notes = newNotesCnt.notes;
|
|
});
|
|
});
|
|
|
|
return songInfo;
|
|
};
|
|
|
|
const fetchSong = async (
|
|
songInfo,
|
|
fetchFunc,
|
|
forceUpdate = false,
|
|
cacheOnly = false,
|
|
errSongId = "",
|
|
hash = null,
|
|
) => {
|
|
if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
|
|
|
|
if (cacheOnly) return null;
|
|
|
|
let bsSuspension = await getCurrentSuspension();
|
|
|
|
try {
|
|
if (
|
|
isSuspended(bsSuspension) ||
|
|
(hash && (await isHashUnavailable(hash)))
|
|
)
|
|
return null;
|
|
|
|
const songInfo = await fetchFunc();
|
|
if (!songInfo) {
|
|
log.warn(`Song "${errSongId}" is no longer available at BeatSaver.`);
|
|
return null;
|
|
}
|
|
|
|
return fixInvalidNotesCount(hash, cacheSongInfo(songInfo, hash));
|
|
} catch (err) {
|
|
if (hash && err instanceof SsrHttpNotFoundError) {
|
|
await setHashNotFound(hash);
|
|
}
|
|
|
|
if (err instanceof SsrNetworkError && err.message === "Network error") {
|
|
try {
|
|
await prolongSuspension(bsSuspension);
|
|
} catch {}
|
|
}
|
|
|
|
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const byHash = async (
|
|
hash,
|
|
forceUpdate = false,
|
|
cacheOnly = false,
|
|
signal = null,
|
|
priority = PRIORITY.FG_LOW,
|
|
) => {
|
|
hash = hash.toLowerCase();
|
|
|
|
const songInfo = await songsBeatMapsRepository().get(hash);
|
|
|
|
return fetchSong(
|
|
songInfo,
|
|
() => hashApiClient.getProcessed({ hash, signal, priority }),
|
|
forceUpdate,
|
|
cacheOnly,
|
|
hash,
|
|
hash,
|
|
);
|
|
};
|
|
|
|
const byKey = async (
|
|
key,
|
|
forceUpdate = false,
|
|
cacheOnly = false,
|
|
signal = null,
|
|
priority = PRIORITY.FG_LOW,
|
|
) => {
|
|
key = key.toLowerCase();
|
|
|
|
const songInfo = await songsBeatMapsRepository().getFromIndex(
|
|
"songs-beatmaps-key",
|
|
key,
|
|
);
|
|
|
|
return fetchSong(
|
|
songInfo,
|
|
() => keyApiClient.getProcessed({ key, signal, priority }),
|
|
forceUpdate,
|
|
cacheOnly,
|
|
key,
|
|
);
|
|
};
|
|
|
|
const convertOldBeatSaverToBeatMaps = (song) => {
|
|
let {
|
|
key,
|
|
hash,
|
|
name,
|
|
metadata: { characteristics },
|
|
} = song;
|
|
|
|
if (
|
|
!key ||
|
|
!hash ||
|
|
!name ||
|
|
!characteristics ||
|
|
!Array.isArray(characteristics)
|
|
)
|
|
return null;
|
|
|
|
if (hash.toLowerCase) hash = hash.toLowerCase();
|
|
|
|
const diffs = characteristics.reduce((diffs, ch) => {
|
|
if (!ch.name || !ch.difficulties) return diffs;
|
|
const characteristic = ch.name;
|
|
|
|
return diffs
|
|
.concat(
|
|
Object.entries(ch.difficulties).map(([difficulty, obj]) => {
|
|
if (!obj) return null;
|
|
difficulty = capitalize(difficulty);
|
|
|
|
const seconds = opt(obj, "length", null);
|
|
const notes = opt(obj, "notes", null);
|
|
|
|
const nps = notes && seconds ? notes / seconds : null;
|
|
|
|
return {
|
|
njs: opt(obj, "njs", null),
|
|
offset: opt(obj, "njsOffset", null),
|
|
notes,
|
|
bombs: opt(obj, "bombs", null),
|
|
obstacles: opt(obj, "obstacles", null),
|
|
nps,
|
|
length: opt(obj, "duration", null),
|
|
characteristic,
|
|
difficulty,
|
|
events: null,
|
|
chroma: null,
|
|
me: null,
|
|
ne: null,
|
|
cinema: null,
|
|
seconds,
|
|
paritySummary: {
|
|
errors: null,
|
|
warns: null,
|
|
resets: null,
|
|
},
|
|
stars: null,
|
|
};
|
|
}),
|
|
)
|
|
.filter((diff) => diff);
|
|
}, []);
|
|
|
|
return {
|
|
lastUpdated: dateFromString(opt(song, "uploaded", new Date())),
|
|
oldBeatSaverId: opt(song, "_id", null),
|
|
id: key,
|
|
hash,
|
|
key,
|
|
name,
|
|
description: "",
|
|
uploader: {
|
|
id: null,
|
|
name: opt(song, "uploader.username", null),
|
|
hash: null,
|
|
avatar: null,
|
|
},
|
|
metadata: {
|
|
bpm: opt(song, "metadata.bpm", null),
|
|
duration: opt(song, "metadata.duration", null),
|
|
songName: opt(song, "metadata.songName", ""),
|
|
songSubName: opt(song, "metadata.songSubName", ""),
|
|
songAuthorName: opt(song, "metadata.songAuthorName", ""),
|
|
levelAuthorName: opt(song, "metadata.levelAuthorName", ""),
|
|
},
|
|
stats: {
|
|
plays: opt(song, "stats.plays", 0),
|
|
downloads: opt(song, "stats.downloads", 0),
|
|
upvotes: opt(song, "stats.upVotes", 0),
|
|
downvotes: opt(song, "stats.downVotes", 0),
|
|
score: null,
|
|
},
|
|
uploaded: opt(song, "uploaded", null),
|
|
automapper: !!opt(song, "metadata.automapper", false),
|
|
ranked: null,
|
|
qualified: null,
|
|
versions: [
|
|
{
|
|
hash,
|
|
key,
|
|
state: "Published",
|
|
createdAt: opt(song, "uploaded", null),
|
|
sageScore: null,
|
|
diffs,
|
|
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
|
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
|
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`,
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
return {
|
|
byHash,
|
|
byKey,
|
|
convertOldBeatSaverToBeatMaps,
|
|
};
|
|
};
|