const https = require('https')
const http = require('http')
const { Readable } = require('stream')
const prettyMS = require('pretty-ms')
const utils = require('../Utils/__defaultUtils.js')
/**
* @class vimeoTrack -> Vimeo Handler Class for Handling Basic Un-Official Extraction and Parsing of Vimeo Video Metadata and Stream Readable
*/
class vimeoTrack {
/**
* @static
* @property {object} __scrapperOptions Default HTML Scrapping and Parsing Options Compilation
*/
static __scrapperOptions = {
htmlOptions: {},
fetchOptions: { fetchStreamReadable: true },
ignoreError: true,
parseRaw: true,
}
/**
* @static
* @property {string | "https://player.vimeo.com/video/" } __playerUrl Player URL for Extraction of Player Metada and Stream Metadata
*/
static __playerUrl = 'https://player.vimeo.com/video/'
/**
* @static
* @property {Regexp[]} __vimeoRegex Array of Vimeo Video URL Supported Regexes
*/
static __vimeoRegex = [
/(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/,
/(https?:\/\/)?(www\.)?(player\.)?vimeo\.com\/?(showcase\/)*([0-9))([a-z]*\/)*([0-9]{6,11})[?]?.*/,
/(?:http:|https:|)\/\/(?:player.|www.)?vimeo\.com\/(?:video\/|embed\/|watch\?\S*v=|v\/)?(\d*)/g,
/((http|https)?:\/\/(?:[\w\-\_]+\.))+(player+\.)vimeo\.com/g,
]
/**
* @static
* @property {Regexp[]} __vimeoPlayerRegex Array of Vimeo Player URL Supported Regexes
*/
static __vimeoPlayerRegex = [
/((http|https)?:\/\/(?:[\w\-\_]+\.))+(player+\.)vimeo\.com/g,
]
/**
* @private
* @property {object} __private Prirvate Caches/Data for further Parsing and Memory Cache for Vimeo Constructor
*/
#__private = {
__raw: undefined,
__scrapperOptions: undefined,
__rawExtra: undefined,
__rawJSON: undefined,
}
/**
* @constructor
* @param {string} rawResponse Response Body like in text or HTML Player's Source Code
* @param {object} __scrapperOptions scrapping Options for raw Fetch Method
* @param {object} extraContents Extra Contents for Merging for cache in "extra Cache"
*/
constructor(
rawResponse,
__scrapperOptions = vimeoTrack.__scrapperOptions,
extraContents = {},
) {
this.#__private = {
__raw: rawResponse,
__scrapperOptions,
__rawExtra: extraContents,
}
this.#__patch(rawResponse, false, extraContents)
}
/**
* @static
* __test() -> Regex Testing with respect to Arrays of Regex and Raw Url Provided
* @param {string} rawUrl raw url for checking if its Vimeo Video URL
* @param {boolean | 'false'} returnRegex Boolean Value for if return the residue or results
* @returns {boolean | RegExpMatchArray} returns Boolean on success and Regex match Array Data if its requested
*/
static __test(rawUrl, returnRegex = false) {
try {
if (!(rawUrl && typeof rawUrl === 'string' && rawUrl !== '')) return false
return returnRegex &&
Boolean(vimeoTrack.__vimeoRegex.find((regExp) => regExp.test(rawUrl)))
? rawUrl?.match(
vimeoTrack.__vimeoRegex.find((regExp) => rawUrl.match(regExp)),
) ?? false
: Boolean(vimeoTrack.__vimeoRegex.find((regExp) => regExp.test(rawUrl)))
} catch {
return false
}
}
/**
* @private
* #__patch() -> Patching Method for constructor Vimeo Handler
* @param {string} rawResponse Response Body like in text or HTML Player's Source Code
* @param {boolean | "false"} returnOnly Boolean value for exceptions of Parsing only method use
* @param {object} extraContents extra keys and values to merge/assign to the constructor on request
* @returns {object} Returns the parsed structured Data for if any use
*/
#__patch(rawResponse, returnOnly = false, extraContents = {}) {
try {
if (
!(rawResponse && typeof rawResponse === 'string' && rawResponse !== '')
)
throw new TypeError(
'Vimeo Internal Error : Invalid Response is Fetched from Axios.get()',
)
const rawJsonData = JSON.parse(
rawResponse
?.split('<script> (function(document, player) { var config = ')?.[1]
?.split(';')?.[0],
)
if (!(rawJsonData?.video && rawJsonData?.request?.files?.progressive))
throw new TypeError(
'Vimeo Internal Error : Invalid Response JSON is Parsed',
)
const __rawStreamData = rawJsonData?.request?.files?.progressive?.find(
(stream) => stream?.url && typeof stream?.url === 'string' && stream?.url !== '',
)
this.#__private.__rawJSON = {
...this.__private?.__rawJSON,
...rawJsonData?.video,
...extraContents,
stream: __rawStreamData,
}
const __cookedStructure = this.#__private?.__scrapperOptions?.parseRaw
? this.parseRaw()
: this.#__private.__rawJSON
if (!returnOnly) Object.assign(this, __cookedStructure)
return __cookedStructure
} catch (rawError) {
if (this.#__private?.__scrapperOptions?.ignoreError)
return utils.__errorHandling(rawError)
else throw rawError
}
}
/**
* parseRaw() -> Parse Raw Object/Properties of teh Class or requested Object Variable
* @param {object} rawObjects Raw Objects Value to be parsed into meaningfull and cleaned
* @returns {object} Return cleaned Object Variable
*/
parseRaw(rawObjects = this.#__private?.__rawJSON) {
try {
if (!(rawObjects && typeof rawObjects === 'object' && rawObjects !== {}))
return undefined
const __rawEntries = Object.entries(rawObjects)
const cookedStructure = {}
cookedStructure.title = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'title' && raw?.[1],
)?.[1]
cookedStructure.url = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'url' && raw?.[1],
)?.[1]
cookedStructure.description = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'description' && raw?.[1],
)?.[1]
cookedStructure.duration = {
ms:
parseInt(
__rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'duration' && raw?.[1],
)?.[1] ?? 0,
) * 1000,
readable: prettyMS(
parseInt(
__rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'duration' && raw?.[1],
)?.[1] ?? 0,
) * 1000,
),
}
cookedStructure.thumbnails = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'thumbs' && raw?.[1],
)?.[1]
cookedStructure.author = {
type: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.account_type,
name: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.name,
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.url,
images: {
normal: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.img,
normal2X: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.img_2x,
},
authorId:
__rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'creator_id' && raw?.[1],
)?.[1] ??
__rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'owner' && raw?.[1],
)?.[1]?.id,
}
cookedStructure.trackId = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'id' && raw?.[1],
)?.[1]
cookedStructure.privacy = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'privacy' && raw?.[1],
)?.[1]
cookedStructure.language = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'lang' && raw?.[1],
)?.[1]
cookedStructure.shareURL = __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'share_url' && raw?.[1],
)?.[1]
cookedStructure.isLive = !!__rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'live_event' && raw?.[1],
)?.[1]
cookedStructure.streamMetadata = {
type: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.mime,
width: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.width,
height: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.height,
fps: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.fps,
quality: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.quality,
streamUrl: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.url,
buffer: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'stream' && raw?.[1],
)?.[1]?.buffer,
}
cookedStructure.htmlPlayer = {
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'player' && raw?.[1],
)?.[1],
width: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'player_width' && raw?.[1],
)?.[1],
height: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'player_height' && raw?.[1],
)?.[1],
}
cookedStructure.mobile = {
ios: {
appName: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'ios_app_name' && raw?.[1],
)?.[1],
appStoreId: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'ios_app_store_id' && raw?.[1],
)?.[1],
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'ios_url' && raw?.[1],
)?.[1],
},
android: {
appName: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'android_app_name' && raw?.[1],
)?.[1],
package: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'android_package' && raw?.[1],
)?.[1],
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'android_url' && raw?.[1],
)?.[1],
},
extraApp: {
iphone: {
name: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_name_iphone' && raw?.[1],
)?.[1],
id: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_id_iphone' && raw?.[1],
)?.[1],
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_url_iphone' && raw?.[1],
)?.[1],
},
ipad: {
name: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_name_ipad' && raw?.[1],
)?.[1],
id: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_id_ipad' && raw?.[1],
)?.[1],
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'app_url_ipad' && raw?.[1],
)?.[1],
},
googlePlay: {
name: __rawEntries?.find(
(raw) => raw?.[0] &&
raw?.[0]?.trim() === 'app_name_googleplay' &&
raw?.[1],
)?.[1],
id: __rawEntries?.find(
(raw) => raw?.[0] &&
raw?.[0]?.trim() === 'app_id_googleplay' &&
raw?.[1],
)?.[1],
url: __rawEntries?.find(
(raw) => raw?.[0] &&
raw?.[0]?.trim() === 'app_url_googleplay' &&
raw?.[1],
)?.[1],
},
},
}
cookedStructure.metadata = {
video: {
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_url' && raw?.[1],
)?.[1],
secureUrl: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_secure_url' && raw?.[1],
)?.[1],
type: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_type' && raw?.[1],
)?.[1],
width: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_width' && raw?.[1],
)?.[1],
height: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_height' && raw?.[1],
)?.[1],
},
overlayImage: {
url: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'image' && raw?.[1],
)?.[1],
secureUrl: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'image_secure_url' && raw?.[1],
)?.[1],
type: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'image_type' && raw?.[1],
)?.[1],
width: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'image_width' && raw?.[1],
)?.[1],
height: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'image_height' && raw?.[1],
)?.[1],
},
updatedTime: __rawEntries?.find(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'updated_time' && raw?.[1],
)?.[1],
otherTags: __rawEntries
?.filter(
(raw) => raw?.[0] && raw?.[0]?.trim() === 'video_other_tag' && raw?.[1],
)
?.map((raw) => raw?.[1]),
}
return cookedStructure
} catch (rawError) {
if (this.#__private?.__scrapperOptions?.ignoreError)
return utils.__errorHandling(rawError)
else throw rawError
}
}
/**
* method getStream() -> Fetch Stream Readable
* @param {string} fetchUrl Fetch Stream Url or normal Vimeo Video Url
* @returns {Promise<Readable>} Returns Stream for HTML5 Pages or Web Apps working on Stream Based or pipeing Stuff
*/
async getStream(
fetchUrl = this.streamMetadata?.url ?? this.url ?? this?.video_url,
) {
try {
if (!(fetchUrl && typeof fetchUrl === 'string' && fetchUrl !== ''))
throw new TypeError(
'Vimeo Internal Error : Invalid Stream Url is Parsed for creating Readable Stream',
)
else if (
!(
fetchUrl?.endsWith('mp3') ||
fetchUrl?.endsWith('mp4') ||
fetchUrl?.startsWith('http')
)
) {
if (!utils.__vimeoVideoIdParser(fetchUrl)) return undefined
const rawResponse = await utils.__rawfetchBody(
vimeoTrack.__playerUrl +
(utils.__vimeoVideoIdParser(fetchUrl) ??
this.videoId ??
this.trackId),
this.#__private?.__scrapperOptions?.htmlOptions,
)
if (
!(
rawResponse &&
typeof rawResponse === 'string' &&
rawResponse !== ''
)
)
throw new TypeError(
'Vimeo Internal Error : Invalid Response is Fetched from Axios.get()',
)
else fetchUrl = this.#__patch(rawResponse, true)?.stream?.url
}
if (!(fetchUrl && typeof fetchUrl === 'string' && fetchUrl !== ''))
throw new TypeError(
'Vimeo Internal Error : Invalid Stream Url is Parsed for creating Readable Stream',
)
const rawDownloadFunction = fetchUrl?.startsWith('https') ? https : http
return new Promise((resolve) => {
rawDownloadFunction.get(fetchUrl, (response) => {
Object.assign(this.streamMetadata, {
...this.streamMetadata,
buffer: response,
})
resolve(response)
})
})
} catch (rawError) {
if (this.#__private?.__scrapperOptions?.ignoreError)
return utils.__errorHandling(rawError)
else throw rawError
}
}
/**
* @static
* __htmlFetch() -> Html 5 Player Fetch for Vimeo Url
* @param {string} rawUrl raw Vimeo Video Url for the Extraction
* @param {object} __scrapperOptions scrapping Options for raw Fetch Method
* @param {object} extraContents Extra Contents to be Added if placed from Html file Parser
* @returns {Promise<vimeoTrack>} Returns Instance of Vimeo with properties of Data
*/
static async __htmlFetch(
rawUrl,
__scrapperOptions = vimeoTrack.__scrapperOptions,
extraContents = {},
) {
try {
if (
!(
rawUrl &&
typeof rawUrl === 'string' &&
rawUrl !== '' &&
utils.__vimeoVideoIdParser(rawUrl)
)
)
throw new TypeError(
'Vimeo Internal Error : Invalid Vimeo Video Url is for Parsing and Extraction',
)
__scrapperOptions = {
...vimeoTrack.__scrapperOptions,
...__scrapperOptions,
htmlOptions: {
...vimeoTrack.__scrapperOptions?.htmlOptions,
...__scrapperOptions?.htmlOptions,
},
fetchOptions: {
...vimeoTrack.__scrapperOptions?.fetchOptions,
...__scrapperOptions?.fetchOptions,
},
}
rawUrl = vimeoTrack.__vimeoPlayerRegex?.find(
(regex) => regex && regex.test(rawUrl),
)
? rawUrl
: vimeoTrack.__playerUrl + utils.__vimeoVideoIdParser(rawUrl)
const rawResponse = await utils.__rawfetchBody(
rawUrl,
__scrapperOptions?.htmlOptions,
)
if (
!(rawResponse && typeof rawResponse === 'string' && rawResponse !== '')
)
throw new TypeError(
'Vimeo Internal Error : Invalid Response is Fetched from Axios.get()',
)
const rawVimeo = new vimeoTrack(
rawResponse,
__scrapperOptions,
extraContents,
)
if (__scrapperOptions?.fetchOptions?.fetchStreamReadable)
await rawVimeo.getStream()
return rawVimeo
} catch (rawError) {
if (__scrapperOptions?.ignoreError) return utils.__errorHandling(rawError)
else throw rawError
}
}
/**
* embedHTMl() -> Embed Frame Method to make a single html code snippet to paste for Embeded HTML Player
* @param {number | string | 640} width width length of the Player in Embeded HTML Player Frame
* @param {number | string | 360} height height length of the Player in Embeded HTML Player Frame
* @param {number | string | 0} frameBorder frameBorder data of the Player in Embeded HTML Player Frame
* @returns {string | void} Returns <frame> Embed Player for HTML pages
*/
embedHTMl(width = 640, height = 360, frameBorder = 0) {
if (
!this.htmlPlayer?.url &&
!this.#__private?.__rawJSON?.embed_code &&
!this.#__private?.__rawExtra?.player
)
return undefined
else
return (
this.#__private?.__rawJSON?.embed_code ??
`<iframe title="vimeo-player" src="${
this.htmlPlayer?.url ?? this.#__private?.__rawExtra?.player
}" width="${width}" height="${height}" frameborder="${
frameBorder ?? 0
}" allowfullscreen></iframe>`
)
}
/**
* @type {object} Raw Data from HTML Fetches and <response.data> Body and Compiled
*/
get raw() {
return this.#__private?.__raw
}
/**
* @type {object} Raw JSON Data from HTML Fetches and <response.data> Body and Compiled
*/
get rawJSON() {
return this.#__private?.__rawJSON
}
/**
* @type {object} Raw Extra Data from HTML Fetches and <response.data> Body and Compiled
*/
get extraRaw() {
return this.#__private?.__rawExtra
}
/**
* @type {string} Vimeo Video's Id Parsed from fetched Url if present
*/
get videoId() {
if (!this.url) return undefined
else return utils.__vimeoVideoIdParser(this.url, vimeoTrack.__vimeoRegex)
}
}
module.exports = vimeoTrack