diff --git a/.gitignore b/.gitignore index cb26987..62e0b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,7 @@ package-lock.json *.ts *.mkv *.mp4 +*.ass *.srt *.resume +*.user.yml diff --git a/funi.js b/funi.js index 7e3dfd1..07f5df6 100644 --- a/funi.js +++ b/funi.js @@ -3,7 +3,6 @@ // modules build-in const fs = require('fs'); const path = require('path'); -const url = require('url'); // package json const packageJson = require('./package.json'); @@ -12,40 +11,26 @@ const packageJson = require('./package.json'); console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`); const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api'; -// request -const got = require('got'); - // modules extra const yaml = require('yaml'); const shlp = require('sei-helper'); const yargs = require('yargs'); -const FormData = require('form-data'); const { lookpath } = require('lookpath'); - -// m3u8 and ttml const m3u8 = require('m3u8-parsed'); const streamdl = require('hls-download'); -const { ttml2srt } = require('ttml2srt'); -// get cfg file -function getYamlCfg(file){ - let data = {}; - if(fs.existsSync(file)){ - try{ - data = yaml.parse(fs.readFileSync(file, 'utf8')); - return data; - } - catch(e){} - } - return data; -} +// extra +const modulesFolder = __dirname + '/modules'; +const getYamlCfg = require(modulesFolder+'/module.cfg-loader'); +const getData = require(modulesFolder + '/module.getdata.js'); +const vttConvert = require(modulesFolder + '/module.vttconvert'); // new-cfg const cfgFolder = __dirname + '/config'; -const binCfgFile = path.join(cfgFolder,'bin-path.yml'); -const dirCfgFile = path.join(cfgFolder,'dir-path.yml'); -const cliCfgFile = path.join(cfgFolder,'cli-defaults.yml'); -const tokenFile = path.join(cfgFolder,'token.yml'); +const binCfgFile = path.join(cfgFolder,'bin-path'); +const dirCfgFile = path.join(cfgFolder,'dir-path'); +const cliCfgFile = path.join(cfgFolder,'cli-defaults'); +const tokenFile = path.join(cfgFolder,'token'); // params let cfg = { @@ -103,10 +88,10 @@ let argv = yargs .boolean('nosubs') // proxy - .describe('proxy','http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080/)') - .describe('proxy-auth','Colon-separated username and password for proxy') - .describe('ssp','Ignore proxy settings for stream downloading') - .boolean('ssp') + // .describe('proxy','http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080/)') + // .describe('proxy-auth','Colon-separated username and password for proxy') + // .describe('ssp','Ignore proxy settings for stream downloading') + // .boolean('ssp') .describe('mp4','Mux into mp4') .boolean('mp4') @@ -153,16 +138,6 @@ let fnTitle = '', stDlPath = false, batchDL = false; -// go to work folder -try { - fs.accessSync(cfg.dir.content, fs.R_OK | fs.W_OK); -} -catch (e) { - console.log(e); - console.log('[ERROR] %s',e.messsage); - process.exit(); -} - // select mode if(argv.auth){ auth(); @@ -170,7 +145,7 @@ if(argv.auth){ else if(argv.search){ searchShow(); } -else if(argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0){ +else if(argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0){ getShow(); } else{ @@ -188,15 +163,16 @@ async function auth(){ url: '/auth/login/', useProxy: true, auth: authOpts, + debug: argv.debug, }); if(authData.ok){ authData = JSON.parse(authData.res.body); if(authData.token){ console.log('[INFO] Authentication success, your token: %s%s\n', authData.token.slice(0,8),'*'.repeat(32)); - fs.writeFileSync(tokenFile,yaml.stringify({'token':authData.token})); + fs.writeFileSync(tokenFile, yaml.stringify({'token': authData.token})); } else if(authData.error){ - console.log('[ERROR]',authData.error,'\n'); + console.log('[ERROR]%s\n', authData.error); process.exit(1); } } @@ -209,8 +185,10 @@ async function searchShow(){ baseUrl: api_host, url: '/source/funimation/search/auto/', querystring: qs, + token: token, useToken: true, useProxy: true, + debug: argv.debug, }); if(!searchData.ok){return;} searchData = JSON.parse(searchData.res.body); @@ -233,9 +211,11 @@ async function getShow(){ // show main data let showData = await getData({ baseUrl: api_host, - url: `/source/catalog/title/${parseInt(argv.s,10)}`, + url: `/source/catalog/title/${parseInt(argv.s, 10)}`, + token: token, useToken: true, useProxy: true, + debug: argv.debug, }); // check errors if(!showData.ok){return;} @@ -257,8 +237,10 @@ async function getShow(){ baseUrl: api_host, url: '/funimation/episodes/', querystring: qs, + token: token, useToken: true, useProxy: true, + debug: argv.debug, }); if(!episodesData.ok){return;} let eps = JSON.parse(episodesData.res.body).items, fnSlug = [], is_selected = false; @@ -345,8 +327,10 @@ async function getEpisode(fnSlug){ let episodeData = await getData({ baseUrl: api_host, url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`, + token: token, useToken: true, useProxy: true, + debug: argv.debug, }); if(!episodeData.ok){return;} let ep = JSON.parse(episodeData.res.body).items[0], streamId = 0; @@ -357,23 +341,27 @@ async function getEpisode(fnSlug){ ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id; } fnEpNum = argv.ep && !batchDL ? ( parseInt(argv.ep, 10) < 10 ? '0' + argv.ep : argv.ep ) : ep.number; + // is uncut let uncut = { Japanese: false, English: false }; + // end console.log( '[INFO] %s - S%sE%s - %s', ep.parent.title, - (ep.parent.seasonNumber?ep.parent.seasonNumber:'?'), - (ep.number?ep.number:'?'), + (ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'), + (ep.number ? ep.number : '?'), ep.title ); + console.log('[INFO] Available streams (Non-Encrypted):'); + // map medias let media = ep.media.map(function(m){ - if(m.mediaType=='experience'){ + if(m.mediaType == 'experience'){ if(m.version.match(/uncut/i)){ uncut[m.language] = true; } @@ -389,6 +377,7 @@ async function getEpisode(fnSlug){ return { id: 0, type: '' }; } }); + // select media = media.reverse(); for(let m of media){ @@ -411,6 +400,7 @@ async function getEpisode(fnSlug){ console.log(`[#${m.id}] ${dub_type} [${m.version}]`,(selected?'(selected)':'')); } } + if(streamId<1){ console.log('[ERROR] Track not selected\n'); return; @@ -419,9 +409,11 @@ async function getEpisode(fnSlug){ let streamData = await getData({ baseUrl: api_host, url: `/source/catalog/video/${streamId}/signed`, + token: token, + dinstid: 'uuid', useToken: true, useProxy: true, - dinstid: 'uuid', + debug: argv.debug, }); if(!streamData.ok){return;} streamData = JSON.parse(streamData.res.body); @@ -455,7 +447,7 @@ function getSubsUrl(m){ for(let i in m){ let fpp = m[i].filePath.split('.'); let fpe = fpp[fpp.length-1]; - if(fpe == 'dfxp'){ // dfxp, srt, vtt + if(fpe == 'vtt'){ // dfxp (TTML), srt, vtt return m[i].filePath; } } @@ -468,14 +460,16 @@ async function downloadStreams(){ let plQualityReq = await getData({ url: tsDlPath, useProxy: (argv.ssp ? false : true), + debug: argv.debug, }); if(!plQualityReq.ok){return;} let plQualityLinkList = m3u8(plQualityReq.res.body); let mainServersList = [ + 'vmfst-api.prd.funimationsvc.com', 'd132fumi6di1wa.cloudfront.net', - 'funiprod.akamaized.net' + 'funiprod.akamaized.net', ]; let plServerList = [], @@ -486,7 +480,7 @@ async function downloadStreams(){ for(let s of plQualityLinkList.playlists){ // set layer and max layer - let plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8$/)[1]); + let plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8/)[1]); plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer; // set urls and servers let plUrlDl = s.uri; @@ -563,6 +557,7 @@ async function downloadStreams(){ let reqVideo = await getData({ url: videoUrl, useProxy: (argv.ssp ? false : true), + debug: argv.debug, }); if (!reqVideo.ok) { return; } @@ -606,23 +601,29 @@ async function downloadStreams(){ console.log('[INFO] Skip video downloading...\n'); } + // add subs + let subsUrl = stDlPath; + let subsExt = !argv.mp4 || argv.mp4 && !argv.mks && argv.ass ? '.ass' : '.srt'; + let addSubs = argv.mks && subsUrl ? true : false; + // download subtitles - if(stDlPath){ + if(subsUrl){ console.log('[INFO] Downloading subtitles...'); - console.log(stDlPath); + console.log(subsUrl); let subsSrc = await getData({ - url: stDlPath, + url: subsUrl, useProxy: true, + debug: argv.debug, }); if(subsSrc.ok){ - let srtData = ttml2srt(subsSrc.res.body); - let srtFile = path.join(cfg.dir.content, fnOutput) + '.srt'; - fs.writeFileSync(srtFile, srtData); + let assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false)); + let assFile = path.join(cfg.dir.content, fnOutput) + subsExt; + fs.writeFileSync(assFile, assData); console.log('[INFO] Subtitles downloaded!'); } else{ console.log('[ERROR] Failed to download subtitles!'); - argv.mks = false; + addSubs = false; } } @@ -639,13 +640,9 @@ async function downloadStreams(){ return; } - // add subs - let addSubs = argv.mks && stDlPath ? true : false; - // usage let usableMKVmerge = true; let usableFFmpeg = true; - console.log(await lookpath(path.join(cfg.bin.ffmpeg + '.exe'))); // check exec path let mkvmergebinfile = await lookpath(path.join(cfg.bin.mkvmerge)); @@ -678,7 +675,7 @@ async function downloadStreams(){ mkvmux.push(`${muxTrg}.ts`); if(addSubs){ mkvmux.push('--language','0:eng'); - mkvmux.push(`${muxTrg}.srt`); + mkvmux.push(`${muxTrg}${subsExt}`); } fs.writeFileSync(`${muxTrg}.json`,JSON.stringify(mkvmux,null,' ')); shlp.exec('mkvmerge',`"${mkvmergebinfile}"`,`@"${muxTrg}.json"`); @@ -687,10 +684,10 @@ async function downloadStreams(){ else if(usableFFmpeg){ let ffext = !argv.mp4 ? 'mkv' : 'mp4'; let ffmux = `-i "${muxTrg}.ts" `; - ffmux += addSubs ? `-i "${muxTrg}.srt" ` : ''; + ffmux += addSubs ? `-i "${muxTrg}${subsExt}" ` : ''; ffmux += '-map 0 -c:v copy -c:a copy '; ffmux += addSubs ? '-map 1 ' : ''; - ffmux += addSubs && !argv.mp4 ? '-c:s srt ' : ''; + ffmux += addSubs && !argv.mp4 ? '-c:s ass ' : ''; ffmux += addSubs && argv.mp4 ? '-c:s mov_text ' : ''; ffmux += '-metadata encoding_tool="no_variable_data" '; ffmux += `-metadata:s:v:0 title="[${argv.a}]" -metadata:s:a:0 language=${argv.sub?'jpn':'eng'} `; @@ -708,116 +705,46 @@ async function downloadStreams(){ } else if(argv.nocleanup){ fs.renameSync(muxTrg+'.ts', tshTrg + '.ts'); - if(stDlPath && argv.mks){ - fs.renameSync(muxTrg+'.srt', tshTrg + '.srt'); + if(subsUrl && addSubs){ + fs.renameSync(muxTrg +subsExt, tshTrg +subsExt); } } else{ fs.unlinkSync(muxTrg+'.ts'); - if(stDlPath && argv.mks){ - fs.unlinkSync(muxTrg+'.srt'); + if(subsUrl && addSubs){ + fs.unlinkSync(muxTrg +subsExt); } } console.log('\n[INFO] Done!\n'); } -// get data from url -async function getData(options){ - let gOptions = { - url: options.url, - headers: { - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0', - } - }; - if(options.baseUrl){ - gOptions.prefixUrl = options.baseUrl; - gOptions.url = gOptions.url.replace(/^\//,''); +// make proxy URL +function buildProxy(proxyBaseUrl, proxyAuth){ + if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){ + proxyBaseUrl = 'http://' + proxyBaseUrl; } - if(options.querystring){ - gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`; - } - if(options.auth){ - gOptions.method = 'POST'; - gOptions.body = new FormData(); - gOptions.body.append('username', options.auth.user); - gOptions.body.append('password', options.auth.pass); - } - if(options.useToken && token){ - gOptions.headers.Authorization = `Token ${token}`; - } - if(options.dinstid){ - gOptions.headers.devicetype = 'Android Phone'; - } - // debug - gOptions.hooks = { - beforeRequest: [ - (options) => { - if(argv.debug){ - console.log('[DEBUG] GOT OPTIONS:'); - console.log(options); - } - } - ] - }; - if(options.useProxy && argv.proxy){ - try{ - const ProxyAgent = require('proxy-agent'); - let proxyUrl = buildProxyUrl(argv.proxy,argv['proxy-auth']); - gOptions.agent = new ProxyAgent(proxyUrl); - gOptions.timeout = 10000; - } - catch(e){ - console.log(`\n[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`); - console.log('[WARN] Skiping...'); - argv.proxy = false; - } - } - try { - if(argv.debug){ - console.log('[Debug] REQ:', gOptions); - } - let res = await got(gOptions); - if(res.body && res.body.match(/^ { + return yaml.parse(fs.readFileSync(file, 'utf8')); +} + +const loadYamlCfg = (file) => { + if(existsFile(`${file}.user.yml`)){ + file += '.user'; + } + file += '.yml'; + if(fs.existsSync(file)){ + + try{ + return loadYamlFile(file, 'utf8'); + } + catch(e){ + return {}; + } + } + return {}; +} + +module.exports = loadYamlCfg; diff --git a/modules/module.colors.json b/modules/module.colors.json new file mode 100644 index 0000000..e25585e --- /dev/null +++ b/modules/module.colors.json @@ -0,0 +1,149 @@ +{ + "aliceblue": "#f0f8ff", + "antiquewhite": "#faebd7", + "aqua": "#00ffff", + "aquamarine": "#7fffd4", + "azure": "#f0ffff", + "beige": "#f5f5dc", + "bisque": "#ffe4c4", + "black": "#000000", + "blanchedalmond": "#ffebcd", + "blue": "#0000ff", + "blueviolet": "#8a2be2", + "brown": "#a52a2a", + "burlywood": "#deb887", + "cadetblue": "#5f9ea0", + "chartreuse": "#7fff00", + "chocolate": "#d2691e", + "coral": "#ff7f50", + "cornflowerblue": "#6495ed", + "cornsilk": "#fff8dc", + "crimson": "#dc143c", + "cyan": "#00ffff", + "darkblue": "#00008b", + "darkcyan": "#008b8b", + "darkgoldenrod": "#b8860b", + "darkgray": "#a9a9a9", + "darkgreen": "#006400", + "darkgrey": "#a9a9a9", + "darkkhaki": "#bdb76b", + "darkmagenta": "#8b008b", + "darkolivegreen": "#556b2f", + "darkorange": "#ff8c00", + "darkorchid": "#9932cc", + "darkred": "#8b0000", + "darksalmon": "#e9967a", + "darkseagreen": "#8fbc8f", + "darkslateblue": "#483d8b", + "darkslategray": "#2f4f4f", + "darkslategrey": "#2f4f4f", + "darkturquoise": "#00ced1", + "darkviolet": "#9400d3", + "deeppink": "#ff1493", + "deepskyblue": "#00bfff", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1e90ff", + "firebrick": "#b22222", + "floralwhite": "#fffaf0", + "forestgreen": "#228b22", + "fuchsia": "#ff00ff", + "gainsboro": "#dcdcdc", + "ghostwhite": "#f8f8ff", + "goldenrod": "#daa520", + "gold": "#ffd700", + "gray": "#808080", + "green": "#008000", + "greenyellow": "#adff2f", + "grey": "#808080", + "honeydew": "#f0fff0", + "hotpink": "#ff69b4", + "indianred": "#cd5c5c", + "indigo": "#4b0082", + "ivory": "#fffff0", + "khaki": "#f0e68c", + "lavenderblush": "#fff0f5", + "lavender": "#e6e6fa", + "lawngreen": "#7cfc00", + "lemonchiffon": "#fffacd", + "lightblue": "#add8e6", + "lightcoral": "#f08080", + "lightcyan": "#e0ffff", + "lightgoldenrodyellow": "#fafad2", + "lightgray": "#d3d3d3", + "lightgreen": "#90ee90", + "lightgrey": "#d3d3d3", + "lightpink": "#ffb6c1", + "lightsalmon": "#ffa07a", + "lightseagreen": "#20b2aa", + "lightskyblue": "#87cefa", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#b0c4de", + "lightyellow": "#ffffe0", + "lime": "#00ff00", + "limegreen": "#32cd32", + "linen": "#faf0e6", + "magenta": "#ff00ff", + "maroon": "#800000", + "mediumaquamarine": "#66cdaa", + "mediumblue": "#0000cd", + "mediumorchid": "#ba55d3", + "mediumpurple": "#9370db", + "mediumseagreen": "#3cb371", + "mediumslateblue": "#7b68ee", + "mediumspringgreen": "#00fa9a", + "mediumturquoise": "#48d1cc", + "mediumvioletred": "#c71585", + "midnightblue": "#191970", + "mintcream": "#f5fffa", + "mistyrose": "#ffe4e1", + "moccasin": "#ffe4b5", + "navajowhite": "#ffdead", + "navy": "#000080", + "oldlace": "#fdf5e6", + "olive": "#808000", + "olivedrab": "#6b8e23", + "orange": "#ffa500", + "orangered": "#ff4500", + "orchid": "#da70d6", + "palegoldenrod": "#eee8aa", + "palegreen": "#98fb98", + "paleturquoise": "#afeeee", + "palevioletred": "#db7093", + "papayawhip": "#ffefd5", + "peachpuff": "#ffdab9", + "peru": "#cd853f", + "pink": "#ffc0cb", + "plum": "#dda0dd", + "powderblue": "#b0e0e6", + "purple": "#800080", + "red": "#ff0000", + "rosybrown": "#bc8f8f", + "royalblue": "#4169e1", + "saddlebrown": "#8b4513", + "salmon": "#fa8072", + "sandybrown": "#f4a460", + "seagreen": "#2e8b57", + "seashell": "#fff5ee", + "sienna": "#a0522d", + "silver": "#c0c0c0", + "skyblue": "#87ceeb", + "slateblue": "#6a5acd", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#fffafa", + "springgreen": "#00ff7f", + "steelblue": "#4682b4", + "tan": "#d2b48c", + "teal": "#008080", + "thistle": "#d8bfd8", + "tomato": "#ff6347", + "turquoise": "#40e0d0", + "violet": "#ee82ee", + "wheat": "#f5deb3", + "white": "#ffffff", + "whitesmoke": "#f5f5f5", + "yellow": "#ffff00", + "yellowgreen": "#9acd32" +} diff --git a/modules/module.getdata.js b/modules/module.getdata.js new file mode 100644 index 0000000..7fb887d --- /dev/null +++ b/modules/module.getdata.js @@ -0,0 +1,73 @@ +const FormData = require('form-data'); +const got = require('got'); + +// do req +const getData = async (options) => { + let gOptions = { + url: options.url, + headers: { + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0', + } + }; + if(options.baseUrl){ + gOptions.prefixUrl = options.baseUrl; + gOptions.url = gOptions.url.replace(/^\//,''); + } + if(options.querystring){ + gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`; + } + if(options.auth){ + gOptions.method = 'POST'; + gOptions.body = new FormData(); + gOptions.body.append('username', options.auth.user); + gOptions.body.append('password', options.auth.pass); + } + if(options.useToken && options.token){ + gOptions.headers.Authorization = `Token ${options.token}`; + } + if(options.dinstid){ + gOptions.headers.devicetype = 'Android Phone'; + } + // debug + gOptions.hooks = { + beforeRequest: [ + (gotOpts) => { + if(options.debug){ + console.log('[DEBUG] GOT OPTIONS:'); + console.log(gotOpts); + } + } + ] + }; + try { + let res = await got(gOptions); + if(res.body && res.body.match(/^ ([\d:.]*)\s?(.*?)\s*$/; + const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); + let data = [], lineBuf = [], record = null; + // check lines + for (let l of lines) { + let m = l.match(rx); + if (m) { + if (lineBuf.length > 0) { + lineBuf.pop(); + } + if (record !== null) { + record.text = lineBuf.join('\n'); + data.push(record); + } + record = { + time_start: m[1], + time_end: m[2], + ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => (p[c[0]] = c[1]) && p, {}), + }; + lineBuf = []; + continue; + } + lineBuf.push(l); + } + if (record !== null) { + if (lineBuf[lineBuf.length - 1] === '') { + lineBuf.pop(); + } + record.text = lineBuf.join('\n'); + data.push(record); + } + return data; +} + +// ass specific +function convertToAss(vttStr){ + let ass = [ + '\ufeff[Script Info]', + 'Title: English', + 'ScriptType: v4.00+', + 'PlayResX: 1280', + 'PlayResY: 720', + 'WrapStyle: 0', + 'ScaledBorderAndShadow: yes', + '', + '[V4+ Styles]', + 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' + + 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, ' + + 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', + 'Style: Main,Noto Sans,55,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1', + 'Style: MainTop,Noto Sans,55,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1', + '', + '[Events]', + 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text', + ]; + + let vttData = loadVtt(vttStr); + for (let l of vttData) { + l = convertToAssLine(l, 'Main'); + ass = ass.concat(l); + } + + return ass.join('\r\n') + '\r\n'; +} + +function convertToAssLine(l, style) { + let start = convertTime(l.time_start); + let end = convertTime(l.time_end); + let text = convertToAssText(l.text); + + // debugger + if (l.ext_param.align != 'middle') { + console.log('[WARN] Detected specific align param, please contact developer'); + cosnole.log(l); + } + if (l.ext_param.vertical) { + console.log('[WARN] Detected specific vertical param, please contact developer'); + cosnole.log(l); + } + if (l.ext_param.line && l.ext_param.line != '7%') { + console.log('[WARN] Detected specific line param, please contact developer'); + cosnole.log(l); + } + if (l.ext_param.position) { + console.log('[WARN] Detected specific position param, please contact developer'); + cosnole.log(l); + } + if (l.text.match(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + // .replace(/]*>[^<]*<\/c>/g, '') + // .replace(/]*>[^<]*<\/ruby>/g, '') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + return text; +} + +// srt specific +function convertToSrt(vttStr){ + let srt = [], srtLineIdx = 0; + + let vttData = loadVtt(vttStr); + for (let l of vttData) { + srtLineIdx++; + l = convertToSrtLine(l, srtLineIdx); + srt = srt.concat(l); + } + + return srt.join('\r\n') + '\r\n'; +} + +function convertToSrtLine(l, idx) { + let bom = idx == 1 ? '\ufeff' : ''; + let start = convertTime(l.time_start, true); + let end = convertTime(l.time_end, true); + let text = l.text; + return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; +} + +// time parser +function convertTime(time, srtFormat) { + let mTime = time.match(/([\d:]*)\.?(\d*)/); + if (!mTime){ + return srtFormat ? '00:00:00,000' : '0:00:00.00'; + } + return toSubsTime(mTime[0], srtFormat); +} + +function toSubsTime(str, srtFormat) { + + let n = [], x, sx; + x = str.split(/[:.]/).map(x => Number(x)); + + let msLen = srtFormat ? 3 : 2; + let hLen = srtFormat ? 2 : 1; + + x[3] = '0.' + ('' + x[3]).padStart(3, '0'); + sx = x[0]*60*60 + x[1]*60 + x[2] + Number(x[3]) + sx = sx.toFixed(msLen).split('.'); + + + n.unshift(padTimeNum('.', sx[1], msLen)); + sx = Number(sx[0]); + + n.unshift(padTimeNum(':', sx%60, 2)); + n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2)); + n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen)); + + return n.join(''); +} + +function padTimeNum(sep, input, pad){ + return sep + ('' + input).padStart(pad, '0'); +} + +// export module +module.exports = (vttStr, toSrt) => { + const convert = toSrt ? convertToSrt : convertToAss; + return convert(vttStr); +}; diff --git a/package.json b/package.json index 28ef7ed..5882695 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "funimation-downloader-nx", "short_name": "funi", - "version": "4.6.1", + "version": "4.7.0-beta.1", "description": "Download videos from Funimation via cli.", "keywords": [ "download", @@ -29,11 +29,9 @@ "hls-download": "^2.5.3", "lookpath": "^1.1.0", "m3u8-parsed": "^1.3.0", - "proxy-agent": "^3.1.1", "sei-helper": "^3.3.0", - "ttml2srt": "^1.2.0", "yaml": "^1.10.0", - "yargs": "^16.0.3" + "yargs": "^16.1.0" }, "devDependencies": { "fs-extra": "^9.0.1",