prettier code
This commit is contained in:
parent
2974efdebc
commit
af5a418d09
@ -94,7 +94,7 @@ export class YouTubePlayList {
|
|||||||
this.views = data.views || 0;
|
this.views = data.views || 0;
|
||||||
this.link = data.link || undefined;
|
this.link = data.link || undefined;
|
||||||
this.channel = new YouTubeChannel(data.channel) || undefined;
|
this.channel = new YouTubeChannel(data.channel) || undefined;
|
||||||
this.thumbnail = (data.thumbnail) ? new YouTubeThumbnail(data.thumbnail) : undefined;
|
this.thumbnail = data.thumbnail ? new YouTubeThumbnail(data.thumbnail) : undefined;
|
||||||
this.videos = data.videos || [];
|
this.videos = data.videos || [];
|
||||||
this.__count++;
|
this.__count++;
|
||||||
this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]);
|
this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]);
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { IncomingMessage } from "http";
|
import { IncomingMessage } from 'http';
|
||||||
import { request_stream } from "../../Request";
|
import { request_stream } from '../../Request';
|
||||||
import { parseAudioFormats, StreamOptions, StreamType } from "../stream";
|
import { parseAudioFormats, StreamOptions, StreamType } from '../stream';
|
||||||
import { video_info } from "../utils";
|
import { video_info } from '../utils';
|
||||||
import { Timer } from "./LiveStream";
|
import { Timer } from './LiveStream';
|
||||||
import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
import { WebmSeeker, WebmSeekerState } from './WebmSeeker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YouTube Stream Class for seeking audio to a timeStamp.
|
* YouTube Stream Class for seeking audio to a timeStamp.
|
||||||
*/
|
*/
|
||||||
export class SeekStream {
|
export class SeekStream {
|
||||||
/**
|
/**
|
||||||
* WebmSeeker Stream through which data passes
|
* WebmSeeker Stream through which data passes
|
||||||
*/
|
*/
|
||||||
@ -61,14 +61,12 @@ import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
|||||||
* @param video_url YouTube video url.
|
* @param video_url YouTube video url.
|
||||||
* @param options Options provided to stream function.
|
* @param options Options provided to stream function.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(url: string, duration: number, contentLength: number, video_url: string, options: StreamOptions) {
|
||||||
url: string,
|
this.stream = new WebmSeeker({
|
||||||
duration: number,
|
highWaterMark: 5 * 1000 * 1000,
|
||||||
contentLength: number,
|
readableObjectMode: true,
|
||||||
video_url: string,
|
mode: options.seekMode
|
||||||
options: StreamOptions
|
});
|
||||||
) {
|
|
||||||
this.stream = new WebmSeeker({ highWaterMark: 5 * 1000 * 1000, readableObjectMode : true, mode : options.seekMode });
|
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.quality = options.quality as number;
|
this.quality = options.quality as number;
|
||||||
this.type = StreamType.Opus;
|
this.type = StreamType.Opus;
|
||||||
@ -85,18 +83,18 @@ import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
|||||||
this.timer.destroy();
|
this.timer.destroy();
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
});
|
});
|
||||||
this.seek(options.seek!)
|
this.seek(options.seek!);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* **INTERNAL Function**
|
* **INTERNAL Function**
|
||||||
*
|
*
|
||||||
* Uses stream functions to parse Webm Head and gets Offset byte to seek to.
|
* Uses stream functions to parse Webm Head and gets Offset byte to seek to.
|
||||||
* @param sec No of seconds to seek to
|
* @param sec No of seconds to seek to
|
||||||
* @returns Nothing
|
* @returns Nothing
|
||||||
*/
|
*/
|
||||||
private async seek(sec : number){
|
private async seek(sec: number) {
|
||||||
await new Promise(async(res) => {
|
await new Promise(async (res) => {
|
||||||
if(!this.stream.headerparsed){
|
if (!this.stream.headerparsed) {
|
||||||
const stream = await request_stream(this.url, {
|
const stream = await request_stream(this.url, {
|
||||||
headers: {
|
headers: {
|
||||||
range: `bytes=0-1000`
|
range: `bytes=0-1000`
|
||||||
@ -111,18 +109,17 @@ import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.request = stream
|
this.request = stream;
|
||||||
stream.pipe(this.stream, { end : false })
|
stream.pipe(this.stream, { end: false });
|
||||||
|
|
||||||
stream.once('end', () => {
|
stream.once('end', () => {
|
||||||
this.stream.state = WebmSeekerState.READING_DATA
|
this.stream.state = WebmSeekerState.READING_DATA;
|
||||||
res('')
|
res('');
|
||||||
})
|
});
|
||||||
}
|
} else res('');
|
||||||
else res('')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const bytes = this.stream.seek(sec)
|
const bytes = this.stream.seek(sec);
|
||||||
if (bytes instanceof Error) {
|
if (bytes instanceof Error) {
|
||||||
this.stream.emit('error', bytes);
|
this.stream.emit('error', bytes);
|
||||||
this.bytes_count = 0;
|
this.bytes_count = 0;
|
||||||
@ -131,10 +128,10 @@ import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stream.seekfound = false
|
this.stream.seekfound = false;
|
||||||
this.bytes_count = bytes
|
this.bytes_count = bytes;
|
||||||
this.timer.reuse()
|
this.timer.reuse();
|
||||||
this.loop()
|
this.loop();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Retry if we get 404 or 403 Errors.
|
* Retry if we get 404 or 403 Errors.
|
||||||
@ -186,7 +183,7 @@ import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.request = stream;
|
this.request = stream;
|
||||||
stream.pipe(this.stream, { end : false })
|
stream.pipe(this.stream, { end: false });
|
||||||
|
|
||||||
stream.once('error', async () => {
|
stream.once('error', async () => {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|||||||
@ -1,217 +1,224 @@
|
|||||||
import { WebmElements, WebmHeader } from 'play-audio'
|
import { WebmElements, WebmHeader } from 'play-audio';
|
||||||
import { Duplex, DuplexOptions } from 'stream'
|
import { Duplex, DuplexOptions } from 'stream';
|
||||||
|
|
||||||
enum DataType { master, string, uint, binary, float }
|
enum DataType {
|
||||||
|
master,
|
||||||
|
string,
|
||||||
|
uint,
|
||||||
|
binary,
|
||||||
|
float
|
||||||
|
}
|
||||||
|
|
||||||
export enum WebmSeekerState{
|
export enum WebmSeekerState {
|
||||||
READING_HEAD = 'READING_HEAD',
|
READING_HEAD = 'READING_HEAD',
|
||||||
READING_DATA = 'READING_DATA',
|
READING_DATA = 'READING_DATA'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebmSeekerOptions extends DuplexOptions{
|
interface WebmSeekerOptions extends DuplexOptions {
|
||||||
mode? : "precise" | "granular"
|
mode?: 'precise' | 'granular';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebmSeeker extends Duplex{
|
export class WebmSeeker extends Duplex {
|
||||||
remaining? : Buffer
|
remaining?: Buffer;
|
||||||
state : WebmSeekerState
|
state: WebmSeekerState;
|
||||||
mode : "precise" | "granular"
|
mode: 'precise' | 'granular';
|
||||||
chunk? : Buffer
|
chunk?: Buffer;
|
||||||
cursor : number
|
cursor: number;
|
||||||
header : WebmHeader
|
header: WebmHeader;
|
||||||
headfound : boolean
|
headfound: boolean;
|
||||||
headerparsed : boolean
|
headerparsed: boolean;
|
||||||
time_left : number
|
time_left: number;
|
||||||
seekfound : boolean
|
seekfound: boolean;
|
||||||
private data_size : number
|
private data_size: number;
|
||||||
private data_length : number
|
private data_length: number;
|
||||||
|
|
||||||
constructor(options : WebmSeekerOptions){
|
constructor(options: WebmSeekerOptions) {
|
||||||
super(options)
|
super(options);
|
||||||
this.state = WebmSeekerState.READING_HEAD
|
this.state = WebmSeekerState.READING_HEAD;
|
||||||
this.cursor = 0
|
this.cursor = 0;
|
||||||
this.header = new WebmHeader()
|
this.header = new WebmHeader();
|
||||||
this.headfound = false
|
this.headfound = false;
|
||||||
this.time_left = 0
|
this.time_left = 0;
|
||||||
this.headerparsed = false
|
this.headerparsed = false;
|
||||||
this.seekfound = false
|
this.seekfound = false;
|
||||||
this.data_length = 0
|
this.data_length = 0;
|
||||||
this.mode = options.mode || "granular"
|
this.mode = options.mode || 'granular';
|
||||||
this.data_size = 0
|
this.data_size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get vint_length(): number{
|
private get vint_length(): number {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (; i < 8; i++){
|
for (; i < 8; i++) {
|
||||||
if ((1 << (7 - i)) & this.chunk![this.cursor])
|
if ((1 << (7 - i)) & this.chunk![this.cursor]) break;
|
||||||
break;
|
}
|
||||||
}
|
return ++i;
|
||||||
return ++i;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get vint_value(): boolean {
|
private get vint_value(): boolean {
|
||||||
if (!this.chunk) return false
|
if (!this.chunk) return false;
|
||||||
const length = this.vint_length
|
const length = this.vint_length;
|
||||||
if(this.chunk.length < this.cursor + length) return false
|
if (this.chunk.length < this.cursor + length) return false;
|
||||||
let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1)
|
let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1);
|
||||||
for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];
|
for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];
|
||||||
this.data_size = length
|
this.data_size = length;
|
||||||
this.data_length = value
|
this.data_length = value;
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup(){
|
cleanup() {
|
||||||
this.cursor = 0
|
this.cursor = 0;
|
||||||
this.chunk = undefined
|
this.chunk = undefined;
|
||||||
this.remaining = undefined
|
this.remaining = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
_read() {}
|
_read() {}
|
||||||
|
|
||||||
seek(sec : number): Error | number{
|
seek(sec: number): Error | number {
|
||||||
let position = 0
|
let position = 0;
|
||||||
let time = (Math.floor(sec / 10) * 10)
|
let time = Math.floor(sec / 10) * 10;
|
||||||
this.time_left = (sec - time) * 1000 || 0
|
this.time_left = (sec - time) * 1000 || 0;
|
||||||
if (!this.header.segment.cues) return new Error("Failed to Parse Cues")
|
if (!this.header.segment.cues) return new Error('Failed to Parse Cues');
|
||||||
|
|
||||||
for(const data of this.header.segment.cues){
|
for (const data of this.header.segment.cues) {
|
||||||
if(Math.floor(data.time as number / 1000) === time) {
|
if (Math.floor((data.time as number) / 1000) === time) {
|
||||||
position = data.position as number
|
position = data.position as number;
|
||||||
break;
|
break;
|
||||||
}
|
} else continue;
|
||||||
else continue;
|
|
||||||
}
|
}
|
||||||
if(position === 0) return Error("Failed to find Cluster Position")
|
if (position === 0) return Error('Failed to find Cluster Position');
|
||||||
else return position
|
else return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
_write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {
|
_write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {
|
||||||
if (this.remaining) {
|
if (this.remaining) {
|
||||||
this.chunk = Buffer.concat([this.remaining, chunk])
|
this.chunk = Buffer.concat([this.remaining, chunk]);
|
||||||
this.remaining = undefined
|
this.remaining = undefined;
|
||||||
}
|
} else this.chunk = chunk;
|
||||||
else this.chunk = chunk
|
|
||||||
|
|
||||||
let err : Error | undefined;
|
let err: Error | undefined;
|
||||||
|
|
||||||
if(this.state === WebmSeekerState.READING_HEAD) err = this.readHead()
|
if (this.state === WebmSeekerState.READING_HEAD) err = this.readHead();
|
||||||
else if(!this.seekfound) err = this.getClosetCluster()
|
else if (!this.seekfound) err = this.getClosetCluster();
|
||||||
else err = this.readTag()
|
else err = this.readTag();
|
||||||
|
|
||||||
if(err) callback(err)
|
if (err) callback(err);
|
||||||
else callback()
|
else callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readHead(): Error | undefined{
|
private readHead(): Error | undefined {
|
||||||
if (!this.chunk) return new Error("Chunk is missing")
|
if (!this.chunk) return new Error('Chunk is missing');
|
||||||
|
|
||||||
while(this.chunk.length > this.cursor ){
|
|
||||||
const oldCursor = this.cursor
|
|
||||||
const id = this.vint_length
|
|
||||||
if(this.chunk.length < this.cursor + id) break;
|
|
||||||
|
|
||||||
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'))
|
while (this.chunk.length > this.cursor) {
|
||||||
this.cursor += id
|
const oldCursor = this.cursor;
|
||||||
const vint = this.vint_value
|
const id = this.vint_length;
|
||||||
|
if (this.chunk.length < this.cursor + id) break;
|
||||||
|
|
||||||
if(!vint) {
|
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));
|
||||||
this.cursor = oldCursor
|
this.cursor += id;
|
||||||
break;
|
const vint = this.vint_value;
|
||||||
}
|
|
||||||
if(!ebmlID){
|
|
||||||
this.cursor += this.data_size + this.data_length
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.headfound){
|
if (!vint) {
|
||||||
if(ebmlID.name === "ebml") this.headfound = true
|
|
||||||
else return new Error("Failed to find EBML ID at start of stream.")
|
|
||||||
}
|
|
||||||
const data = this.chunk.slice(this.cursor + this.data_size, this.cursor + this.data_size + this.data_length)
|
|
||||||
const parse = this.header.parse(ebmlID, data)
|
|
||||||
if(parse instanceof Error) return parse
|
|
||||||
|
|
||||||
if(ebmlID.type === DataType.master) {
|
|
||||||
this.cursor += this.data_size
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
|
||||||
this.cursor = oldCursor;
|
this.cursor = oldCursor;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else this.cursor += this.data_size + this.data_length
|
if (!ebmlID) {
|
||||||
|
this.cursor += this.data_size + this.data_length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.headfound) {
|
||||||
|
if (ebmlID.name === 'ebml') this.headfound = true;
|
||||||
|
else return new Error('Failed to find EBML ID at start of stream.');
|
||||||
|
}
|
||||||
|
const data = this.chunk.slice(
|
||||||
|
this.cursor + this.data_size,
|
||||||
|
this.cursor + this.data_size + this.data_length
|
||||||
|
);
|
||||||
|
const parse = this.header.parse(ebmlID, data);
|
||||||
|
if (parse instanceof Error) return parse;
|
||||||
|
|
||||||
|
if (ebmlID.type === DataType.master) {
|
||||||
|
this.cursor += this.data_size;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
||||||
|
this.cursor = oldCursor;
|
||||||
|
break;
|
||||||
|
} else this.cursor += this.data_size + this.data_length;
|
||||||
}
|
}
|
||||||
this.remaining = this.chunk.slice(this.cursor)
|
this.remaining = this.chunk.slice(this.cursor);
|
||||||
this.cursor = 0
|
this.cursor = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readTag(): Error | undefined{
|
private readTag(): Error | undefined {
|
||||||
if (!this.chunk) return new Error("Chunk is missing")
|
if (!this.chunk) return new Error('Chunk is missing');
|
||||||
|
|
||||||
while(this.chunk.length > this.cursor ){
|
|
||||||
const oldCursor = this.cursor
|
|
||||||
const id = this.vint_length
|
|
||||||
if(this.chunk.length < this.cursor + id) break;
|
|
||||||
|
|
||||||
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'))
|
while (this.chunk.length > this.cursor) {
|
||||||
this.cursor += id
|
const oldCursor = this.cursor;
|
||||||
const vint = this.vint_value
|
const id = this.vint_length;
|
||||||
|
if (this.chunk.length < this.cursor + id) break;
|
||||||
|
|
||||||
if(!vint) {
|
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));
|
||||||
this.cursor = oldCursor
|
this.cursor += id;
|
||||||
break;
|
const vint = this.vint_value;
|
||||||
}
|
|
||||||
if(!ebmlID){
|
|
||||||
this.cursor += this.data_size + this.data_length
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = this.chunk.slice(this.cursor + this.data_size, this.cursor + this.data_size + this.data_length)
|
if (!vint) {
|
||||||
const parse = this.header.parse(ebmlID, data)
|
|
||||||
if(parse instanceof Error) return parse
|
|
||||||
|
|
||||||
if(ebmlID.type === DataType.master) {
|
|
||||||
this.cursor += this.data_size
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
|
||||||
this.cursor = oldCursor;
|
this.cursor = oldCursor;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else this.cursor += this.data_size + this.data_length
|
if (!ebmlID) {
|
||||||
|
this.cursor += this.data_size + this.data_length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(ebmlID.name === 'simpleBlock'){
|
const data = this.chunk.slice(
|
||||||
if(this.time_left !== 0 && this.mode === "precise"){
|
this.cursor + this.data_size,
|
||||||
if(data.readUInt16BE(1) === this.time_left) this.time_left = 0
|
this.cursor + this.data_size + this.data_length
|
||||||
|
);
|
||||||
|
const parse = this.header.parse(ebmlID, data);
|
||||||
|
if (parse instanceof Error) return parse;
|
||||||
|
|
||||||
|
if (ebmlID.type === DataType.master) {
|
||||||
|
this.cursor += this.data_size;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
||||||
|
this.cursor = oldCursor;
|
||||||
|
break;
|
||||||
|
} else this.cursor += this.data_size + this.data_length;
|
||||||
|
|
||||||
|
if (ebmlID.name === 'simpleBlock') {
|
||||||
|
if (this.time_left !== 0 && this.mode === 'precise') {
|
||||||
|
if (data.readUInt16BE(1) === this.time_left) this.time_left = 0;
|
||||||
else continue;
|
else continue;
|
||||||
}
|
}
|
||||||
const track = this.header.segment.tracks![this.header.audioTrack]
|
const track = this.header.segment.tracks![this.header.audioTrack];
|
||||||
if(!track || track.trackType !== 2) return new Error("No audio Track in this webm file.")
|
if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');
|
||||||
if((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4))
|
if ((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.remaining = this.chunk.slice(this.cursor)
|
this.remaining = this.chunk.slice(this.cursor);
|
||||||
this.cursor = 0
|
this.cursor = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClosetCluster(): Error | undefined{
|
private getClosetCluster(): Error | undefined {
|
||||||
if(!this.chunk) return new Error("Chunk is missing")
|
if (!this.chunk) return new Error('Chunk is missing');
|
||||||
const count = this.chunk.indexOf('1f43b675', 0, 'hex')
|
const count = this.chunk.indexOf('1f43b675', 0, 'hex');
|
||||||
if(count === -1) throw new Error("Failed to find nearest Cluster.")
|
if (count === -1) throw new Error('Failed to find nearest Cluster.');
|
||||||
else this.chunk = this.chunk.slice(count)
|
else this.chunk = this.chunk.slice(count);
|
||||||
this.seekfound = true
|
this.seekfound = true;
|
||||||
return this.readTag()
|
return this.readTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseEbmlID(ebmlID : string){
|
private parseEbmlID(ebmlID: string) {
|
||||||
if(Object.keys(WebmElements).includes(ebmlID)) return WebmElements[ebmlID]
|
if (Object.keys(WebmElements).includes(ebmlID)) return WebmElements[ebmlID];
|
||||||
else return false
|
else return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_destroy(error : Error | null, callback : (error : Error | null) => void) : void {
|
_destroy(error: Error | null, callback: (error: Error | null) => void): void {
|
||||||
this.cleanup()
|
this.cleanup();
|
||||||
callback(error);
|
callback(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,4 +226,4 @@ export class WebmSeeker extends Duplex{
|
|||||||
this.cleanup();
|
this.cleanup();
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,8 @@ export enum StreamType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamOptions {
|
export interface StreamOptions {
|
||||||
seekMode? : "precise" | "granular"
|
seekMode?: 'precise' | 'granular';
|
||||||
seek? : number
|
seek?: number;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
htmldata?: boolean;
|
htmldata?: boolean;
|
||||||
}
|
}
|
||||||
@ -80,25 +80,25 @@ export async function stream_from_info(
|
|||||||
else final.push(info.format[info.format.length - 1]);
|
else final.push(info.format[info.format.length - 1]);
|
||||||
let type: StreamType =
|
let type: StreamType =
|
||||||
final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;
|
final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;
|
||||||
if(options.seek){
|
if (options.seek) {
|
||||||
if(type === StreamType.WebmOpus) {
|
if (type === StreamType.WebmOpus) {
|
||||||
if(options.seek >= info.video_details.durationInSec) throw new Error(`Seeking beyond limit. [0 - ${info.video_details.durationInSec - 1}]`)
|
if (options.seek >= info.video_details.durationInSec || options.seek <= 0)
|
||||||
|
throw new Error(`Seeking beyond limit. [ 1 - ${info.video_details.durationInSec - 1}]`);
|
||||||
return new SeekStream(
|
return new SeekStream(
|
||||||
final[0].url,
|
final[0].url,
|
||||||
info.video_details.durationInSec,
|
info.video_details.durationInSec,
|
||||||
Number(final[0].contentLength),
|
Number(final[0].contentLength),
|
||||||
info.video_details.url,
|
info.video_details.url,
|
||||||
options
|
options
|
||||||
)
|
);
|
||||||
}
|
} else throw new Error('Seek is only supported in Webm Opus Files.');
|
||||||
else throw new Error("Seek is only supported in Webm Opus Files.")
|
} else
|
||||||
}
|
return new Stream(
|
||||||
else return new Stream(
|
final[0].url,
|
||||||
final[0].url,
|
type,
|
||||||
type,
|
info.video_details.durationInSec,
|
||||||
info.video_details.durationInSec,
|
Number(final[0].contentLength),
|
||||||
Number(final[0].contentLength),
|
info.video_details.url,
|
||||||
info.video_details.url,
|
options
|
||||||
options
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -327,12 +327,12 @@ export async function decipher_info<T extends InfoData | StreamInfoData>(data: T
|
|||||||
*/
|
*/
|
||||||
export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise<YouTubePlayList> {
|
export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise<YouTubePlayList> {
|
||||||
if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);
|
if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);
|
||||||
if (!url.startsWith('https')) url = `https://www.youtube.com/playlist?list=${url}`
|
if (!url.startsWith('https')) url = `https://www.youtube.com/playlist?list=${url}`;
|
||||||
if (url.indexOf('list=') === -1 ) throw new Error('This is not a Playlist URL');
|
if (url.indexOf('list=') === -1) throw new Error('This is not a Playlist URL');
|
||||||
|
|
||||||
if(yt_validate(url) === 'playlist') {
|
if (yt_validate(url) === 'playlist') {
|
||||||
const id = extractID(url)
|
const id = extractID(url);
|
||||||
url = `https://www.youtube.com/playlist?list=${id}`
|
url = `https://www.youtube.com/playlist?list=${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request(url, {
|
const body = await request(url, {
|
||||||
@ -353,10 +353,9 @@ export async function playlist_info(url: string, options: PlaylistOptions = {}):
|
|||||||
throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`);
|
throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`);
|
||||||
else throw new Error('While parsing playlist url\nUnknown Playlist Error');
|
else throw new Error('While parsing playlist url\nUnknown Playlist Error');
|
||||||
}
|
}
|
||||||
if(url.indexOf('watch?v=') !== -1){
|
if (url.indexOf('watch?v=') !== -1) {
|
||||||
return getWatchPlaylist(response, body)
|
return getWatchPlaylist(response, body);
|
||||||
}
|
} else return getNormalPlaylist(response, body);
|
||||||
else return getNormalPlaylist(response, body)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Function to parse Playlist from YouTube search
|
* Function to parse Playlist from YouTube search
|
||||||
@ -377,7 +376,7 @@ export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {
|
|||||||
id: info.videoId,
|
id: info.videoId,
|
||||||
duration: parseInt(info.lengthSeconds) || 0,
|
duration: parseInt(info.lengthSeconds) || 0,
|
||||||
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
||||||
thumbnails : info.thumbnail.thumbnails,
|
thumbnails: info.thumbnail.thumbnails,
|
||||||
title: info.title.runs[0].text,
|
title: info.title.runs[0].text,
|
||||||
channel: {
|
channel: {
|
||||||
id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,
|
id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,
|
||||||
@ -403,19 +402,18 @@ export function getContinuationToken(data: any): string {
|
|||||||
.continuationEndpoint?.continuationCommand?.token;
|
.continuationEndpoint?.continuationCommand?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWatchPlaylist(response: any, body: any): YouTubePlayList {
|
||||||
|
const playlist_details = response.contents.twoColumnWatchNextResults.playlist.playlist;
|
||||||
|
|
||||||
function getWatchPlaylist(response : any, body : any) : YouTubePlayList{
|
const videos = getWatchPlaylistVideos(playlist_details.contents);
|
||||||
const playlist_details = response.contents.twoColumnWatchNextResults.playlist.playlist
|
|
||||||
|
|
||||||
const videos = getWatchPlaylistVideos(playlist_details.contents)
|
|
||||||
const API_KEY =
|
const API_KEY =
|
||||||
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
|
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
|
||||||
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
|
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
|
||||||
DEFAULT_API_KEY;
|
DEFAULT_API_KEY;
|
||||||
|
|
||||||
const videoCount = playlist_details.totalVideos
|
const videoCount = playlist_details.totalVideos;
|
||||||
const channel = playlist_details.shortBylineText?.runs?.[0]
|
const channel = playlist_details.shortBylineText?.runs?.[0];
|
||||||
const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase()
|
const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();
|
||||||
|
|
||||||
return new YouTubePlayList({
|
return new YouTubePlayList({
|
||||||
continuation: {
|
continuation: {
|
||||||
@ -426,12 +424,12 @@ function getWatchPlaylist(response : any, body : any) : YouTubePlayList{
|
|||||||
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
|
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
|
||||||
'<some version>'
|
'<some version>'
|
||||||
},
|
},
|
||||||
id : playlist_details.playlistId || '',
|
id: playlist_details.playlistId || '',
|
||||||
title : playlist_details.title || '',
|
title: playlist_details.title || '',
|
||||||
videoCount : parseInt(videoCount) || 0,
|
videoCount: parseInt(videoCount) || 0,
|
||||||
videos : videos,
|
videos: videos,
|
||||||
url : `https://www.youtube.com/playlist?list=${playlist_details.playlistId}`,
|
url: `https://www.youtube.com/playlist?list=${playlist_details.playlistId}`,
|
||||||
channel : {
|
channel: {
|
||||||
id: channel?.navigationEndpoint?.browseEndpoint?.browseId || null,
|
id: channel?.navigationEndpoint?.browseEndpoint?.browseId || null,
|
||||||
name: channel?.text || null,
|
name: channel?.text || null,
|
||||||
url: `https://www.youtube.com${
|
url: `https://www.youtube.com${
|
||||||
@ -441,12 +439,13 @@ function getWatchPlaylist(response : any, body : any) : YouTubePlayList{
|
|||||||
verified: Boolean(badge?.includes('verified')),
|
verified: Boolean(badge?.includes('verified')),
|
||||||
artist: Boolean(badge?.includes('artist'))
|
artist: Boolean(badge?.includes('artist'))
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNormalPlaylist(response : any, body : any): YouTubePlayList{
|
function getNormalPlaylist(response: any, body: any): YouTubePlayList {
|
||||||
|
const json_data =
|
||||||
const json_data = response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;
|
response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0]
|
||||||
|
.itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;
|
||||||
const playlist_details = response.sidebar.playlistSidebarRenderer.items;
|
const playlist_details = response.sidebar.playlistSidebarRenderer.items;
|
||||||
|
|
||||||
const API_KEY =
|
const API_KEY =
|
||||||
@ -507,21 +506,21 @@ function getNormalPlaylist(response : any, body : any): YouTubePlayList{
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWatchPlaylistVideos(data : any, limit = Infinity): YouTubeVideo[] {
|
function getWatchPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {
|
||||||
const videos: YouTubeVideo[] = []
|
const videos: YouTubeVideo[] = [];
|
||||||
|
|
||||||
for(let i = 0; i < data.length ; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
if(limit === videos.length) break;
|
if (limit === videos.length) break;
|
||||||
const info = data[i].playlistPanelVideoRenderer;
|
const info = data[i].playlistPanelVideoRenderer;
|
||||||
if(!info || !info.shortBylineText) continue;
|
if (!info || !info.shortBylineText) continue;
|
||||||
const channel_info = info.shortBylineText.runs[0]
|
const channel_info = info.shortBylineText.runs[0];
|
||||||
|
|
||||||
videos.push(
|
videos.push(
|
||||||
new YouTubeVideo({
|
new YouTubeVideo({
|
||||||
id: info.videoId,
|
id: info.videoId,
|
||||||
duration: parseDuration(info.lengthText?.simpleText) || 0,
|
duration: parseDuration(info.lengthText?.simpleText) || 0,
|
||||||
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
||||||
thumbnails : info.thumbnail.thumbnails,
|
thumbnails: info.thumbnail.thumbnails,
|
||||||
title: info.title.simpleText,
|
title: info.title.simpleText,
|
||||||
channel: {
|
channel: {
|
||||||
id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,
|
id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,
|
||||||
@ -536,21 +535,21 @@ function getWatchPlaylistVideos(data : any, limit = Infinity): YouTubeVideo[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return videos
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDuration(text : string): number{
|
function parseDuration(text: string): number {
|
||||||
if(!text) return 0
|
if (!text) return 0;
|
||||||
const split = text.split(':')
|
const split = text.split(':');
|
||||||
|
|
||||||
switch (split.length){
|
switch (split.length) {
|
||||||
case 2:
|
case 2:
|
||||||
return (parseInt(split[0]) * 60) + (parseInt(split[1]))
|
return parseInt(split[0]) * 60 + parseInt(split[1]);
|
||||||
|
|
||||||
case 3:
|
|
||||||
return (parseInt(split[0]) * 60 * 60) + (parseInt(split[1]) * 60) + (parseInt(split[2]))
|
|
||||||
|
|
||||||
default :
|
case 3:
|
||||||
return 0
|
return parseInt(split[0]) * 60 * 60 + parseInt(split[1]) * 60 + parseInt(split[2]);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user