mirror of https://github.com/Chocobozzz/PeerTube
Use a single file instead of segments for HLS
parent
6ec0b75beb
commit
4c280004ce
|
@ -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})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
64
yarn.lock
64
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue