UNPKG

12.8 kBJavaScriptView Raw
1const fs = require('fs')
2const path = require('path')
3const rm = require('rimraf')
4const mk = require('mkdirp')
5const ck = require('chalk')
6const numeral = require('numeral')
7const moment = require('moment')
8const async = require('async')
9const ncp = require('ncp').ncp
10const tb = require('easy-table')
11const Listr = require('listr')
12const _ = require('underscore')
13const g = require('got')
14const pi = require('package-info')
15
16const api = require('./source')
17const cfg = require('./lib/config')
18const unzip = require('./lib/unzip')
19const ads = require('./lib/wowaads').load()
20const pkg = require('./package.json')
21const log = console.log
22
23const cl = {
24 i: ck.yellow,
25 h: ck.red,
26 x: ck.dim,
27 i2: ck.blue
28}
29
30function getAd(ad, info, tmp, hook) {
31 let v = info.version[0]
32
33 if (!v) {
34 log('fatal: version not found')
35 return hook()
36 }
37
38 if (ad.version) v = _.find(info.version, d => d.name === ad.version)
39
40 let src = path.join(tmp, '1.zip')
41 let dst = path.join(tmp, 'dec')
42
43 // log('streaming', v.link)
44
45 // fix version
46 ad.version = v.name
47 g.stream(v.link, {
48 headers: {
49 'user-agent': require('ua-string')
50 }
51 })
52 .on('downloadProgress', hook)
53 .on('error', err => {
54 // log('stream error', typeof err, err)
55 hook(err ? err.toString() : 'download error')
56 })
57 .pipe(fs.createWriteStream(src))
58 .on('close', () => {
59 unzip(src, dst, err => {
60 if (err) return hook('unzip failed')
61 hook('done')
62 })
63 })
64}
65
66function _install(from, to, sub, done) {
67 let ls = fs.readdirSync(from)
68
69 let toc = _.find(ls, x => x.match(/\.toc$/))
70
71 // log('\n\n searching', from, toc, to)
72 if (toc) {
73 toc = toc.replace(/\.toc$/, '')
74 let target = path.join(to, toc)
75
76 // log('\n\ntoc found, copy', from, '>>', target, '\n\n')
77 rm(target, err => {
78 // log('\n\n', 'rm err', err)
79 mk(target, err => {
80 ncp(from, target, done)
81 sub.push(toc)
82 })
83 })
84 } else {
85 async.eachLimit(
86 _.filter(ls.map(x => path.join(from, x)), x =>
87 fs.statSync(x).isDirectory()
88 ),
89 1,
90 (d, cb) => {
91 _install(d, to, sub, err => {
92 if (err) {
93 log('\n\nerr??', err, '\n\n')
94 done(err)
95 cb(false)
96 return
97 }
98 // log('\n\ninstalling from', d, 'to', to, sub, '\n\n')
99 cb()
100 })
101 },
102 () => {
103 done()
104 }
105 )
106 }
107}
108
109function install(ad, update, hook) {
110 let tmp = path.join(cfg.getPath('tmp'), ad.key.replace(/\//g, '-'))
111 let notify = (status, msg) => {
112 hook({
113 status,
114 msg
115 })
116 }
117
118 notify('ongoing', update ? 'checking for updates...' : 'waiting...')
119
120 api.info(ad, info => {
121 if (!info) return notify('failed', 'not availabe')
122
123 // fix source
124 ad.source = info.source
125
126 if (update && ads.data[ad.key] && ads.data[ad.key].update >= info.update)
127 return notify('skip', 'is already up to date')
128
129 notify('ongoing', 'preparing download...')
130 rm(tmp, err => {
131 if (err) return notify('failed', 'failed to rmdir ' + JSON.stringify(err))
132
133 let dec = path.join(tmp, 'dec')
134 mk(dec, err => {
135 if (err)
136 return notify('failed', 'failed to mkdir ' + JSON.stringify(err))
137
138 let size = 0
139 notify('ongoing', 'downloading...')
140 getAd(ad, info, tmp, evt => {
141 if (!evt || (typeof evt === 'string' && evt !== 'done')) {
142 notify('failed', !evt ? 'failed to download' : evt)
143 } else if (evt === 'done') {
144 notify('ongoing', 'clearing previous install...')
145
146 ads.clearUp(ad.key, () => {
147 ads.data[ad.key] = {
148 name: info.name,
149 version: ad.version,
150 size,
151 source: info.source,
152 update: info.update,
153 sub: []
154 }
155
156 if (ad.anyway) ads.data[ad.key].anyway = ad.anyway
157 if (ad.branch) ads.data[ad.key].branch = ad.branch
158
159 _install(dec, cfg.getPath('addon'), ads.data[ad.key].sub, err => {
160 if (err) return notify('failed', 'failed to copy file')
161
162 ads.save()
163 notify('done', update ? 'updated' : 'installed')
164 })
165 })
166 } else {
167 notify(
168 'ongoing',
169 `downloading... ${(evt.percent * 100).toFixed(0)}%`
170 )
171 size = evt.transferred
172 // log(evt)
173 }
174 })
175 })
176 })
177 })
178}
179
180function batchInstall(aa, update, done) {
181 let t0 = moment().unix()
182
183 if (!cfg.checkPath()) return
184
185 let list = new Listr([], { concurrent: 10 })
186 let ud = 0
187 let id = 0
188
189 aa.forEach(ad => {
190 list.add({
191 title: `${cl.h(ad.key)} waiting...`,
192 task(ctx, task) {
193 let promise = new Promise((res, rej) => {
194 install(ad, update, evt => {
195 if (!task.$st) {
196 task.title = ''
197 task.title += cl.h(ad.key)
198 if (ad.version) task.title += cl.i2(' @' + cl.i2(ad.version))
199 if (ad.source) task.title += cl.i(` [${ad.source}]`)
200
201 // log('ad is', ad)
202
203 task.title += ' ' + cl.x(evt.msg)
204 }
205
206 if (
207 evt.status === 'done' ||
208 evt.status === 'skip' ||
209 evt.status === 'failed'
210 ) {
211 task.$st = evt.status
212 if (evt.status !== 'done') task.skip()
213 else {
214 if (update) ud++
215 id++
216 }
217
218 res('ok')
219 }
220 })
221 })
222
223 return promise
224 }
225 })
226 })
227
228 list.run().then(res => {
229 ads.save()
230 log(`\n${id} addons` + (update ? `, ${ud} updated` : ' installed'))
231 log(`✨ done in ${moment().unix() - t0}s.\n`)
232 if (done) done({ count: id, update, ud })
233 })
234}
235
236let core = {
237 add(aa, done) {
238 log('\nInstalling addon' + (aa.length > 1 ? 's...' : '...') + '\n')
239 batchInstall(aa.map(x => api.parseName(x)), 0, done)
240 },
241
242 rm(key, done) {
243 ads.clearUp(key, err => {
244 ads.save()
245 log(`✨ ${err ? 0 : cl.h(key)} removed.`)
246 if (done) done()
247 })
248 },
249
250 search(text, done) {
251 // log(text)
252
253 api.search(api.parseName(text), info => {
254 if (!info) {
255 log('\nNothing is found\n')
256 if (done) done(info)
257 return
258 }
259
260 let kv = (k, v) => {
261 let c = cl.i
262 let h = cl.x
263
264 return `${h(k + ':') + c(' ' + v + '')}`
265 }
266
267 let data = info.data.slice(0, 15)
268
269 log(`\n${cl.i(data.length)} results from ${cl.i(info.source)}`)
270
271 data.forEach((v, i) => {
272 log()
273 log(cl.h(v.name) + ' ' + cl.x('(' + v.page + ')'))
274 log(
275 ` ${kv('key', v.key)} ${kv(
276 'download',
277 numeral(v.download).format('0.0a')
278 )} ${kv('version', moment(v.update * 1000).format('MM/DD/YYYY'))}`
279 )
280 // log('\n ' + v.desc)
281 })
282
283 log()
284 if (done) done(info)
285 })
286 },
287
288 ls() {
289 let t = new tb()
290 for (let k in ads.data) {
291 let v = ads.data[k]
292
293 t.cell(cl.x('Addon keys'), cl.h(k) + (v.anyway ? cl.i2(' [anyway]') : ''))
294 t.cell(cl.x('Version'), cl.i2(v.version))
295 t.cell(cl.x('Source'), cl.i(v.source))
296 t.cell(cl.x('Update'), cl.i(moment(v.update * 1000).format('YYYY-MM-DD')))
297 t.newRow()
298 }
299
300 log(`\nMode: ${cl.i(cfg.getPath('mode'))}\n`)
301 if (!Object.keys(ads.data).length) log('no addons\n')
302 else log(t.toString())
303
304 ads.checkDuplicate()
305
306 let ukn = ads.unknownDirs()
307
308 if (ukn.length) {
309 log('---')
310 log(
311 cl.h(
312 `❗ ${ukn.length} folder${
313 ukn.length > 1 ? 's' : ''
314 } not managing by wowa\n`
315 )
316 )
317 log(ukn)
318 log()
319 }
320
321 return t.toString()
322 },
323
324 info(ad, done) {
325 let t = new tb()
326
327 ad = api.parseName(ad)
328 api.info(ad, info => {
329 log('\n' + cl.h(ad.key) + '\n')
330 if (!info) {
331 log('Not available\n')
332 if (done) done()
333 return
334 }
335
336 let kv = (k, v) => {
337 // log('adding', k, v)
338 t.cell(cl.x('Item'), cl.x(k))
339 t.cell(cl.x('Info'), cl.i(v))
340 t.newRow()
341 }
342
343 for (let k in info) {
344 if (k === 'version' || info[k] === undefined) continue
345
346 kv(
347 k,
348 k === 'create' || k === 'update'
349 ? moment(info[k] * 1000).format('MM/DD/YYYY')
350 : k === 'download'
351 ? numeral(info[k]).format('0.0a')
352 : info[k]
353 )
354 }
355
356 let v = info.version[0]
357 if (v) {
358 kv('version', v.name)
359 if (v.size) kv('size', v.size)
360 if (v.game)
361 kv('game version', _.uniq(info.version.map(x => x.game)).join(', '))
362 kv('link', v.link)
363 }
364
365 log(t.toString())
366 if (done) done(t.toString())
367 })
368 },
369
370 update(done) {
371 let aa = []
372 for (let k in ads.data) {
373 aa.push({
374 key: k,
375 source: ads.data[k].source,
376 anyway: ads.data[k].anyway && cfg.anyway(),
377 branch: ads.data[k].branch
378 })
379 }
380 if (!aa.length) {
381 log('\nnothing to update\n')
382 return
383 }
384
385 if (ads.checkDuplicate()) return
386
387 log('\nUpdating addons:\n')
388 batchInstall(aa, 1, done)
389 },
390
391 restore(repo, done) {
392 if (repo) {
393 log('\nrestore from remote is not implemented yet\n')
394 return
395 }
396
397 let aa = []
398 for (let k in ads.data) {
399 aa.push({ key: k, source: ads.data[k].source })
400 }
401
402 if (!aa.length) {
403 log('\nnothing to restore\n')
404 return
405 }
406
407 log('\nRestoring addons:')
408 batchInstall(aa, 0, done)
409 },
410
411 updateSummary(done) {
412 let p = cfg.getPath('db')
413
414 if (
415 !fs.existsSync(p) ||
416 new Date() - fs.statSync(p).mtime > 24 * 3600 * 1000
417 ) {
418 mk(path.dirname(p), err => {
419 process.stdout.write(cl.i('\nUpdating database...'))
420 api.summary(s => {
421 fs.writeFileSync(p, JSON.stringify(s, null, 2), 'utf-8')
422 log(cl.i('done'))
423 done()
424 })
425 })
426
427 return
428 }
429
430 done()
431 return
432 },
433
434 pickup(done) {
435 let db = cfg.getDB()
436 let p = cfg.getPath('addon')
437 let imported = 0
438 let importedDirs = 0
439
440 if (!db) {
441 if (done) done()
442 return
443 }
444
445 ads.unknownDirs().forEach(dir => {
446 if (ads.dirStatus(dir)) return
447
448 // log('picking up', dir)
449 let l = _.find(
450 db,
451 a => a.dir.indexOf(dir) >= 0 && a.mode === cfg.getMode()
452 )
453
454 if (!l) return
455
456 // log('found', l)
457 importedDirs++
458 let update = Math.floor(fs.statSync(path.join(p, dir)).mtimeMs / 1000)
459 let k =
460 l.id +
461 '-' +
462 _.filter(l.name.split(''), s => s.match(/^[a-z0-9]+$/i)).join('')
463 if (ads.data[k]) ads.data[k].sub.push(dir)
464 else {
465 ads.data[k] = {
466 name: l.name,
467 version: 'unknown',
468 source: l.source,
469 update,
470 sub: [dir]
471 }
472 imported++
473 }
474 })
475
476 log(`\n✨ imported ${imported} addons (${importedDirs} folders)\n`)
477
478 let ukn = ads.unknownDirs()
479 if (ukn.length) {
480 log(
481 cl.h(
482 `❗ ${ukn.length} folder${
483 ukn.length > 1 ? 's are' : ' is'
484 } not recgonized\n`
485 )
486 )
487 log(ukn)
488 log()
489 }
490
491 ads.save()
492 if (done) done(ukn)
493 },
494
495 switch() {
496 let pf = cfg.getPath('pathfile')
497 let p = fs.readFileSync(pf, 'utf-8').trim()
498 let mode = path.basename(p)
499
500 // log('pf', pf, 'p', p, 'mode', mode)
501
502 if (mode === '_retail_') mode = '_classic_'
503 else mode = '_retail_'
504
505 p = path.join(path.dirname(p), mode)
506 fs.writeFileSync(pf, p, 'utf-8')
507 log('\nMode switched to:', cl.i(mode), '\n')
508
509 ads.load()
510 },
511
512 checkUpdate(done) {
513 let v2n = v => {
514 let _v = 0
515 v.split('.').forEach((n, i) => {
516 _v *= 100
517 _v += parseInt(n)
518 })
519
520 return _v
521 }
522
523 let p = cfg.getPath('update')
524 let e = fs.existsSync(p)
525 let i
526
527 if (!e || new Date() - fs.statSync(p).mtime > 24 * 3600 * 1000) {
528 // fetch new data
529 pi('wowa').then(res => {
530 fs.writeFileSync(p, JSON.stringify(res), 'utf-8')
531 done(res)
532 })
533 return
534 } else if (e) i = JSON.parse(fs.readFileSync(p, 'utf-8'))
535
536 if (i) {
537 // log(v2n(i.version), v2n(pkg.version))
538 if (v2n(i.version) > v2n(pkg.version)) {
539 log(
540 cl.i('\nNew wowa version'),
541 cl.i2(i.version),
542 cl.i('is available, use the command below to update\n'),
543 ' npm install -g wowa\n'
544 )
545 }
546 }
547
548 done(i)
549 }
550}
551
552module.exports = core