openapi: 3.1.0
info:
  title: ytapi API
  version: 1.0.0
  description: |
    YouTube transcript & metadata API. 计费规则: 仅 HTTP 200 的请求消耗 credits（`x-credits` 扩展字段标注每个端点的费用）；
    失败请求一律 0 credit。契约源头见 `PLAN.md` §6，冻结于 M0.2。

servers:
  - url: https://api.ytapi.dev
    description: Production edge gateway (Cloudflare Worker → yt-go-api)

security:
  - bearerAuth: []

tags:
  - name: transcripts
  - name: videos
  - name: search
  - name: channels
  - name: batch

paths:
  /v1/transcripts/{videoId}:
    get:
      tags: [transcripts]
      operationId: getTranscript
      summary: 获取视频 transcript/字幕（含分段时间戳）
      description: 返回分段 transcript；`lang` 缺省取第一个可用轨道，`format` 控制 JSON/纯文本/SRT/VTT。
      x-credits: 1
      parameters:
        - $ref: '#/components/parameters/VideoId'
        - name: lang
          in: query
          schema: { type: string }
          description: BCP-47 语言代码；缺省返回第一个可用轨道
        - name: format
          in: query
          schema: { type: string, enum: [json, text, srt, vtt], default: json }
      responses:
        '200':
          description: 成功
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
            X-Cache: { $ref: '#/components/headers/XCache' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Transcript' }
        '422': { $ref: '#/components/responses/TranscriptNotAvailable' }
        '404': { $ref: '#/components/responses/VideoNotFound' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientCredits' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/transcripts/{videoId}/languages:
    get:
      tags: [transcripts]
      operationId: listTranscriptLanguages
      summary: 列出该视频可用的 transcript 语言
      description: 返回可用轨道（含 asr 自动生成标注），供调用方先探语言再取全文。
      x-credits: 0
      parameters:
        - $ref: '#/components/parameters/VideoId'
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LanguagesResponse' }

  /v1/videos/{videoId}:
    get:
      tags: [videos]
      operationId: getVideoMetadata
      summary: 获取视频元数据
      description: 标题、频道、时长、观看/点赞数、发布时间、关键词、缩略图等；点赞数解析失败时为 null。
      x-credits: 1
      parameters:
        - $ref: '#/components/parameters/VideoId'
        - $ref: '#/components/parameters/Gl'
        - $ref: '#/components/parameters/Hl'
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VideoMetadata' }
        '404': { $ref: '#/components/responses/VideoNotFound' }
        '403': { $ref: '#/components/responses/VideoPrivate' }

  /v1/search:
    get:
      tags: [search]
      operationId: search
      summary: 搜索视频或频道（每页 1 credit）
      description: 返回视频或频道结果与下一页页码；`gl`/`hl` 影响结果地域与语言。
      x-credits: 1
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
        - name: type
          in: query
          schema: { type: string, enum: [video, channel], default: video }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - $ref: '#/components/parameters/Gl'
        - $ref: '#/components/parameters/Hl'
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResponse' }

  /v1/channels/{idOrHandle}:
    get:
      tags: [channels]
      operationId: resolveChannel
      summary: 解析频道（ID / @handle / 自定义 URL → channelId + 基础信息）
      description: 0 credit 获客漏斗端点（M4.1 上线）
      x-credits: 0
      parameters:
        - name: idOrHandle
          in: path
          required: true
          schema: { type: string }
          description: channelId（UC...）、@handle 或 /c/ 自定义路径
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Channel' }

  /v1/channels/{channelId}/latest:
    get:
      tags: [channels]
      operationId: listLatestVideos
      summary: 频道最新视频（RSS 源，最多 15 条）
      description: 0 credit 获客漏斗端点（M4.1 上线）
      x-credits: 0
      parameters:
        - name: channelId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LatestVideosResponse' }

  /v1/transcripts/batch:
    post:
      tags: [batch]
      operationId: batchTranscripts
      summary: 批量获取 transcripts（≤50 个，仅成功项计费）
      description: M4.5 上线。同步返回，部分失败逐项标注 error，失败项 0 credit。
      x-credits: 成功项各 1
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchRequest' }
      responses:
        '200':
          description: 成功（部分条目可含 error）
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BatchResponse' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'API key，格式 `yta_...`，在控制台创建'

  parameters:
    VideoId:
      name: videoId
      in: path
      required: true
      schema: { type: string, minLength: 11, maxLength: 11 }
      description: 11 位 YouTube 视频 ID
    Gl:
      name: gl
      in: query
      schema: { type: string, default: US }
      description: 地区代码（影响结果地理相关性）
    Hl:
      name: hl
      in: query
      schema: { type: string, default: en }
      description: 界面语言

  headers:
    XRequestId:
      description: 请求追踪 ID，贯穿网关与源站日志
      schema: { type: string }
    XCache:
      description: 缓存命中情况（HIT=Redis L1 / L2=R2 / STALE / MISS）
      schema: { type: string, enum: [HIT, L2, STALE, MISS] }
    XRateLimitLimit:
      schema: { type: integer }
    XRateLimitRemaining:
      schema: { type: integer }
    XRateLimitReset:
      schema: { type: integer, description: 重置的 unix 时间戳（秒） }

  responses:
    Unauthorized:
      description: API key 缺失/无效/已吊销
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientCredits:
      description: 余额不足（在转发前拦截）
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: 触发速率限制，响应含 X-RateLimit-* 头
      headers:
        X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/XRateLimitReset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    VideoNotFound:
      description: 视频不存在或已删除
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    VideoPrivate:
      description: 视频为私有
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TranscriptNotAvailable:
      description: 视频无字幕/自动字幕，或指定语言不可用（message 内列可用语言）
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: >
                invalid_video_id | unauthorized | insufficient_credits | rate_limited |
                video_not_found | video_private | video_unavailable |
                transcript_not_available | transcript_language_not_available |
                upstream_error | timeout
            message: { type: string }
            request_id: { type: string }
            docs:
              type: string
              description: 'https://ytapi.dev/docs/errors#{code}'

    TranscriptSegment:
      type: object
      required: [start, text]
      properties:
        start: { type: number, description: 起始秒 }
        duration: { type: number, description: 持续秒 }
        text: { type: string }

    Transcript:
      type: object
      required: [video_id, lang, segments]
      properties:
        video_id: { type: string }
        lang: { type: string }
        is_auto_generated: { type: boolean }
        is_translated: { type: boolean }
        available_languages:
          type: array
          items: { type: string }
        segments:
          type: array
          items: { $ref: '#/components/schemas/TranscriptSegment' }

    Language:
      type: object
      required: [code, name]
      properties:
        code: { type: string }
        name: { type: string }
        is_auto_generated: { type: boolean }

    LanguagesResponse:
      type: object
      required: [video_id, languages]
      properties:
        video_id: { type: string }
        languages:
          type: array
          items: { $ref: '#/components/schemas/Language' }

    VideoMetadata:
      type: object
      required: [video_id, title, channel_id]
      properties:
        video_id: { type: string }
        title: { type: string }
        description: { type: string }
        channel_id: { type: string }
        author: { type: string }
        length_seconds: { type: integer }
        view_count: { type: integer }
        like_count: { type: [integer, 'null'], description: 三代格式解析失败时为 null }
        published_at: { type: string, format: date-time }
        keywords: { type: array, items: { type: string } }
        category: { type: string }
        is_live: { type: boolean }
        is_family_safe: { type: boolean }
        thumbnails:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              width: { type: integer }
              height: { type: integer }

    VideoResult:
      type: object
      properties:
        type: { type: string, enum: [video] }
        video_id: { type: string }
        title: { type: string }
        channel_id: { type: string }
        author: { type: string }
        length_seconds: { type: integer }
        view_count_text: { type: string, description: 展示用浏览量文本（如 "424K views"） }
        published_text: { type: string, description: 展示用发布时间文本（如 "1 month ago"） }

    ChannelResult:
      type: object
      properties:
        type: { type: string, enum: [channel] }
        channel_id: { type: string }
        name: { type: string }
        description: { type: string }
        subscriber_count_text: { type: string }
        video_count_text: { type: string }

    SearchResponse:
      type: object
      required: [query, page, items]
      properties:
        query: { type: string }
        type: { type: string, enum: [video, channel] }
        page: { type: integer }
        next_page: { type: [integer, 'null'] }
        items:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/VideoResult'
              - $ref: '#/components/schemas/ChannelResult'

    Channel:
      type: object
      required: [channel_id]
      properties:
        channel_id: { type: string }
        name: { type: string }
        handle: { type: string }
        url: { type: string }
        avatar_url: { type: string }

    LatestVideo:
      type: object
      properties:
        video_id: { type: string }
        title: { type: string }
        published_at: { type: string }

    LatestVideosResponse:
      type: object
      required: [channel_id, videos]
      properties:
        channel_id: { type: string }
        videos:
          type: array
          items: { $ref: '#/components/schemas/LatestVideo' }

    BatchRequest:
      type: object
      required: [video_ids]
      properties:
        video_ids:
          type: array
          maxItems: 50
          items: { type: string, minLength: 11, maxLength: 11 }
        lang: { type: string }

    BatchItem:
      type: object
      required: [video_id, status]
      properties:
        video_id: { type: string }
        status: { type: string, enum: [ok, error] }
        transcript: { $ref: '#/components/schemas/Transcript' }
        error: { $ref: '#/components/schemas/Error' }

    BatchResponse:
      type: object
      required: [results]
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/BatchItem' }
