├── .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 | --------------------------------------------------------------------------------