Class: OllamaChat::Invidious

Inherits:
Object
  • Object
show all
Defined in:
lib/ollama_chat/invidious.rb

Constant Summary collapse

YOUTUBE_URLS =
%r{
  \A
  (?:https?://)?
  (?:www\.|m\.|music\.)?
  (?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)
  ([\w-]{11})
}x

Class Method Summary collapse

Class Method Details

.fetch_transcript(tracks, chat) ⇒ String? (private)

Selects the best caption track by language priority, fetches its timedtext XML from the YouTube baseUrl, and extracts the transcript text via Nokogiri.

Parameters:

  • tracks (Array<Hash>, nil) —

    captionTracks from the player response

  • chat (OllamaChat::Chat) —

    the chat instance

Returns:

  • (String, nil) —

    cleaned transcript or nil



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ollama_chat/invidious.rb', line 90

def fetch_transcript(tracks, chat)
  langs = OC::OLLAMA::CHAT::INVIDIOUS::CAPTION_LANGUAGES
  track = pick_track(tracks, langs) or return nil

  xml = nil
  chat.get_url(
    track['baseUrl'], remember: false
  ) { |tmp| xml = tmp.read }

  xml && strip_xml(xml)
end

.fetch_video_info(url, chat:) ⇒ StringIO?

Fetches video metadata (title, channel, description, duration) and, when available, a clean transcript via the Invidious companion player endpoint.

Steps:

1. POST /companion/youtubei/v1/player -> videoDetails +
 captionTracks (single response)
2. Match languageCode against configured priority regexes
3. GET the matched track's baseUrl → timedtext XML
4. Extract <text> elements via Nokogiri → plain text

Parameters:

  • url (String) —

    the YouTube source URL

  • chat (OllamaChat::Chat) —

    the chat instance (for HTTP middleware)

Returns:

  • (StringIO, nil) —

    video info + transcript as a text IO, or nil if not a YouTube URL, Invidious not configured, or video details unavailable



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/ollama_chat/invidious.rb', line 39

def fetch_video_info(url, chat:)
  invidious_url = OC::OLLAMA::CHAT::INVIDIOUS::URL? or return
  video_id = url[YOUTUBE_URLS, 1] or return

  base = invidious_url.to_s
  key  = OC::OLLAMA::CHAT::INVIDIOUS::COMPANION_KEY

  # Step 1: POST /companion/youtubei/v1/player
  player_response = nil
  chat.request_url_response(
    :post, "#{base}/companion/youtubei/v1/player",
    body:        JSON.dump('videoId' => video_id),
    headers:     {
      'Content-Type'  => 'application/json',
      'Authorization' => "Bearer #{key}",
    },
    expects:     200,
    middlewares: OllamaChat::Utils::Fetcher.middlewares,
  ) do |response|
    player_response = JSON.parse(response.body)
  rescue JSON::ParserError => e
    chat.log(:error, e, data: { url: })
  end
  return unless player_response

  details = player_response['videoDetails']
  return unless details

  header = format_details(details)

  # Steps 2-4: pick track, fetch VTT, strip (best effort)
  tracks = player_response.dig(
    'captions', 'playerCaptionsTracklistRenderer', 'captionTracks'
  )
  transcript = fetch_transcript(tracks, chat)

  text = transcript ? header + transcript
                     : header + "No captions available.\n"
  OllamaChat::Utils::Fetcher::ResponseMetadata.as_text(text)
end

.format_details(details) ⇒ String (private)

Formats videoDetails into a markdown header.

Parameters:

  • details (Hash) —

    videoDetails from the player API

Returns:

  • (String) —

    formatted header with trailing separator



139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/ollama_chat/invidious.rb', line 139

def format_details(details)
  <<~HDR.chomp
    # #{details['title']}

    **Channel:** #{details['author']}
    **Duration:** #{format_duration(details['lengthSeconds'].to_i)}

    #{details['shortDescription'].to_s.strip}

    ---

  HDR
end

.format_duration(seconds) ⇒ String (private)

Formats a duration in seconds as a human-readable string.

Parameters:

  • seconds (Integer) —

    duration in seconds

Returns:

  • (String) —

    e.g. "03:25:45"



157
158
159
# File 'lib/ollama_chat/invidious.rb', line 157

def format_duration(seconds)
  Tins::Duration.new(seconds).to_s
end

.pick_track(tracks, langs) ⇒ Hash? (private)

Iterates the priority list of language regexes and returns the first track whose languageCode matches any pattern.

Parameters:

  • tracks (Array<Hash>, nil) —

    caption entries from the API

  • langs (Array<Regexp>) —

    priority-ordered regexes

Returns:

  • (Hash, nil) —

    the selected track entry



108
109
110
111
112
113
114
115
116
117
# File 'lib/ollama_chat/invidious.rb', line 108

def pick_track(tracks, langs)
  return nil unless tracks&.any?

  langs.each do |pattern|
    track = tracks.find { |t| t['languageCode'] =~ pattern }
    return track if track
  end

  nil
end

.strip_xml(xml) ⇒ String? (private)

Extracts transcript text from YouTube timedtext XML using Nokogiri. Word-level elements are joined into a readable paragraph.

Parameters:

  • xml (String) —

    raw timedtext XML content

Returns:

  • (String, nil) —

    cleaned transcript or nil



125
126
127
128
129
130
131
132
133
# File 'lib/ollama_chat/invidious.rb', line 125

def strip_xml(xml)
  doc  = Nokogiri::XML(xml)
  text = doc.xpath('//text')
    .map { |n| n.text.strip }
    .reject(&:blank?)
    .join(' ')
    .strip
  text.full? { Nokogiri::HTML.fragment(_1).text }
end