Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
retrouser955 committed Jun 28, 2024
0 parents commit de5a627
Show file tree
Hide file tree
Showing 13 changed files with 3,811 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/*
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
./README.md
./LEGAL.md
Binary file added .yarn/install-state.gz
Binary file not shown.
894 changes: 894 additions & 0 deletions .yarn/releases/yarn-berry.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
yarnPath: ".yarn/releases/yarn-berry.js"
nodeLinker: node-modules
5 changes: 5 additions & 0 deletions LEGAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**Legal Notice Addressing Streaming**

Please note that streaming from third-parties such as YouTube may break their Terms of Service. This may cause these third-parties to persue legal actions against these project.

discord-player-youtubei merely provide users with tools and it is entirely up to the user (and is actively encouraged) to use this library in the way they see fit. Thus, in case of any legal/physical/digital damages caused, the author of this project (retrouser955/retro_ig) wil not be responsible for it.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Discord Player YouTubei

This is a preview the v7 version of the YouTube system that discord-player will be using made backwards compatiable with v6.

## Installation

```bash
$ npm install discord-player-youtubei
# ----------- or -----------
$ yarn add discord-player-youtubei
```

## Usage

#### Typescript and ESM

```ts
import { YoutubeiExtractor } from "discord-player-youtubei"

const player = getPlayerSomehow()

player.extractors.register(YoutubeiExtractor, {})
```

#### CommonJS

```ts
const { YoutubeiExtractor } = require("discord-player-youtubei")

const player = getPlayerSomehow()

player.extractors.register(YoutubeiExtractor, {})
```

## Signing into YouTube

With the power of youtubei.js, we can sign into YouTube through their YouTube TV API.

```ts
import { generateOauthTokens } from "discord-player-youtubei";

(async () => {
await generateOauthTokens()
})()
```

*Oauth Tokens will be printed out shortly*

These tokens can be used as an option for `YoutubeiExtractor`

```ts
import { YoutubeiExtractor } from "discord-player-youtubei"

const player = getPlayerSomehow()
const oauthTokens = getOauthTokens() // The tokens printed from `generateOauthTokens()

player.extractors.register(YoutubeiExtractor, {
authenication: oauthTokens
})
```

## Options for YoutubeiExtractor

```ts
interface YoutubeiOptions {
authenication?: OAuth2Tokens;
overrideDownloadOptions?: DownloadOptions;
createStream?: (q: string, extractor: BaseExtractor<object>) => Promise<string|Readable>;
signOutOnDeactive?: boolean;
}
```

### Notice Regarding YouTube Streaming

Streaming from YouTube is against their Terms of Service (ToS). Refer to `LEGAL.md` to view the risks using YouTube.
203 changes: 203 additions & 0 deletions lib/Extractor/Youtube.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { BaseExtractor, ExtractorStreamable, Track, SearchQueryType, QueryType, ExtractorInfo, ExtractorSearchContext, Playlist, Util } from "discord-player";
import Innertube, { type OAuth2Tokens } from "youtubei.js";
import { type DownloadOptions } from "youtubei.js/dist/src/types";
import { Readable } from "node:stream"
import { YouTubeExtractor } from "@discord-player/extractor";
import { type Video } from "youtubei.js/dist/src/parser/nodes";
import { VideoInfo } from "youtubei.js/dist/src/parser/youtube";

export interface YoutubeiOptions {
authenication?: OAuth2Tokens;
overrideDownloadOptions?: DownloadOptions;
createStream?: (q: string, extractor: BaseExtractor<object>) => Promise<string|Readable>;
signOutOnDeactive?: boolean;
}

export interface YTStreamingOptions {
extractor?: BaseExtractor<object>;
authenication?: OAuth2Tokens;
demuxable?: boolean;
overrideDownloadOptions?: DownloadOptions;
}

const DEFAULT_DOWNLOAD_OPTIONS: DownloadOptions = {
quality: "best",
format: "mp4",
type: "audio"
}


async function streamFromYT(query: string, innerTube: Innertube, options: YTStreamingOptions = { demuxable: false, overrideDownloadOptions: DEFAULT_DOWNLOAD_OPTIONS }) {
const ytId = query.includes("shorts") ? query.split("/").at(-1)!.split("?")[0]! : new URL(query).searchParams.get("v")!

if(options.demuxable) {
const readable = await innerTube.download(ytId, options.overrideDownloadOptions ?? DEFAULT_DOWNLOAD_OPTIONS)

// @ts-expect-error
const stream = Readable.fromWeb(readable)

return {
$fmt: options.overrideDownloadOptions?.format ?? "mp4",
stream
}
}

const streamData = await innerTube.getStreamingData(ytId, options.overrideDownloadOptions ?? DEFAULT_DOWNLOAD_OPTIONS)

if(!streamData.url) throw new Error("Unable to get stream data from video.")

return streamData.url
}

export class YoutubeiExtractor extends BaseExtractor<YoutubeiOptions> {
public static identifier: string = "";
public innerTube!: Innertube
public _stream!: (q: string, extractor: BaseExtractor<object>) => Promise<ExtractorStreamable>

async activate(): Promise<void> {
this.protocols = ['ytsearch', 'youtube']

this.innerTube = await Innertube.create()
if(this.options.authenication) {
try {
this.innerTube.session.signIn(this.options.authenication)
} catch (error) {
this.context.player.debug(`Unable to sign into Innertube:\n\n${error}`)
}
}

if(typeof this.options.createStream === "function") {
this._stream = this.options.createStream
} else {
this._stream = (q, _) => {
return streamFromYT(q, this.innerTube, {
overrideDownloadOptions: this.options.overrideDownloadOptions ?? DEFAULT_DOWNLOAD_OPTIONS,
demuxable: this.supportsDemux
})
}
}
}

async deactivate(): Promise<void> {
this.protocols = []
if(this.options.signOutOnDeactive) await this.innerTube.session.signOut()
}

async validate(query: string, type?: SearchQueryType | null | undefined): Promise<boolean> {
if (typeof query !== 'string') return false;
// prettier-ignore
return ([
QueryType.YOUTUBE,
QueryType.YOUTUBE_PLAYLIST,
QueryType.YOUTUBE_SEARCH,
QueryType.YOUTUBE_VIDEO,
QueryType.AUTO,
QueryType.AUTO_SEARCH
] as SearchQueryType[]).some((r) => r === type);
}

async handle(query: string, context: ExtractorSearchContext): Promise<ExtractorInfo> {
if (context.protocol === 'ytsearch') context.type = QueryType.YOUTUBE_SEARCH;
query = query.includes('youtube.com') ? query.replace(/(m(usic)?|gaming)\./, '') : query;
if (!query.includes('list=RD') && YouTubeExtractor.validateURL(query)) context.type = QueryType.YOUTUBE_VIDEO;

switch(context.type) {
case QueryType.YOUTUBE_PLAYLIST: {
const playlistUrl = new URL(query)
const plId = playlistUrl.searchParams.get("list")!
const playlist = await this.innerTube.getPlaylist(plId)

const pl = new Playlist(this.context.player, {
title: playlist.info.title ?? "UNKNOWN PLAYLIST",
thumbnail: playlist.info.thumbnails[0].url,
description: playlist.info.description ?? playlist.info.title ?? "UNKNOWN DESCRIPTION",
type: "playlist",
author: {
name: playlist.channels[0].author.name,
url: playlist.channels[0].author.url
},
tracks: [],
id: plId,
url: query,
rawPlaylist: playlist,
source: "youtube"
})

pl.tracks = (playlist.videos as Video[]).map((vid) => this.buildTrack(vid, context, pl))

return {
playlist: pl,
tracks: pl.tracks
}
}
case QueryType.YOUTUBE_VIDEO: {
const videoId = new URL(query).searchParams.get("v")!
const vid = await this.innerTube.getBasicInfo(videoId)

return {
playlist: null,
tracks: [
this.buildTrack(vid, context)
]
}
}
default: {
const search = await this.innerTube.search(query, {
type: "video"
})
const videos = (search.videos as Video[])

return {
playlist: null,
tracks: videos.map((v) => this.buildTrack(v, context))
}
}
}
}

buildTrack(vid: Video | VideoInfo, context: ExtractorSearchContext, pl?: Playlist) {
const track = vid instanceof VideoInfo ? new Track(this.context.player, {
title: vid.basic_info.title ?? "UNKNOWN TITLE",
thumbnail: vid.basic_info.thumbnail?.at(0)?.url,
description: vid.basic_info.short_description,
author: vid.basic_info.channel?.name,
requestedBy: context.requestedBy,
url: `https://youtube.com/watch?v=${vid.basic_info.id}`,
views: vid.basic_info.view_count,
duration: Util.buildTimeCode(Util.parseMS(vid.basic_info.duration ?? 0)),
raw: vid,
playlist: pl,
source: "youtube",
queryType: "youtubeVideo",
metadata: vid,
async requestMetadata() {
return vid
},
}) : new Track(this.context.player, {
title: vid.title.text ?? "UNKNOWN YOUTUBE VIDEO",
thumbnail: vid.best_thumbnail?.url ?? vid.thumbnails[0].url,
description: vid.description ?? vid.title ?? "UNKNOWN DESCRIPTION",
author: vid.author.name,
requestedBy: context.requestedBy,
url: `https://youtube.com/watch?v=${vid.id}`,
views: parseInt(vid.view_count.text ?? "0"),
duration: vid.duration.text,
raw: vid,
playlist: pl,
source: "youtube",
queryType: "youtubeVideo",
metadata: vid,
async requestMetadata() {
return vid
},
})

track.extractor = this

return track
}

stream(info: Track<unknown>): Promise<ExtractorStreamable> {
return this._stream(info.url, this)
}
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Extractor/Youtube"
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "discord-player-youtubei",
"version": "0.0.1",
"description": "An unofficial package to test the use of youtubei in discord-player v6.",
"main": "dist/index.js",
"repository": "https://github.com/retrouser955/discord-player-youtubei",
"author": "retro_ig",
"license": "Creative Commons",
"devDependencies": {
"@discord-player/extractor": "^4.4.7",
"discord-player": "^6.6.10",
"tsup": "^8.1.0",
"typescript": "^5.5.2"
},
"dependencies": {
"youtubei.js": "^10.0.0"
},
"scripts": {
"build": "tsup"
}
}
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"exclude": ["node_modules/"],
"include": ["./lib/**/*.ts"]
}
9 changes: 9 additions & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup"

export default defineConfig({
format: "cjs",
entry: ['./lib/index.ts'],
outDir: "./dist",
skipNodeModulesBundle: true,
dts: true
})
Loading

0 comments on commit de5a627

Please sign in to comment.