├── .gitignore
├── README.md
├── index.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | # Vercel deploy
91 | .vercel/
92 | vercel.json
93 |
94 | yarn.lock
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Twitch-API
2 |
3 | ## 기능
4 | - [HLS](#hls)
5 |
6 | ### HLS
7 | ### Requests
8 | ```http
9 | GET /hls/
10 | ```
11 | | Parameter | Type | Description |
12 | | :--- | :--- | :--- |
13 | | `Channel_ID` | `string` | 트위치 채널 ID |
14 |
15 | ### Responses
16 | JSON list 타입으로 화질에 따라 인덱스 처음에서 끝으로 정렬됩니다.
17 | ex) [0] = 1080p60, [1] = 720p60 ....
18 | ```javascript
19 | [
20 | "http://video-weaver.....m3u8",
21 | "http://video-weaver.....m3u8",
22 | "http://video-weaver.....m3u8"
23 | ]
24 | ```
25 |
26 | ### Status Codes
27 | | Status Code | Description |
28 | | :--- | :--- |
29 | | 200 | m3u8 데이터 존재 |
30 | | 404 | m3u8 데이터 없음 |
31 | | 500 | 트위치 API와 통신 오류 |
32 |
33 | ### 문의
34 | [이메일](mailto:kwabang2827@gmail.com) 또는 [디스코드](https://kwabang.net/join)로 문의를 넣을 수 있습니다.
35 |
36 | ### 책임
37 | 프로그램을 이용하여 생기는 문제의 책임은 **사용자**에게 있습니다.
38 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const got = require('got')
2 | const express = require('express')
3 |
4 | const app = express()
5 |
6 | app.disable('etag')
7 |
8 | app.get('/', (request, response) => {
9 | response.status(200).json({
10 | 'message': "Welcome to Twitch API"
11 | })
12 | })
13 |
14 | app.get('/hls', (request, response) => {
15 | response.status(404).json({
16 | 'message': 'unknown channel name'
17 | })
18 | })
19 |
20 | app.get('/hls/:id', async (request, response) => {
21 | try {
22 | let id = request.params.id
23 | let token = await got(`https://gql.twitch.tv/gql`, {
24 | method: 'POST',
25 | responseType: 'json',
26 | retry: {
27 | limit: 4
28 | },
29 | throwHttpErrors: false,
30 | headers: {
31 | 'Client-ID': 'kimne78kx3ncx6brgo4mv6wki5h1ko',
32 | 'Content-Type': 'application/json',
33 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
34 | 'X-Device-Id': 'twitch-web-wall-mason',
35 | 'Device-ID': 'twitch-web-wall-mason'
36 | },
37 | body: JSON.stringify({
38 | "operationName": "PlaybackAccessToken",
39 | "extensions": {
40 | "persistedQuery": {
41 | "version": 1,
42 | "sha256Hash": "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712"
43 | }
44 | },
45 | "variables": {
46 | "isLive": true,
47 | "login": id,
48 | "isVod": false,
49 | "vodID": "",
50 | "playerType": "embed"
51 | }
52 | })
53 | })
54 |
55 | switch (token.statusCode) {
56 | default: //Error with connect with Twitch API
57 | response.status(500).json({
58 | 'message': 'Error with Twitch API'
59 | })
60 | break
61 | case 200: //Channel founded
62 | if (token.body.data.streamPlaybackAccessToken === null) { //Channel not found
63 | response.status(404).json({
64 | 'message': 'Channel not found'
65 | })
66 | } else {
67 | function base64Encode(data) {
68 | return Buffer.from(data).toString('base64')
69 | }
70 |
71 | function cleanupAllAdStuff(data) {
72 | return data
73 | .replace(/X-TV-TWITCH-AD-URL="[^"]+"/g, 'X-TV-TWITCH-AD-URL="javascript:alert(\'pogo\')"')
74 | .replace(
75 | /X-TV-TWITCH-AD-CLICK-TRACKING-URL="[^"]+"/g,
76 | 'X-TV-TWITCH-AD-CLICK-TRACKING-URL="javascript:alert(\'pogo\')"'
77 | )
78 | .replace(/X-TV-TWITCH-AD-ADVERIFICATIONS="[^"]+"/g, `X-TV-TWITCH-AD-ADVERIFICATIONS="${base64Encode('{}')}"`)
79 | .replace(/#EXT-X-DATERANGE.+CLASS=".*ad.*".+\n/g, '')
80 | .replace(/\n#EXTINF.+(? {
119 | try {
120 | const url = request.params['0'] + '?' + Object.entries(request.query).map(element => element.join('=')).join('&')
121 | const domain = (new URL(url)).hostname
122 | if (domain === 'usher.ttvnw.net') {
123 | const headers = {
124 | 'host': domain,
125 | 'user-agent': request.headers['user-agent']
126 | }
127 | let hls = await got(url, {
128 | method: 'GET',
129 | responseType: 'text',
130 | retry: {
131 | limit: 4
132 | },
133 | throwHttpErrors: false,
134 | headers: headers
135 | })
136 | response.status(hls.statusCode).send(hls.body)
137 | } else {
138 | response.status(403).json({
139 | message: 'URL not allowed'
140 | })
141 | }
142 | } catch (error) {
143 | response.status(500).json({
144 | message: 'processing error',
145 | error: error
146 | })
147 | }
148 | })
149 |
150 | app.listen(8080, () => {
151 | console.log("API started")
152 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitch-api",
3 | "version": "1.0.0",
4 | "description": "Twitch-API",
5 | "main": "index.js",
6 | "dependencies": {
7 | "express": "^4.17.1",
8 | "got": "11.8.6"
9 | },
10 | "devDependencies": {},
11 | "scripts": {
12 | "test": "node index.js"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/Kwabang/Twitch-API.git"
17 | },
18 | "author": "Kwabang",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/Kwabang/Twitch-API/issues"
22 | },
23 | "homepage": "https://github.com/Kwabang/Twitch-API#readme"
24 | }
25 |
--------------------------------------------------------------------------------