-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
751 additions
and
502 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,8 @@ typings/ | |
# Node.js dependency directory | ||
node_modules/ | ||
|
||
.env | ||
.env* | ||
.secret.local | ||
*.http | ||
*.log | ||
streamerMaster.json |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { DocumentReference, DocumentData } from "firebase-admin/firestore"; | ||
import { Platform, Channel, BaseStream } from "../../types"; | ||
|
||
export abstract class Client { | ||
private token = ""; | ||
|
||
constructor( | ||
protected tokenDoc: DocumentReference<DocumentData>, | ||
private platform: Platform, | ||
) {} | ||
|
||
protected abstract generateToken(): Promise<string>; | ||
abstract getChannels(userIds: string[]): Promise<Channel[]>; | ||
abstract getStreams(userIds: string[]): Promise<BaseStream[]>; | ||
|
||
protected async getToken(): Promise<string> { | ||
if (this.token) return this.token; | ||
|
||
const doc = await this.tokenDoc.get(); | ||
this.token = doc.data()?.[this.platform]; | ||
|
||
if (!this.token) this.token = await this.generateToken(); | ||
|
||
return this.token; | ||
} | ||
|
||
protected async setToken(token: string): Promise<void> { | ||
this.token = token; | ||
await this.tokenDoc.update({ [this.platform]: token }); | ||
} | ||
|
||
protected async request( | ||
createRequest: (token: string) => Request, | ||
): Promise<any> { | ||
const token = await this.getToken(); | ||
|
||
const req = createRequest(token); | ||
const response = await fetch(req); | ||
|
||
if (response.ok) return response.json(); | ||
|
||
if (response.status === 401) { | ||
const newToken = await this.generateToken(); | ||
const secondResponse = await fetch(createRequest(newToken)); | ||
return secondResponse.json(); | ||
} | ||
|
||
throw new Error( | ||
`request failed.\n${response.url}\n${response.status}:${response.statusText}`, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from "./baseClient"; | ||
export * from "./youtube"; | ||
export * from "./twitch"; | ||
export * from "./twitCasting"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { DocumentReference, DocumentData } from "firebase-admin/firestore"; | ||
import { Client } from "./baseClient"; | ||
import { BaseStream, Channel, Config } from "../../types"; | ||
import { calcTTL } from "../utils"; | ||
|
||
export class TwitCastingClient extends Client { | ||
private clientId: string; | ||
private clientCode: string; | ||
private clientSecret: string; | ||
|
||
constructor( | ||
tokenDoc: DocumentReference<DocumentData>, | ||
config: Config["twitCasting"], | ||
) { | ||
super(tokenDoc, "twitCasting"); | ||
this.clientId = config.clientId.value(); | ||
this.clientCode = config.clientCode.value(); | ||
this.clientSecret = config.clientSecret.value(); | ||
} | ||
|
||
// codeの更新がCFからできないので、機能しない | ||
protected override async generateToken(): Promise<string> { | ||
const params = new URLSearchParams({ | ||
code: this.clientCode, | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
grant_type: "authorization_code", | ||
redirect_uri: "https://vspo-stream-schedule.web.app/", | ||
}); | ||
const request = new Request( | ||
"https://apiv2.twitcasting.tv/oauth2/access_token", | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: params, | ||
}, | ||
); | ||
|
||
const response = await fetch(request); | ||
|
||
if (!response.ok) | ||
throw new Error( | ||
`generateToken request failed. ${response.status}:${response.statusText}`, | ||
); | ||
|
||
const { ["access_token"]: token } = await response.json(); | ||
|
||
await this.setToken(token); | ||
return token; | ||
} | ||
|
||
override async getChannels(userIds: string[]): Promise<Channel[]> { | ||
if (!userIds.length) return []; | ||
|
||
const token = await this.getToken(); | ||
|
||
const requests = userIds.map((id) => { | ||
const createRequest = (token: string) => { | ||
return new Request(`https://apiv2.twitcasting.tv/users/${id}`, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
Accept: "application/json", | ||
"X-Api-Version": "2.0", | ||
}, | ||
}); | ||
}; | ||
|
||
// return this.request(createRequest); | ||
|
||
return fetch(createRequest(token)); | ||
}); | ||
|
||
const responses = await Promise.all(requests); | ||
|
||
if (responses.some((r) => !r.ok)) return []; | ||
|
||
const bodies = await Promise.all(responses.map((r) => r.json())); | ||
|
||
return bodies.map((v) => ({ | ||
id: v.user.screen_id, | ||
name: v.user.name, | ||
icon: v.user.image, | ||
platform: "twitCasting", | ||
})); | ||
} | ||
|
||
override async getStreams(userIds: string[]): Promise<BaseStream[]> { | ||
if (!userIds.length) return []; | ||
|
||
const token = await this.getToken(); | ||
|
||
const requests = userIds.map((id) => { | ||
const createRequest = (token: string) => { | ||
return new Request( | ||
`https://apiv2.twitcasting.tv/users/${id}/current_live`, | ||
{ | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
Accept: "application/json", | ||
"X-Api-Version": "2.0", | ||
}, | ||
}, | ||
); | ||
}; | ||
|
||
// return this.request(createRequest); | ||
|
||
return fetch(createRequest(token)); | ||
}); | ||
|
||
const responses = await Promise.all(requests); | ||
|
||
if (responses.some((r) => !r.ok)) return []; | ||
|
||
const bodies = await Promise.all(responses.map((r) => r.json())); | ||
|
||
return bodies.map((v) => { | ||
const startTime = new Date(v.movie.created * 1000).toISOString(); | ||
|
||
return { | ||
id: v.movie.id, | ||
channelId: v.movie.user_id, | ||
title: v.movie.title, | ||
thumbnail: v.movie.large_thumbnail, | ||
url: v.movie.link, | ||
scheduledStartTime: startTime, | ||
startTime, | ||
platform: "twitCasting", | ||
ttl: calcTTL(startTime, 7), | ||
}; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { DocumentReference, DocumentData } from "firebase-admin/firestore"; | ||
import { Client } from "./baseClient"; | ||
import { BaseStream, Channel, Config } from "../../types"; | ||
import { calcTTL } from "../utils"; | ||
|
||
export class TwitchClient extends Client { | ||
private clientId: string; | ||
private clientSecret: string; | ||
|
||
constructor( | ||
tokenDoc: DocumentReference<DocumentData>, | ||
config: Config["twitch"], | ||
) { | ||
super(tokenDoc, "twitch"); | ||
this.clientId = config.clientId.value(); | ||
this.clientSecret = config.clientSecret.value(); | ||
} | ||
|
||
setThumbnailSize(url: string) { | ||
return url.replace(/%?{width}/, "320").replace(/%?{height}/, "180"); | ||
} | ||
|
||
protected override async generateToken(): Promise<string> { | ||
const query = new URLSearchParams({ | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
grant_type: "client_credentials", | ||
}); | ||
const request = new Request(`https://id.twitch.tv/oauth2/token?${query}`, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
}); | ||
|
||
const response = await fetch(request); | ||
|
||
if (!response.ok) | ||
throw new Error( | ||
`generateToken request failed. ${response.status}:${response.statusText}`, | ||
); | ||
|
||
const { ["access_token"]: token } = await response.json(); | ||
|
||
await this.setToken(token); | ||
return token; | ||
} | ||
|
||
override async getChannels(userIds: string[]): Promise<Channel[]> { | ||
if (!userIds.length) return []; | ||
|
||
const createRequest = (token: string) => { | ||
const query = new URLSearchParams(userIds.map((id) => ["id", id])); | ||
return new Request(`https://api.twitch.tv/helix/users?${query}`, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
"Client-Id": this.clientId, | ||
"Content-Type": "application/json", | ||
}, | ||
}); | ||
}; | ||
|
||
const bodies = await this.request(createRequest); | ||
|
||
return bodies.data.map((v: any) => ({ | ||
id: v.id, | ||
name: v.display_name, | ||
icon: v.profile_image_url, | ||
platform: "twitch", | ||
})); | ||
} | ||
|
||
override async getStreams(userIds: string[]): Promise<BaseStream[]> { | ||
if (!userIds.length) return []; | ||
|
||
const createRequest = (token: string) => { | ||
const query = new URLSearchParams([ | ||
["first", `${userIds.length}`], | ||
...userIds.map((id) => ["user_id", id]), | ||
]); | ||
return new Request(`https://api.twitch.tv/helix/streams?${query}`, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
"Client-Id": this.clientId, | ||
"Content-Type": "application/json", | ||
}, | ||
}); | ||
}; | ||
|
||
const body = await this.request(createRequest); | ||
|
||
return body.data.map((v: any) => ({ | ||
id: v.id, | ||
channelId: v.user_id, | ||
title: v.title, | ||
thumbnail: this.setThumbnailSize(v.thumbnail_url), | ||
url: `https://www.twitch.tv/${v.user_login}`, | ||
scheduledStartTime: v.started_at, | ||
startTime: v.started_at, | ||
platform: "twitch", | ||
ttl: calcTTL(v.started_at, 7), | ||
})); | ||
} | ||
|
||
async updateStreamToVideo<T extends BaseStream>(stream: T): Promise<T> { | ||
const createRequest = (token: string) => { | ||
const query = new URLSearchParams([ | ||
["user_id", stream.channelId], | ||
["type", "archive"], | ||
["first", "1"], | ||
]); | ||
return new Request(`https://api.twitch.tv/helix/videos?${query}`, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
"Client-Id": this.clientId, | ||
"Content-Type": "application/json", | ||
}, | ||
}); | ||
}; | ||
|
||
const result = await this.request(createRequest); | ||
const video = result.data.shift(); | ||
|
||
if (stream.id !== video.stream_id) throw new Error("can not updated."); | ||
|
||
return { | ||
...stream, | ||
url: video.url, | ||
thumbnail: this.setThumbnailSize(video.thumbnail_url), | ||
}; | ||
} | ||
} |
Oops, something went wrong.