SoundCloud work + prettier

This commit is contained in:
killer069
2021-09-17 14:36:32 +05:30
parent f9876ce5c5
commit 90b13629b4
21 changed files with 1615 additions and 1162 deletions

View File

@@ -34,19 +34,19 @@ export class Channel {
* @param {object} options Icon options
* @param {number} [options.size=0] Icon size. **Default is 0**
*/
iconURL(options = { size: 0 }): string | undefined{
if (typeof options.size !== "number" || options.size < 0) throw new Error("invalid icon size");
iconURL(options = { size: 0 }): string | undefined {
if (typeof options.size !== 'number' || options.size < 0) throw new Error('invalid icon size');
if (!this.icon?.url) return undefined;
const def = this.icon.url.split("=s")[1].split("-c")[0];
const def = this.icon.url.split('=s')[1].split('-c')[0];
return this.icon.url.replace(`=s${def}-c`, `=s${options.size}-c`);
}
get type(): "channel" {
return "channel";
get type(): 'channel' {
return 'channel';
}
toString(): string {
return this.name || "";
return this.name || '';
}
toJSON() {
@@ -60,4 +60,4 @@ export class Channel {
subscribers: this.subscribers
};
}
}
}

View File

@@ -1,222 +1,235 @@
import { PassThrough } from 'stream'
import { PassThrough } from 'stream';
import { IncomingMessage } from 'http';
import { StreamType } from '../stream';
import { request, request_stream } from '../utils/request';
import { video_info } from '..';
export interface FormatInterface{
url : string;
targetDurationSec : number;
maxDvrDurationSec : number
export interface FormatInterface {
url: string;
targetDurationSec: number;
maxDvrDurationSec: number;
}
export class LiveStreaming{
type : StreamType
stream : PassThrough
private base_url : string
private url : string
private interval : number
private packet_count : number
private timer : NodeJS.Timer | null
private video_url : string
private dash_timer : NodeJS.Timer | null
private segments_urls : string[]
private request : IncomingMessage | null
constructor(dash_url : string, target_interval : number, video_url : string){
this.type = StreamType.Arbitrary
this.url = dash_url
this.base_url = ''
this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 })
this.segments_urls = []
this.packet_count = 0
this.request = null
this.timer = null
this.video_url = video_url
this.interval = target_interval * 1000 || 0
export class LiveStreaming {
type: StreamType;
stream: PassThrough;
private base_url: string;
private url: string;
private interval: number;
private packet_count: number;
private timer: NodeJS.Timer | null;
private video_url: string;
private dash_timer: NodeJS.Timer | null;
private segments_urls: string[];
private request: IncomingMessage | null;
constructor(dash_url: string, target_interval: number, video_url: string) {
this.type = StreamType.Arbitrary;
this.url = dash_url;
this.base_url = '';
this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 });
this.segments_urls = [];
this.packet_count = 0;
this.request = null;
this.timer = null;
this.video_url = video_url;
this.interval = target_interval * 1000 || 0;
this.dash_timer = setTimeout(() => {
this.dash_updater()
}, 1800000)
this.dash_updater();
}, 1800000);
this.stream.on('close', () => {
this.cleanup()
this.cleanup();
});
this.start()
this.start();
}
private async dash_updater(){
let info = await video_info(this.video_url)
if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && info.video_details.durationInSec === '0'){
this.url = info.LiveStreamData.dashManifestUrl
private async dash_updater() {
const info = await video_info(this.video_url);
if (
info.LiveStreamData.isLive === true &&
info.LiveStreamData.hlsManifestUrl !== null &&
info.video_details.durationInSec === '0'
) {
this.url = info.LiveStreamData.dashManifestUrl;
}
this.dash_timer = setTimeout(() => {
this.dash_updater()
}, 1800000)
this.dash_updater();
}, 1800000);
}
private async dash_getter(){
let response = await request(this.url)
let audioFormat = response.split('<AdaptationSet id="0"')[1].split('</AdaptationSet>')[0].split('</Representation>')
if(audioFormat[audioFormat.length - 1] === '') audioFormat.pop()
this.base_url = audioFormat[audioFormat.length - 1].split('<BaseURL>')[1].split('</BaseURL>')[0]
let list = audioFormat[audioFormat.length - 1].split('<SegmentList>')[1].split('</SegmentList>')[0]
this.segments_urls = list.replace(new RegExp('<SegmentURL media="', 'g'), '').split('"/>')
if(this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop()
private async dash_getter() {
const response = await request(this.url);
const audioFormat = response
.split('<AdaptationSet id="0"')[1]
.split('</AdaptationSet>')[0]
.split('</Representation>');
if (audioFormat[audioFormat.length - 1] === '') audioFormat.pop();
this.base_url = audioFormat[audioFormat.length - 1].split('<BaseURL>')[1].split('</BaseURL>')[0];
const list = audioFormat[audioFormat.length - 1].split('<SegmentList>')[1].split('</SegmentList>')[0];
this.segments_urls = list.replace(new RegExp('<SegmentURL media="', 'g'), '').split('"/>');
if (this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop();
}
private cleanup(){
clearTimeout(this.timer as NodeJS.Timer)
clearTimeout(this.dash_timer as NodeJS.Timer)
this.request?.unpipe(this.stream)
this.request?.destroy()
this.dash_timer = null
this.video_url = ''
this.request = null
this.timer = null
this.url = ''
this.base_url = ''
this.segments_urls = []
this.packet_count = 0
this.interval = 0
private cleanup() {
clearTimeout(this.timer as NodeJS.Timer);
clearTimeout(this.dash_timer as NodeJS.Timer);
this.request?.unpipe(this.stream);
this.request?.destroy();
this.dash_timer = null;
this.video_url = '';
this.request = null;
this.timer = null;
this.url = '';
this.base_url = '';
this.segments_urls = [];
this.packet_count = 0;
this.interval = 0;
}
private async start(){
if(this.stream.destroyed){
this.cleanup()
return
private async start() {
if (this.stream.destroyed) {
this.cleanup();
return;
}
await this.dash_getter()
if(this.segments_urls.length > 3) this.segments_urls.splice(0, this.segments_urls.length - 3)
if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0])
for await (let segment of this.segments_urls){
if(Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count){
continue
await this.dash_getter();
if (this.segments_urls.length > 3) this.segments_urls.splice(0, this.segments_urls.length - 3);
if (this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0]);
for await (const segment of this.segments_urls) {
if (Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count) {
continue;
}
await new Promise(async(resolve, reject) => {
let stream = await request_stream(this.base_url + segment).catch((err: Error) => err)
if(stream instanceof Error){
this.stream.emit('error', stream)
return
await new Promise(async (resolve, reject) => {
const stream = await request_stream(this.base_url + segment).catch((err: Error) => err);
if (stream instanceof Error) {
this.stream.emit('error', stream);
return;
}
this.request = stream
stream.pipe(this.stream, { end : false })
this.request = stream;
stream.pipe(this.stream, { end: false });
stream.on('end', () => {
this.packet_count++
resolve('')
})
this.packet_count++;
resolve('');
});
stream.once('error', (err) => {
this.stream.emit('error', err)
})
})
this.stream.emit('error', err);
});
});
}
this.timer = setTimeout(() => {
this.start()
}, this.interval)
this.start();
}, this.interval);
}
}
export class Stream {
type : StreamType
stream : PassThrough
private url : string
private bytes_count : number;
private per_sec_bytes : number;
private content_length : number;
private video_url : string;
private timer : NodeJS.Timer | null;
private cookie : string;
private data_ended : boolean;
private playing_count : number;
private request : IncomingMessage | null
constructor(url : string, type : StreamType, duration : number, contentLength : number, video_url : string, cookie : string){
this.url = url
this.type = type
this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 })
this.bytes_count = 0
this.video_url = video_url
this.cookie = cookie
type: StreamType;
stream: PassThrough;
private url: string;
private bytes_count: number;
private per_sec_bytes: number;
private content_length: number;
private video_url: string;
private timer: NodeJS.Timer | null;
private cookie: string;
private data_ended: boolean;
private playing_count: number;
private request: IncomingMessage | null;
constructor(
url: string,
type: StreamType,
duration: number,
contentLength: number,
video_url: string,
cookie: string
) {
this.url = url;
this.type = type;
this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 });
this.bytes_count = 0;
this.video_url = video_url;
this.cookie = cookie;
this.timer = setInterval(() => {
this.retry()
}, 7200 * 1000)
this.per_sec_bytes = Math.ceil(contentLength / duration)
this.content_length = contentLength
this.request = null
this.data_ended = false
this.playing_count = 0
this.retry();
}, 7200 * 1000);
this.per_sec_bytes = Math.ceil(contentLength / duration);
this.content_length = contentLength;
this.request = null;
this.data_ended = false;
this.playing_count = 0;
this.stream.on('close', () => {
this.cleanup()
})
this.cleanup();
});
this.stream.on('pause', () => {
this.playing_count++;
if(this.data_ended){
this.bytes_count = 0
this.per_sec_bytes = 0
this.cleanup()
this.stream.removeAllListeners('pause')
if (this.data_ended) {
this.bytes_count = 0;
this.per_sec_bytes = 0;
this.cleanup();
this.stream.removeAllListeners('pause');
} else if (this.playing_count === 280) {
this.playing_count = 0;
this.loop();
}
else if(this.playing_count === 280){
this.playing_count = 0
this.loop()
}
})
this.loop()
});
this.loop();
}
private async retry(){
let info = await video_info(this.video_url, this.cookie)
this.url = info.format[info.format.length - 1].url
private async retry() {
const info = await video_info(this.video_url, this.cookie);
this.url = info.format[info.format.length - 1].url;
}
private cleanup(){
clearInterval(this.timer as NodeJS.Timer)
this.request?.unpipe(this.stream)
this.request?.destroy()
this.timer = null
this.request = null
this.url = ''
private cleanup() {
clearInterval(this.timer as NodeJS.Timer);
this.request?.unpipe(this.stream);
this.request?.destroy();
this.timer = null;
this.request = null;
this.url = '';
}
private async loop(){
if(this.stream.destroyed){
this.cleanup()
return
private async loop() {
if (this.stream.destroyed) {
this.cleanup();
return;
}
let end : number = this.bytes_count + this.per_sec_bytes * 300;
let stream = await request_stream(this.url, {
headers : {
"range" : `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`
const end: number = this.bytes_count + this.per_sec_bytes * 300;
const stream = await request_stream(this.url, {
headers: {
range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`
}
}).catch((err: Error) => err)
if(stream instanceof Error){
this.stream.emit('error', stream)
this.data_ended = true
this.bytes_count = 0
this.per_sec_bytes = 0
this.cleanup()
return
}).catch((err: Error) => err);
if (stream instanceof Error) {
this.stream.emit('error', stream);
this.data_ended = true;
this.bytes_count = 0;
this.per_sec_bytes = 0;
this.cleanup();
return;
}
if(Number(stream.statusCode) >= 400){
this.cleanup()
await this.retry()
this.loop()
if(!this.timer){
if (Number(stream.statusCode) >= 400) {
this.cleanup();
await this.retry();
this.loop();
if (!this.timer) {
this.timer = setInterval(() => {
this.retry()
}, 7200 * 1000)
this.retry();
}, 7200 * 1000);
}
return
return;
}
this.request = stream
stream.pipe(this.stream, { end : false })
this.request = stream;
stream.pipe(this.stream, { end: false });
stream.once('error', (err) => {
this.stream.emit('error', err)
})
this.stream.emit('error', err);
});
stream.on('data', (chunk: any) => {
this.bytes_count += chunk.length
})
this.bytes_count += chunk.length;
});
stream.on('end', () => {
if(end >= this.content_length) this.data_ended = true
})
if (end >= this.content_length) this.data_ended = true;
});
}
}

View File

@@ -1,11 +1,11 @@
import { getPlaylistVideos, getContinuationToken } from "../utils/extractor";
import { request } from "../utils/request";
import { Thumbnail } from "./Thumbnail";
import { Channel } from "./Channel";
import { Video } from "./Video";
const BASE_API = "https://www.youtube.com/youtubei/v1/browse?key=";
import { getPlaylistVideos, getContinuationToken } from '../utils/extractor';
import { request } from '../utils/request';
import { Thumbnail } from './Thumbnail';
import { Channel } from './Channel';
import { Video } from './Video';
const BASE_API = 'https://www.youtube.com/youtubei/v1/browse?key=';
export class PlayList{
export class PlayList {
id?: string;
title?: string;
videoCount?: number;
@@ -16,19 +16,23 @@ export class PlayList{
channel?: Channel;
thumbnail?: Thumbnail;
private videos?: [];
private fetched_videos : Map<string, Video[]>
private _continuation: { api?: string; token?: string; clientVersion?: string } = {};
private __count : number
private fetched_videos: Map<string, Video[]>;
private _continuation: {
api?: string;
token?: string;
clientVersion?: string;
} = {};
private __count: number;
constructor(data : any, searchResult : Boolean = false){
constructor(data: any, searchResult = false) {
if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);
this.__count = 0
this.fetched_videos = new Map()
if(searchResult) this.__patchSearch(data)
else this.__patch(data)
this.__count = 0;
this.fetched_videos = new Map();
if (searchResult) this.__patchSearch(data);
else this.__patch(data);
}
private __patch(data:any){
private __patch(data: any) {
this.id = data.id || undefined;
this.url = data.url || undefined;
this.title = data.title || undefined;
@@ -39,14 +43,14 @@ export class PlayList{
this.channel = data.author || undefined;
this.thumbnail = data.thumbnail || undefined;
this.videos = data.videos || [];
this.__count ++
this.fetched_videos.set(`page${this.__count}`, this.videos as Video[])
this.__count++;
this.fetched_videos.set(`page${this.__count}`, this.videos as Video[]);
this._continuation.api = data.continuation?.api ?? undefined;
this._continuation.token = data.continuation?.token ?? undefined;
this._continuation.clientVersion = data.continuation?.clientVersion ?? "<important data>";
this._continuation.clientVersion = data.continuation?.clientVersion ?? '<important data>';
}
private __patchSearch(data: any){
private __patchSearch(data: any) {
this.id = data.id || undefined;
this.url = this.id ? `https://www.youtube.com/playlist?list=${this.id}` : undefined;
this.title = data.title || undefined;
@@ -59,19 +63,19 @@ export class PlayList{
this.views = 0;
}
async next(limit: number = Infinity): Promise<Video[]> {
async next(limit = Infinity): Promise<Video[]> {
if (!this._continuation || !this._continuation.token) return [];
let nextPage = await request(`${BASE_API}${this._continuation.api}`, {
method: "POST",
const nextPage = await request(`${BASE_API}${this._continuation.api}`, {
method: 'POST',
body: JSON.stringify({
continuation: this._continuation.token,
context: {
client: {
utcOffsetMinutes: 0,
gl: "US",
hl: "en",
clientName: "WEB",
gl: 'US',
hl: 'en',
clientName: 'WEB',
clientVersion: this._continuation.clientVersion
},
user: {},
@@ -79,24 +83,25 @@ export class PlayList{
}
})
});
let contents = JSON.parse(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems
if(!contents) return []
let playlist_videos = getPlaylistVideos(contents, limit)
this.fetched_videos.set(`page${this.__count}`, playlist_videos)
this._continuation.token = getContinuationToken(contents)
return playlist_videos
const contents =
JSON.parse(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems;
if (!contents) return [];
const playlist_videos = getPlaylistVideos(contents, limit);
this.fetched_videos.set(`page${this.__count}`, playlist_videos);
this._continuation.token = getContinuationToken(contents);
return playlist_videos;
}
async fetch(max: number = Infinity) {
let continuation = this._continuation.token;
async fetch(max = Infinity) {
const continuation = this._continuation.token;
if (!continuation) return this;
if (max < 1) max = Infinity;
while (typeof this._continuation.token === "string" && this._continuation.token.length) {
if (this.videos?.length as number >= max) break;
this.__count++
while (typeof this._continuation.token === 'string' && this._continuation.token.length) {
if ((this.videos?.length as number) >= max) break;
this.__count++;
const res = await this.next();
if (!res.length) break;
}
@@ -104,23 +109,23 @@ export class PlayList{
return this;
}
get type(): "playlist" {
return "playlist";
get type(): 'playlist' {
return 'playlist';
}
page(number : number): Video[]{
if(!number) throw new Error('Page number is not provided')
if(!this.fetched_videos.has(`page${number}`)) throw new Error('Given Page number is invalid')
return this.fetched_videos.get(`page${number}`) as Video[]
page(number: number): Video[] {
if (!number) throw new Error('Page number is not provided');
if (!this.fetched_videos.has(`page${number}`)) throw new Error('Given Page number is invalid');
return this.fetched_videos.get(`page${number}`) as Video[];
}
get total_pages(){
return this.fetched_videos.size
get total_pages() {
return this.fetched_videos.size;
}
get total_videos(){
let page_number: number = this.total_pages
return (page_number - 1) * 100 + (this.fetched_videos.get(`page${page_number}`) as Video[]).length
get total_videos() {
const page_number: number = this.total_pages;
return (page_number - 1) * 100 + (this.fetched_videos.get(`page${page_number}`) as Video[]).length;
}
toJSON() {
@@ -129,12 +134,12 @@ export class PlayList{
title: this.title,
thumbnail: this.thumbnail,
channel: {
name : this.channel?.name,
id : this.channel?.id,
icon : this.channel?.iconURL()
name: this.channel?.name,
id: this.channel?.id,
icon: this.channel?.iconURL()
},
url: this.url,
videos: this.videos
};
}
}
}

View File

@@ -1,4 +1,4 @@
type ThumbnailType = "default" | "hqdefault" | "mqdefault" | "sddefault" | "maxresdefault" | "ultrares";
type ThumbnailType = 'default' | 'hqdefault' | 'mqdefault' | 'sddefault' | 'maxresdefault' | 'ultrares';
export class Thumbnail {
id?: string;
@@ -21,20 +21,21 @@ export class Thumbnail {
this.url = data.url || undefined;
}
displayThumbnailURL(thumbnailType: ThumbnailType = "maxresdefault"): string {
if (!["default", "hqdefault", "mqdefault", "sddefault", "maxresdefault", "ultrares"].includes(thumbnailType)) throw new Error(`Invalid thumbnail type "${thumbnailType}"!`);
if (thumbnailType === "ultrares") return this.url as string;
displayThumbnailURL(thumbnailType: ThumbnailType = 'maxresdefault'): string {
if (!['default', 'hqdefault', 'mqdefault', 'sddefault', 'maxresdefault', 'ultrares'].includes(thumbnailType))
throw new Error(`Invalid thumbnail type "${thumbnailType}"!`);
if (thumbnailType === 'ultrares') return this.url as string;
return `https://i3.ytimg.com/vi/${this.id}/${thumbnailType}.jpg`;
}
defaultThumbnailURL(id: "0" | "1" | "2" | "3" | "4"): string {
if (!id) id = "0";
if (!["0", "1", "2", "3", "4"].includes(id)) throw new Error(`Invalid thumbnail id "${id}"!`);
defaultThumbnailURL(id: '0' | '1' | '2' | '3' | '4'): string {
if (!id) id = '0';
if (!['0', '1', '2', '3', '4'].includes(id)) throw new Error(`Invalid thumbnail id "${id}"!`);
return `https://i3.ytimg.com/vi/${this.id}/${id}.jpg`;
}
toString(): string {
return this.url ? `${this.url}` : "";
return this.url ? `${this.url}` : '';
}
toJSON() {

View File

@@ -1,9 +1,9 @@
import { Channel } from "./Channel";
import { Thumbnail } from "./Thumbnail";
import { Channel } from './Channel';
import { Thumbnail } from './Thumbnail';
interface VideoOptions {
id?: string;
url? : string;
url?: string;
title?: string;
description?: string;
durationRaw: string;
@@ -12,21 +12,21 @@ interface VideoOptions {
views: number;
thumbnail?: {
id: string | undefined;
width: number | undefined ;
width: number | undefined;
height: number | undefined;
url: string | undefined;
};
channel?: {
name : string,
id : string,
icon : string
name: string;
id: string;
icon: string;
};
videos?: Video[];
type : string;
ratings : {
type: string;
ratings: {
likes: number;
dislikes: number;
}
};
live: boolean;
private: boolean;
tags: string[];
@@ -34,7 +34,7 @@ interface VideoOptions {
export class Video {
id?: string;
url? : string;
url?: string;
title?: string;
description?: string;
durationRaw: string;
@@ -50,35 +50,35 @@ export class Video {
private: boolean;
tags: string[];
constructor(data : any){
if(!data) throw new Error(`Can not initiate ${this.constructor.name} without data`)
constructor(data: any) {
if (!data) throw new Error(`Can not initiate ${this.constructor.name} without data`);
this.id = data.id || undefined;
this.url = `https://www.youtube.com/watch?v=${this.id}`
this.url = `https://www.youtube.com/watch?v=${this.id}`;
this.title = data.title || undefined;
this.description = data.description || undefined;
this.durationRaw = data.duration_raw || "0:00";
this.durationRaw = data.duration_raw || '0:00';
this.durationInSec = (data.duration < 0 ? 0 : data.duration) || 0;
this.uploadedAt = data.uploadedAt || undefined;
this.views = parseInt(data.views) || 0;
this.thumbnail = data.thumbnail || {};
this.channel = data.channel || {};
this.likes = data.ratings?.likes as number || 0;
this.likes = (data.ratings?.likes as number) || 0;
this.dislikes = data.ratings?.dislikes || 0;
this.live = !!data.live;
this.private = !!data.private;
this.tags = data.tags || [];
}
get type(): "video" {
return "video";
get type(): 'video' {
return 'video';
}
get toString(): string {
return this.url || "";
return this.url || '';
}
get toJSON(): VideoOptions{
get toJSON(): VideoOptions {
return {
id: this.id,
url: this.url,
@@ -104,4 +104,4 @@ export class Video {
private: this.private
};
}
}
}