Skip to content

Commit

Permalink
Merge branch 'release/0.1.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
maro5397 committed Dec 22, 2024
2 parents 23fb573 + 3d6b35a commit 1c858db
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 17 deletions.
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,140 @@
# soop

[![npm version](https://img.shields.io/npm/v/soop-extension.svg?style=for-the-badge)](https://www.npmjs.com/package/soop-extension)
[![npm downloads](https://img.shields.io/npm/dm/soop-extension.svg?style=for-the-badge)](http://npm-stat.com/charts.html?package=soop-extension)
[![license](https://img.shields.io/github/license/maro5397/soop?style=for-the-badge)](https://github.com/maro5397/soop/blob/main/LICENSE)

![createAt](https://img.shields.io/github/created-at/maro5397/soop?style=for-the-badge)
![language](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)

라이브 스트리밍 서비스 숲(soop)의 비공식 API 라이브러리

라이브 스트리밍 서비스 SOOP의 비공식 API 라이브러리입니다.

현재 구현된 기능은 다음과 같습니다.

- 방송 상태 및 상세 정보 조회
- 채팅

## 설치

> Node 20.17.0 버전에서 개발되었습니다.
```bash
npm install soop-extension
```

## 예시

```ts
const streamerId = "cotton1217"
const client = new SoopClient();

// 라이브 세부정보
const liveDetail = await client.live.detail(streamerId);
console.log(liveDetail)

// 채널 정보
const stationInfo = await client.channel.station(streamerId);
console.log(stationInfo)

const soopChat = client.chat({
streamerId: streamerId
})

// 연결 성공
soopChat.on('connect', response => {
console.log(`[${response.receivedTime}] Connected to ${response.streamerId}`)
})

// 채팅방 입장
soopChat.on('enterChatRoom', response => {
console.log(`[${response.receivedTime}] Enter to ${response.streamerId}'s chat room`)
})

// 채팅방 공지
soopChat.on('notification', response => {
console.log('-'.repeat(50))
console.log(`[${response.receivedTime}]`)
console.log(response.notification.replace(/\r\n/g, '\n'))
console.log('-'.repeat(50))
})

// 일반 채팅
soopChat.on('chat', response => {
console.log(`[${response.receivedTime}] ${response.username}(${response.userId}): ${response.comment}`)
})

// 이모티콘 채팅
soopChat.on('emoticon', response => {
console.log(`[${response.receivedTime}] ${response.username}(${response.userId}): ${response.emoticonId}`)
})

// 텍스트/음성 후원 채팅
soopChat.on('textDonation', response => {
console.log(`\n[${response.receivedTime}] ${response.fromUsername}(${response.from})님이 ${response.to}님에게 ${response.amount}개 후원`)
if (Number(response.fanClubOrdinal) !== 0) {
console.log(`[${response.receivedTime}] ${response.fanClubOrdinal}번째 팬클럽 가입을 환영합니다.\n`)
} else {
console.log(`[${response.receivedTime}] 이미 팬클럽에 가입된 사용자입니다.\n`)
}
})

// 영상 후원 채팅
soopChat.on('videoDonation', response => {
console.log(`\n[${response.receivedTime}] ${response.fromUsername}(${response.from})님이 ${response.to}님에게 ${response.amount}개 후원`)
if (Number(response.fanClubOrdinal) !== 0) {
console.log(`[${response.receivedTime}] ${response.fanClubOrdinal}번째 팬클럽 가입을 환영합니다.\n`)
} else {
console.log(`[${response.receivedTime}] 이미 팬클럽에 가입된 사용자입니다.\n`)
}
})

// 애드벌룬 후원 채팅
soopChat.on('adBalloonDonation', response => {
console.log(`\n[${response.receivedTime}] ${response.fromUsername}(${response.from})님이 ${response.to}님에게 ${response.amount}개 후원`)
if (Number(response.fanClubOrdinal) !== 0) {
console.log(`[${response.receivedTime}] ${response.fanClubOrdinal}번째 팬클럽 가입을 환영합니다.\n`)
} else {
console.log(`[${response.receivedTime}] 이미 팬클럽에 가입된 사용자입니다.\n`)
}
})

// 구독 채팅
soopChat.on('subscribe', response => {
console.log(`\n[${response.receivedTime}] ${response.fromUsername}(${response.from})님이 ${response.to}님을 구독하셨습니다.`)
console.log(`[${response.receivedTime}] ${response.monthCount}개월, ${response.tier}티어\n`)
})

// 퇴장 정보
soopChat.on('exit', response => {
console.log(`\n[${response.receivedTime}] ${response.username}(${response.userId})이/가 퇴장하셨습니다\n`)
})

// 입장 정보
soopChat.on('viewer', response => {
if(response.userId.length > 1) {
console.log(`[${response.receivedTime}] 수신한 채팅방 사용자는 ${response.userId.length}명 입니다.`)
} else {
console.log(`[${response.receivedTime}] ${response.userId[0]}이/가 입장하셨습니다`)
}
})

// 방송 종료
soopChat.on('disconnect', response => {
console.log(`[${response.receivedTime}] ${response.streamerId}의 방송이 종료됨`)
})

// 특정하지 못한 패킷
soopChat.on('unknown', packet => {
console.log(packet)
})

// 패킷을 바이너리 형태로 확인
soopChat.on('raw', packet => {
console.log(packet)
})

// 채팅 연결
await soopChat.connect()
```
10 changes: 7 additions & 3 deletions example.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { SoopClient } from "./src"

(async function () {
const streamerId = "cotton1217"
const streamerId = "jingburger1"
const client = new SoopClient();

// 라이브 세부정보
const data = await client.live.detail(streamerId);
console.log(data)
const liveDetail = await client.live.detail(streamerId);
console.log(liveDetail)

// 채널 정보
const stationInfo = await client.channel.station(streamerId);
console.log(stationInfo)

const soopChat = client.chat({
streamerId: streamerId
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "soop",
"version": "0.0.1",
"name": "soop-extension",
"version": "0.1.2",
"license": "MIT",
"description": "라이브 스트리밍 서비스 숲(soop)의 비공식 API 라이브러리",
"keywords": [
Expand Down
178 changes: 178 additions & 0 deletions src/api/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {SoopClient} from "../client"
import {DEFAULT_BASE_URLS} from "../const"

interface StationInfo {
profile_image: string;
station: Station;
broad: Broad;
starballoon_top: StarBalloonTop[];
sticker_top: StickerTop[];
subscription: Subscription;
is_best_bj: boolean;
is_partner_bj: boolean;
is_ppv_bj: boolean;
is_af_supporters_bj: boolean;
is_shopfreeca_bj: boolean;
is_favorite: boolean;
is_subscription: boolean;
is_owner: boolean;
is_manager: boolean;
is_notice: boolean;
is_adsence: boolean;
is_mobile_push: boolean;
subscribe_visible: string;
country: string;
current_timestamp: string;
}

interface Station {
display: Display;
groups: Group[];
menus: Menu[];
upd: Upd;
vods: Vod[];
broad_start: string;
grade: number;
jointime: string;
station_name: string;
station_no: number;
station_title: string;
total_broad_time: number;
user_id: string;
user_nick: string;
active_no: number;
}

interface Display {
main_type: string;
title_type: string;
title_text: string;
profile_text: string;
skin_type: number;
skin_no: number;
title_skin_image: string;
}

interface Group {
idx: number;
group_no: number;
priority: number;
info: {
group_name: string;
group_class_name: string;
group_background_color: string;
};
}

interface Menu {
bbs_no: number;
station_no: number;
auth_no: number;
w_auth_no: number;
display_type: number;
rnum: number;
line: number;
indention: number;
name: string;
name_font: number;
main_view_yn: number;
view_type: number;
}

interface Upd {
station_no: number;
user_id: string;
asp_code: number;
fan_cnt: number;
today0_visit_cnt: number;
today1_visit_cnt: number;
total_visit_cnt: number;
today0_ok_cnt: number;
today1_ok_cnt: number;
today0_fav_cnt: number;
today1_fav_cnt: number;
total_ok_cnt: number;
total_view_cnt: number;
}

interface Vod {
bbs_no: number;
station_no: number;
auth_no: number;
w_auth_no: number;
display_type: number;
rnum: number;
line: number;
indention: number;
name: string;
name_font: number;
main_view_yn: number;
view_type: number;
}

interface Broad {
user_id: string;
broad_no: number;
broad_title: string;
current_sum_viewer: number;
broad_grade: number;
is_password: boolean;
}

interface StarBalloonTop {
user_id: string;
user_nick: string;
profile_image: string;
}

interface StickerTop {
user_id: string;
user_nick: string;
profile_image: string;
}

interface Subscription {
total: number;
tier1: number;
tier2: number;
}

export class SoopChannel {
private client: SoopClient

constructor(client: SoopClient) {
this.client = client
}

async station(streamerId: string, baseUrl: string = DEFAULT_BASE_URLS.soopChannelBaseUrl): Promise<StationInfo> {
return this.client.fetch(`${baseUrl}/api/${streamerId}/station`, {
method: "GET",
})
.then(response => response.json())
.then(data => {
return {
profile_image: data["profile_image"],
station: data["station"],
broad: data["broad"],
starballoon_top: data["starballoon_top"],
sticker_top: data["sticker_top"],
subscription: data["subscription"],
is_best_bj: data["is_best_bj"],
is_partner_bj: data["is_partner_bj"],
is_ppv_bj: data["is_ppv_bj"],
is_af_supporters_bj: data["is_af_supporters_bj"],
is_shopfreeca_bj: data["is_shopfreeca_bj"],
is_favorite: data["is_favorite"],
is_subscription: data["is_subscription"],
is_owner: data["is_owner"],
is_manager: data["is_manager"],
is_notice: data["is_notice"],
is_adsence: data["is_adsence"],
is_mobile_push: data["is_mobile_push"],
subscribe_visible: data["subscribe_visible"],
country: data["country"],
current_timestamp: data["current_timestamp"]
};
})
}
}
9 changes: 7 additions & 2 deletions src/api/live.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {SoopClient} from "../client"
import {DEFAULT_BASE_URLS} from "../const"

export interface LiveDetail {
CHANNEL: {
Expand Down Expand Up @@ -124,7 +125,11 @@ export class SoopLive {
this.client = client
}

async detail(streamerId: string, options: LiveDetailOptions = DEFAULT_REQUEST_BODY_FOR_LIVE_STATUS): Promise<LiveDetail> {
async detail(
streamerId: string,
options: LiveDetailOptions = DEFAULT_REQUEST_BODY_FOR_LIVE_STATUS,
baseUrl: string = DEFAULT_BASE_URLS.soopLiveBaseUrl
): Promise<LiveDetail> {
const body = {
bid: streamerId,
...(options || {})
Expand All @@ -135,7 +140,7 @@ export class SoopLive {
return acc;
}, {} as Record<string, string>)
);
return this.client.fetch(`/afreeca/player_live_api.php?bjid=${streamerId}`, {
return this.client.fetch(`${baseUrl}/afreeca/player_live_api.php?bjid=${streamerId}`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down
Loading

0 comments on commit 1c858db

Please sign in to comment.