first version
This commit is contained in:
commit
1fda2ff1f6
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
public/
|
||||||
|
downloaded/
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM node:lts-alpine
|
||||||
|
WORKDIR /data
|
||||||
|
COPY . .
|
||||||
|
RUN apk add --no-cache ffmpeg python python3 curl wget \
|
||||||
|
&& curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl \
|
||||||
|
&& chmod a+rx /usr/local/bin/youtube-dl \
|
||||||
|
&& npm install --prod
|
||||||
|
EXPOSE 10230
|
||||||
|
CMD ["npm", "run", "start"]
|
4
config/index.js
Normal file
4
config/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const path = require('path')
|
||||||
|
module.exports = {
|
||||||
|
download_location: process.env.DL_LOC || path.resolve(__dirname, '..', 'downloaded')
|
||||||
|
}
|
9
index.js
Normal file
9
index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const config = require('./config/index.js')
|
||||||
|
require('module-alias/register')
|
||||||
|
|
||||||
|
if (!fs.existsSync(config.download_location)) {
|
||||||
|
fs.mkdirSync(config.download_location)
|
||||||
|
}
|
||||||
|
|
||||||
|
require('./server')
|
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@libs/*": ["libs/*"],
|
||||||
|
"@config/*": ["config/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
libs/memstore.js
Normal file
36
libs/memstore.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const store = {}
|
||||||
|
|
||||||
|
const storeFunc = {
|
||||||
|
/**
|
||||||
|
* set store key vlaue
|
||||||
|
* @param {string} key
|
||||||
|
* @param {object} val
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
set: (key, val) => {
|
||||||
|
if (typeof key !== 'string' || key.length === 0) return false
|
||||||
|
store[key] = val
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* get store key value
|
||||||
|
* @param {string} key
|
||||||
|
* @return {object?}
|
||||||
|
*/
|
||||||
|
get: (key) => {
|
||||||
|
if (typeof key !== 'string' || key.length === 0) return null
|
||||||
|
return store[key] || null
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* del store key vlaue
|
||||||
|
* @param {string} key
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
del: (key) => {
|
||||||
|
if (typeof key !== 'string' || key.length === 0) return false
|
||||||
|
delete store[key]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = storeFunc
|
40
libs/youtube.js
Normal file
40
libs/youtube.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const exec = require('child_process').exec
|
||||||
|
const command = 'youtube-dl'
|
||||||
|
const cmdOpts = ['--id', '-x', '--audio-format', 'mp3']
|
||||||
|
const config = require('@config/index.js')
|
||||||
|
const cwd = config.download_location
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download youtube video
|
||||||
|
* @param {string} url
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
module.exports.download = async (url = '') => {
|
||||||
|
if (typeof url !== 'string' || url.trim().length === 0) return false
|
||||||
|
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
exec(`${command} ${cmdOpts.join(' ')} "${url}"`, { cwd }, (err, sout, serr) => {
|
||||||
|
if (err) return resolve(false)
|
||||||
|
return resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get youtube video title
|
||||||
|
* @param {string} url
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
module.exports.getTitle = async (url = '') => {
|
||||||
|
if (typeof url !== 'string' || url.trim().length === 0) return ''
|
||||||
|
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
exec(`${command} -e "${url}"`, { cwd }, (err, sout, serr) => {
|
||||||
|
if (err) return resolve('')
|
||||||
|
resolve(sout)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
2344
package-lock.json
generated
Normal file
2344
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "ytdl",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"check": "standard --fix --verbose",
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@koa/cors": "^3.0.0",
|
||||||
|
"@koa/router": "^8.0.0",
|
||||||
|
"koa": "^2.7.0",
|
||||||
|
"koa-body": "^4.1.0",
|
||||||
|
"koa-logger": "^3.2.1",
|
||||||
|
"koa-mount": "^4.0.0",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
|
"module-alias": "^2.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"standard": "^13.1.0"
|
||||||
|
},
|
||||||
|
"_moduleAliases": {
|
||||||
|
"@root": ".",
|
||||||
|
"@libs": "libs",
|
||||||
|
"@config": "config"
|
||||||
|
}
|
||||||
|
}
|
10
route/api/index.js
Normal file
10
route/api/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const Router = require('@koa/router')
|
||||||
|
const r = new Router()
|
||||||
|
module.exports = r
|
||||||
|
|
||||||
|
r.get('/health', async c => {
|
||||||
|
c.status = 200
|
||||||
|
c.body = 'ok'
|
||||||
|
})
|
||||||
|
|
||||||
|
r.use('/youtube', require('./youtube/index.js').routes())
|
61
route/api/youtube/index.js
Normal file
61
route/api/youtube/index.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
const Router = require('@koa/router')
|
||||||
|
const r = new Router()
|
||||||
|
module.exports = r
|
||||||
|
|
||||||
|
const koaBody = require('koa-body')
|
||||||
|
const {
|
||||||
|
download: ytdl,
|
||||||
|
getTitle
|
||||||
|
} = require('@libs/youtube.js')
|
||||||
|
const qs = require('querystring')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const config = require('@config/index.js')
|
||||||
|
const store = require('@libs/memstore.js')
|
||||||
|
|
||||||
|
r.post('/download', koaBody(), async c => {
|
||||||
|
console.log(c.request)
|
||||||
|
const { url } = c.request.body
|
||||||
|
if (typeof url !== 'string' || url.trim().length === 0) c.throw(400, 'url is empty')
|
||||||
|
let id = ''
|
||||||
|
if (/:\/\/youtu\.be/i.test(url)) {
|
||||||
|
const arr = url.split('/')
|
||||||
|
id = arr[arr.length - 1]
|
||||||
|
} else {
|
||||||
|
const arr = url.split('?')
|
||||||
|
if (arr.length !== 2) c.throw(400, 'url format error')
|
||||||
|
const q = qs.parse(arr[1])
|
||||||
|
if (!('v' in q) || typeof q.v !== 'string' || q.v.length === 0) c.throw(400, 'url format error')
|
||||||
|
id = q.v
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = await getTitle(url)
|
||||||
|
|
||||||
|
const result = await ytdl(url)
|
||||||
|
if (!result) c.throw(500, 'download fail')
|
||||||
|
|
||||||
|
store.set(`${id}.mp3`, { title })
|
||||||
|
|
||||||
|
c.body = {
|
||||||
|
url: `${c.protocol}://${c.host.replace(/\/$/, '')}/api/youtube/download?file=${id}.mp3`
|
||||||
|
}
|
||||||
|
c.status = 200
|
||||||
|
})
|
||||||
|
|
||||||
|
r.get('/download', async c => {
|
||||||
|
const { file } = c.query
|
||||||
|
if (typeof file !== 'string' || file.trim().length === 0) c.throw(400, 'file is empty')
|
||||||
|
|
||||||
|
const fp = path.resolve(config.download_location, file)
|
||||||
|
if (!fs.existsSync(fp)) c.throw(404, 'file not found')
|
||||||
|
|
||||||
|
const fileInfo = store.get(file)
|
||||||
|
let fn = file
|
||||||
|
if (fileInfo !== null && 'title' in fileInfo) fn = fileInfo.title
|
||||||
|
|
||||||
|
const rs = fs.createReadStream(fp)
|
||||||
|
c.set('Content-disposition', `attachment; filename="${encodeURIComponent(fn)}"; filename*="utf8''${encodeURIComponent(fn)}";`)
|
||||||
|
c.set('Content-Type', 'audio/mpeg')
|
||||||
|
c.body = rs
|
||||||
|
c.status = 200
|
||||||
|
})
|
10
route/index.js
Normal file
10
route/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const Router = require('@koa/router')
|
||||||
|
const r = new Router()
|
||||||
|
module.exports = r
|
||||||
|
|
||||||
|
r.get('/', async c => {
|
||||||
|
c.body = 'ok'
|
||||||
|
c.status = 200
|
||||||
|
})
|
||||||
|
|
||||||
|
r.use('/api', require('./api/index.js').routes())
|
22
server.js
Normal file
22
server.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const Koa = require('koa')
|
||||||
|
const app = new Koa()
|
||||||
|
const koaLogger = require('koa-logger')
|
||||||
|
const cors = require('@koa/cors')
|
||||||
|
const koaStatic = require('koa-static')
|
||||||
|
const rootRoouter = require('./route/index.js')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const server = app.listen(10230, () => {
|
||||||
|
console.log(`server start on port ${server.address().port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = server
|
||||||
|
|
||||||
|
app.use(koaLogger())
|
||||||
|
app.use(cors({
|
||||||
|
origin: ctx => ctx.get('origin'),
|
||||||
|
credentials: true
|
||||||
|
}))
|
||||||
|
app.use(koaStatic(path.resolve(__dirname, 'public')))
|
||||||
|
app.use(rootRoouter.allowedMethods())
|
||||||
|
app.use(rootRoouter.routes())
|
Loading…
Reference in New Issue
Block a user