Use a single file instead of segments for HLS

pull/1625/head
Chocobozzz 2019-02-07 15:08:19 +01:00 committed by Chocobozzz
parent 6ec0b75beb
commit 4c280004ce
16 changed files with 189 additions and 146 deletions

View File

@ -3,18 +3,25 @@ import { basename } from 'path'
function segmentValidatorFactory (segmentsSha256Url: string) {
const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
const regex = /bytes=(\d+)-(\d+)/
return async function segmentValidator (segment: Segment) {
const segmentName = basename(segment.url)
const filename = basename(segment.url)
const captured = regex.exec(segment.range)
const hashShouldBe = (await segmentsJSON)[segmentName]
const range = captured[1] + '-' + captured[2]
const hashShouldBe = (await segmentsJSON)[filename][range]
if (hashShouldBe === undefined) {
throw new Error(`Unknown segment name ${segmentName} in segment validator`)
throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
}
const calculatedSha = bufferToEx(await sha256(segment.data))
if (calculatedSha !== hashShouldBe) {
throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`)
throw new Error(
`Hashes does not correspond for segment ${filename}/${range}` +
`(expected: ${hashShouldBe} instead of ${calculatedSha})`
)
}
}
}

View File

@ -117,7 +117,6 @@
"fluent-ffmpeg": "^2.1.0",
"fs-extra": "^7.0.0",
"helmet": "^3.12.1",
"hlsdownloader": "https://github.com/Chocobozzz/hlsdownloader#build",
"http-signature": "^1.2.0",
"ip-anonymize": "^0.0.6",
"ipaddr.js": "1.8.1",

View File

@ -41,7 +41,7 @@ async function run () {
}
function get (url: string, headers: any = {}) {
return doRequest({
return doRequest<any>({
uri: url,
json: true,
headers: Object.assign(headers, {

View File

@ -122,7 +122,9 @@ type TranscodeOptions = {
resolution: VideoResolution
isPortraitMode?: boolean
generateHlsPlaylist?: boolean
hlsPlaylist?: {
videoFilename: string
}
}
function transcode (options: TranscodeOptions) {
@ -161,14 +163,16 @@ function transcode (options: TranscodeOptions) {
command = command.withFPS(fps)
}
if (options.generateHlsPlaylist) {
const segmentFilename = `${dirname(options.outputPath)}/${options.resolution}_%03d.ts`
if (options.hlsPlaylist) {
const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
command = command.outputOption('-hls_time 4')
.outputOption('-hls_list_size 0')
.outputOption('-hls_playlist_type vod')
.outputOption('-hls_segment_filename ' + segmentFilename)
.outputOption('-hls_segment_filename ' + videoPath)
.outputOption('-hls_segment_type fmp4')
.outputOption('-f hls')
.outputOption('-hls_flags single_file')
}
command

View File

@ -7,7 +7,7 @@ import { join } from 'path'
function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
): Bluebird<{ response: request.RequestResponse, body: any }> {
): Bluebird<{ response: request.RequestResponse, body: T }> {
if (requestOptions.activityPub === true) {
if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER

View File

@ -7,7 +7,6 @@ import { join } from 'path'
import { Instance as ParseTorrent } from 'parse-torrent'
import { remove } from 'fs-extra'
import * as memoizee from 'memoizee'
import { isArray } from './custom-validators/misc'
function deleteFileAsync (path: string) {
remove(path)

View File

@ -355,10 +355,10 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
logger.info('Fetching remote actor %s.', actorUrl)
const requestResult = await doRequest(options)
const requestResult = await doRequest<ActivityPubActor>(options)
normalizeActor(requestResult.body)
const actorJSON: ActivityPubActor = requestResult.body
const actorJSON = requestResult.body
if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON })
return { result: undefined, statusCode: requestResult.response.statusCode }

View File

@ -1,13 +1,14 @@
import { VideoModel } from '../models/video/video'
import { basename, dirname, join } from 'path'
import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers'
import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra'
import { basename, join, dirname } from 'path'
import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
import { sha256 } from '../helpers/core-utils'
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
import HLSDownloader from 'hlsdownloader'
import { logger } from '../helpers/logger'
import { parse } from 'url'
import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
import { generateRandomString } from '../helpers/utils'
import { flatten, uniq } from 'lodash'
async function updateMasterHLSPlaylist (video: VideoModel) {
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
@ -37,66 +38,119 @@ async function updateMasterHLSPlaylist (video: VideoModel) {
}
async function updateSha256Segments (video: VideoModel) {
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
const files = await readdir(directory)
const json: { [filename: string]: string} = {}
const json: { [filename: string]: { [range: string]: string } } = {}
for (const file of files) {
if (file.endsWith('.ts') === false) continue
const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
const buffer = await readFile(join(directory, file))
const filename = basename(file)
// For all the resolutions available for this video
for (const file of video.VideoFiles) {
const rangeHashes: { [range: string]: string } = {}
json[filename] = sha256(buffer)
const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
// Maybe the playlist is not generated for this resolution yet
if (!await pathExists(playlistPath)) continue
const playlistContent = await readFile(playlistPath)
const ranges = getRangesFromPlaylist(playlistContent.toString())
const fd = await open(videoPath, 'r')
for (const range of ranges) {
const buf = Buffer.alloc(range.length)
await read(fd, buf, 0, range.length, range.offset)
rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
}
await close(fd)
const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
json[videoFilename] = rangeHashes
}
const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
await outputJSON(outputPath, json)
}
function getRangesFromPlaylist (playlistContent: string) {
const ranges: { offset: number, length: number }[] = []
const lines = playlistContent.split('\n')
const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
for (const line of lines) {
const captured = regex.exec(line)
if (captured) {
ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
}
}
return ranges
}
function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
let timer
logger.info('Importing HLS playlist %s', playlistUrl)
const params = {
playlistURL: playlistUrl,
destination: CONFIG.STORAGE.TMP_DIR
}
const downloader = new HLSDownloader(params)
const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname))
return new Promise<string>(async (res, rej) => {
downloader.startDownload(err => {
clearTimeout(timer)
const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
if (err) {
deleteTmpDirectory(hlsDestinationDir)
return rej(err)
}
move(hlsDestinationDir, destinationDir, { overwrite: true })
.then(() => res())
.catch(err => {
deleteTmpDirectory(hlsDestinationDir)
return rej(err)
})
})
await ensureDir(tmpDirectory)
timer = setTimeout(() => {
deleteTmpDirectory(hlsDestinationDir)
deleteTmpDirectory(tmpDirectory)
return rej(new Error('HLS download timeout.'))
}, timeout)
function deleteTmpDirectory (directory: string) {
remove(directory)
.catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
try {
// Fetch master playlist
const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
const fileUrls = uniq(flatten(await Promise.all(subRequests)))
logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
for (const fileUrl of fileUrls) {
const destPath = join(tmpDirectory, basename(fileUrl))
await doRequestAndSaveToFile({ uri: fileUrl }, destPath)
}
clearTimeout(timer)
await move(tmpDirectory, destinationDir, { overwrite: true })
return res()
} catch (err) {
deleteTmpDirectory(tmpDirectory)
return rej(err)
}
})
function deleteTmpDirectory (directory: string) {
remove(directory)
.catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
}
async function fetchUniqUrls (playlistUrl: string) {
const { body } = await doRequest<string>({ uri: playlistUrl })
if (!body) return []
const urls = body.split('\n')
.filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
.map(url => {
if (url.startsWith('http://') || url.startsWith('https://')) return url
return `${dirname(playlistUrl)}/${url}`
})
return uniq(urls)
}
}
// ---------------------------------------------------------------------------

View File

@ -100,7 +100,10 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
outputPath,
resolution,
isPortraitMode,
generateHlsPlaylist: true
hlsPlaylist: {
videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
}
}
await transcode(transcodeOptions)

View File

@ -125,6 +125,10 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
return 'segments-sha256.json'
}
static getHlsVideoName (uuid: string, resolution: number) {
return `${uuid}-${resolution}-fragmented.mp4`
}
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
}

View File

@ -17,7 +17,7 @@ import {
viewVideo,
wait,
waitUntilLog,
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash
} from '../../../../shared/utils'
import { waitJobs } from '../../../../shared/utils/server/jobs'
@ -178,20 +178,24 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
}
await makeGetRequest({
url: servers[0].url,
statusCodeExpected: 200,
path: `/static/redundancy/hls/${videoUUID}/360_000.ts`,
contentType: null
})
const baseUrlPlaylist = servers[1].url + '/static/playlists/hls'
const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
const res = await getVideo(servers[0].url, videoUUID)
const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
for (const resolution of [ 240, 360, 480, 720 ]) {
await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
}
for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) {
const files = await readdir(join(root(), directory, videoUUID))
expect(files).to.have.length.at.least(4)
for (const resolution of [ 240, 360, 480, 720 ]) {
expect(files.find(f => f === `${resolution}_000.ts`)).to.not.be.undefined
expect(files.find(f => f === `${resolution}_001.ts`)).to.not.be.undefined
const filename = `${videoUUID}-${resolution}-fragmented.mp4`
expect(files.find(f => f === filename)).to.not.be.undefined
}
}
}

View File

@ -4,13 +4,12 @@ import * as chai from 'chai'
import 'mocha'
import {
checkDirectoryIsEmpty,
checkSegmentHash,
checkTmpIsEmpty,
doubleFollow,
flushAndRunMultipleServers,
flushTests,
getPlaylist,
getSegment,
getSegmentSha256,
getVideo,
killallServers,
removeVideo,
@ -22,7 +21,6 @@ import {
} from '../../../../shared/utils'
import { VideoDetails } from '../../../../shared/models/videos'
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
import { sha256 } from '../../../helpers/core-utils'
import { join } from 'path'
const expect = chai.expect
@ -56,19 +54,15 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`)
const subPlaylist = res2.text
expect(subPlaylist).to.contain(resolution + '_000.ts')
expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
}
}
{
const baseUrl = 'http://localhost:9001/static/playlists/hls'
for (const resolution of resolutions) {
const res2 = await getSegment(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}_000.ts`)
const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
const sha256Server = resSha.body[ resolution + '_000.ts' ]
expect(sha256(res2.body)).to.equal(sha256Server)
await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
}
}
}

View File

@ -2,6 +2,9 @@ export interface ActivityPubOrderedCollection<T> {
'@context': string[]
type: 'OrderedCollection' | 'OrderedCollectionPage'
totalItems: number
partOf?: string
orderedItems: T[]
partOf?: string
next?: string
first?: string
}

View File

@ -3,10 +3,10 @@ import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
import { isAbsolute, join } from 'path'
import { parse } from 'url'
function makeRawRequest (url: string, statusCodeExpected?: number) {
function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
const { host, protocol, pathname } = parse(url)
return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected })
return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
}
function makeGetRequest (options: {
@ -15,7 +15,8 @@ function makeGetRequest (options: {
query?: any,
token?: string,
statusCodeExpected?: number,
contentType?: string
contentType?: string,
range?: string
}) {
if (!options.statusCodeExpected) options.statusCodeExpected = 400
if (options.contentType === undefined) options.contentType = 'application/json'
@ -25,6 +26,7 @@ function makeGetRequest (options: {
if (options.contentType) req.set('Accept', options.contentType)
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
if (options.query) req.query(options.query)
if (options.range) req.set('Range', options.range)
return req.expect(options.statusCodeExpected)
}

View File

@ -1,21 +1,51 @@
import { makeRawRequest } from '../requests/requests'
import { sha256 } from '../../../server/helpers/core-utils'
import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
import { expect } from 'chai'
function getPlaylist (url: string, statusCodeExpected = 200) {
return makeRawRequest(url, statusCodeExpected)
}
function getSegment (url: string, statusCodeExpected = 200) {
return makeRawRequest(url, statusCodeExpected)
function getSegment (url: string, statusCodeExpected = 200, range?: string) {
return makeRawRequest(url, statusCodeExpected, range)
}
function getSegmentSha256 (url: string, statusCodeExpected = 200) {
return makeRawRequest(url, statusCodeExpected)
}
async function checkSegmentHash (
baseUrlPlaylist: string,
baseUrlSegment: string,
videoUUID: string,
resolution: number,
hlsPlaylist: VideoStreamingPlaylist
) {
const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
const playlist = res.text
const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
const length = parseInt(matches[1], 10)
const offset = parseInt(matches[2], 10)
const range = `${offset}-${offset + length - 1}`
const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`)
const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
const sha256Server = resSha.body[ videoName ][range]
expect(sha256(res2.body)).to.equal(sha256Server)
}
// ---------------------------------------------------------------------------
export {
getPlaylist,
getSegment,
getSegmentSha256
getSegmentSha256,
checkSegmentHash
}

View File

@ -2,14 +2,6 @@
# yarn lockfile v1
"@babel/polyfill@^7.2.5":
version "7.2.5"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
dependencies:
core-js "^2.5.7"
regenerator-runtime "^0.12.0"
"@iamstarkov/listr-update-renderer@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
@ -3593,17 +3585,6 @@ hide-powered-by@1.0.0:
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
"hlsdownloader@https://github.com/Chocobozzz/hlsdownloader#build":
version "0.0.0-semantic-release"
resolved "https://github.com/Chocobozzz/hlsdownloader#e19f9d803dcfe7ec25fd734b4743184f19a9b0cc"
dependencies:
"@babel/polyfill" "^7.2.5"
async "^2.6.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
request "^2.88.0"
request-promise "^4.2.2"
hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
@ -4870,7 +4851,7 @@ lodash@=3.10.1:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
lodash@^4.0.0, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@ -6651,11 +6632,6 @@ psl@^1.1.24:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
psl@^1.1.28:
version "1.1.31"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
pstree.remy@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a"
@ -6699,7 +6675,7 @@ punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0, punycode@^2.1.1:
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@ -6982,11 +6958,6 @@ reflect-metadata@^0.1.12:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@ -7036,23 +7007,6 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
request-promise-core@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
dependencies:
lodash "^4.13.1"
request-promise@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4"
integrity sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=
dependencies:
bluebird "^3.5.0"
request-promise-core "1.1.1"
stealthy-require "^1.1.0"
tough-cookie ">=2.3.3"
request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@ -7970,11 +7924,6 @@ statuses@~1.4.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
stealthy-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stream-each@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
@ -8467,15 +8416,6 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
tough-cookie@>=2.3.3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
dependencies:
ip-regex "^2.1.0"
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"