UNPKG

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