rill-node 0.0.0-stage → 2026.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +64 -2
  4. package/binding.gyp +31 -0
  5. package/index.d.ts +45 -0
  6. package/index.js +112 -0
  7. package/native/darwin-arm64/librill.dylib +0 -0
  8. package/native/darwin-arm64/rill-media +0 -0
  9. package/native/darwin-arm64/rill_node.node +0 -0
  10. package/native/darwin-x64/librill.dylib +0 -0
  11. package/native/darwin-x64/rill-media +0 -0
  12. package/native/darwin-x64/rill_node.node +0 -0
  13. package/native/linux-arm64-gnu/librill.so +0 -0
  14. package/native/linux-arm64-gnu/rill-media +0 -0
  15. package/native/linux-arm64-gnu/rill_node.node +0 -0
  16. package/native/linux-x64-gnu/librill.so +0 -0
  17. package/native/linux-x64-gnu/rill-media +0 -0
  18. package/native/linux-x64-gnu/rill_node.node +0 -0
  19. package/native-src/include/rill.h +90 -0
  20. package/native-src/include/rill_media.h +22 -0
  21. package/native-src/shard.yml +9 -0
  22. package/native-src/src/c_api.cr +194 -0
  23. package/native-src/src/cli.cr +637 -0
  24. package/native-src/src/rill/c_api/job.cr +336 -0
  25. package/native-src/src/rill/c_api/memory.cr +32 -0
  26. package/native-src/src/rill/c_api/status.cr +25 -0
  27. package/native-src/src/rill/c_api/sync.cr +125 -0
  28. package/native-src/src/rill/c_api/worker_pool.cr +403 -0
  29. package/native-src/src/rill/cancellation.cr +29 -0
  30. package/native-src/src/rill/client.cr +52 -0
  31. package/native-src/src/rill/cookie_jar.cr +130 -0
  32. package/native-src/src/rill/dash/manifest.cr +279 -0
  33. package/native-src/src/rill/dash/parser.cr +227 -0
  34. package/native-src/src/rill/download_options.cr +96 -0
  35. package/native-src/src/rill/download_plan.cr +27 -0
  36. package/native-src/src/rill/download_progress.cr +16 -0
  37. package/native-src/src/rill/downloader.cr +662 -0
  38. package/native-src/src/rill/errors.cr +155 -0
  39. package/native-src/src/rill/extractor.cr +18 -0
  40. package/native-src/src/rill/extractor_registry.cr +98 -0
  41. package/native-src/src/rill/extractors/bandcamp.cr +265 -0
  42. package/native-src/src/rill/extractors/dash.cr +77 -0
  43. package/native-src/src/rill/extractors/direct_media.cr +73 -0
  44. package/native-src/src/rill/extractors/generic.cr +441 -0
  45. package/native-src/src/rill/extractors/hls.cr +202 -0
  46. package/native-src/src/rill/extractors/internet_archive.cr +180 -0
  47. package/native-src/src/rill/extractors/m3u.cr +81 -0
  48. package/native-src/src/rill/extractors/media_ccc_de.cr +219 -0
  49. package/native-src/src/rill/extractors/mixcloud.cr +495 -0
  50. package/native-src/src/rill/extractors/peer_tube.cr +226 -0
  51. package/native-src/src/rill/extractors/rumble.cr +325 -0
  52. package/native-src/src/rill/extractors/soundcloud.cr +553 -0
  53. package/native-src/src/rill/extractors/spotify.cr +184 -0
  54. package/native-src/src/rill/extractors/twitch_clip.cr +273 -0
  55. package/native-src/src/rill/extractors/youtube.cr +520 -0
  56. package/native-src/src/rill/format.cr +82 -0
  57. package/native-src/src/rill/format_selector.cr +143 -0
  58. package/native-src/src/rill/hls/parser.cr +257 -0
  59. package/native-src/src/rill/hls/playlist.cr +100 -0
  60. package/native-src/src/rill/http_client.cr +503 -0
  61. package/native-src/src/rill/http_url.cr +28 -0
  62. package/native-src/src/rill/json_value.cr +66 -0
  63. package/native-src/src/rill/media.cr +151 -0
  64. package/native-src/src/rill/media_muxer.cr +333 -0
  65. package/native-src/src/rill/mime_type.cr +58 -0
  66. package/native-src/src/rill/output_file_name.cr +91 -0
  67. package/native-src/src/rill/playlist.cr +109 -0
  68. package/native-src/src/rill/request_options.cr +179 -0
  69. package/native-src/src/rill/segment_assembly.cr +70 -0
  70. package/native-src/src/rill/timestamp.cr +34 -0
  71. package/native-src/src/rill/version.cr +3 -0
  72. package/native-src/src/rill.cr +47 -0
  73. package/package.json +48 -4
  74. package/scripts/build-native.js +46 -0
  75. package/scripts/install-gyp.js +10 -0
  76. package/scripts/install.js +19 -0
  77. package/scripts/lib.js +1 -0
  78. package/scripts/libdir.js +23 -0
  79. package/scripts/package-native.js +35 -0
  80. package/scripts/platform.js +41 -0
  81. package/scripts/prepack.js +47 -0
  82. package/src/addon.c +498 -0
@@ -0,0 +1,520 @@
1
+ require "http/params"
2
+ require "json"
3
+ require "set"
4
+ require "uri"
5
+
6
+ require "../errors"
7
+ require "../extractor"
8
+ require "../format"
9
+ require "../http_url"
10
+ require "../json_value"
11
+ require "../media"
12
+ require "../mime_type"
13
+ require "../timestamp"
14
+
15
+ module Rill::Extractors
16
+ class YouTube < Extractor
17
+ DEFAULT_API_BASE = "https://www.youtube.com"
18
+ API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
19
+ # NOTE: full browser user agents make YouTube exceed Crystal's 16 KB header limit.
20
+ WATCH_PAGE_USER_AGENT = "Mozilla/5.0"
21
+ VISIONOS_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) " \
22
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) " \
23
+ "Version/26.0 Safari/605.1.15"
24
+ VIDEO_ID = /\A[A-Za-z0-9_-]{11}\z/
25
+
26
+ HOSTS = {
27
+ "youtube.com",
28
+ "www.youtube.com",
29
+ "m.youtube.com",
30
+ "music.youtube.com",
31
+ "youtu.be",
32
+ "www.youtu.be",
33
+ "youtube-nocookie.com",
34
+ "www.youtube-nocookie.com",
35
+ }
36
+
37
+ struct PlayerClient
38
+ getter name : String
39
+ getter identifier : String
40
+ getter version : String
41
+ getter user_agent : String
42
+ getter context : Hash(String, String)
43
+
44
+ def initialize(
45
+ @name : String,
46
+ @identifier : String,
47
+ @version : String,
48
+ @user_agent : String,
49
+ @context : Hash(String, String),
50
+ )
51
+ end
52
+ end
53
+
54
+ struct Bootstrap
55
+ getter visitor_data : String?
56
+ getter api_key : String?
57
+
58
+ def initialize(@visitor_data : String? = nil, @api_key : String? = nil)
59
+ end
60
+ end
61
+
62
+ VISIONOS = PlayerClient.new(
63
+ "visionos",
64
+ "101",
65
+ "1.02",
66
+ VISIONOS_USER_AGENT,
67
+ {
68
+ "clientName" => "VISIONOS",
69
+ "deviceMake" => "Apple",
70
+ "deviceModel" => "RealityDevice17,1",
71
+ "osName" => "visionOS",
72
+ "osVersion" => "26.5.23O471",
73
+ },
74
+ )
75
+
76
+ TV_SAMSUNG = PlayerClient.new(
77
+ "tv_samsung",
78
+ "7",
79
+ "5.20260707",
80
+ "Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebKit/538.1 " \
81
+ "(KHTML, like Gecko) Version/2.4.0 TV Safari/538.1",
82
+ {
83
+ "clientName" => "TVHTML5",
84
+ "deviceMake" => "Samsung",
85
+ "deviceModel" => "UKS9800",
86
+ "osName" => "Tizen",
87
+ "osVersion" => "2.4.0",
88
+ },
89
+ )
90
+
91
+ CLIENTS = {VISIONOS, TV_SAMSUNG}
92
+
93
+ SEARCH_CLIENT = PlayerClient.new(
94
+ "web",
95
+ "1",
96
+ "2.20260707.01.00",
97
+ "Mozilla/5.0",
98
+ {"clientName" => "WEB"},
99
+ )
100
+
101
+ struct SearchResult
102
+ getter video_id : String
103
+ getter title : String
104
+ getter duration_seconds : Float64?
105
+
106
+ def initialize(
107
+ @video_id : String,
108
+ @title : String,
109
+ @duration_seconds : Float64? = nil,
110
+ )
111
+ end
112
+ end
113
+
114
+ def initialize(@api_base : String = DEFAULT_API_BASE)
115
+ end
116
+
117
+ def name : String
118
+ "youtube"
119
+ end
120
+
121
+ def matches?(url : URI) : Bool
122
+ return false unless url.scheme == "http" || url.scheme == "https"
123
+
124
+ !video_id_for(url).nil?
125
+ end
126
+
127
+ def extract(url : URI, client : HTTPClient) : Media
128
+ video_id = video_id_for(url)
129
+ unless video_id
130
+ raise UnsupportedUrlError.new(url.to_s)
131
+ end
132
+
133
+ bootstrap = watch_page(client, video_id)
134
+ errors = [] of String
135
+
136
+ CLIENTS.each do |player_client|
137
+ begin
138
+ payload = player_payload(client, player_client, video_id, bootstrap, url)
139
+
140
+ if reason = playability_error(payload)
141
+ errors << "#{player_client.name}: #{reason}"
142
+ next
143
+ end
144
+
145
+ details = payload["videoDetails"]?.try(&.as_h?)
146
+ unless details
147
+ errors << "#{player_client.name}: video details are missing"
148
+ next
149
+ end
150
+
151
+ if details["isLiveContent"]?.try(&.as_bool?)
152
+ raise MediaUnavailableError.new(url.to_s, "live videos are not supported")
153
+ end
154
+
155
+ formats = formats_from(payload["streamingData"]?)
156
+ if formats.empty?
157
+ errors << "#{player_client.name}: no direct formats; JavaScript challenge support is required"
158
+ next
159
+ end
160
+
161
+ return build_media(details, payload, formats, video_id, url)
162
+ rescue ex : MediaUnavailableError
163
+ errors << "#{player_client.name}: #{ex.message}"
164
+ rescue ex : HTTPError
165
+ errors << "#{player_client.name}: HTTP #{ex.status.code}"
166
+ end
167
+ end
168
+
169
+ raise MediaUnavailableError.new(url.to_s, errors.uniq.join("; "))
170
+ end
171
+
172
+ def search(query : String, client : HTTPClient, limit : Int32 = 5) : Array(SearchResult)
173
+ body = JSON.build do |json|
174
+ json.object do
175
+ json.field "context" do
176
+ json.object do
177
+ json.field "client" do
178
+ json.object do
179
+ json.field "clientName", SEARCH_CLIENT.context["clientName"]
180
+ json.field "clientVersion", SEARCH_CLIENT.version
181
+ json.field "hl", "en"
182
+ json.field "gl", "US"
183
+ end
184
+ end
185
+ end
186
+ end
187
+ json.field "query", query
188
+ json.field "params", "EgIQAQ%3D%3D"
189
+ end
190
+ end
191
+
192
+ headers = HTTP::Headers.new
193
+ headers["User-Agent"] = SEARCH_CLIENT.user_agent
194
+ headers["X-YouTube-Client-Name"] = SEARCH_CLIENT.identifier
195
+ headers["X-YouTube-Client-Version"] = SEARCH_CLIENT.version
196
+ headers["Origin"] = @api_base
197
+ headers["Referer"] = "#{@api_base}/"
198
+
199
+ endpoint = "#{@api_base}/youtubei/v1/search?key=#{API_KEY}&prettyPrint=false"
200
+ response = client.post(endpoint, body, headers)
201
+ payload = JSON.parse(response.body)
202
+
203
+ results = [] of SearchResult
204
+ collect_search_results(payload, results, limit)
205
+ results
206
+ rescue JSON::ParseException
207
+ raise MediaUnavailableError.new("#{@api_base}/", "search response is not valid JSON")
208
+ rescue ex : HTTPError
209
+ raise MediaUnavailableError.new("#{@api_base}/", "search request failed (#{ex.status.code})")
210
+ end
211
+
212
+ private def collect_search_results(
213
+ value : JSON::Any,
214
+ results : Array(SearchResult),
215
+ limit : Int32,
216
+ ) : Nil
217
+ return if results.size >= limit
218
+
219
+ if hash = value.as_h?
220
+ if renderer = hash["videoRenderer"]?.try(&.as_h?)
221
+ result = search_result(renderer)
222
+ results << result if result
223
+ end
224
+
225
+ hash.each_value do |child|
226
+ collect_search_results(child, results, limit)
227
+ return if results.size >= limit
228
+ end
229
+ elsif array = value.as_a?
230
+ array.each do |child|
231
+ collect_search_results(child, results, limit)
232
+ return if results.size >= limit
233
+ end
234
+ end
235
+ end
236
+
237
+ private def search_result(renderer : Hash(String, JSON::Any)) : SearchResult?
238
+ video_id = JsonValue.string(renderer["videoId"]?)
239
+ return nil unless video_id
240
+
241
+ title = text_runs(renderer["title"]?) || video_id
242
+ duration = Timestamp.clock_seconds(text_runs(renderer["lengthText"]?))
243
+
244
+ SearchResult.new(video_id, title, duration)
245
+ end
246
+
247
+ private def text_runs(value : JSON::Any?) : String?
248
+ hash = value.try(&.as_h?)
249
+ return nil unless hash
250
+
251
+ if runs = hash["runs"]?.try(&.as_a?)
252
+ text = runs.compact_map do |run|
253
+ run_hash = run.as_h?
254
+ JsonValue.string(run_hash.try(&.["text"]?))
255
+ end.join
256
+
257
+ return text unless text.empty?
258
+ end
259
+
260
+ JsonValue.string(hash["simpleText"]?)
261
+ end
262
+
263
+ private def build_media(
264
+ details : Hash(String, JSON::Any),
265
+ payload : Hash(String, JSON::Any),
266
+ formats : Array(Format),
267
+ video_id : String,
268
+ url : URI,
269
+ ) : Media
270
+ Media.new(
271
+ id: JsonValue.string(details["videoId"]?) || video_id,
272
+ title: JsonValue.string(details["title"]?) || video_id,
273
+ description: JsonValue.string(details["shortDescription"]?),
274
+ webpage_url: url,
275
+ uploader: JsonValue.string(details["author"]?),
276
+ duration_seconds: JsonValue.seconds(details["lengthSeconds"]?),
277
+ thumbnail_url: thumbnail_url(details),
278
+ upload_date: upload_date(payload),
279
+ extractor: name,
280
+ formats: formats,
281
+ )
282
+ end
283
+
284
+ private def watch_page(client : HTTPClient, video_id : String) : Bootstrap
285
+ headers = HTTP::Headers.new
286
+ headers["User-Agent"] = WATCH_PAGE_USER_AGENT
287
+ headers["Accept-Language"] = "en-US,en;q=0.9"
288
+
289
+ endpoint = "#{@api_base}/watch?v=#{video_id}&bpctr=9999999999&has_verified=1"
290
+ body = client.get(endpoint, headers).body
291
+
292
+ visitor = body.match(/"VISITOR_DATA":"([^"]+)"/) ||
293
+ body.match(/"visitorData":"([^"]+)"/)
294
+ api_key = body.match(/"INNERTUBE_API_KEY":"([^"]+)"/)
295
+
296
+ Bootstrap.new(visitor.try(&.[1]), api_key.try(&.[1]))
297
+ rescue Rill::Error
298
+ Bootstrap.new
299
+ end
300
+
301
+ private def player_payload(
302
+ client : HTTPClient,
303
+ player_client : PlayerClient,
304
+ video_id : String,
305
+ bootstrap : Bootstrap,
306
+ url : URI,
307
+ ) : Hash(String, JSON::Any)
308
+ body = JSON.build do |json|
309
+ json.object do
310
+ json.field "context" do
311
+ json.object do
312
+ json.field "client" do
313
+ json.object do
314
+ player_client.context.each do |key, value|
315
+ json.field key, value
316
+ end
317
+ json.field "clientVersion", player_client.version
318
+ json.field "hl", "en"
319
+ json.field "timeZone", "UTC"
320
+ json.field "utcOffsetMinutes", 0
321
+ end
322
+ end
323
+ end
324
+ end
325
+ json.field "videoId", video_id
326
+ json.field "contentCheckOk", true
327
+ json.field "racyCheckOk", true
328
+ end
329
+ end
330
+
331
+ headers = HTTP::Headers.new
332
+ headers["User-Agent"] = player_client.user_agent
333
+ headers["X-YouTube-Client-Name"] = player_client.identifier
334
+ headers["X-YouTube-Client-Version"] = player_client.version
335
+ headers["Origin"] = @api_base
336
+ headers["Referer"] = "#{@api_base}/"
337
+ if visitor = bootstrap.visitor_data
338
+ headers["X-Goog-Visitor-Id"] = visitor
339
+ end
340
+
341
+ api_key = bootstrap.api_key || API_KEY
342
+ endpoint = "#{@api_base}/youtubei/v1/player?key=#{api_key}&prettyPrint=false"
343
+ response = client.post(endpoint, body, headers)
344
+
345
+ payload = JSON.parse(response.body).as_h?
346
+ unless payload
347
+ raise MediaUnavailableError.new(url.to_s, "player response is not a JSON object")
348
+ end
349
+
350
+ payload
351
+ rescue JSON::ParseException
352
+ raise MediaUnavailableError.new(url.to_s, "player response is not valid JSON")
353
+ rescue ex : HTTPError
354
+ case ex.status.code
355
+ when 400, 403, 429
356
+ raise MediaUnavailableError.new(url.to_s, "player API rejected the request (#{ex.status.code})")
357
+ else
358
+ raise ex
359
+ end
360
+ end
361
+
362
+ private def playability_error(payload : Hash(String, JSON::Any)) : String?
363
+ status = payload["playabilityStatus"]?.try(&.as_h?)
364
+ state = status.try(&.["status"]?).try(&.as_s?)
365
+ return nil if state == "OK"
366
+
367
+ reason = status.try(&.["reason"]?).try(&.as_s?)
368
+ reason || "video is not playable (#{state || "unknown"})"
369
+ end
370
+
371
+ private def formats_from(streaming_value : JSON::Any?) : Array(Format)
372
+ streaming = streaming_value.try(&.as_h?)
373
+ return [] of Format unless streaming
374
+
375
+ formats = [] of Format
376
+ seen = Set(String).new
377
+
378
+ {"formats", "adaptiveFormats"}.each do |key|
379
+ items = streaming[key]?.try(&.as_a?) || [] of JSON::Any
380
+
381
+ items.each do |item|
382
+ format = format_from(item)
383
+ next unless format
384
+ next if seen.includes?(format.format_id)
385
+
386
+ seen << format.format_id
387
+ formats << format
388
+ end
389
+ end
390
+
391
+ formats
392
+ end
393
+
394
+ private def format_from(item : JSON::Any) : Format?
395
+ value = item.as_h?
396
+ return nil unless value
397
+
398
+ url = HttpUrl.absolute(JsonValue.string(value["url"]?))
399
+ return nil unless url
400
+ return nil if value["signatureCipher"]?
401
+
402
+ itag = JsonValue.integer(value["itag"]?)
403
+ return nil unless itag
404
+
405
+ mime_type = JsonValue.string(value["mimeType"]?) || ""
406
+ mime = MimeType.parse(mime_type)
407
+ return nil if mime.empty?
408
+
409
+ width = JsonValue.dimension(value["width"]?)
410
+ height = JsonValue.dimension(value["height"]?)
411
+ has_video = mime.video? && (!width.nil? || !height.nil?)
412
+ has_audio = mime.audio? ||
413
+ (JsonValue.integer(value["audioChannels"]?) || 0) > 0
414
+ return nil unless has_video || has_audio
415
+
416
+ codec_list = mime.codec_list
417
+
418
+ video_codec = if has_video
419
+ codec_list.first?
420
+ end
421
+ audio_codec = if has_audio
422
+ has_video ? codec_list[1]? : codec_list.first?
423
+ end
424
+
425
+ Format.new(
426
+ format_id: itag.to_s,
427
+ url: url,
428
+ kind: format_kind(has_video, has_audio),
429
+ container: mime.container,
430
+ bitrate: JsonValue.integer(value["bitrate"]?),
431
+ video_codec: video_codec,
432
+ audio_codec: audio_codec,
433
+ width: width,
434
+ height: height,
435
+ fps: JsonValue.positive_number(value["fps"]?),
436
+ filesize: JsonValue.integer(value["contentLength"]?),
437
+ range_chunk_size: 1024_i64 * 1024,
438
+ language: audio_language(value),
439
+ note: JsonValue.string(value["qualityLabel"]?) || JsonValue.string(value["audioQuality"]?),
440
+ )
441
+ end
442
+
443
+ private def format_kind(has_video : Bool, has_audio : Bool) : FormatKind
444
+ if has_video && has_audio
445
+ FormatKind::Combined
446
+ elsif has_video
447
+ FormatKind::Video
448
+ else
449
+ FormatKind::Audio
450
+ end
451
+ end
452
+
453
+ private def audio_language(value : Hash(String, JSON::Any)) : String?
454
+ track = value["audioTrack"]?.try(&.as_h?)
455
+ return nil unless track
456
+
457
+ JsonValue.string(track["displayName"]?) || JsonValue.string(track["id"]?)
458
+ end
459
+
460
+ private def thumbnail_url(details : Hash(String, JSON::Any)) : URI?
461
+ thumbnail = details["thumbnail"]?.try(&.as_h?)
462
+ return nil unless thumbnail
463
+
464
+ images = thumbnail["thumbnails"]?.try(&.as_a?)
465
+ return nil unless images
466
+
467
+ images.reverse_each do |image|
468
+ image_hash = image.as_h?
469
+ next unless image_hash
470
+
471
+ uri = HttpUrl.absolute(JsonValue.string(image_hash["url"]?))
472
+ return uri if uri
473
+ end
474
+
475
+ nil
476
+ end
477
+
478
+ private def upload_date(payload : Hash(String, JSON::Any)) : String?
479
+ microformat = payload["microformat"]?.try(&.as_h?)
480
+ return nil unless microformat
481
+
482
+ renderer = microformat["playerMicroformatRenderer"]?.try(&.as_h?)
483
+ return nil unless renderer
484
+
485
+ value = JsonValue.string(renderer["publishDate"]?) || JsonValue.string(renderer["uploadDate"]?)
486
+ return nil unless value
487
+ return nil if value.size < 10
488
+
489
+ value[0, 10]
490
+ end
491
+
492
+ private def video_id_for(url : URI) : String?
493
+ host = url.host.try(&.downcase)
494
+ return nil unless host && HOSTS.includes?(host)
495
+
496
+ path = url.path
497
+
498
+ if host.ends_with?("youtu.be")
499
+ return valid_id?(path.lchop('/').split('/').first?)
500
+ end
501
+
502
+ if path == "/watch"
503
+ query = HTTP::Params.parse(url.query || "")
504
+ return valid_id?(query["v"]?)
505
+ end
506
+
507
+ match = %r{\A/(?:shorts|embed|live|v)/([^/]+)/?\z}.match(path)
508
+ return nil unless match
509
+
510
+ valid_id?(match[1])
511
+ end
512
+
513
+ private def valid_id?(value : String?) : String?
514
+ return nil unless value
515
+ return nil unless VIDEO_ID.matches?(value)
516
+
517
+ value
518
+ end
519
+ end
520
+ end
@@ -0,0 +1,82 @@
1
+ require "json"
2
+ require "uri"
3
+
4
+ module Rill
5
+ enum FormatKind
6
+ Video
7
+ Audio
8
+ Combined
9
+ end
10
+
11
+ enum FormatProtocol
12
+ Http
13
+ Hls
14
+ Dash
15
+ end
16
+
17
+ class Format
18
+ getter format_id : String
19
+ getter url : URI
20
+ getter kind : FormatKind
21
+ getter protocol : FormatProtocol
22
+ getter container : String?
23
+ getter bitrate : Int64?
24
+ getter video_codec : String?
25
+ getter audio_codec : String?
26
+ getter width : Int32?
27
+ getter height : Int32?
28
+ getter fps : Float64?
29
+ getter video_bitrate : Int32?
30
+ getter audio_bitrate : Int32?
31
+ getter filesize : Int64?
32
+ getter range_chunk_size : Int64?
33
+ getter duration_seconds : Float64?
34
+ getter language : String?
35
+ getter note : String?
36
+
37
+ def initialize(
38
+ @format_id : String,
39
+ @url : URI,
40
+ @kind : FormatKind = FormatKind::Combined,
41
+ @protocol : FormatProtocol = FormatProtocol::Http,
42
+ @container : String? = nil,
43
+ @bitrate : Int64? = nil,
44
+ @video_codec : String? = nil,
45
+ @audio_codec : String? = nil,
46
+ @width : Int32? = nil,
47
+ @height : Int32? = nil,
48
+ @fps : Float64? = nil,
49
+ @video_bitrate : Int32? = nil,
50
+ @audio_bitrate : Int32? = nil,
51
+ @filesize : Int64? = nil,
52
+ @range_chunk_size : Int64? = nil,
53
+ @duration_seconds : Float64? = nil,
54
+ @language : String? = nil,
55
+ @note : String? = nil,
56
+ )
57
+ end
58
+
59
+ def to_json(json : JSON::Builder) : Nil
60
+ json.object do
61
+ json.field "format_id", format_id
62
+ json.field "url", url.to_s
63
+ json.field "kind", kind.to_s.downcase
64
+ json.field "protocol", protocol.to_s.downcase
65
+ json.field "container", container
66
+ json.field "bitrate", bitrate
67
+ json.field "video_codec", video_codec
68
+ json.field "audio_codec", audio_codec
69
+ json.field "width", width
70
+ json.field "height", height
71
+ json.field "fps", fps
72
+ json.field "video_bitrate", video_bitrate
73
+ json.field "audio_bitrate", audio_bitrate
74
+ json.field "filesize", filesize
75
+ json.field "range_chunk_size", range_chunk_size
76
+ json.field "duration_seconds", duration_seconds
77
+ json.field "language", language
78
+ json.field "note", note
79
+ end
80
+ end
81
+ end
82
+ end