├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jscsrc ├── LICENSE ├── README.md ├── config.yml ├── convert.js ├── convert.yml ├── favicon.ico ├── index.html ├── main.js ├── package.json ├── progress.py ├── screenshot.jpg ├── screenshot1.jpg ├── screenshot2.jpg └── screenshot3.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*.js] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | }, 7 | "rules": { 8 | "quotes": [2, "single"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": null 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mfc-node-recorder 2 | ================= 3 | Note: mfc-node lets you follow and record your favorite models shows on myfreecams.com 4 | 5 | What's new? 6 | ========== 7 | In v.1.0.8 So far, the most common error was that the DL programs were not in the windows path, so I decided to end. In the future it will be necessary to edit all paths for all DL programs in `config.yml`. This change will allow to choose which version of a program you want use with MFC Recorder, if you have multiple versions of the same program installed. 8 | Now there is also the option of selecting 4 combinations of subdirectory names as previously existed in the basic version of the MFC Recorder. More is explained in the `config.yml` file. 9 | Also is added possibility to see the size of the recorded files in the MB every three minutes. 10 | Now it is possible by simply editing the `config.yml` and `index.html` file to select the program we want to use to record your favorite models show's on myfreecams.com. 11 | 12 | mfc-node 13 | ========== 14 | This is an attempt to create a script similar to [capturbate-node](https://github.com/SN4T14/capturebate-node) based on different pieces of code found on the Internet. 15 | 16 | Credits: 17 | * [capturbate-node](https://github.com/SN4T14/capturebate-node) 18 | * [Sembiance/get_mfc_video_url.js](https://gist.github.com/Sembiance/df151de0006a0bf8ae54) 19 | * [mfc-node](https://github.com/sstativa/mfc-node) 20 | * [MFCD.exe](https://github.com/ruzzy/) 21 | 22 | Requirements 23 | ============ 24 | 1. [Node.js](https://nodejs.org/download/release/) used to run mfc-node, hence the name. (tested with node v.11.9.0) 25 | 2. [Livestreamer](https://github.com/chrippa/livestreamer/releases) last version 1.12.2. It's best to install it individually in 'C:/Livestreamer' 26 | 3. [Streamlink](https://github.com/streamlink/streamlink/releases) (tested with the last version 1.0.0) - better to install it independently in in 'C:/Streamlink' 27 | 4. [ffmpeg](https://ffmpeg.zeranoe.com/builds/) It is recommended to install the latest version. 28 | 5. [MFCD.exe](http://www.mediafire.com/file/aim84bicrsbbvci/MFCD.rar) MFC Dump by @RuzzyRullez (little modified) 29 | 6. [hlsdl.exe](https://github.com/samsamsam-iptvplayer/hlsdl) or (https://www.mediafire.com/file/d9obqdq71cqeehr/hlsdl.exe/file) for windows. 30 | 31 | Setup 32 | ===== 33 | 1. Install [Node.js](https://nodejs.org/download/release/) (v8.1.3 or higher). 34 | 2. Install [Git](https://git-scm.com/downloads). 35 | 2. Download and unpack the [code](https://codeload.github.com/horacio9a/mfc-node/zip/master), create directory 'C:/-nm-mfc' and move all files from 'rar' there. 36 | 3. Open console and go into the default directory ('C:/-nm-mfc') where are located unpacked files. 37 | 4. Install requirements by running `npm install` in the same directory as `main.js` is (Windows users have to install [Git](https://git-scm.com/download/win)). 38 | 5. Edit `config.yml` file with the all necessary data. 39 | 6. `ffmpeg.exe`, `MFCD.exe` and `hlsdl.exe` now can be anywhere but the path's must be edited in `config.yml`. 40 | 41 | Running 42 | ======= 43 | 1. Open console and go into the directory where is 'main.js'. 44 | 2. Start program with 'node main.js'. 45 | 3. Open http://localhost:8888 in your browser. 46 | 47 | - The online model list can be sorted by various criteria (default is 'state' because at the top are the models currently being recorded). Thumbnails of models ('menu' small preview) with camscore greater than 350 will be displayed immediately. After about 60 seconds, thumbnails of other models will be visible when the mouse cursor is hover and will be updated. 48 | - If you want to look at some of the models, click on the model name and a 'spinner' will appear with the model image in resolution 380x285 with all available model data. 49 | - If you want to start recording, you need to click the red button ('Japanese flag'). If you want to stop recording, you need to click the stop button (right of the red button). 50 | - All this can be done online and track what is happening on the console, and you can view recorded file immediately if you start some media player, for example VLC. 51 | - When you click on a preview thumbnail the large image is obtained in the next tab of your browser. 52 | - The MFC Recorder now captures the MFC streams with five different programs (livestreamer, streamlink, ffmpeg, hlsdl and MFCD) depending on the data in config.yml ('ls', 'sl', 'ff-ts', 'ff-flv', 'hls' and 'rtmp'). Currently it is better to use 'livestreamer', 'streamlink' and 'rtmp' because they don't have the so-called 'freeze' problem as it currently has 'ffmpeg' for some models. 53 | - Lot of people are asking, so I added the option that every model now has its own subdirectory which can be selected in the 'config.yml'. 54 | - By pressing 'State/Online' or by press the far right button in the spinner you can enter in the model room with your browser. 55 | - By pressing the model 'Quality' you get a video preview of the current model in separate window of your browser. For this feature in My recommendation is to use the Chrome browser with the installed add-on [Play HLS M3u8](https://chrome.google.com/webstore/detail/play-hls-m3u8/ckblfoghkjhaclegefojbgllenffajdc/related) but if you want firefox then need to install [Native HLS Playback](https://addons.mozilla.org/en-US/firefox/addon/native_hls_playback/) 56 | - For better view `livestreamer` recording line you can replace original file: 57 | 58 | `.../livestreamer_cli/utils/progress.py` with 'progress.py' on this page. 59 | 60 | The list of online models will be displayed with a set of allowed commands for each model: 61 | Include - if you want to record the model 62 | Exclude - if you don't want to record the model anymore 63 | Delete - if you are not interested in the model and wanna hide her permanently 64 | 65 | This is not a real-time application. Whenever your 'include', 'exclude' or 'delete' the model your changes will be applied only with the next iteration of 'mainLoop' function of the script. 'mainLoop' runs every 30 seconds (default value for 'modelScanInterval'). 66 | There is no 'auto reload' feature, you have to reload the list manually with 'big red button', however, keep in mind the script updates the list internally every 30 seconds ('modelScanInterval'), therefore sometimes you'll have to wait 30 seconds to see any updates. 67 | Be mindful when capturing many streams at once to have plenty of space on disk and the bandwidth available or you’ll end up dropping a lot of frames and the files will be useless. 68 | 69 | For advanced users 70 | =========== 71 | There are several special URLs that allow implementing some operations with a model even if she is offline. 72 | 73 | __Include__ 74 | ``` 75 | http://localhost:8888/models/include?nm=modelname 76 | http://localhost:8888/models/include?uid=12345678 77 | ``` 78 | __Exclude__ 79 | ``` 80 | http://localhost:8888/models/exclude?nm=modelname 81 | http://localhost:8888/models/exclude?uid=12345678 82 | ``` 83 | __Delete__ 84 | ``` 85 | http://localhost:8888/models/delete?nm=modelname 86 | http://localhost:8888/models/delete?uid=12345678 87 | ``` 88 | Places that can be clicked 89 | 90 | ![alt screenshot](./screenshot.jpg) 91 | 92 | New look of 'spinner' 93 | 94 | ![alt screenshot](./screenshot1.jpg) 95 | 96 | Look after replacement of 'progress.py' 97 | 98 | ![alt screenshot](./screenshot2.jpg) 99 | 100 | Look after 'MFCD.exe' addition for RTMP recording 101 | 102 | ![alt screenshot](./screenshot3.jpg) 103 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | captureDirectory: 'C:\Videos\MFC' # Choose a directory for the recorded files 2 | createModelDirectory: true # If you want all the files of the same model to be recorded in a separate subdirectory with various 'directoryFormat' names. 3 | directoryFormat: id+nm # You can choose between 'id+nm','nm+id','id' and 'nm' separate subdirectory format (Of course only if the previous option is 'true'). 4 | dateFormat: DDMMYYYY-HHmmss # Choose date format for the filename, for example: YYYY-MM-DD_HH-mm-ss 5 | downloadProgram: rtmp # You can choose between 'ls' for 'livestreamer', 'sl' for 'streamlink', 'ff-ts' and 'ff-flv' for 'ffmpeg', 'hls' for 'hlsdl' or 'rtmp' for 'MFCD.exe' 6 | livestreamer: 'C:/Livestreamer/livestreamer.exe' # Enter the path of the program in your computer. 7 | streamlink: 'C:/Streamlink/bin/streamlink.exe' # Enter the path of the program in your computer. 8 | ffmpeg: 'C:/Windows/ffmpeg.exe' # Enter the path of the program in your computer. 9 | hlsdl: 'C:/Windows/hlsdl.exe' # Enter the path of the program in your computer. 10 | mfcd: 'C:/Windows/mfcd.exe' # Enter the path of the program in your computer. 11 | modelScanInterval: 30 # In seconds, how often mfc-node checks for newly online models 12 | minFileSizeMb: 0 # If you do not want to save a file smaller than 5 MB enter number 5 13 | port: 8888 # number of port for your browser url for example: http://localhost:8888/ 14 | debug: true # If you want a more detailed view put 'true' or 'false' to skip this option 15 | models: # This is only example - all your models will be added with your browser 16 | - uid: 18363406 17 | mode: 1 18 | nm: HOTREAL36DD 19 | includeModels: [] 20 | includeUids: [] 21 | excludeModels: [] 22 | excludeUids: [] 23 | deleteModels: [] 24 | deleteUids: [] -------------------------------------------------------------------------------- /convert.js: -------------------------------------------------------------------------------- 1 | // MyFreeCams File Converter v.1.0.6 2 | 3 | 'use strict'; 4 | 5 | let Promise = require('bluebird'); 6 | let fs = Promise.promisifyAll(require('fs')); 7 | let yaml = require('js-yaml'); 8 | let colors = require('colors'); 9 | let childProcess = require('child_process'); 10 | let mkdirp = require('mkdirp'); 11 | let path = require('path'); 12 | let moment = require('moment'); 13 | let Queue = require('promise-queue'); 14 | let filewalker = require('filewalker'); 15 | 16 | function getCurrentTime() { 17 | return moment().format(`HH:mm:ss`); 18 | } 19 | 20 | function printMsg(msg) { 21 | console.log(colors.gray(`[` + getCurrentTime() + `]`), msg); 22 | } 23 | 24 | function printErrorMsg(msg) { 25 | console.log(colors.gray(`[` + getCurrentTime() + `]`), colors.red(`[ERROR]`), msg); 26 | } 27 | 28 | function getTimestamp() { 29 | return Math.floor(new Date().getTime() / 1000); 30 | } 31 | 32 | let config = yaml.safeLoad(fs.readFileSync(path.join(__dirname, 'convert.yml'), 'utf8')); 33 | 34 | let srcDirectory = path.resolve(__dirname, config.srcDirectory || 'complete'); 35 | let dstDirectory = path.resolve(__dirname, config.dstDirectory || 'converted'); 36 | let convertProgram = config.convertProgram || 'ffmpeg284'; 37 | let dirScanInterval = config.dirScanInterval || 300; 38 | let maxConcur = config.maxConcur || 1; 39 | 40 | Queue.configure(Promise.Promise); 41 | 42 | let queue = new Queue(maxConcur, Infinity); 43 | 44 | function getFiles() { 45 | let files = []; 46 | 47 | return new Promise((resolve, reject) => { 48 | filewalker(srcDirectory, { maxPending: 1, matchRegExp: /(\.ts|\.flv|\.mp4)$/ }) 49 | .on('file', (p, stats) => { 50 | // select only "not hidden" files and not empty files (>10KBytes) 51 | if (!p.match(/(^\.|\/\.)/) && stats.size > 10240) { 52 | // push path relative to srcDirectory 53 | files.push(p); 54 | } 55 | }) 56 | .on('done', () => { 57 | resolve(files); 58 | }) 59 | .walk(); 60 | }); 61 | } 62 | 63 | function convertFile(srcFile) { 64 | let startTs = moment(); 65 | let src = path.join(srcDirectory, srcFile); 66 | let dstPath = path.resolve(path.dirname(path.join(dstDirectory, srcFile))); 67 | let dstFile = path.basename(srcFile, path.extname(srcFile)) + '.flv'; 68 | let dst = path.join(dstDirectory, `~${dstFile}`); 69 | let tempDst = path.join(dstPath, dstFile); 70 | 71 | mkdirp.sync(dstPath); 72 | 73 | let stats = fs.statSync(src); 74 | 75 | printMsg(`Starting ${colors.gray(srcFile)} @ size ${colors.yellow((stats.size/1048576).toFixed(2))} MB`); 76 | 77 | let convertProcess; 78 | 79 | if (convertProgram == 'ffmpeg') { 80 | convertProcess = childProcess.spawnSync(convertProgram,['-i',src,'-y','-hide_banner','-loglevel','panic','-c:v','copy','-c:a','aac','-b:a','128k','-copyts','-start_at_zero',dst])}; 81 | 82 | if (convertProgram == 'ffmpeg284') { 83 | convertProcess = childProcess.spawnSync(convertProgram,['-i',src,'-y','-hide_banner','-loglevel','panic','-c:v','copy','-c:a','libvo_aacenc','-b:a','128k','-copyts','-start_at_zero',dst])}; 84 | 85 | let duration = moment.duration(moment().diff(startTs)).asSeconds().toString(); 86 | printMsg(`Finished ${colors.green(dstFile)} after ${colors.cyan(duration)} sec.`); 87 | 88 | if (convertProcess.status != 0) { 89 | printErrorMsg(`Failed to convert ${colors.red(srcFile)}`); 90 | 91 | if (convertProcess.error) { 92 | printErrorMsg(convertProcess.error.toString()); 93 | } 94 | 95 | return; 96 | } 97 | 98 | if (config.deleteAfter) { 99 | fs.unlink(src, function(err) { 100 | // do nothing, shit happens 101 | }); 102 | } else { 103 | fs.renameAsync(src, `${src}.bak`, function(err) { 104 | if (err) { 105 | printErrorMsg(err.toString()); 106 | } 107 | }); 108 | } 109 | 110 | fs.renameAsync(dst, tempDst, function(err) { 111 | if (err) { 112 | printErrorMsg(err.toString()); 113 | } 114 | }); 115 | } 116 | 117 | function mainLoop() { 118 | let startTs = moment().unix(); 119 | 120 | Promise 121 | .try(() => getFiles()) 122 | .then(files => new Promise((resolve, reject) => { 123 | printMsg(files.length + ` file(s) to convert.`); 124 | 125 | if (files.length === 0) { 126 | resolve(); 127 | } else { 128 | files.forEach(file => { 129 | queue 130 | .add(() => convertFile(file)) 131 | .catch(err => { 132 | printErrorMsg(err); 133 | }) 134 | .finally(() => { 135 | if ((queue.getPendingLength() + queue.getQueueLength()) === 0) { 136 | resolve(); 137 | } 138 | }); 139 | }); 140 | } 141 | })) 142 | .catch(err => { 143 | if (err) { 144 | printErrorMsg(err); 145 | } 146 | }) 147 | .finally(() => { 148 | let seconds = startTs - moment().unix() + dirScanInterval; 149 | 150 | if (seconds < 5) { 151 | seconds = 5; 152 | } 153 | 154 | printMsg(`Done >>> will scan the folder in ${seconds} seconds.`); 155 | 156 | setTimeout(mainLoop, seconds * 1000); 157 | }); 158 | } 159 | 160 | mkdirp.sync(srcDirectory); 161 | mkdirp.sync(dstDirectory); 162 | 163 | mainLoop(); 164 | -------------------------------------------------------------------------------- /convert.yml: -------------------------------------------------------------------------------- 1 | srcDirectory: 'C:/Videos/MFC' # directory where you store your '.ts', '.mp4' and '.flv' files. 2 | dstDirectory: 'C:/Videos/MFC_Convert' # directory where do you want to store your converted '.flv' files. 3 | convertProgram: 'ffmpeg284' # choose version of ffmpeg ('ffmpeg' or 'ffmpeg284') 4 | dirScanInterval: 300 # in seconds, min: 5 seconds. 5 | deleteAfter: false # if you want to keep the original files then set to 'false'. 6 | maxConcur: 1 # how many files to convert simultaneously. -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacio9a/mfc-node/a9720047804e8816089ed0fb7dec19b89b34508b/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MFC Recorder v.1.0.9 9 | 10 | 11 | 12 | 13 | 189 | 190 | 191 |
192 |
193 |
194 |
195 |
196 |
197 | MyFreeCams Recorder v.1.0.9 by horacio9a 198 |
199 |
200 | 201 | search 202 | clear 203 |
204 | 209 |
210 |
211 |
212 |
213 |
214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 |
Th.NameStateQualityScoreRankCont.NewMissViewersBlurb
{{ model.state }}{{ model.camserv < 840 ? 'LD' : '' || model.camserv > 1544 ? 'HD' : 'SD' }}
246 |
247 |
248 |
249 |
250 |
251 |
252 | 253 |
254 |

{{ vm.model.nm }}

255 |
256 | 257 | fiber_manual_record 258 | stop 259 | delete 260 | add_a_photo 261 | open_in_browser 262 | 263 |
Previous model
264 |
Include to the list for recording
265 |
Exclude from the list for recording
266 |
Delete
267 |
Add photo
268 |
Open in browser
269 |
Next model
270 |
271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 |
.
State:{{ vm.model.state }}
NMF:{{ vm.model.camserv < 840 ? 'Yes - Recording is not supported !!!' : 'No' }}
Quality:{{ vm.model.camserv < 840 ? 'Low' : '' || vm.model.camserv > 1544 ? 'High - Recording is not supported !!!' : 'Standard' }}
Continent:{{ vm.model.continent || '-' }}
Blurb:{{ vm.model.blurb || '-' }}
Topic:{{ vm.model.topic || '-' }}
UID:{{ vm.model.uid || '-' }}
Server:{{ vm.model.camserv }}
Viewers:{{ vm.model.rc }}
283 |
284 |
285 | ondemand_video 286 |
287 |
288 |
289 | 290 | 291 | 292 | 293 | 294 | 558 | 559 | 560 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // MyFreeCams Recorder v.1.0.9 2 | 3 | 'use strict'; 4 | 5 | var Promise = require('bluebird'); 6 | var fs = Promise.promisifyAll(require('fs')); 7 | var mvAsync = Promise.promisify(require('mv')); 8 | var mkdirp = require('mkdirp'); 9 | var moment = require('moment'); 10 | var colors = require('colors'); 11 | var yaml = require('js-yaml'); 12 | var path = require('path'); 13 | var spawn = require('child_process').spawn; 14 | var HttpDispatcher = require('httpdispatcher'); 15 | var dispatcher = new HttpDispatcher(); 16 | var http = require('http'); 17 | var WebSocketClient = require('websocket').client; 18 | var bhttp = require('bhttp'); 19 | var _ = require('underscore'); 20 | 21 | 22 | var config = yaml.safeLoad(fs.readFileSync('config.yml', 'utf8')); 23 | 24 | config.captureDirectory = config.captureDirectory || 'C:/Videos/MFC'; 25 | config.createModelDirectory = config.createModelDirectory || false; 26 | config.directoryFormat = config.directoryFormat || 'id+nm'; 27 | config.dateFormat = config.dateFormat || 'DDMMYYYY-HHmmss'; 28 | config.downloadProgram = config.downloadProgram || 'ls'; 29 | config.modelScanInterval = config.modelScanInterval || 30; 30 | config.minFileSizeMb = config.minFileSizeMb || 0; 31 | config.port = config.port || 8888; 32 | config.debug = config.debug || true; 33 | 34 | config.includeModels = Array.isArray(config.includeModels) ? config.includeModels : []; 35 | config.excludeModels = Array.isArray(config.excludeModels) ? config.excludeModels : []; 36 | config.deleteModels = Array.isArray(config.deleteModels) ? config.deleteModels : []; 37 | 38 | config.includeUids = Array.isArray(config.includeUids) ? config.includeUids : []; 39 | config.excludeUids = Array.isArray(config.excludeUids) ? config.excludeUids : []; 40 | config.deleteUids = Array.isArray(config.deleteUids) ? config.deleteUids : []; 41 | 42 | var captureDirectory = path.resolve(config.captureDirectory); 43 | var minFileSize = config.minFileSizeMb * 1048576; 44 | 45 | function getCurrentDateTime() { 46 | return moment().format(config.dateFormat); 47 | }; 48 | 49 | function getCurrentTime() { 50 | return moment().format('HH:mm:ss'); 51 | }; 52 | 53 | function printMsg(msg) { 54 | console.log(colors.gray(`[` + getCurrentTime() + `]`), msg); 55 | }; 56 | 57 | function printErrorMsg(msg) { 58 | console.log(colors.gray(`[` + getCurrentTime() + `]`), colors.red(`[ERROR]`), msg); 59 | }; 60 | 61 | function printDebugMsg(msg) { 62 | if (config.debug && msg) { 63 | console.log(colors.gray(`[` + getCurrentTime() + `]`), colors.magenta(`[DEBUG]`), msg); 64 | }; 65 | }; 66 | 67 | function getTimestamp() { 68 | return Math.floor(new Date().getTime() / 1000); 69 | }; 70 | 71 | function dumpModelsCurrentlyCapturing() { 72 | _.each(modelsCurrentlyCapturing, function(m) { 73 | printMsg(`>>> ${colors.cyan(m.filename)} @ ${colors.yellow(config.downloadProgram)} recording <<<`); 74 | }); 75 | }; 76 | 77 | function getUid(nm) { 78 | var onlineModel = _.findWhere(onlineModels, {nm: nm}); 79 | 80 | return _.isUndefined(onlineModel) ? false : onlineModel.uid; 81 | } 82 | 83 | function remove(value, array) { 84 | var idx = array.indexOf(value); 85 | 86 | if (idx != -1) { 87 | array.splice(idx, 1); 88 | } 89 | } 90 | 91 | // returns true, if the mode has been changed 92 | function setMode(uid, mode) { 93 | var configModel = _.findWhere(config.models, {uid: uid}); 94 | 95 | if (_.isUndefined(configModel)) { 96 | config.models.push({uid: uid, mode: mode}); 97 | 98 | return true; 99 | } else if (configModel.mode != mode) { 100 | configModel.mode = mode; 101 | 102 | return true; 103 | } 104 | 105 | return false; 106 | } 107 | 108 | function getFileno() { 109 | return new Promise(function(resolve, reject) { 110 | var client = new WebSocketClient(); 111 | 112 | client.on('connectFailed', function(err) { 113 | reject(err); 114 | }); 115 | 116 | client.on('connect', function(connection) { 117 | 118 | connection.on('error', function(err) { 119 | reject(err); 120 | }); 121 | 122 | connection.on('message', function(message) { 123 | if (message.type === 'utf8') { 124 | var parts = /%22opts%22:([0-9]*),%22respkey%22:([0-9]*),%22serv%22:([0-9]*),%22type%22:([0-9]*)\}/.exec(message.utf8Data); 125 | 126 | if (parts && parts[1] && parts[2] && parts[3] && parts[4]) { 127 | connection.close(); 128 | resolve(`respkey=${parts[2]}&type=${parts[4]}&opts=${parts[1]}&serv=${parts[3]}`); 129 | } 130 | } 131 | }); 132 | 133 | connection.sendUTF("hello fcserver\n\0"); 134 | connection.sendUTF("1 0 0 20071025 0 guest:guest\n\0"); 135 | }); 136 | 137 | var servers = ["xchat20","xchat22","xchat23","xchat24","xchat25","xchat26","xchat27","xchat28","xchat29","xchat39", 138 | "xchat62","xchat63","xchat64","xchat65","xchat66","xchat67","xchat68","xchat69","xchat70","xchat71", 139 | "xchat72","xchat73","xchat74","xchat75","xchat76","xchat77","xchat78","xchat79","xchat80","xchat81", 140 | "xchat83","xchat84","xchat85","xchat86","xchat87","xchat88","xchat89","xchat91","xchat94","xchat95", 141 | "xchat96","xchat97","xchat98","xchat99","xchat100","xchat101","xchat102","xchat103","xchat104","xchat105", 142 | "xchat106","xchat108","xchat109","xchat111","xchat100","xchat101","xchat102","xchat103","xchat104","xchat105", 143 | "xchat106","xchat108","xchat109","xchat111","xchat112","xchat113","xchat114","xchat115","xchat116","xchat118", 144 | "xchat119","xchat120","xchat121","xchat122","xchat123","xchat124","xchat125","xchat126","xchat127", 145 | "ychat30","ychat31","ychat32","ychat33"]; 146 | 147 | var server = _.sample(servers); // pick a random chat server 148 | 149 | printDebugMsg(`>>> ${colors.gray(`Start searching new models on server`)} ${colors.green(server)} <<<`); 150 | 151 | client.connect('ws://' + server + '.myfreecams.com:8080/fcsl','','http://' + server + '.myfreecams.com:8080',{Cookie: 'company_id=3149; guest_welcome=1; history=7411522,5375294'}) 152 | }).timeout(20000); // 20 secs 153 | } 154 | 155 | function getOnlineModels(fileno) { 156 | var url = `http://www.myfreecams.com/php/FcwExtResp.php?${fileno}`; 157 | // printDebugMsg(`>>> ${colors.gray(fileno)} <<<`); 158 | 159 | return Promise 160 | .try(function() { 161 | return session.get(url); 162 | }) 163 | .then(function(response) { 164 | onlineModels = []; 165 | 166 | try { 167 | var data = JSON.parse(response.body.toString('utf8')); 168 | var m; 169 | 170 | for (var i = 1; i < data.rdata.length; i += 1) { 171 | m = data.rdata[i]; 172 | onlineModels.push({ 173 | nm: m[0], 174 | sid:m[1], 175 | uid:m[2], 176 | vs:m[3], 177 | pid:m[4], 178 | lv:m[5], 179 | camserv:m[6], 180 | phase:m[7], 181 | creation:m[11], 182 | photos:m[14], 183 | blurb:m[15], 184 | new_model:m[16], 185 | missmfc:m[17], 186 | camscore:m[18], 187 | continent: m[19], 188 | flags:m[20], 189 | rank:m[21], 190 | rc:m[22], 191 | topic:m[23] 192 | }); 193 | } 194 | } catch (err) { 195 | throw new Error(`Failed to parse data.`); 196 | } 197 | 198 | printMsg(`${colors.green(onlineModels.length)} models online.`); 199 | }) 200 | .timeout(20000); // 20 secs 201 | }; 202 | 203 | function selectMyModels() { 204 | return Promise 205 | .try(function() { 206 | printDebugMsg(`${config.models.length} models in ${colors.yellow(`config.`)}`); 207 | 208 | // to include the model only knowing her name, we need to know her uid, 209 | // if we could not find model's uid in array of online models we skip this model till the next iteration 210 | config.includeModels = _.filter(config.includeModels, function(nm) { 211 | var uid = getUid(nm); 212 | 213 | if (uid === false) { 214 | return true; // keep the model till the next iteration 215 | } 216 | 217 | config.includeUids.push(uid); 218 | dirty = true; 219 | }); 220 | 221 | config.excludeModels = _.filter(config.excludeModels, function(nm) { 222 | var uid = getUid(nm); 223 | 224 | if (uid === false) { 225 | return true; // keep the model till the next iteration 226 | } 227 | 228 | config.excludeUids.push(uid); 229 | dirty = true; 230 | }); 231 | 232 | config.deleteModels = _.filter(config.deleteModels, function(nm) { 233 | var uid = getUid(nm); 234 | 235 | if (uid === false) { 236 | return true; // keep the model till the next iteration 237 | } 238 | 239 | config.deleteUids.push(uid); 240 | dirty = true; 241 | }); 242 | 243 | _.each(config.includeUids, function(uid) { 244 | dirty = setMode(uid, 1) || dirty; 245 | }); 246 | 247 | config.includeUids = []; 248 | 249 | _.each(config.excludeUids, function(uid) { 250 | dirty = setMode(uid, 0) || dirty; 251 | }); 252 | 253 | config.excludeUids = []; 254 | 255 | _.each(config.deleteUids, function(uid) { 256 | dirty = setMode(uid, -1) || dirty; 257 | }); 258 | 259 | config.deleteUids = []; 260 | 261 | // remove duplicates 262 | if (dirty) { 263 | config.models = _.uniq(config.models, function(m) { 264 | return m.uid; 265 | }); 266 | } 267 | 268 | var myModels = []; 269 | 270 | _.each(config.models, function(configModel) { 271 | var onlineModel = _.findWhere(onlineModels, {uid: configModel.uid}); 272 | 273 | if (!_.isUndefined(onlineModel)) { 274 | // if the model does not have a name in config.models we use her name by default 275 | if (!configModel.nm) { 276 | configModel.nm = onlineModel.nm; 277 | dirty = true; 278 | } 279 | 280 | onlineModel.mode = configModel.mode; 281 | 282 | if (onlineModel.mode == 1) { 283 | if (onlineModel.vs === 0) { 284 | myModels.push(onlineModel); 285 | } else if (onlineModel.vs === 2) { 286 | printMsg(colors.green(`${onlineModel.nm} ${colors.cyan(`is Away.`)}`)); 287 | } else if (onlineModel.vs === 12) { 288 | printMsg(colors.green(`${onlineModel.nm} ${colors.cyan(`is in Private.`)}`)); 289 | } else if (onlineModel.vs === 13) { 290 | printMsg(colors.green(`${onlineModel.nm} ${colors.cyan(`is in Group Show.`)}`)); 291 | } else if (onlineModel.vs === 14) { 292 | printMsg(colors.green(`${onlineModel.nm} ${colors.cyan(`is in Club Show.`)}`)); 293 | } else if (onlineModel.vs === 90) { 294 | printMsg(colors.green(`${onlineModel.nm} ${colors.cyan(`is Cam Off.`)}`)); 295 | } 296 | } 297 | } 298 | }); 299 | 300 | printDebugMsg(`${myModels.length} model(s) to recording.`); 301 | 302 | if (dirty) { 303 | printDebugMsg(`Save changes in ${colors.yellow('config.')}`); 304 | 305 | fs.writeFileSync('config.yml', yaml.safeDump(config), 'utf8'); 306 | 307 | dirty = false; 308 | } 309 | 310 | return myModels; 311 | }); 312 | } 313 | 314 | function createCaptureProcess(model) { 315 | var modelCurrentlyCapturing = _.findWhere(modelsCurrentlyCapturing, {uid: model.uid}); 316 | 317 | if (!_.isUndefined(modelCurrentlyCapturing)) { 318 | return; // resolve immediately 319 | } 320 | 321 | if ((model.camserv) < 840) { 322 | printDebugMsg(colors.green(model.nm) + (colors.cyan(` is NO MOBILE FEED - Exclude or Delete this model`))); 323 | return;} // resolve immediately 324 | 325 | if (model.phase == 'a') { 326 | printDebugMsg(colors.green(model.nm) + (colors.cyan(` is HD model - use v.3.0.9 for recording HD models`))); 327 | return;} // resolve immediately 328 | 329 | var fileFormat; 330 | if (config.downloadProgram == 'ls') { 331 | fileFormat = 'mp4'} 332 | if (config.downloadProgram == 'sl') { 333 | fileFormat = 'mp4'} 334 | if (config.downloadProgram == 'ff-ts') { 335 | fileFormat = 'ts'} 336 | if (config.downloadProgram == 'ff-flv') { 337 | fileFormat = 'flv'} 338 | if (config.downloadProgram == 'hls') { 339 | fileFormat = 'mp4'} 340 | if (config.downloadProgram == 'rtmp') { 341 | fileFormat = 'flv'} 342 | 343 | var dlProgram; 344 | if (config.downloadProgram == 'ls') { 345 | dlProgram = config.livestreamer} 346 | if (config.downloadProgram == 'sl') { 347 | dlProgram = config.streamlink} 348 | if (config.downloadProgram == 'ff-ts') { 349 | dlProgram = config.ffmpeg} 350 | if (config.downloadProgram == 'ff-flv') { 351 | dlProgram = config.ffmpeg} 352 | if (config.downloadProgram == 'hls') { 353 | dlProgram = config.hlsdl} 354 | if (config.downloadProgram == 'rtmp') { 355 | dlProgram = config.mfcd} 356 | 357 | printMsg(colors.green(model.nm) + ` now online >>> Starting ${colors.yellow(config.downloadProgram)} recording <<<`); 358 | 359 | return Promise 360 | .try(function() { 361 | var filename = model.nm + '_MFC_' + getCurrentDateTime() + '.' + fileFormat; 362 | 363 | var modelDir; 364 | if (config.directoryFormat == 'id+nm') { 365 | modelDir = model.uid + '_' + model.nm} 366 | if (config.directoryFormat == 'id') { 367 | modelDir = (model.uid).toString()} 368 | if (config.directoryFormat == 'nm') { 369 | modelDir = model.nm} 370 | if (config.directoryFormat == 'nm+id') { 371 | modelDir = model.nm + '_' + model.uid} 372 | 373 | function mkdir(dir) { 374 | mkdirp(dir, err => { 375 | if (err) { 376 | printErrorMsg(err); 377 | process.exit(1); 378 | } 379 | }); 380 | } 381 | 382 | var src = path.join(captureDirectory, filename); 383 | 384 | var roomId = 100000000 + model.uid; 385 | 386 | var sdUrl = `http://video${model.camserv - 500}.myfreecams.com:1935/NxServer/ngrp:mfc_${roomId}.f4v_mobile/playlist.m3u8?nc=${Date.now()}`; 387 | 388 | var captureProcess; 389 | if (config.downloadProgram == 'ls') { 390 | captureProcess = spawn(dlProgram, ['-Q','hlsvariant://' + sdUrl,'best','--stream-sorting-excludes=>950p,>1500k','-o', src])}; 391 | 392 | if (config.downloadProgram == 'sl') { 393 | captureProcess = spawn(dlProgram, ['-Q','hls://' + sdUrl,'best','--stream-sorting-excludes=>950p,>1500k','-o', src])}; 394 | 395 | if (config.downloadProgram == 'ff-ts') { 396 | captureProcess = spawn(dlProgram, ['-hide_banner','-v','fatal','-i',sdUrl,'-map','0:1','-map','0:2','-c','copy','-vsync','2','-r','60','-b:v','500k', src])}; 397 | 398 | if (config.downloadProgram == 'ff-flv') { 399 | captureProcess = spawn(dlProgram, ['-hide_banner','-v','fatal','-i',sdUrl,'-c:v','copy','-map','0:1','-map','0:2','-c:a','aac','-b:a','192k','-ar','32000', src])}; 400 | 401 | if (config.downloadProgram == 'hls') { 402 | captureProcess = spawn(dlProgram, [sdUrl,'-b','-q','-o', src])}; 403 | 404 | if (config.downloadProgram == 'rtmp') { 405 | captureProcess = spawn(dlProgram, [model.nm, src])}; 406 | 407 | captureProcess.stdout.on('data', function(data) { 408 | printMsg(data.toString()); 409 | }); 410 | 411 | captureProcess.stderr.on('data', function(data) { 412 | printMsg(data.toString()); 413 | }); 414 | 415 | captureProcess.on('close', function(code) { 416 | printMsg(`${colors.green(model.nm)} <<< stopped recording.`); 417 | 418 | var modelCurrentlyCapturing = _.findWhere(modelsCurrentlyCapturing, { 419 | pid: captureProcess.pid}); 420 | 421 | if (!_.isUndefined(modelCurrentlyCapturing)) { 422 | var modelIndex = modelsCurrentlyCapturing.indexOf(modelCurrentlyCapturing); 423 | 424 | if (modelIndex !== -1) { 425 | modelsCurrentlyCapturing.splice(modelIndex, 1); 426 | }}; 427 | 428 | var dst = config.createModelDirectory 429 | ? path.join(captureDirectory, modelDir, filename) 430 | : src; 431 | 432 | fs.statAsync(src) 433 | // if the file is big enough we keep it otherwise we delete it 434 | .then(stats => (stats.size <= minFileSize) ? fs.unlinkAsync(src) : mvAsync(src, dst, { mkdirp: true })) 435 | .catch(err => { 436 | if (err.code !== 'ENOENT') { 437 | printErrorMsg(`[` + colors.green(model.nm) + `] ` + err.toString()); 438 | } 439 | }); 440 | }); 441 | 442 | if (!!captureProcess.pid) { 443 | modelsCurrentlyCapturing.push({ 444 | nm: model.nm, 445 | uid: model.uid, 446 | filename: filename, 447 | captureProcess: captureProcess, 448 | pid: captureProcess.pid, 449 | checkAfter: getTimestamp() + 180, // we are gonna check the process after 3 min 450 | size: 0 451 | }); 452 | } 453 | }) 454 | .catch(function(err) { 455 | printErrorMsg(`[` + colors.green(model.nm) + `] ` + err.toString()); 456 | }); 457 | } 458 | 459 | function checkCaptureProcess(model) { 460 | var onlineModel = _.findWhere(onlineModels, {uid: model.uid}); 461 | 462 | if (!_.isUndefined(onlineModel)) { 463 | if (onlineModel.mode == 1) { 464 | onlineModel.capturing = true; 465 | } else if (!!model.captureProcess) { 466 | // if the model has been excluded or deleted we stop capturing process and resolve immediately 467 | printDebugMsg(colors.green(model.nm) + ` <<< has to be stopped.`); 468 | model.captureProcess.kill(); 469 | return; 470 | } 471 | } 472 | 473 | // if this is not the time to check the process then we resolve immediately 474 | if (model.checkAfter > getTimestamp()) { 475 | return; 476 | } 477 | 478 | return fs 479 | .statAsync(path.join(captureDirectory, model.filename)) 480 | .then(function(stats) { 481 | printDebugMsg(colors.green(model.nm) + ` @ ` + colors.cyan((stats.size/1048576).toFixed(2)) + ` MB >>> recording in progress <<<`); 482 | // we check the process every 10 minutes since its start, 483 | // if the size of the file has not changed for the last 10 min, we kill the process 484 | if (stats.size - model.size > 0) { 485 | 486 | model.checkAfter = getTimestamp() + 180; // 3 minutes 487 | model.size = stats.size; 488 | } else if (!!model.captureProcess) { 489 | // we assume that onClose will do all clean up for us 490 | printErrorMsg(`[` + colors.green(model.nm) + `] Process is dead.`); 491 | model.captureProcess.kill(); 492 | } else { 493 | // suppose here we should forcefully remove the model from modelsCurrentlyCapturing 494 | // because her captureProcess is unset, but let's leave this as is 495 | } 496 | }) 497 | .catch(function(err) { 498 | if (err.code == 'ENOENT') { 499 | // do nothing, file does not exists, 500 | // this is kind of impossible case, however, probably there should be some code to "clean up" the process 501 | } else { 502 | printErrorMsg('[' + colors.green(model.nm) + '] ' + err.toString()); 503 | } 504 | }); 505 | } 506 | 507 | function mainLoop() { 508 | 509 | Promise 510 | .try(function() { 511 | return getFileno(); 512 | }) 513 | .then(function(fileno) { 514 | return getOnlineModels(fileno); 515 | }) 516 | .then(function() { 517 | return selectMyModels(); 518 | }) 519 | .then(function(myModels) { 520 | return Promise.all(myModels.map(createCaptureProcess)); 521 | }) 522 | .then(function() { 523 | return Promise.all(modelsCurrentlyCapturing.map(checkCaptureProcess)); 524 | }) 525 | .then(function() { 526 | models = onlineModels; 527 | }) 528 | .catch(function(err) { 529 | printErrorMsg(err); 530 | }) 531 | .finally(function() { 532 | dumpModelsCurrentlyCapturing(); 533 | 534 | printDebugMsg(`>>> ${colors.gray(`Will search for new models in ${config.modelScanInterval} seconds ...`)} <<<`); 535 | 536 | setTimeout(mainLoop, config.modelScanInterval * 1000); 537 | }); 538 | } 539 | 540 | var session = bhttp.session(); 541 | 542 | var models = new Array(); 543 | var onlineModels = new Array(); 544 | var modelsCurrentlyCapturing = new Array(); 545 | 546 | // convert the list of models to the new format 547 | var dirty = false; 548 | 549 | if (config.models.length > 0) { 550 | config.models = config.models.map(function(m) { 551 | 552 | if (typeof m === 'number') { // then this "simple" uid 553 | m = {uid: m, include: 1}; 554 | 555 | dirty = true; 556 | } else if (_.isUndefined(m.mode)) { // if there is no mode field this old version 557 | m.mode = !m.excluded ? 1 : 0; 558 | dirty = true; 559 | } 560 | 561 | return m; 562 | }); 563 | } 564 | 565 | if (dirty) {printDebugMsg(`Save changes in ${colors.yellow(`config.`)}`); // then there were some changes in the list of models 566 | 567 | fs.writeFileSync('config.yml', yaml.safeDump(config), 0, 'utf8'); 568 | 569 | dirty = false} 570 | 571 | mainLoop(); 572 | 573 | dispatcher.onGet('/', (req, res) => { 574 | fs.readFile(path.join(__dirname, 'index.html'), (err, data) => { 575 | if (err) { 576 | res.writeHead(404, { 'Content-Type': 'text/html' }); 577 | res.end('Not Found'); 578 | } else { 579 | res.writeHead(200, { 'Content-Type': 'text/html' }); 580 | res.end(data, 'utf-8'); 581 | } 582 | }); 583 | }); 584 | 585 | dispatcher.onGet('/favicon.ico', (req, res) => { 586 | fs.readFile(path.join(__dirname, 'favicon.ico'), (err, data) => { 587 | if (err) { 588 | res.writeHead(404, { 'Content-Type': 'image/x-icon' }); 589 | res.end('Not Found'); 590 | } else { 591 | res.writeHead(200, { 'Content-Type': 'image/x-icon' }); 592 | res.end(data); 593 | } 594 | }); 595 | }); 596 | 597 | // return an array of online models 598 | dispatcher.onGet('/models', (req, res) => { 599 | res.writeHead(200, {'Content-Type': 'application/json'}); 600 | res.end(JSON.stringify(models)); 601 | }); 602 | 603 | // when we include the model we only "express our intention" to do so, 604 | // in fact the model will be included in the config only with the next iteration of mainLoop 605 | dispatcher.onGet('/models/include', function(req, res) { 606 | if (req.params && req.params.uid) { 607 | var uid = parseInt(req.params.uid, 10); 608 | 609 | if (!isNaN(uid)) { 610 | printDebugMsg(`${colors.green(uid)}${colors.cyan(` >>> include >>>`)}`); 611 | 612 | // before we include the model we check that the model is not in our "to exclude" or "to delete" lists 613 | remove(req.params.nm, config.excludeUids); 614 | remove(req.params.nm, config.deleteUids); 615 | 616 | config.includeUids.push(uid); 617 | 618 | res.writeHead(200, {'Content-Type': 'application/json'}); 619 | res.end(JSON.stringify({uid: uid})); // this will be sent back to the browser 620 | 621 | var model = _.findWhere(models, {uid: uid}); 622 | 623 | if (!_.isUndefined(model)) { 624 | model.nextMode = 1; 625 | } 626 | 627 | return; 628 | } 629 | } else if (req.params && req.params.nm) { 630 | printDebugMsg(`${colors.green(req.params.nm)}${colors.cyan(` >>> include >>>`)}`); 631 | 632 | // before we include the model we check that the model is not in our "to exclude" or "to delete" lists 633 | remove(req.params.nm, config.excludeModels); 634 | remove(req.params.nm, config.deleteModels); 635 | 636 | config.includeModels.push(req.params.nm); 637 | 638 | dirty = true; 639 | 640 | res.writeHead(200, {'Content-Type': 'application/json'}); 641 | res.end(JSON.stringify({nm: req.params.nm})); // this will be sent back to the browser 642 | 643 | var model = _.findWhere(models, {nm: req.params.nm}); 644 | 645 | if (!_.isUndefined(model)) { 646 | model.nextMode = 1; 647 | } 648 | 649 | return; 650 | } 651 | 652 | res.writeHead(422, {'Content-Type': 'application/json'}); 653 | res.end(JSON.stringify({error: 'Invalid request'})); 654 | }); 655 | 656 | // whenever we exclude the model we only "express our intention" to do so, 657 | // in fact the model will be exclude from config only with the next iteration of mainLoop 658 | dispatcher.onGet('/models/exclude', function(req, res) { 659 | if (req.params && req.params.uid) { 660 | var uid = parseInt(req.params.uid, 10); 661 | 662 | if (!isNaN(uid)) { 663 | printDebugMsg(`${colors.green(uid)}${colors.cyan(` <<< exclude <<<`)}`); 664 | 665 | // before we exclude the model we check that the model is not in our "to include" or "to delete" lists 666 | remove(req.params.nm, config.includeUids); 667 | remove(req.params.nm, config.deleteUids); 668 | 669 | config.excludeUids.push(uid); 670 | 671 | res.writeHead(200, {'Content-Type': 'application/json'}); 672 | res.end(JSON.stringify({uid: uid})); // this will be sent back to the browser 673 | 674 | var model = _.findWhere(models, {uid: uid}); 675 | 676 | if (!_.isUndefined(model)) { 677 | model.nextMode = 0; 678 | } 679 | 680 | return; 681 | } 682 | } else if (req.params && req.params.nm) { 683 | printDebugMsg(`${colors.green(req.params.nm)}${colors.cyan(` <<< exclude <<<`)}`); 684 | 685 | // before we exclude the model we check that the model is not in our "to include" or "to delete" lists 686 | remove(req.params.nm, config.includeModels); 687 | remove(req.params.nm, config.deleteModels); 688 | 689 | config.excludeModels.push(req.params.nm); 690 | 691 | dirty = true; 692 | 693 | res.writeHead(200, {'Content-Type': 'application/json'}); 694 | res.end(JSON.stringify({nm: req.params.nm})); // this will be sent back to the browser 695 | 696 | var model = _.findWhere(models, {nm: req.params.nm}); 697 | 698 | if (!_.isUndefined(model)) { 699 | model.nextMode = 0; 700 | } 701 | 702 | return; 703 | } 704 | 705 | res.writeHead(422, {'Content-Type': 'application/json'}); 706 | res.end(JSON.stringify({error: `Invalid request.`})); 707 | }); 708 | 709 | // whenever we delete the model we only "express our intention" to do so, 710 | // in fact the model will be markd as "deleted" in config only with the next iteration of mainLoop 711 | dispatcher.onGet('/models/delete', function(req, res) { 712 | if (req.params && req.params.uid) { 713 | var uid = parseInt(req.params.uid, 10); 714 | 715 | if (!isNaN(uid)) { 716 | printDebugMsg(`${colors.green(uid)}${colors.red(` >>> delete <<<`)}`); 717 | 718 | // before we exclude the model we check that the model is not in our "to include" or "to exclude" lists 719 | remove(req.params.nm, config.includeUids); 720 | remove(req.params.nm, config.excludeUids); 721 | 722 | config.deleteUids.push(uid); 723 | 724 | res.writeHead(200, {'Content-Type': 'application/json'}); 725 | res.end(JSON.stringify({uid: uid})); // this will be sent back to the browser 726 | 727 | var model = _.findWhere(models, {uid: uid}); 728 | 729 | if (!_.isUndefined(model)) { 730 | model.nextMode = -1; 731 | } 732 | 733 | return; 734 | } 735 | } else if (req.params && req.params.nm) { 736 | printDebugMsg(`${colors.green(req.params.nm)}${colors.red(` >>> delete <<<`)}`); 737 | 738 | // before we exclude the model we check that the model is not in our "include" or "exclude" lists 739 | remove(req.params.nm, config.includeModels); 740 | remove(req.params.nm, config.excludeModels); 741 | 742 | config.deleteModels.push(req.params.nm); 743 | 744 | dirty = true; 745 | 746 | res.writeHead(200, {'Content-Type': 'application/json'}); 747 | res.end(JSON.stringify({nm: req.params.nm})); // this will be sent back to the browser 748 | 749 | var model = _.findWhere(models, {nm: req.params.nm}); 750 | 751 | if (!_.isUndefined(model)) { 752 | model.nextMode = -1; 753 | } 754 | 755 | return; 756 | } 757 | 758 | res.writeHead(422, {'Content-Type': 'application/json'}); 759 | res.end(JSON.stringify({error: `Invalid request.`})); 760 | }); 761 | 762 | dispatcher.onError(function(req, res) { 763 | res.writeHead(404); 764 | }); 765 | 766 | http.createServer((req, res) => { 767 | dispatcher.dispatch(req, res); 768 | }).listen(config.port, () => { 769 | printMsg(`Server listening on: ` + colors.green(`0.0.0.0:` + config.port)); 770 | }); 771 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mfc-node", 3 | "version": "1.0.9", 4 | "description": "MyFreeCams Recorder", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/horacio9a/mfc-node.git" 12 | }, 13 | "author": "horacio9a", 14 | "license": "WTFPL", 15 | "bugs": { 16 | "url": "https://github.com/horacio9a/myfreecams-anonymous/issues/" 17 | }, 18 | "homepage": "https://github.com/horacio9a/mfc-node", 19 | "dependencies": { 20 | "bluebird": "^2.11.0", 21 | "mv": "^2.1.1", 22 | "js-yaml": "^3.12.2", 23 | "bhttp": "^1.2.4", 24 | "mkdirp": "^0.5.1", 25 | "moment": "^2.24.0", 26 | "string": "^3.3.3", 27 | "underscore": "^1.9.1", 28 | "websocket": "^1.0.28", 29 | "colors": "^1.3.3", 30 | "httpdispatcher": "^2.1.2", 31 | "filewalker": "^0.1.3" 32 | }, 33 | "bin": "main.js", 34 | "pkg": { 35 | "scripts": [ 36 | "main.js" 37 | ], 38 | "assets": [ 39 | "index.html", 40 | "favicon.ico" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /progress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from collections import deque 4 | from time import time 5 | 6 | from ..compat import is_win32, get_terminal_size 7 | 8 | PROGRESS_FORMATS = ( 9 | "[download] >>> {prefix} ({written}) ", 10 | ) 11 | 12 | def terminal_len(value): 13 | """Returns the length of the string it would be when displayed. 14 | 15 | Attempts to decode the string as UTF-8 first if it's a bytestring. 16 | """ 17 | if isinstance(value, bytes): 18 | value = value.decode("utf8", "ignore") 19 | 20 | return len(value) 21 | 22 | def print_inplace(msg): 23 | """Clears out the previous line and prints a new one.""" 24 | term_width = get_terminal_size().columns 25 | spacing = term_width - terminal_len(msg) 26 | 27 | # On windows we need one less space or we overflow the line for some reason. 28 | if is_win32: 29 | spacing -= 1 30 | 31 | sys.stderr.write("\r{0}".format(msg)) 32 | sys.stderr.write(" " * max(0, spacing)) 33 | sys.stderr.flush() 34 | 35 | def format_filesize(size): 36 | """Formats the file size into a human readable format.""" 37 | for suffix in ('bytes', 'KB', 'MB', 'GB', 'TB'): 38 | if size < 1024.0: 39 | if suffix in ('GB', 'TB'): 40 | return '{0:3.2f} {1}'.format(size, suffix) 41 | else: 42 | return '{0:3.1f} {1}'.format(size, suffix) 43 | 44 | size /= 1024.0 45 | 46 | def format_time(elapsed): 47 | """Formats elapsed seconds into a human readable format.""" 48 | hours = int(elapsed / 3600) 49 | minutes = int(elapsed % 3600 / 60) 50 | seconds = int(elapsed % 60) 51 | 52 | rval = '' 53 | if hours: 54 | rval += '{0}h'.format(hours) 55 | 56 | if elapsed > 60: 57 | rval += '{0}m'.format(minutes) 58 | 59 | rval += '{0}s'.format(seconds) 60 | return rval 61 | 62 | def create_status_line(**params): 63 | """Creates a status line with appropriate size.""" 64 | max_size = get_terminal_size().columns - 1 65 | 66 | for fmt in PROGRESS_FORMATS: 67 | status = fmt.format(**params) 68 | 69 | if len(status) <= max_size: 70 | break 71 | 72 | return status 73 | 74 | def progress(iterator, prefix): 75 | """Progress an iterator and updates a pretty status line to the terminal. 76 | 77 | The status line contains: 78 | - Amount of data read from the iterator 79 | - Time elapsed 80 | - Average speed, based on the last few seconds. 81 | """ 82 | prefix = (prefix[-50:]) if len(prefix) > 52 else prefix 83 | speed_updated = start = time() 84 | speed_written = written = 0 85 | speed_history = deque(maxlen=5) 86 | 87 | for data in iterator: 88 | yield data 89 | 90 | now = time() 91 | elapsed = now - start 92 | written += len(data) 93 | 94 | speed_elapsed = now - speed_updated 95 | if speed_elapsed >= 0.5: 96 | speed_history.appendleft(( 97 | written - speed_written, 98 | speed_updated, 99 | )) 100 | speed_updated = now 101 | speed_written = written 102 | 103 | speed_history_written = sum(h[0] for h in speed_history) 104 | speed_history_elapsed = now - speed_history[-1][1] 105 | speed = speed_history_written / speed_history_elapsed 106 | 107 | status = create_status_line( 108 | prefix=prefix, 109 | written=format_filesize(written), 110 | elapsed=format_time(elapsed), 111 | speed=format_filesize(speed) 112 | ) 113 | print_inplace(status) 114 | sys.stderr.write("\n") 115 | sys.stderr.flush() 116 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacio9a/mfc-node/a9720047804e8816089ed0fb7dec19b89b34508b/screenshot.jpg -------------------------------------------------------------------------------- /screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacio9a/mfc-node/a9720047804e8816089ed0fb7dec19b89b34508b/screenshot1.jpg -------------------------------------------------------------------------------- /screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacio9a/mfc-node/a9720047804e8816089ed0fb7dec19b89b34508b/screenshot2.jpg -------------------------------------------------------------------------------- /screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacio9a/mfc-node/a9720047804e8816089ed0fb7dec19b89b34508b/screenshot3.jpg --------------------------------------------------------------------------------