UNPKG

wakitsu

Version:

Hobby project for managing anime watch list on Kitsu through CLI

270 lines 10.4 kB
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