w-dwload-reel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +63 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-dwload-reel.umd.js +7 -0
  10. package/dist/w-dwload-reel.umd.js.map +1 -0
  11. package/docs/WDwloadReel.mjs.html +317 -0
  12. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  13. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  14. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  15. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  16. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  17. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  18. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  23. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  30. package/docs/global.html +520 -0
  31. package/docs/index.html +84 -0
  32. package/docs/scripts/collapse.js +39 -0
  33. package/docs/scripts/commonNav.js +28 -0
  34. package/docs/scripts/linenumber.js +25 -0
  35. package/docs/scripts/nav.js +12 -0
  36. package/docs/scripts/polyfill.js +4 -0
  37. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  38. package/docs/scripts/prettify/lang-css.js +2 -0
  39. package/docs/scripts/prettify/prettify.js +28 -0
  40. package/docs/scripts/search.js +99 -0
  41. package/docs/styles/jsdoc.css +776 -0
  42. package/docs/styles/prettify.css +80 -0
  43. package/g.mjs +42 -0
  44. package/package.json +29 -0
  45. package/script.txt +17 -0
  46. package/src/WDwloadReel.mjs +245 -0
  47. package/test/WDwloadReel.test.mjs +47 -0
  48. package/toolg/addVersion.mjs +4 -0
  49. package/toolg/cleanFolder.mjs +4 -0
  50. package/toolg/gDistRollup.mjs +26 -0
  51. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,80 @@
1
+ .pln {
2
+ color: #ddd;
3
+ }
4
+
5
+ /* string content */
6
+ .str {
7
+ color: #61ce3c;
8
+ }
9
+
10
+ /* a keyword */
11
+ .kwd {
12
+ color: #fbde2d;
13
+ }
14
+
15
+ /* a comment */
16
+ .com {
17
+ color: #aeaeae;
18
+ }
19
+
20
+ /* a type name */
21
+ .typ {
22
+ color: #8da6ce;
23
+ }
24
+
25
+ /* a literal value */
26
+ .lit {
27
+ color: #fbde2d;
28
+ }
29
+
30
+ /* punctuation */
31
+ .pun {
32
+ color: #ddd;
33
+ }
34
+
35
+ /* lisp open bracket */
36
+ .opn {
37
+ color: #000000;
38
+ }
39
+
40
+ /* lisp close bracket */
41
+ .clo {
42
+ color: #000000;
43
+ }
44
+
45
+ /* a markup tag name */
46
+ .tag {
47
+ color: #8da6ce;
48
+ }
49
+
50
+ /* a markup attribute name */
51
+ .atn {
52
+ color: #fbde2d;
53
+ }
54
+
55
+ /* a markup attribute value */
56
+ .atv {
57
+ color: #ddd;
58
+ }
59
+
60
+ /* a declaration */
61
+ .dec {
62
+ color: #EF5050;
63
+ }
64
+
65
+ /* a variable name */
66
+ .var {
67
+ color: #c82829;
68
+ }
69
+
70
+ /* a function name */
71
+ .fun {
72
+ color: #4271ae;
73
+ }
74
+
75
+ /* Specify class=linenums on a pre to get line numbering */
76
+ ol.linenums {
77
+ margin-top: 0;
78
+ margin-bottom: 0;
79
+ padding-bottom: 2px;
80
+ }
package/g.mjs ADDED
@@ -0,0 +1,42 @@
1
+ import fs from 'fs'
2
+ import WDwloadReel from './src/WDwloadReel.mjs'
3
+
4
+
5
+ async function test() {
6
+
7
+ //url
8
+ let url = `https://www.facebook.com/reel/3403300299820247`
9
+
10
+ //fp
11
+ let fp = './abc.mp4'
12
+
13
+ //funProg
14
+ let funProg = (prog, nn, na) => {
15
+ console.log('prog', `${prog.toFixed(2)}%`, nn, na)
16
+ }
17
+
18
+ //WDwloadReel
19
+ await WDwloadReel(url, fp, {
20
+ clean: true, //單一程序執行時, 事先清除之前暫存檔, 減少浪費硬碟空間
21
+ funProg,
22
+ })
23
+
24
+ //len
25
+ let len = fs.statSync(fp).size
26
+ console.log('len', len)
27
+
28
+ console.log('done:', fp)
29
+ }
30
+ test()
31
+ .catch((err) => {
32
+ console.log('catch', err)
33
+ })
34
+ // prog 0.49% 1 99
35
+ // prog 5.05% 4 99
36
+ // ...
37
+ // prog 99.00% 98 99
38
+ // prog 100.00% 99 99
39
+ // len 8468825
40
+ // done: ./abc.mp4
41
+
42
+ //node g.mjs
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "w-dwload-reel",
3
+ "version": "1.0.0",
4
+ "main": "dist/w-dwload-reel.umd.js",
5
+ "dependencies": {
6
+ "lodash-es": "^4.18.1",
7
+ "wsemi": "^1.8.54"
8
+ },
9
+ "devDependencies": {
10
+ "w-package-tools": "^1.1.7"
11
+ },
12
+ "scripts": {
13
+ "test": "mocha --parallel --timeout 60000",
14
+ "deploy": "gh-pages -d docs"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/yuda-lyu/w-dwload-reel.git"
19
+ },
20
+ "keywords": [
21
+ "package",
22
+ "tool",
23
+ "facebook",
24
+ "reel",
25
+ "nodejs"
26
+ ],
27
+ "author": "yuda-lyu(semisphere)",
28
+ "license": "MIT"
29
+ }
package/script.txt ADDED
@@ -0,0 +1,17 @@
1
+ #node toolg/addVersion.mjs
2
+ node toolg/modifyReadme.mjs
3
+
4
+ node toolg/cleanFolder.mjs
5
+ ./node_modules/.bin/jsdoc -c .jsdoc
6
+
7
+ node toolg/gDistRollup.mjs
8
+
9
+ git add . -A
10
+ git commit -m 'modify: '
11
+ git push origin master:master
12
+
13
+ npm run deploy
14
+
15
+ #npm test
16
+
17
+ #npm publish
@@ -0,0 +1,245 @@
1
+ import fs from 'fs'
2
+ import get from 'lodash-es/get.js'
3
+ import genPm from 'wsemi/src/genPm.mjs'
4
+ import isestr from 'wsemi/src/isestr.mjs'
5
+ import isfun from 'wsemi/src/isfun.mjs'
6
+ import cint from 'wsemi/src/cint.mjs'
7
+ import getPathParent from 'wsemi/src/getPathParent.mjs'
8
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
9
+ import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
10
+
11
+
12
+ /**
13
+ * 下載Facebook Reel為mp4檔案至本機指定位置
14
+ *
15
+ * @param {String} url 輸入Facebook Reel網址字串(facebook.com / fb.watch)
16
+ * @param {String} fp 輸入儲存video(*.mp4)檔案路徑字串
17
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
18
+ * @param {Function} [opt.funProg=null] 輸入回傳進度函數,傳入參數為prog代表進度百分比(0~100)、nn代表當前已下載bytes、na代表全部須下載bytes,預設null
19
+ * @returns {Promise} 回傳Promise,resolve回傳'ok',reject回傳錯誤訊息
20
+ * @example
21
+ * import fs from 'fs'
22
+ * import WDwloadReel from './src/WDwloadReel.mjs'
23
+ *
24
+ * async function test() {
25
+ *
26
+ * //url
27
+ * let url = 'https://www.facebook.com/reel/XXXXXXXXXX'
28
+ *
29
+ * //fp
30
+ * let fp = './abc.mp4'
31
+ *
32
+ * //funProg
33
+ * let funProg = (prog, nn, na) => {
34
+ * console.log('prog', `${prog.toFixed(2)}%`, nn, na)
35
+ * }
36
+ *
37
+ * //WDwloadReel
38
+ * await WDwloadReel(url, fp, {
39
+ * funProg,
40
+ * })
41
+ *
42
+ * //len
43
+ * let len = fs.statSync(fp).size
44
+ * console.log('len', len)
45
+ *
46
+ * console.log('done:', fp)
47
+ * }
48
+ * test()
49
+ * .catch((err) => {
50
+ * console.log('catch', err)
51
+ * })
52
+ * // prog 1.00% 23456 2345678
53
+ * // prog 2.00% 46912 2345678
54
+ * // ...
55
+ * // prog 99.00% 2322221 2345678
56
+ * // prog 100.00% 2345678 2345678
57
+ * // len 2345678
58
+ * // done: ./abc.mp4
59
+ *
60
+ */
61
+ async function WDwloadReel(url, fp, opt = {}) {
62
+
63
+ //funProg
64
+ let funProg = get(opt, 'funProg')
65
+ let bFunProg = isfun(funProg)
66
+
67
+ //check
68
+ if (!isestr(url)) {
69
+ return Promise.reject('invalid url')
70
+ }
71
+ if (!isestr(fp)) {
72
+ return Promise.reject('fp is not a string')
73
+ }
74
+
75
+ let isMp4 = (buf) => {
76
+ //mp4 magic: bytes 4..7 = 'ftyp' (ISO base media file type box), 再驗 bytes 8..11 為常見 mp4 brand, 排除 JPEG2000 等其他 ftyp 容器格式
77
+ if (buf.length < 12) {
78
+ return false
79
+ }
80
+ if (buf.slice(4, 8).toString('ascii') !== 'ftyp') {
81
+ return false
82
+ }
83
+ let brand = buf.slice(8, 12).toString('ascii')
84
+ return ['isom', 'mp41', 'mp42', 'avc1', 'iso2', 'iso5', 'iso6', 'mp4v'].includes(brand)
85
+ }
86
+
87
+ let getRealData = async (url) => {
88
+
89
+ //pm
90
+ let pm = genPm()
91
+
92
+ //check
93
+ if (!isestr(url)) {
94
+ pm.reject('invalid url')
95
+ return pm
96
+ }
97
+
98
+ //parseString
99
+ let parseString = (string) => {
100
+ return JSON.parse(`{"text": "${string}"}`).text
101
+ }
102
+
103
+ //fetch, 參數與解析機制來源: https://github.com/victorsouzaleal/fb-downloader-scrapper/blob/main/src/facebook.ts
104
+ let headers = {
105
+ 'sec-fetch-user': '?1',
106
+ 'sec-ch-ua-mobile': '?0',
107
+ 'sec-fetch-site': 'none',
108
+ 'sec-fetch-dest': 'document',
109
+ 'sec-fetch-mode': 'navigate',
110
+ 'cache-control': 'max-age=0',
111
+ 'authority': 'www.facebook.com',
112
+ 'upgrade-insecure-requests': '1',
113
+ 'accept-language': 'en-GB,en;q=0.9,tr-TR;q=0.8,tr;q=0.7,en-US;q=0.6',
114
+ 'sec-ch-ua': '"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"',
115
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36',
116
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
117
+ 'cookie': 'sb=Rn8BYQvCEb2fpMQZjsd6L382; datr=Rn8BYbyhXgw9RlOvmsosmVNT; c_user=100003164630629; _fbp=fb.1.1629876126997.444699739; wd=1920x939; spin=r.1004812505_b.trunk_t.1638730393_s.1_v.2_; xs=28%3A8ROnP0aeVF8XcQ%3A2%3A1627488145%3A-1%3A4916%3A%3AAcWIuSjPy2mlTPuZAeA2wWzHzEDuumXI89jH8a_QIV8; fr=0jQw7hcrFdas2ZeyT.AWVpRNl_4noCEs_hb8kaZahs-jA.BhrQqa.3E.AAA.0.0.BhrQqa.AWUu879ZtCw',
118
+ }
119
+ fetch(url, { headers })
120
+ .then((res) => {
121
+
122
+ //check
123
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
124
+
125
+ return res.text()
126
+ })
127
+ .then((data) => {
128
+
129
+ data = data.replace(/&quot;/g, '"').replace(/&amp;/g, '&')
130
+ let sdMatch = data.match(/"browser_native_sd_url":"(.*?)"/) || data.match(/"playable_url":"(.*?)"/) || data.match(/sd_src\s*:\s*"([^"]*)"/) || data.match(/(?<="src":")[^"]*(https:\/\/[^"]*)/)
131
+ let hdMatch = data.match(/"browser_native_hd_url":"(.*?)"/) || data.match(/"playable_url_quality_hd":"(.*?)"/) || data.match(/hd_src\s*:\s*"([^"]*)"/)
132
+ let titleMatch = data.match(/<meta\sname="description"\scontent="(.*?)"/)
133
+ let thumbMatch = data.match(/"preferred_thumbnail":{"image":{"uri":"(.*?)"/)
134
+ let duration = data.match(/"playable_duration_in_ms":[0-9]+/gm)
135
+
136
+ //check
137
+ if (sdMatch && sdMatch[1]) {
138
+ let result = {
139
+ url,
140
+ duration_ms: cint(duration[0].split(':')[1]),
141
+ sd: parseString(sdMatch[1]),
142
+ hd: hdMatch && hdMatch[1] ? parseString(hdMatch[1]) : '',
143
+ title: titleMatch && titleMatch[1] ? parseString(titleMatch[1]) : data.match(/<title>(.*?)<\/title>/)?.[1] ?? '',
144
+ thumbnail: thumbMatch && thumbMatch[1] ? parseString(thumbMatch[1]) : '',
145
+ }
146
+ pm.resolve(result)
147
+ }
148
+ else {
149
+ pm.reject('no video url found in page')
150
+ }
151
+
152
+ })
153
+ .catch((err) => {
154
+ pm.reject(err)
155
+ })
156
+
157
+ return pm
158
+ }
159
+
160
+ //getRealData, 解析FB頁面取得mp4直連
161
+ let info
162
+ try {
163
+ info = await getRealData(url)
164
+ }
165
+ catch (err) {
166
+ return Promise.reject(err)
167
+ }
168
+
169
+ //check, 須有hd或sd其中之一
170
+ if (!info.hd && !info.sd) {
171
+ return Promise.reject('can not get hd/sd mp4 url (reel may be private/deleted/FB changed)')
172
+ }
173
+
174
+ //target, 優先hd
175
+ let target = info.hd || info.sd
176
+
177
+ //fetch
178
+ let res
179
+ try {
180
+ res = await fetch(target)
181
+ }
182
+ catch (err) {
183
+ return Promise.reject(`fetch mp4 fail: ${err.message}`)
184
+ }
185
+
186
+ //check
187
+ if (!res.ok) {
188
+ return Promise.reject(`HTTP ${res.status} ${res.statusText}`)
189
+ }
190
+
191
+ //total, Content-Length未必提供, 取不到視為0(中段不觸發funProg, 最後仍補100%)
192
+ let total = cint(res.headers.get('content-length'))
193
+
194
+ //讀取串流並計算進度
195
+ let reader = res.body.getReader()
196
+ let chunks = []
197
+ let received = 0
198
+ let progPre = -1
199
+ while (true) {
200
+ let { done, value } = await reader.read()
201
+ if (done) {
202
+ break
203
+ }
204
+ chunks.push(value)
205
+ received += value.length
206
+
207
+ //funProg, 整數百分比變動才觸發避免高頻
208
+ if (bFunProg && total > 0) {
209
+ let prog = (received / total) * 100
210
+ let progInt = Math.floor(prog)
211
+ if (progInt > progPre) {
212
+ progPre = progInt
213
+ funProg(prog, received, total)
214
+ }
215
+ }
216
+ }
217
+
218
+ //buf
219
+ let buf = Buffer.concat(chunks)
220
+
221
+ //check mp4 magic, 避免HTML錯誤頁面或非mp4回應被當mp4寫入
222
+ if (!isMp4(buf)) {
223
+ return Promise.reject(`downloaded but not valid mp4 (first 16 bytes: ${buf.slice(0, 16).toString('hex')})`)
224
+ }
225
+
226
+ //mkdir, 上層目錄不存在時自動建立
227
+ let dir = getPathParent(fp)
228
+ if (isestr(dir) && !fsIsFolder(dir)) {
229
+ fsCreateFolder(dir)
230
+ }
231
+
232
+ //write
233
+ fs.writeFileSync(fp, buf)
234
+
235
+ //funProg, 最終補100%(涵蓋無Content-Length時中段未觸發的情況)
236
+ if (bFunProg) {
237
+ let na = total || buf.length
238
+ funProg(100, na, na)
239
+ }
240
+
241
+ return 'ok'
242
+ }
243
+
244
+
245
+ export default WDwloadReel
@@ -0,0 +1,47 @@
1
+ import fs from 'fs'
2
+ import assert from 'assert'
3
+ import WDwloadReel from '../src/WDwloadReel.mjs'
4
+
5
+
6
+ describe('WDwloadReel', function() {
7
+
8
+ async function test() {
9
+
10
+ //url
11
+ let url = `https://www.facebook.com/reel/3403300299820247`
12
+
13
+ //fp
14
+ let fp = './abc.mp4'
15
+
16
+ //funProg
17
+ let funProg = (prog, nn, na) => {
18
+ console.log('prog', `${prog.toFixed(2)}%`, nn, na)
19
+ }
20
+
21
+ //WDwloadReel
22
+ await WDwloadReel(url, fp, {
23
+ clean: true, //單一程序執行時, 事先清除之前暫存檔, 減少浪費硬碟空間
24
+ funProg,
25
+ })
26
+
27
+ //len
28
+ let len = fs.statSync(fp).size
29
+ // console.log('len', len)
30
+
31
+ //unlinkSync
32
+ fs.unlinkSync(fp)
33
+
34
+ return len
35
+ }
36
+ // test()
37
+ // .catch((err) => {
38
+ // console.log('catch', err)
39
+ // })
40
+
41
+ it('download', async function() {
42
+ let r = 8468825
43
+ let rr = await test()
44
+ assert.strict.deepEqual(r, rr)
45
+ })
46
+
47
+ })
@@ -0,0 +1,4 @@
1
+ import addVersion from 'w-package-tools/src/addVersion.mjs'
2
+
3
+
4
+ addVersion()
@@ -0,0 +1,4 @@
1
+ import cleanFolder from 'w-package-tools/src/cleanFolder.mjs'
2
+
3
+
4
+ cleanFolder('docs')
@@ -0,0 +1,26 @@
1
+ import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
2
+ import getFiles from 'w-package-tools/src/getFiles.mjs'
3
+
4
+
5
+ let fdSrc = './src'
6
+ let fdTar = './dist'
7
+
8
+
9
+ rollupFiles({
10
+ fns: 'WDwloadReel.mjs',
11
+ fdSrc,
12
+ fdTar,
13
+ // nameDistType: 'kebabCase',
14
+ hookNameDist: () => {
15
+ return 'w-dwload-reel'
16
+ },
17
+ globals: {
18
+ 'path': 'path',
19
+ 'fs': 'fs',
20
+ },
21
+ external: [
22
+ 'path',
23
+ 'fs',
24
+ ],
25
+ })
26
+
@@ -0,0 +1,4 @@
1
+ import modifyReadme from 'w-package-tools/src/modifyReadme.mjs'
2
+
3
+
4
+ modifyReadme()