
[![npm version](https://badge.fury.io/js/opex-yt-info.svg)](https://badge.fury.io/js/opex-yt-info)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fnpmjs.com%2Fopex-yt-info&countColor=%23263759&style=flat)](https://visitorbadge.io/status?path=https%3A%2F%2Fnpmjs.com%2Fopex-yt-info)

# Opex YouTube Search / Поиск Opex YouTube

[English](#english) | [Русский](#русский)

---

## English

A Node.js library (ESM) providing separate, intuitive functions for searching YouTube content (videos, channels, live streams) and retrieving basic metadata for videos and playlists.

### Features

*   **Clear Separation:** Dedicated functions like `searchVideos`, `searchChannels`, `searchLive`, `getVideo`, `getPlaylist`.
*   **Promise-based:** All functions return Promises for easy async/await usage.
*   **Type Definitions:** Includes TypeScript definitions for better developer experience.
*   **Search Options:** Supports common search options like language (`hl`), region (`gl`), pagination (`pageStart`, `pageEnd`), and User-Agent.
*   **URL/ID Flexibility:** `getVideo` and `getPlaylist` accept both YouTube URLs and IDs.
*   **Combined Search:** `searchAll` function to get videos, channels, and live streams in one call.

### Installation

```bash
npm install opex-yt-info
# or
yarn add opex-yt-info
# or
pnpm add opex-yt-info
```

### Usage (ESM)

```javascript
import {
  searchVideos,
  searchChannels,
  searchLive,
  searchAll,
  getVideo,
  getPlaylist
} from 'opex-yt-info';

// --- Search Examples ---

async function runSearches() {
  try {
    console.log('--- Searching Videos ---');
    const videos = await searchVideos('lofi hip hop radio', { pageEnd: 1 });
    if (videos.length > 0) {
        videos.slice(0, 3).forEach(v => console.log(`[Video] ${v.title} (${v.timestamp}) - ${v.views} views`));
    } else {
        console.log('No videos found.');
    }


    console.log('\n--- Searching Channels ---');
    const channels = await searchChannels('freecodecamp', { pageEnd: 1 });
     if (channels.length > 0) {
        channels.slice(0, 1).forEach(c => console.log(`[Channel] ${c.name} (${c.subCountLabel})`));
    } else {
        console.log('No channels found.');
    }

    console.log('\n--- Searching Live ---');
    const liveStreams = await searchLive('live news', { pageEnd: 1 });
    if (liveStreams.length > 0) {
        liveStreams.slice(0, 3).forEach(l => console.log(`[Live] ${l.title} (${l.watching} watching)`));
    } else {
        console.log('No live streams found (or none matching "live news" currently).');
    }


     console.log('\n--- Searching All (Videos, Channels, Live, maybe Playlists) ---');
    const allResults = await searchAll('synthwave', { pageEnd: 1 });
     if (allResults.length > 0) {
        allResults.slice(0, 5).forEach(item => {
            const title = item.title || item.name || 'Unknown Title'; // Use name for channel
            console.log(`[All:${item.type}] ${title}`);
        });
    } else {
        console.log('No results found for "synthwave".');
    }

  } catch (error) {
    console.error('Search failed:', error);
  }
}

// --- Get Metadata Examples ---

async function runGetters() {
  try {
    const videoIdOrUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; // Rick Astley - Never Gonna Give You Up
    console.log(`\n--- Getting Video Info [${videoIdOrUrl}] ---`);
    const video = await getVideo(videoIdOrUrl);
    if (video) {
      console.log(`Title: ${video.title}`);
      console.log(`Views: ${video.views}`);
      console.log(`Upload Date: ${video.uploadDate}`);
      console.log(`Genre: ${video.genre}`);
    } else {
      console.log('Video not found or error fetching.');
    }

    const playlistIdOrUrl = 'PLzDFYTz8KSyBUDaNaPOKVjomV2lCm5W4G'; // Example playlist ID
    console.log(`\n--- Getting Playlist Info [${playlistIdOrUrl}] ---`);
    const playlist = await getPlaylist(playlistIdOrUrl);
     if (playlist) {
      console.log(`Title: ${playlist.title}`);
      console.log(`Video Count: ${playlist.size}`);
      console.log(`Update Date: ${playlist.date}`);
      console.log(`First video: ${playlist.videos[0]?.title} (${playlist.videos[0]?.duration.timestamp})`);
    } else {
      console.log('Playlist not found or error fetching.');
    }

  } catch (error) {
    console.error('Metadata fetch failed:', error);
  }
}


// Run the examples
await runSearches();
await runGetters();

```

### API

*(See `index.d.ts` for detailed TypeScript definitions)*

#### Search Functions

All search functions accept `query` (string) and optional `options` (object).
Options include: `pageStart`, `pageEnd`, `hl`, `gl`, `category`, `sp`, `userAgent`.

*   **`searchVideos(query, [options])`**: Returns `Promise<Video[]>`
*   **`searchChannels(query, [options])`**: Returns `Promise<Channel[]>`
*   **`searchLive(query, [options])`**: Returns `Promise<LiveVideo[]>`
*   **`searchAll(query, [options])`**: Returns `Promise<Array<Video | Channel | LiveVideo | PlaylistSummary>>`

#### Metadata Functions

These functions accept an ID or URL (string) and optional `options` (object).
Options include: `hl`, `gl`, `userAgent`.

*   **`getVideo(videoIdOrUrl, [options])`**: Accepts Video ID or URL. Returns `Promise<VideoDetail | null>`
*   **`getPlaylist(playlistIdOrUrl, [options])`**: Accepts Playlist ID or URL. Returns `Promise<PlaylistDetail | null>`

#### Returned Object Structures

##### `Video` (from search)
```typescript
{
  type: 'video';
  videoId: string;
  url: string;
  title: string;
  description: string; // May be truncated
  image: string;       // Thumbnail URL
  thumbnail: string;   // Thumbnail URL
  seconds: number;     // Duration in seconds
  timestamp: string;   // Duration formatted (e.g., "3:10")
  duration: { seconds: number; timestamp: string; toString: () => string; };
  ago: string;         // Upload time relative (e.g., "1 year ago")
  views: number;
  author: { name: string; url: string; };
}
```

##### `Channel` (from search)
```typescript
{
  type: 'channel';
  name: string;
  url: string;
  baseUrl?: string;    // Relative URL path
  id?: string;         // Channel ID (if available)
  title: string;
  about?: string;      // Short description
  image: string;       // Avatar URL
  thumbnail: string;   // Avatar URL
  videoCount: number;
  videoCountLabel: string; // e.g., "1.2K videos"
  verified: boolean;
  subCount: number;    // Approximate subscriber count
  subCountLabel: string; // e.g., "1.2M subscribers"
}
```

##### `LiveVideo` (from search)
```typescript
{
  type: 'live';
  videoId: string;
  url: string;
  title: string;
  description: string;
  image: string;
  thumbnail: string;
  watching: number;    // Current viewers
  author: { name: string; url: string; };
  status: 'LIVE' | 'UPCOMING';
  startTime?: number;  // Unix timestamp (ms) for upcoming
  startDate?: string;  // Formatted date string for upcoming
}
```

##### `PlaylistSummary` (from `searchAll`)
```typescript
{
    type: 'list';
    listId: string;
    url: string;
    title: string;
    thumbnail: string;
    image: string; // Alias
    videoCount: number;
    author: { name: string; url: string; };
}
```

##### `VideoDetail` (from `getVideo`)
*Inherits from `Video` and adds fields potentially available from the underlying lookup:*
```typescript
{
  // ... all properties from Video ...
  type: 'video';
  genre?: string;
  uploadDate?: string; // e.g., "2023-10-27"
}
```

##### `PlaylistDetail` (from `getPlaylist`)
```typescript
{
  title: string;
  listId: string;
  url: string;
  size?: number;       // Video count
  views?: number;      // Playlist views (if available)
  date?: string;       // Last updated date (YYYY-MM-DD) if available
  image?: string;      // Playlist thumbnail URL
  thumbnail?: string;  // Playlist thumbnail URL
  videos: Array<{      // Array of video summaries within the playlist
    title: string;
    videoId: string;
    listId: string;
    thumbnail: string;
    duration: { seconds: number; timestamp: string; toString: () => string; };
    author: { name: string; url: string; };
  }>;
  author: { name: string; url: string; }; // Playlist creator
}
```

### License

MIT

---

## Русский

Node.js библиотека (ESM), предоставляющая отдельные, интуитивно понятные функции для поиска контента YouTube (видео, каналы, трансляции) и получения базовых метаданных для видео и плейлистов.

### Возможности

*   **Четкое разделение:** Выделенные функции, такие как `searchVideos`, `searchChannels`, `searchLive`, `getVideo`, `getPlaylist`.
*   **На основе Promise:** Все функции возвращают Promise для удобного использования с async/await.
*   **Определения типов:** Включает определения TypeScript для улучшения опыта разработки.
*   **Опции поиска:** Поддерживает общие опции поиска, такие как язык (`hl`), регион (`gl`), пагинация (`pageStart`, `pageEnd`) и User-Agent.
*   **Гибкость URL/ID:** `getVideo` и `getPlaylist` принимают как URL YouTube, так и ID.
*   **Общий поиск:** Функция `searchAll` для получения видео, каналов и трансляций одним вызовом.

### Установка

```bash
npm install opex-yt-info
# или
yarn add opex-yt-info
# или
pnpm add opex-yt-info
```

### Использование (ESM)

```javascript
import {
  searchVideos,
  searchChannels,
  searchLive,
  searchAll,
  getVideo,
  getPlaylist
} from 'opex-yt-info';

// --- Примеры поиска ---

async function runSearches() {
  try {
    console.log('--- Поиск Видео ---');
    const videos = await searchVideos('lofi hip hop radio', { pageEnd: 1 });
     if (videos.length > 0) {
        videos.slice(0, 3).forEach(v => console.log(`[Видео] ${v.title} (${v.timestamp}) - ${v.views} просмотров`));
    } else {
        console.log('Видео не найдены.');
    }

    console.log('\n--- Поиск Каналов ---');
    const channels = await searchChannels('freecodecamp', { pageEnd: 1 });
    if (channels.length > 0) {
        channels.slice(0, 1).forEach(c => console.log(`[Канал] ${c.name} (${c.subCountLabel})`));
    } else {
        console.log('Каналы не найдены.');
    }

    console.log('\n--- Поиск Трансляций ---');
    const liveStreams = await searchLive('live news', { pageEnd: 1 });
    if (liveStreams.length > 0) {
        liveStreams.slice(0, 3).forEach(l => console.log(`[Live] ${l.title} (${l.watching} смотрят)`));
    } else {
        console.log('Трансляции не найдены (или нет совпадений с "live news" в данный момент).');
    }

     console.log('\n--- Поиск Всего (Видео, Каналы, Live, возможно Плейлисты) ---');
    const allResults = await searchAll('synthwave', { pageEnd: 1 });
    if (allResults.length > 0) {
        allResults.slice(0, 5).forEach(item => {
            const title = item.title || item.name || 'Неизвестное название'; // Используем name для канала
            console.log(`[Все:${item.type}] ${title}`);
        });
    } else {
        console.log('Результаты для "synthwave" не найдены.');
    }

  } catch (error) {
    console.error('Поиск не удался:', error);
  }
}

// --- Примеры получения метаданных ---

async function runGetters() {
  try {
    const videoIdOrUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; // Rick Astley - Never Gonna Give You Up
    console.log(`\n--- Получение информации о видео [${videoIdOrUrl}] ---`);
    const video = await getVideo(videoIdOrUrl);
    if (video) {
      console.log(`Название: ${video.title}`);
      console.log(`Просмотры: ${video.views}`);
      console.log(`Дата загрузки: ${video.uploadDate}`);
      console.log(`Жанр: ${video.genre}`);
    } else {
      console.log('Видео не найдено или ошибка загрузки.');
    }

    const playlistIdOrUrl = 'PLzDFYTz8KSyBUDaNaPOKVjomV2lCm5W4G'; // Пример ID плейлиста
    console.log(`\n--- Получение информации о плейлисте [${playlistIdOrUrl}] ---`);
    const playlist = await getPlaylist(playlistIdOrUrl);
     if (playlist) {
      console.log(`Название: ${playlist.title}`);
      console.log(`Кол-во видео: ${playlist.size}`);
      console.log(`Дата обновления: ${playlist.date}`);
      console.log(`Первое видео: ${playlist.videos[0]?.title} (${playlist.videos[0]?.duration.timestamp})`);
    } else {
      console.log('Плейлист не найден или ошибка загрузки.');
    }

  } catch (error) {
    console.error('Получение метаданных не удалось:', error);
  }
}

// Запуск примеров
await runSearches();
await runGetters();
```

### API

*(См. `index.d.ts` для подробных определений TypeScript)*

#### Функции Поиска

Все функции поиска принимают `query` (string) и опциональный `options` (object).
Опции включают: `pageStart`, `pageEnd`, `hl`, `gl`, `category`, `sp`, `userAgent`.

*   **`searchVideos(query, [options])`**: Возвращает `Promise<Video[]>`
*   **`searchChannels(query, [options])`**: Возвращает `Promise<Channel[]>`
*   **`searchLive(query, [options])`**: Возвращает `Promise<LiveVideo[]>`
*   **`searchAll(query, [options])`**: Возвращает `Promise<Array<Video | Channel | LiveVideo | PlaylistSummary>>`

#### Функции Получения Метаданных

Эти функции принимают ID или URL (string) и опциональный `options` (object).
Опции включают: `hl`, `gl`, `userAgent`.

*   **`getVideo(videoIdOrUrl, [options])`**: Принимает ID или URL видео. Возвращает `Promise<VideoDetail | null>`
*   **`getPlaylist(playlistIdOrUrl, [options])`**: Принимает ID или URL плейлиста. Возвращает `Promise<PlaylistDetail | null>`

#### Структуры Возвращаемых Объектов

##### `Video` (из поиска)
```typescript
{
  type: 'video';
  videoId: string;
  url: string;
  title: string;
  description: string; // Может быть усеченным
  image: string;       // URL превью
  thumbnail: string;   // URL превью
  seconds: number;     // Длительность в секундах
  timestamp: string;   // Длительность в формате "Ч:ММ:СС"
  duration: { seconds: number; timestamp: string; toString: () => string; };
  ago: string;         // Как давно загружено (напр., "1 год назад")
  views: number;
  author: { name: string; url: string; };
}
```

##### `Channel` (из поиска)
```typescript
{
  type: 'channel';
  name: string;
  url: string;
  baseUrl?: string;    // Относительный URL
  id?: string;         // ID канала
  title: string;
  about?: string;      // Краткое описание
  image: string;       // URL аватара
  thumbnail: string;   // URL аватара
  videoCount: number;
  videoCountLabel: string; // напр., "1.2K видео"
  verified: boolean;
  subCount: number;    // Примерное кол-во подписчиков
  subCountLabel: string; // напр., "1.2M подписчиков"
}
```

##### `LiveVideo` (из поиска)
```typescript
{
  type: 'live';
  videoId: string;
  url: string;
  title: string;
  description: string;
  image: string;
  thumbnail: string;
  watching: number;    // Зрителей сейчас
  author: { name: string; url: string; };
  status: 'LIVE' | 'UPCOMING';
  startTime?: number;  // Unix timestamp (ms) для предстоящих
  startDate?: string;  // Форматированная дата для предстоящих
}
```

##### `PlaylistSummary` (из `searchAll`)
```typescript
{
    type: 'list';
    listId: string;
    url: string;
    title: string;
    thumbnail: string;
    image: string; // Alias
    videoCount: number;
    author: { name: string; url: string; };
}
```

##### `VideoDetail` (из `getVideo`)
*Наследует от `Video` и добавляет поля, потенциально доступные при запросе по ID:*
```typescript
{
  // ... все поля из Video ...
  type: 'video';
  genre?: string;
  uploadDate?: string; // напр., "2023-10-27"
}
```

##### `PlaylistDetail` (из `getPlaylist`)
```typescript
{
  title: string;
  listId: string;
  url: string;
  size?: number;       // Кол-во видео
  views?: number;      // Просмотры плейлиста (если есть)
  date?: string;       // Дата последнего обновления (YYYY-MM-DD) если есть
  image?: string;      // URL превью плейлиста
  thumbnail?: string;  // URL превью плейлиста
  videos: Array<{      // Массив кратких данных о видео в плейлисте
    title: string;
    videoId: string;
    listId: string;
    thumbnail: string;
    duration: { seconds: number; timestamp: string; toString: () => string; };
    author: { name: string; url: string; };
  }>;
  author: { name: string; url: string; }; // Создатель плейлиста
}
```

### Лицензия

MIT