wakitsu
Version:
Hobby project for managing anime watch list on Kitsu through CLI
270 lines • 10.4 kB
JavaScript
import { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';
import { parseFansubFilename, pathJoin } from './utils.js';
import { Printer } from './printer/printer.js';
import { Kitsu } from './kitsu/kitsu.js';
import { Config } from './config.js';
import { colorWord } from './printer/print-colors.js';
export async function useAnimeWatcher({ titleOrCache, episode, workingDir, isDiscovering, }) {
const rootDir = workingDir ?? process.cwd();
const [epNum, forcedEpNum] = episode;
let anime;
let fileNameQuery;
let fileBinding;
let cacheIndex;
const discover = isDiscovering ?? false;
tryCreateWatchedDir(rootDir);
if (typeof titleOrCache == 'string') {
let animeTitle = titleOrCache;
if (discover) {
const files = filterFansubFilenames(rootDir, titleOrCache, epNum);
if (files.length > 1) {
return [
{ msg: 'MULTIPLE_FILES', data: { fileNames: files, epNum } },
null,
];
}
animeTitle = await Printer.prompt('Enter anime title (can be partial)');
}
const cachedAnime = Kitsu.findCachedAnime(animeTitle);
if (!cachedAnime.length) {
return [{ msg: 'CACHE_NOT_FOUND', data: null }, null];
}
if (cachedAnime.length > 1) {
return [{ msg: 'MULTIPLE_CACHES', data: cachedAnime }, null];
}
if (discover) {
const hasConsent = await Printer.promptYesNo(`Is the japanese title: "${cachedAnime[0][0].jpTitle} and English ` +
`title: "${cachedAnime[0][0].enTitle}" correct`);
if (!hasConsent) {
return useAnimeWatcher({ titleOrCache, episode, workingDir, isDiscovering });
}
}
anime = cachedAnime[0][0];
cacheIndex = cachedAnime[0][1];
fileBinding = Kitsu.getFileBinding(anime.libID);
fileNameQuery = fileBinding ?? discover ? titleOrCache : animeTitle;
}
else {
anime = titleOrCache[0];
cacheIndex = titleOrCache[1];
fileBinding = titleOrCache[2];
fileNameQuery = fileBinding;
}
function setProgress(newEp, newForcedEp) {
return saveProgress({
anime,
cacheIndex,
epNum: newEp ?? epNum,
forcedEpNum: newForcedEp ?? forcedEpNum,
});
}
function useFansubMover(props) {
const filterEpNum = props?.filterEpNum ?? true;
const newEp = props?.newEp ?? epNum;
const fileNames = filterFansubFilenames(rootDir, fileNameQuery, filterEpNum ? newEp : undefined);
const firstFileName = fileNames[0] ? [fileNames[0]] : [];
return manageFile(filterEpNum ? fileNames : firstFileName, rootDir, anime.libID, newEp ?? epNum, fileBinding);
}
return [
null,
{
anime,
fileBinding,
setProgress,
useFansubMover,
},
];
}
export async function useAnimeAutoWatcher({ titleOrCache, workingDir, }) {
const rootDir = workingDir ?? process.cwd();
tryCreateWatchedDir(rootDir);
const [error, watcher] = await useAnimeWatcher({
titleOrCache,
episode: [0, 0],
workingDir: rootDir,
});
if (error) {
return [error, null];
}
const { anime, useFansubMover } = watcher;
const newProgress = anime.epProgress + 1;
const [fansubError, mover] = useFansubMover({ newEp: newProgress, filterEpNum: false });
if (fansubError) {
return [fansubError, null];
}
return [
null,
{
anime,
fileData: mover.fileData,
progress: anime.epProgress,
newProgress,
setProgress: () => watcher.setProgress(newProgress),
moveFansubFile: () => mover.move(),
},
];
}
function manageFile(fansubFileNames, rootDir, libID, epNum, fileBinding) {
if (!fansubFileNames.length) {
return [{ msg: 'FILE_NOT_FOUND', data: { epNum } }, null];
}
if (fansubFileNames.length > 1) {
return [
{ msg: 'MULTIPLE_FILES', data: { fileNames: fansubFileNames, epNum } },
null,
];
}
const [fileName] = fansubFileNames;
const [error, data] = parseFansubFilename(fileName);
if (error) {
throw Error(`watch::${error.parseError}`);
}
if (!fileBinding) {
Kitsu.setFileBinding(libID, data.title.toLowerCase());
}
return [
null,
{
fileData: data,
move: () => renameSync(pathJoin(rootDir, fileName), pathJoin(rootDir, 'watched', fileName)),
},
];
}
function tryCreateWatchedDir(rootDir) {
const watchDir = pathJoin(rootDir, 'watched');
if (!existsSync(watchDir)) {
mkdirSync(watchDir);
Printer.print([null, ['', `;c;Watch Dir: ;by;${watchDir}`]]);
}
}
function filterFansubFilenames(workingDir, epName, epNum) {
const includesEpName = (str) => {
return str.match(/^\[([\w|\d|\s-]+)\]/gi) && str.toLowerCase().includes(epName);
};
const includesEpNum = (str) => {
const epStr = (epNum && `${epNum}`.padStart(2, '0')) || undefined;
return epStr && (str.includes(`- ${epStr}`) || str.includes(`E${epStr}`));
};
return readdirSync(workingDir, { withFileTypes: true })
.filter((file) => file.isFile())
.map((file) => file.name)
.filter((name) => epNum ? includesEpName(name) && includesEpNum(name) : includesEpName(name));
}
async function saveProgress(opt) {
const { anime, cacheIndex, forcedEpNum, epNum } = opt;
const [progress, episodeCount, tokenExpiresIn] = await Kitsu.updateAnime(...buildLibPatchReqArgs(anime.libID, forcedEpNum || epNum));
anime.epProgress = progress;
anime.epCount = episodeCount ?? 0;
if (progress > 0 && progress == episodeCount) {
Kitsu.removeAnimeFromCache(anime, { saveConfig: false });
Config.save();
return {
completed: true,
tokenExpiresIn,
anime,
};
}
Config.getKitsuProp('cache')[cacheIndex] = anime;
Config.save();
return {
completed: false,
tokenExpiresIn,
anime,
};
}
function buildLibPatchReqArgs(id, progress) {
return [
`https://kitsu.app/api/edge/library-entries/${id}`,
{
data: {
id,
type: 'library-entries',
attributes: {
progress,
},
},
},
];
}
export function displayWatchError(error, title) {
switch (error.msg) {
case 'CACHE_NOT_FOUND':
return displayCacheNotFound(title);
case 'MULTIPLE_CACHES':
return displayCacheCollisions(error.data.map((v) => v[0]), title);
case 'FILE_NOT_FOUND':
return displayFansubFileNotFound(title, error.data.epNum);
case 'MULTIPLE_FILES':
return displayFileCollisions(title, error.data.fileNames);
}
}
function displayCacheNotFound(title) {
Printer.printError([
`[;c;${title};y;] could not be found in the cache.`,
'',
';bc;... ;y;Possible Issues ;bc;...',
'(;bc;1;y;) ;c;You ;m;misspelled ;c;the file name or anime name.',
'(;bc;2;y;) ;c;The Anime is ;m;not ;c;in your ;m;watch list;c;.',
'(;bc;3;y;) ;c;You forgot to ;by;-rc ;c;after updating your ;m;watch list;c; manually.',
'(;bc;4;y;) ;c;File name has not been ;m;bound ;c;to the cache yet.',
], 'Anime Not Found', 3);
}
function displayCacheCollisions(cache, title) {
const files = cache.map((c, i) => `(;bc;${i + 1};y;): ;bk;${colorWord(c.jpTitle, title, 'bw', 'bk')}`);
Printer.printError([
...files,
'',
';bc;... ;y;Solutions ;bc;...',
'(;bc;1;y;) ;c;Use a ;m;different word ;c;of the title to reference the episode.',
'(;bc;2;y;) ;c;Use a ;m;whole segment ;c;of the title to reference the episode.',
'(;bc;3;y;) ;c;Use part of the ;m;English title ;c;to reference the episode.',
'(;bc;3;y;) ;c;Use one of the ;m;Alt Titles ;c;to reference the episode.',
], 'Multiple Titles Found', 3);
}
function displayFansubFileNotFound(title, ep) {
const paddedEp = `${ep}`.padStart(2, '0');
Printer.printError([
`File: ;c;${title} ;y;episode ;c;${paddedEp};y;`,
'',
'(Possible Issues)',
`(;bc;1;y;) ;c;Make sure you didn't ;m;misspell ;c;the file name.`,
`(;bc;2;y;) ;c;Make sure the ;m;episode number ;c;matches the file name.`,
`(;bc;3;y;) ;c;Make sure the file is the ;m;same fansub ;c;as previous updates.`,
], 'File Not Found', 3);
}
function displayFileCollisions(title, fileNames) {
const highlightedFiles = fileNames.map((n, i) => `(;bc;${i + 1};y;): ;bk;${colorWord(n, title, 'bw', 'bk')}`);
Printer.printError([
...highlightedFiles,
'',
'(Possible Issues)',
`(;bc;1;y;) ;c;Multiple fansubs with the same episode on disk`,
], 'Multiple Files Found', 3);
}
export function displayWatchProgress({ anime, completed, autoIncrement, tokenExpiresIn, }) {
const { epCount, epProgress, jpTitle, enTitle } = anime;
autoIncrement ??= false;
const percent = epCount ? Math.floor((epProgress / epCount) * 100) : 0;
const percentText = percent ? `;bk;(;c;~${percent}%;bk;)` : '';
const progressText = completed
? ';bg;Completed!'
: `;bg;${epProgress} ;x;/ ;y;${epCount || ';r;Unknown'} ${percentText}`;
const titles = [
`JP Title: ;x;${jpTitle || ';m;None'}`,
`EN Title: ;bk;${enTitle || ';m;None'}`,
];
const log = autoIncrement
? [`Progress: ${progressText}`]
: [...titles, `Progress: ${progressText}`];
if (tokenExpiresIn <= 7) {
Printer.printWarning([
`;bw;Your ;c;auth token ;bw;expires in ;by;${tokenExpiresIn} days`,
'',
';bc;... ;y;Solutions ;bc;...',
';y;(;bc;1;y;) ;c;Use the command: ;m;wak -t refresh ;c;to refresh your token',
';y;(;bc;2;y;) ;c;Use the command: ;m;wak -t reset ;c;to reset your token',
], 'Token Needs Attention', 3);
}
Printer.printInfo(log, 'Success', 3);
}
//# sourceMappingURL=watch.js.map