rill-node 0.0.0-stage → 2026.11.15
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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +64 -2
- package/binding.gyp +31 -0
- package/index.d.ts +45 -0
- package/index.js +112 -0
- package/native/darwin-arm64/librill.dylib +0 -0
- package/native/darwin-arm64/rill-media +0 -0
- package/native/darwin-arm64/rill_node.node +0 -0
- package/native/darwin-x64/librill.dylib +0 -0
- package/native/darwin-x64/rill-media +0 -0
- package/native/darwin-x64/rill_node.node +0 -0
- package/native/linux-arm64-gnu/librill.so +0 -0
- package/native/linux-arm64-gnu/rill-media +0 -0
- package/native/linux-arm64-gnu/rill_node.node +0 -0
- package/native/linux-x64-gnu/librill.so +0 -0
- package/native/linux-x64-gnu/rill-media +0 -0
- package/native/linux-x64-gnu/rill_node.node +0 -0
- package/native-src/include/rill.h +90 -0
- package/native-src/include/rill_media.h +22 -0
- package/native-src/shard.yml +9 -0
- package/native-src/src/c_api.cr +194 -0
- package/native-src/src/cli.cr +637 -0
- package/native-src/src/rill/c_api/job.cr +336 -0
- package/native-src/src/rill/c_api/memory.cr +32 -0
- package/native-src/src/rill/c_api/status.cr +25 -0
- package/native-src/src/rill/c_api/sync.cr +125 -0
- package/native-src/src/rill/c_api/worker_pool.cr +403 -0
- package/native-src/src/rill/cancellation.cr +29 -0
- package/native-src/src/rill/client.cr +52 -0
- package/native-src/src/rill/cookie_jar.cr +130 -0
- package/native-src/src/rill/dash/manifest.cr +279 -0
- package/native-src/src/rill/dash/parser.cr +227 -0
- package/native-src/src/rill/download_options.cr +96 -0
- package/native-src/src/rill/download_plan.cr +27 -0
- package/native-src/src/rill/download_progress.cr +16 -0
- package/native-src/src/rill/downloader.cr +662 -0
- package/native-src/src/rill/errors.cr +155 -0
- package/native-src/src/rill/extractor.cr +18 -0
- package/native-src/src/rill/extractor_registry.cr +98 -0
- package/native-src/src/rill/extractors/bandcamp.cr +265 -0
- package/native-src/src/rill/extractors/dash.cr +77 -0
- package/native-src/src/rill/extractors/direct_media.cr +73 -0
- package/native-src/src/rill/extractors/generic.cr +441 -0
- package/native-src/src/rill/extractors/hls.cr +202 -0
- package/native-src/src/rill/extractors/internet_archive.cr +180 -0
- package/native-src/src/rill/extractors/m3u.cr +81 -0
- package/native-src/src/rill/extractors/media_ccc_de.cr +219 -0
- package/native-src/src/rill/extractors/mixcloud.cr +495 -0
- package/native-src/src/rill/extractors/peer_tube.cr +226 -0
- package/native-src/src/rill/extractors/rumble.cr +325 -0
- package/native-src/src/rill/extractors/soundcloud.cr +553 -0
- package/native-src/src/rill/extractors/spotify.cr +184 -0
- package/native-src/src/rill/extractors/twitch_clip.cr +273 -0
- package/native-src/src/rill/extractors/youtube.cr +520 -0
- package/native-src/src/rill/format.cr +82 -0
- package/native-src/src/rill/format_selector.cr +143 -0
- package/native-src/src/rill/hls/parser.cr +257 -0
- package/native-src/src/rill/hls/playlist.cr +100 -0
- package/native-src/src/rill/http_client.cr +503 -0
- package/native-src/src/rill/http_url.cr +28 -0
- package/native-src/src/rill/json_value.cr +66 -0
- package/native-src/src/rill/media.cr +151 -0
- package/native-src/src/rill/media_muxer.cr +333 -0
- package/native-src/src/rill/mime_type.cr +58 -0
- package/native-src/src/rill/output_file_name.cr +91 -0
- package/native-src/src/rill/playlist.cr +109 -0
- package/native-src/src/rill/request_options.cr +179 -0
- package/native-src/src/rill/segment_assembly.cr +70 -0
- package/native-src/src/rill/timestamp.cr +34 -0
- package/native-src/src/rill/version.cr +3 -0
- package/native-src/src/rill.cr +47 -0
- package/package.json +48 -4
- package/scripts/build-native.js +46 -0
- package/scripts/install-gyp.js +10 -0
- package/scripts/install.js +19 -0
- package/scripts/lib.js +1 -0
- package/scripts/libdir.js +23 -0
- package/scripts/package-native.js +35 -0
- package/scripts/platform.js +41 -0
- package/scripts/prepack.js +47 -0
- package/src/addon.c +498 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
require "./errors"
|
|
5
|
+
require "./media"
|
|
6
|
+
|
|
7
|
+
module Rill
|
|
8
|
+
class PlaylistEntry
|
|
9
|
+
getter url : URI
|
|
10
|
+
getter title : String?
|
|
11
|
+
getter id : String?
|
|
12
|
+
getter media : Media?
|
|
13
|
+
getter error : String?
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
@url : URI,
|
|
17
|
+
@title : String? = nil,
|
|
18
|
+
@id : String? = nil,
|
|
19
|
+
@media : Media? = nil,
|
|
20
|
+
@error : String? = nil,
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_json(json : JSON::Builder) : Nil
|
|
25
|
+
json.object do
|
|
26
|
+
json.field "url", url.to_s
|
|
27
|
+
json.field "title", title
|
|
28
|
+
json.field "id", id
|
|
29
|
+
json.field "error", error
|
|
30
|
+
json.field "media" do
|
|
31
|
+
if item = media
|
|
32
|
+
item.to_json(json)
|
|
33
|
+
else
|
|
34
|
+
json.null
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Playlist
|
|
42
|
+
getter id : String?
|
|
43
|
+
getter title : String
|
|
44
|
+
getter description : String?
|
|
45
|
+
getter webpage_url : URI
|
|
46
|
+
getter extractor : String
|
|
47
|
+
getter entries : Array(PlaylistEntry)
|
|
48
|
+
|
|
49
|
+
def initialize(
|
|
50
|
+
@title : String,
|
|
51
|
+
@webpage_url : URI,
|
|
52
|
+
@extractor : String,
|
|
53
|
+
@entries : Array(PlaylistEntry),
|
|
54
|
+
@id : String? = nil,
|
|
55
|
+
@description : String? = nil,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate! : Nil
|
|
60
|
+
if title.empty?
|
|
61
|
+
raise InvalidExtractionError.new(webpage_url.to_s, "playlist title is empty")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if entries.empty?
|
|
65
|
+
raise InvalidExtractionError.new(webpage_url.to_s, "playlist has no entries")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
entries.each do |entry|
|
|
69
|
+
validate_entry!(entry)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_json(json : JSON::Builder) : Nil
|
|
74
|
+
json.object do
|
|
75
|
+
json.field "id", id
|
|
76
|
+
json.field "title", title
|
|
77
|
+
json.field "description", description
|
|
78
|
+
json.field "webpage_url", webpage_url.to_s
|
|
79
|
+
json.field "extractor", extractor
|
|
80
|
+
json.field "entries" do
|
|
81
|
+
json.array do
|
|
82
|
+
entries.each(&.to_json(json))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_pretty_json : String
|
|
89
|
+
JSON.build(indent: 2) do |json|
|
|
90
|
+
to_json(json)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private def validate_entry!(entry : PlaylistEntry) : Nil
|
|
95
|
+
scheme = entry.url.scheme
|
|
96
|
+
if scheme != "http" && scheme != "https"
|
|
97
|
+
raise InvalidExtractionError.new(webpage_url.to_s, "entry has a non-HTTP URL")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if entry.url.host.nil?
|
|
101
|
+
raise InvalidExtractionError.new(webpage_url.to_s, "entry has no host")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if entry.media && entry.error
|
|
105
|
+
raise InvalidExtractionError.new(webpage_url.to_s, "entry has both media and an error")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require "http/headers"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
require "./errors"
|
|
5
|
+
require "./version"
|
|
6
|
+
|
|
7
|
+
module Rill
|
|
8
|
+
class RequestOptions
|
|
9
|
+
DEFAULT_USER_AGENT = "rill/#{VERSION}"
|
|
10
|
+
DEFAULT_CONNECT_TIMEOUT = 15.seconds
|
|
11
|
+
DEFAULT_READ_TIMEOUT = 30.seconds
|
|
12
|
+
DEFAULT_MAX_REDIRECTS = 10
|
|
13
|
+
DEFAULT_RETRIES = 2
|
|
14
|
+
DEFAULT_RETRY_DELAY = 1.second
|
|
15
|
+
DEFAULT_MAX_RESPONSE_BYTES = 16_i64 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
property user_agent : String = DEFAULT_USER_AGENT
|
|
18
|
+
property headers : HTTP::Headers = HTTP::Headers.new
|
|
19
|
+
property cookies : Hash(String, String) = {} of String => String
|
|
20
|
+
property connect_timeout : Time::Span = DEFAULT_CONNECT_TIMEOUT
|
|
21
|
+
property read_timeout : Time::Span = DEFAULT_READ_TIMEOUT
|
|
22
|
+
property max_redirects : Int32 = DEFAULT_MAX_REDIRECTS
|
|
23
|
+
property retries : Int32 = DEFAULT_RETRIES
|
|
24
|
+
property retry_delay : Time::Span = DEFAULT_RETRY_DELAY
|
|
25
|
+
property max_response_bytes : Int64 = DEFAULT_MAX_RESPONSE_BYTES
|
|
26
|
+
|
|
27
|
+
def retry_wait(attempt : Int32) : Time::Span
|
|
28
|
+
exponent = Math.min(attempt - 1, 5)
|
|
29
|
+
@retry_delay * (1 << exponent)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.from_json(source : String) : RequestOptions
|
|
33
|
+
object = begin
|
|
34
|
+
JSON.parse(source)
|
|
35
|
+
rescue JSON::ParseException
|
|
36
|
+
raise InvalidOptionsError.new("options are not valid JSON")
|
|
37
|
+
end.as_h?
|
|
38
|
+
|
|
39
|
+
unless object
|
|
40
|
+
raise InvalidOptionsError.new("options must be a JSON object")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
options = new
|
|
44
|
+
|
|
45
|
+
object.each do |key, value|
|
|
46
|
+
case key
|
|
47
|
+
when "user_agent"
|
|
48
|
+
options.user_agent = string_option(key, value)
|
|
49
|
+
when "headers"
|
|
50
|
+
apply_headers(options, value)
|
|
51
|
+
when "cookies"
|
|
52
|
+
apply_cookies(options, value)
|
|
53
|
+
when "connect_timeout_seconds"
|
|
54
|
+
options.connect_timeout = seconds_option(key, value)
|
|
55
|
+
when "read_timeout_seconds"
|
|
56
|
+
options.read_timeout = seconds_option(key, value)
|
|
57
|
+
when "retry_delay_seconds"
|
|
58
|
+
options.retry_delay = seconds_option(key, value, allow_zero: true)
|
|
59
|
+
when "max_redirects"
|
|
60
|
+
options.max_redirects = int32_option(key, value)
|
|
61
|
+
when "retries"
|
|
62
|
+
options.retries = int32_option(key, value)
|
|
63
|
+
when "max_response_bytes"
|
|
64
|
+
options.max_response_bytes = integer_option(key, value)
|
|
65
|
+
else
|
|
66
|
+
raise InvalidOptionsError.new("unknown option #{key}")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
validate(options)
|
|
71
|
+
options
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def self.apply_headers(options : RequestOptions, value : JSON::Any) : Nil
|
|
75
|
+
headers = value.as_h?
|
|
76
|
+
unless headers
|
|
77
|
+
raise InvalidOptionsError.new("headers must be a JSON object")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
headers.each do |name, header_value|
|
|
81
|
+
if name.empty?
|
|
82
|
+
raise InvalidOptionsError.new("header names must not be empty")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if text = header_value.as_s?
|
|
86
|
+
options.headers.add(name, text)
|
|
87
|
+
elsif values = header_value.as_a?
|
|
88
|
+
values.each do |item|
|
|
89
|
+
text = item.as_s?
|
|
90
|
+
unless text
|
|
91
|
+
raise InvalidOptionsError.new("header #{name} must be a string or an array of strings")
|
|
92
|
+
end
|
|
93
|
+
options.headers.add(name, text)
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
raise InvalidOptionsError.new("header #{name} must be a string or an array of strings")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private def self.apply_cookies(options : RequestOptions, value : JSON::Any) : Nil
|
|
102
|
+
cookies = value.as_h?
|
|
103
|
+
unless cookies
|
|
104
|
+
raise InvalidOptionsError.new("cookies must be a JSON object")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
cookies.each do |name, cookie_value|
|
|
108
|
+
text = cookie_value.as_s?
|
|
109
|
+
unless text
|
|
110
|
+
raise InvalidOptionsError.new("cookie #{name} must be a string")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if name.empty?
|
|
114
|
+
raise InvalidOptionsError.new("cookie names must not be empty")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
options.cookies[name] = text
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private def self.string_option(key : String, value : JSON::Any) : String
|
|
122
|
+
value.as_s? || raise InvalidOptionsError.new("#{key} must be a string")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private def self.integer_option(key : String, value : JSON::Any) : Int64
|
|
126
|
+
value.as_i64? || raise InvalidOptionsError.new("#{key} must be an integer")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private def self.int32_option(key : String, value : JSON::Any) : Int32
|
|
130
|
+
integer = integer_option(key, value)
|
|
131
|
+
unless Int32::MIN <= integer <= Int32::MAX
|
|
132
|
+
raise InvalidOptionsError.new("#{key} is out of range")
|
|
133
|
+
end
|
|
134
|
+
integer.to_i32
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private def self.seconds_option(
|
|
138
|
+
key : String,
|
|
139
|
+
value : JSON::Any,
|
|
140
|
+
allow_zero : Bool = false,
|
|
141
|
+
) : Time::Span
|
|
142
|
+
number = if float = value.as_f?
|
|
143
|
+
float
|
|
144
|
+
elsif integer = value.as_i64?
|
|
145
|
+
integer.to_f
|
|
146
|
+
else
|
|
147
|
+
raise InvalidOptionsError.new("#{key} must be a number")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if allow_zero ? number < 0 : number <= 0
|
|
151
|
+
raise InvalidOptionsError.new("#{key} must be greater than zero")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
number.seconds
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private def self.validate(options : RequestOptions) : Nil
|
|
158
|
+
if options.user_agent.empty?
|
|
159
|
+
raise InvalidOptionsError.new("user_agent must not be empty")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if options.max_redirects < 0
|
|
163
|
+
raise InvalidOptionsError.new("max_redirects must not be negative")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if options.retries < 0
|
|
167
|
+
raise InvalidOptionsError.new("retries must not be negative")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if options.retries > 10
|
|
171
|
+
raise InvalidOptionsError.new("retries must not exceed 10")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if options.max_response_bytes <= 0
|
|
175
|
+
raise InvalidOptionsError.new("max_response_bytes must be positive")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
require "./errors"
|
|
4
|
+
require "./mime_type"
|
|
5
|
+
|
|
6
|
+
module Rill::SegmentAssembly
|
|
7
|
+
enum Family
|
|
8
|
+
MpegTs
|
|
9
|
+
FragmentedMp4
|
|
10
|
+
Webm
|
|
11
|
+
Adts
|
|
12
|
+
Mp3
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
FRAGMENTED_EXTENSIONS = {
|
|
16
|
+
"m4s", "mp4", "m4a", "m4v", "cmfv", "cmfa",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def self.for_hls(map_url : URI?, extensions : Array(String)) : Family
|
|
20
|
+
unique = extensions.map(&.downcase).uniq
|
|
21
|
+
|
|
22
|
+
if map_url
|
|
23
|
+
unless unique.all? { |extension| extension.empty? || FRAGMENTED_EXTENSIONS.includes?(extension) }
|
|
24
|
+
raise UnsupportedManifestError.new("HLS segments are not a supported fragmented MP4 layout")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return Family::FragmentedMp4
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
case unique
|
|
31
|
+
when ["ts"]
|
|
32
|
+
Family::MpegTs
|
|
33
|
+
when ["aac"]
|
|
34
|
+
Family::Adts
|
|
35
|
+
when ["mp3"]
|
|
36
|
+
Family::Mp3
|
|
37
|
+
else
|
|
38
|
+
raise UnsupportedManifestError.new(
|
|
39
|
+
"HLS segment concatenation is not verified for #{unique.inspect}",
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.for_dash(mime : MimeType, has_init : Bool) : Family
|
|
45
|
+
case mime.subtype
|
|
46
|
+
when "mp4", "m4a", "m4v"
|
|
47
|
+
unless has_init
|
|
48
|
+
raise UnsupportedManifestError.new("DASH fragmented MP4 needs an initialization segment")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Family::FragmentedMp4
|
|
52
|
+
when "webm"
|
|
53
|
+
unless has_init
|
|
54
|
+
raise UnsupportedManifestError.new("DASH WebM needs an initialization segment")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Family::Webm
|
|
58
|
+
when "mp2t"
|
|
59
|
+
Family::MpegTs
|
|
60
|
+
when "aac"
|
|
61
|
+
Family::Adts
|
|
62
|
+
when "mpeg"
|
|
63
|
+
Family::Mp3
|
|
64
|
+
else
|
|
65
|
+
raise UnsupportedManifestError.new(
|
|
66
|
+
"DASH segment concatenation is not verified for #{mime.type}/#{mime.subtype}",
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Rill
|
|
2
|
+
module Timestamp
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def clock_seconds(value : String?) : Float64?
|
|
6
|
+
return nil unless value
|
|
7
|
+
|
|
8
|
+
total = 0.0
|
|
9
|
+
value.split(':').each do |part|
|
|
10
|
+
number = part.to_f?
|
|
11
|
+
return nil unless number && number.finite? && number >= 0
|
|
12
|
+
|
|
13
|
+
total = total * 60 + number
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
total
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def iso8601_seconds(value : String?) : Float64?
|
|
20
|
+
return nil unless value
|
|
21
|
+
|
|
22
|
+
match = /\AP(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?\z/.match(value)
|
|
23
|
+
return nil unless match
|
|
24
|
+
|
|
25
|
+
days = match[1]?.try(&.to_f) || 0.0
|
|
26
|
+
hours = match[2]?.try(&.to_f) || 0.0
|
|
27
|
+
minutes = match[3]?.try(&.to_f) || 0.0
|
|
28
|
+
seconds = match[4]?.try(&.to_f) || 0.0
|
|
29
|
+
|
|
30
|
+
total = days * 86_400 + hours * 3600 + minutes * 60 + seconds
|
|
31
|
+
total if total > 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
require "./rill/version"
|
|
4
|
+
require "./rill/errors"
|
|
5
|
+
require "./rill/request_options"
|
|
6
|
+
require "./rill/http_client"
|
|
7
|
+
require "./rill/format"
|
|
8
|
+
require "./rill/http_url"
|
|
9
|
+
require "./rill/json_value"
|
|
10
|
+
require "./rill/mime_type"
|
|
11
|
+
require "./rill/segment_assembly"
|
|
12
|
+
require "./rill/timestamp"
|
|
13
|
+
require "./rill/media"
|
|
14
|
+
require "./rill/extractor"
|
|
15
|
+
require "./rill/playlist"
|
|
16
|
+
require "./rill/extractors/bandcamp"
|
|
17
|
+
require "./rill/extractors/dash"
|
|
18
|
+
require "./rill/extractors/direct_media"
|
|
19
|
+
require "./rill/extractors/generic"
|
|
20
|
+
require "./rill/extractors/hls"
|
|
21
|
+
require "./rill/extractors/internet_archive"
|
|
22
|
+
require "./rill/extractors/m3u"
|
|
23
|
+
require "./rill/extractors/media_ccc_de"
|
|
24
|
+
require "./rill/extractors/mixcloud"
|
|
25
|
+
require "./rill/extractors/peer_tube"
|
|
26
|
+
require "./rill/extractors/rumble"
|
|
27
|
+
require "./rill/extractors/soundcloud"
|
|
28
|
+
require "./rill/extractors/spotify"
|
|
29
|
+
require "./rill/extractors/twitch_clip"
|
|
30
|
+
require "./rill/extractors/youtube"
|
|
31
|
+
require "./rill/extractor_registry"
|
|
32
|
+
require "./rill/format_selector"
|
|
33
|
+
require "./rill/cancellation"
|
|
34
|
+
require "./rill/cookie_jar"
|
|
35
|
+
require "./rill/download_progress"
|
|
36
|
+
require "./rill/output_file_name"
|
|
37
|
+
require "./rill/download_options"
|
|
38
|
+
require "./rill/download_plan"
|
|
39
|
+
require "./rill/downloader"
|
|
40
|
+
require "./rill/client"
|
|
41
|
+
require "./rill/media_muxer"
|
|
42
|
+
|
|
43
|
+
module Rill
|
|
44
|
+
def self.extract(url : String | URI, options : RequestOptions = RequestOptions.new) : Media | Playlist
|
|
45
|
+
ExtractorRegistry.default.extract(url, options)
|
|
46
|
+
end
|
|
47
|
+
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rill-node",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2026.11.15",
|
|
4
|
+
"description": "Node.js bindings for the Rill media extractor and downloader",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"gypfile": true,
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"index.d.ts",
|
|
12
|
+
"binding.gyp",
|
|
13
|
+
"src/**/*",
|
|
14
|
+
"scripts/**/*",
|
|
15
|
+
"native-src/**/*",
|
|
16
|
+
"native/**/*",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"NOTICE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/SimaoMoreira5228/rill.git",
|
|
27
|
+
"directory": "bindings/node"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/SimaoMoreira5228/rill",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/SimaoMoreira5228/rill/issues"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"rill",
|
|
35
|
+
"media",
|
|
36
|
+
"video",
|
|
37
|
+
"audio",
|
|
38
|
+
"download",
|
|
39
|
+
"extractor"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prepack": "node scripts/prepack.js",
|
|
43
|
+
"install": "node scripts/install.js",
|
|
44
|
+
"package-native": "node scripts/package-native.js",
|
|
45
|
+
"test": "node smoke.js"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
|
|
5
|
+
const packageDir = path.resolve(__dirname, "..");
|
|
6
|
+
const nativeDir = path.join(packageDir, "native");
|
|
7
|
+
const libraryName =
|
|
8
|
+
process.platform === "darwin"
|
|
9
|
+
? "librill.dylib"
|
|
10
|
+
: process.platform === "win32"
|
|
11
|
+
? "rill.dll"
|
|
12
|
+
: "librill.so";
|
|
13
|
+
const library = path.join(nativeDir, libraryName);
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(library) || process.env.RILL_LIBRARY || process.env.RILL_LIB_DIR) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const vendored = path.join(packageDir, "native-src");
|
|
20
|
+
const root = fs.existsSync(path.join(vendored, "src", "c_api.cr"))
|
|
21
|
+
? vendored
|
|
22
|
+
: path.resolve(packageDir, "..", "..");
|
|
23
|
+
const source = path.join(root, "src", "c_api.cr");
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(source)) {
|
|
26
|
+
console.error(`Rill source not found at ${source}`);
|
|
27
|
+
console.error("Set RILL_LIB_DIR or RILL_LIBRARY to an existing librill build.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(nativeDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const linkFlag = process.platform === "win32" ? "/DLL" : "-shared";
|
|
34
|
+
const result = spawnSync(
|
|
35
|
+
process.env.CRYSTAL || "crystal",
|
|
36
|
+
["build", "--single-module", "--link-flags", linkFlag, "-o", library, source],
|
|
37
|
+
{ cwd: root, stdio: "inherit" },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (result.error) {
|
|
41
|
+
console.error("Unable to run crystal. Install Crystal 1.22 or set RILL_LIB_DIR.");
|
|
42
|
+
console.error(result.error.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const { spawnSync } = require("node:child_process");
|
|
2
|
+
|
|
3
|
+
const result = spawnSync("node-gyp", ["rebuild"], { stdio: "inherit" });
|
|
4
|
+
|
|
5
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
6
|
+
const fallback = spawnSync("npx", ["--yes", "node-gyp", "rebuild"], { stdio: "inherit" });
|
|
7
|
+
process.exit(fallback.status ?? 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const { nativeDir } = require("./platform.js");
|
|
5
|
+
|
|
6
|
+
const packageDir = path.resolve(__dirname, "..");
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync(path.join(nativeDir(packageDir), "rill_node.node"))) {
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const script of ["build-native.js", "install-gyp.js"]) {
|
|
13
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, script)], {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
});
|
|
16
|
+
if (result.status !== 0) {
|
|
17
|
+
process.exit(result.status ?? 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/scripts/lib.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log(process.env.RILL_LIBRARY || "-lrill");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const packageDir = path.resolve(__dirname, "..");
|
|
5
|
+
const libraryName =
|
|
6
|
+
process.platform === "darwin"
|
|
7
|
+
? "librill.dylib"
|
|
8
|
+
: process.platform === "win32"
|
|
9
|
+
? "rill.dll"
|
|
10
|
+
: "librill.so";
|
|
11
|
+
|
|
12
|
+
if (process.env.RILL_LIBRARY) {
|
|
13
|
+
console.log(path.dirname(process.env.RILL_LIBRARY));
|
|
14
|
+
} else if (process.env.RILL_LIB_DIR) {
|
|
15
|
+
console.log(process.env.RILL_LIB_DIR);
|
|
16
|
+
} else {
|
|
17
|
+
const native = path.join(packageDir, "native");
|
|
18
|
+
if (fs.existsSync(path.join(native, libraryName))) {
|
|
19
|
+
console.log(native);
|
|
20
|
+
} else {
|
|
21
|
+
console.log(path.resolve(packageDir, "..", "..", "build"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { helperName, libraryName, platformKey } = require("./platform.js");
|
|
4
|
+
|
|
5
|
+
const packageDir = path.resolve(__dirname, "..");
|
|
6
|
+
const root = path.resolve(packageDir, "..", "..");
|
|
7
|
+
const output = path.join(packageDir, "native", platformKey());
|
|
8
|
+
|
|
9
|
+
const library =
|
|
10
|
+
process.env.RILL_LIBRARY || path.join(root, "build", libraryName());
|
|
11
|
+
const helper = path.join(root, "build", helperName());
|
|
12
|
+
const addon = process.env.RILL_NODE_ADDON || path.join(packageDir, "build", "Release", "rill_node.node");
|
|
13
|
+
|
|
14
|
+
for (const [label, source] of [
|
|
15
|
+
["librill", library],
|
|
16
|
+
["rill-media", helper],
|
|
17
|
+
["rill_node.node", addon],
|
|
18
|
+
]) {
|
|
19
|
+
if (!fs.existsSync(source)) {
|
|
20
|
+
console.error(`Missing ${label}: ${source}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fs.mkdirSync(output, { recursive: true });
|
|
26
|
+
fs.copyFileSync(library, path.join(output, path.basename(library)));
|
|
27
|
+
fs.copyFileSync(helper, path.join(output, path.basename(helper)));
|
|
28
|
+
fs.copyFileSync(addon, path.join(output, "rill_node.node"));
|
|
29
|
+
|
|
30
|
+
if (process.platform !== "win32") {
|
|
31
|
+
fs.chmodSync(path.join(output, path.basename(library)), 0o755);
|
|
32
|
+
fs.chmodSync(path.join(output, path.basename(helper)), 0o755);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(output);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
function platformKey() {
|
|
4
|
+
if (process.env.RILL_PLATFORM_KEY) {
|
|
5
|
+
return process.env.RILL_PLATFORM_KEY;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const arch = process.arch;
|
|
9
|
+
if (process.platform === "linux") {
|
|
10
|
+
const report = process.report?.getReport?.();
|
|
11
|
+
const libc = report?.header?.glibcVersionRuntime ? "gnu" : "musl";
|
|
12
|
+
return `linux-${arch}-${libc}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${process.platform}-${arch}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function libraryName(platform = process.platform) {
|
|
19
|
+
if (platform === "darwin") {
|
|
20
|
+
return "librill.dylib";
|
|
21
|
+
}
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
return "rill.dll";
|
|
24
|
+
}
|
|
25
|
+
return "librill.so";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function helperName(platform = process.platform) {
|
|
29
|
+
return platform === "win32" ? "rill-media.exe" : "rill-media";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nativeDir(packageDir) {
|
|
33
|
+
return path.join(packageDir, "native", platformKey());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
helperName,
|
|
38
|
+
libraryName,
|
|
39
|
+
nativeDir,
|
|
40
|
+
platformKey,
|
|
41
|
+
};
|