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.
- 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,180 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
require "../errors"
|
|
5
|
+
require "../extractor"
|
|
6
|
+
require "../format"
|
|
7
|
+
require "../json_value"
|
|
8
|
+
require "../media"
|
|
9
|
+
require "../timestamp"
|
|
10
|
+
|
|
11
|
+
module Rill::Extractors
|
|
12
|
+
class InternetArchive < Extractor
|
|
13
|
+
BASE_URL = "https://archive.org"
|
|
14
|
+
DETAILS_PATH = %r{\A/details/([^/]+)/?\z}
|
|
15
|
+
|
|
16
|
+
AUDIO_EXTENSIONS = %w(
|
|
17
|
+
aac aif aiff ape flac m4a mp3 oga ogg opus shn wav wma
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
MEDIA_EXTENSIONS = {
|
|
21
|
+
"3gp", "avi", "flv", "m2ts", "mkv", "mov", "mp4", "m4v",
|
|
22
|
+
"mpg", "mpeg", "ogv", "ts", "webm", "wmv",
|
|
23
|
+
"aac", "aif", "aiff", "ape", "flac", "m4a", "mp3", "oga",
|
|
24
|
+
"ogg", "opus", "shn", "wav", "wma",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def initialize(@base_url : String = BASE_URL)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def name : String
|
|
31
|
+
"internet_archive"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def matches?(url : URI) : Bool
|
|
35
|
+
return false unless url.scheme == "http" || url.scheme == "https"
|
|
36
|
+
return false unless archive_host?(url.host)
|
|
37
|
+
|
|
38
|
+
!DETAILS_PATH.match(url.path).nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract(url : URI, client : HTTPClient) : Media
|
|
42
|
+
identifier = identifier_for(url)
|
|
43
|
+
unless identifier
|
|
44
|
+
raise UnsupportedUrlError.new(url.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
metadata_url = URI.parse("#{@base_url}/metadata/#{URI.encode_path_segment(identifier)}")
|
|
48
|
+
response = client.get(metadata_url)
|
|
49
|
+
payload = parse_payload(response.body, url)
|
|
50
|
+
|
|
51
|
+
if payload["is_dark"]?.try(&.as_bool?)
|
|
52
|
+
raise MediaUnavailableError.new(url.to_s, "item is dark")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
metadata = payload["metadata"]?.try(&.as_h?)
|
|
56
|
+
unless metadata
|
|
57
|
+
raise MediaUnavailableError.new(url.to_s, "item metadata is missing")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
formats = formats_from(payload["files"]?, identifier)
|
|
61
|
+
if formats.empty?
|
|
62
|
+
raise MediaUnavailableError.new(url.to_s, "item has no downloadable media")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
title = JsonValue.string(metadata["title"]?) || identifier
|
|
66
|
+
duration = Timestamp.clock_seconds(JsonValue.string(metadata["length"]?))
|
|
67
|
+
duration ||= formats.compact_map(&.duration_seconds).first?
|
|
68
|
+
|
|
69
|
+
Media.new(
|
|
70
|
+
id: JsonValue.string(metadata["identifier"]?) || identifier,
|
|
71
|
+
title: title,
|
|
72
|
+
description: JsonValue.string(metadata["description"]?),
|
|
73
|
+
webpage_url: url,
|
|
74
|
+
uploader: JsonValue.string(metadata["creator"]?) || JsonValue.string(metadata["uploader"]?),
|
|
75
|
+
duration_seconds: duration,
|
|
76
|
+
thumbnail_url: thumbnail_url_for(identifier),
|
|
77
|
+
upload_date: date_value(metadata),
|
|
78
|
+
extractor: name,
|
|
79
|
+
formats: formats,
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private def identifier_for(url : URI) : String?
|
|
84
|
+
match = DETAILS_PATH.match(url.path)
|
|
85
|
+
return nil unless match
|
|
86
|
+
|
|
87
|
+
identifier = URI.decode(match[1])
|
|
88
|
+
identifier.empty? ? nil : identifier
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def archive_host?(host : String?) : Bool
|
|
92
|
+
case host.try(&.downcase)
|
|
93
|
+
when "archive.org", "www.archive.org"
|
|
94
|
+
true
|
|
95
|
+
else
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private def parse_payload(body : String, url : URI) : Hash(String, JSON::Any)
|
|
101
|
+
payload = JSON.parse(body).as_h?
|
|
102
|
+
unless payload
|
|
103
|
+
raise MediaUnavailableError.new(url.to_s, "item metadata is not a JSON object")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
payload
|
|
107
|
+
rescue JSON::ParseException
|
|
108
|
+
raise MediaUnavailableError.new(url.to_s, "item metadata is not valid JSON")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private def formats_from(files_value : JSON::Any?, identifier : String) : Array(Format)
|
|
112
|
+
files = files_value.try(&.as_a?)
|
|
113
|
+
return [] of Format unless files
|
|
114
|
+
|
|
115
|
+
formats = [] of Format
|
|
116
|
+
|
|
117
|
+
files.each do |file|
|
|
118
|
+
file_hash = file.as_h?
|
|
119
|
+
next unless file_hash
|
|
120
|
+
|
|
121
|
+
file_name = file_hash["name"]?.try(&.as_s?)
|
|
122
|
+
next unless file_name
|
|
123
|
+
|
|
124
|
+
extension = File.extname(file_name).lchop('.').downcase
|
|
125
|
+
next unless MEDIA_EXTENSIONS.includes?(extension)
|
|
126
|
+
|
|
127
|
+
label = JsonValue.string(file_hash["format"]?)
|
|
128
|
+
format = Format.new(
|
|
129
|
+
format_id: file_name,
|
|
130
|
+
url: download_url(identifier, file_name),
|
|
131
|
+
kind: kind_for(extension),
|
|
132
|
+
container: extension,
|
|
133
|
+
width: JsonValue.dimension(file_hash["width"]?),
|
|
134
|
+
height: JsonValue.dimension(file_hash["height"]?),
|
|
135
|
+
filesize: int_value(file_hash["size"]?),
|
|
136
|
+
duration_seconds: Timestamp.clock_seconds(JsonValue.string(file_hash["length"]?)),
|
|
137
|
+
note: label,
|
|
138
|
+
)
|
|
139
|
+
formats << format
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
formats
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private def download_url(identifier : String, file_name : String) : URI
|
|
146
|
+
encoded_name = file_name.split('/').reject(&.empty?).map do |segment|
|
|
147
|
+
URI.encode_path_segment(segment)
|
|
148
|
+
end.join('/')
|
|
149
|
+
|
|
150
|
+
encoded_identifier = URI.encode_path_segment(identifier)
|
|
151
|
+
URI.parse("#{@base_url}/download/#{encoded_identifier}/#{encoded_name}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private def thumbnail_url_for(identifier : String) : URI
|
|
155
|
+
URI.parse("#{@base_url}/services/img/#{URI.encode_path_segment(identifier)}")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private def kind_for(extension : String) : FormatKind
|
|
159
|
+
if AUDIO_EXTENSIONS.includes?(extension)
|
|
160
|
+
FormatKind::Audio
|
|
161
|
+
else
|
|
162
|
+
FormatKind::Combined
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private def date_value(metadata : Hash(String, JSON::Any)) : String?
|
|
167
|
+
value = JsonValue.string(metadata["date"]?) || JsonValue.string(metadata["addeddate"]?)
|
|
168
|
+
return nil unless value
|
|
169
|
+
|
|
170
|
+
value.size >= 10 ? value[0, 10] : value
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private def int_value(value : JSON::Any?) : Int64?
|
|
174
|
+
parsed = JsonValue.integer(value)
|
|
175
|
+
return nil unless parsed && parsed >= 0
|
|
176
|
+
|
|
177
|
+
parsed
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
require "../errors"
|
|
4
|
+
require "../extractor"
|
|
5
|
+
require "../http_url"
|
|
6
|
+
require "../media"
|
|
7
|
+
require "../playlist"
|
|
8
|
+
|
|
9
|
+
module Rill::Extractors
|
|
10
|
+
class M3u < Extractor
|
|
11
|
+
EXTINF = "#EXTINF:"
|
|
12
|
+
|
|
13
|
+
def name : String
|
|
14
|
+
"m3u"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def matches?(url : URI) : Bool
|
|
18
|
+
return false unless url.scheme == "http" || url.scheme == "https"
|
|
19
|
+
|
|
20
|
+
File.extname(url.path).downcase == ".m3u"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def extract(url : URI, client : HTTPClient) : Playlist
|
|
24
|
+
response = client.get(url)
|
|
25
|
+
|
|
26
|
+
if response.body.includes?("#EXT-X-")
|
|
27
|
+
raise UnsupportedManifestError.new("HLS playlists must use a .m3u8 URL")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
entries = parse_entries(response.body, response.url)
|
|
31
|
+
if entries.empty?
|
|
32
|
+
raise MediaUnavailableError.new(url.to_s, "playlist has no entries")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Playlist.new(
|
|
36
|
+
title: title_for(url),
|
|
37
|
+
webpage_url: url,
|
|
38
|
+
extractor: name,
|
|
39
|
+
entries: entries,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def parse_entries(body : String, base_url : URI) : Array(PlaylistEntry)
|
|
44
|
+
entries = [] of PlaylistEntry
|
|
45
|
+
pending_title : String? = nil
|
|
46
|
+
|
|
47
|
+
body.lchop('\uFEFF').each_line do |raw|
|
|
48
|
+
line = raw.strip
|
|
49
|
+
next if line.empty?
|
|
50
|
+
|
|
51
|
+
if line.starts_with?(EXTINF)
|
|
52
|
+
pending_title = parse_title(line[EXTINF.size..])
|
|
53
|
+
elsif line.starts_with?('#')
|
|
54
|
+
next
|
|
55
|
+
else
|
|
56
|
+
if entry_url = HttpUrl.resolve(base_url, line)
|
|
57
|
+
entries << PlaylistEntry.new(url: entry_url, title: pending_title)
|
|
58
|
+
end
|
|
59
|
+
pending_title = nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
entries
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def parse_title(value : String) : String?
|
|
67
|
+
_, separator, title = value.partition(',')
|
|
68
|
+
return nil if separator.empty?
|
|
69
|
+
|
|
70
|
+
title = title.strip
|
|
71
|
+
title.empty? ? nil : title
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def title_for(url : URI) : String
|
|
75
|
+
basename = File.basename(url.path)
|
|
76
|
+
stem = File.basename(basename, File.extname(basename))
|
|
77
|
+
decoded = URI.decode(stem)
|
|
78
|
+
decoded.empty? ? "playlist" : decoded
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
require "../errors"
|
|
5
|
+
require "../extractor"
|
|
6
|
+
require "../format"
|
|
7
|
+
require "../http_url"
|
|
8
|
+
require "../json_value"
|
|
9
|
+
require "../media"
|
|
10
|
+
require "../mime_type"
|
|
11
|
+
|
|
12
|
+
module Rill::Extractors
|
|
13
|
+
class MediaCccDe < Extractor
|
|
14
|
+
DEFAULT_API_BASE = "https://media.ccc.de"
|
|
15
|
+
EVENT_PATH = %r{\A/v/([^/]+)/?\z}
|
|
16
|
+
CONFERENCE_PATH = %r{\A/(?:c|conferences)/([^/]+)/?\z}
|
|
17
|
+
|
|
18
|
+
def initialize(@api_base : String = DEFAULT_API_BASE)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def name : String
|
|
22
|
+
"media_ccc_de"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def matches?(url : URI) : Bool
|
|
26
|
+
return false unless url.scheme == "http" || url.scheme == "https"
|
|
27
|
+
return false unless host?(url.host)
|
|
28
|
+
|
|
29
|
+
path = url.path
|
|
30
|
+
!EVENT_PATH.match(path).nil? || !CONFERENCE_PATH.match(path).nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extract(url : URI, client : HTTPClient) : Media | Playlist
|
|
34
|
+
if slug = slug_for(url, EVENT_PATH)
|
|
35
|
+
extract_event(url, client, slug)
|
|
36
|
+
elsif acronym = slug_for(url, CONFERENCE_PATH)
|
|
37
|
+
extract_conference(url, client, acronym)
|
|
38
|
+
else
|
|
39
|
+
raise UnsupportedUrlError.new(url.to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def extract_event(url : URI, client : HTTPClient, slug : String) : Media
|
|
44
|
+
api_url = URI.parse("#{@api_base}/public/events/#{URI.encode_path_segment(slug)}")
|
|
45
|
+
payload = api_payload(client, api_url, url, "event")
|
|
46
|
+
|
|
47
|
+
formats = formats_from(payload["recordings"]?)
|
|
48
|
+
if formats.empty?
|
|
49
|
+
raise MediaUnavailableError.new(url.to_s, "event has no downloadable recordings")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Media.new(
|
|
53
|
+
id: JsonValue.string(payload["guid"]?),
|
|
54
|
+
title: JsonValue.string(payload["title"]?) || slug,
|
|
55
|
+
description: JsonValue.string(payload["description"]?),
|
|
56
|
+
webpage_url: url,
|
|
57
|
+
uploader: persons(payload["persons"]?),
|
|
58
|
+
duration_seconds: JsonValue.seconds(payload["duration"]?),
|
|
59
|
+
thumbnail_url: thumbnail_url(payload),
|
|
60
|
+
upload_date: date_value(payload),
|
|
61
|
+
extractor: name,
|
|
62
|
+
formats: formats,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private def extract_conference(
|
|
67
|
+
url : URI,
|
|
68
|
+
client : HTTPClient,
|
|
69
|
+
acronym : String,
|
|
70
|
+
) : Playlist
|
|
71
|
+
api_url = URI.parse("#{@api_base}/public/conferences/#{URI.encode_path_segment(acronym)}")
|
|
72
|
+
payload = api_payload(client, api_url, url, "conference")
|
|
73
|
+
|
|
74
|
+
entries = [] of PlaylistEntry
|
|
75
|
+
events = payload["events"]?.try(&.as_a?) || [] of JSON::Any
|
|
76
|
+
|
|
77
|
+
events.each do |event|
|
|
78
|
+
event_hash = event.as_h?
|
|
79
|
+
next unless event_hash
|
|
80
|
+
|
|
81
|
+
entry_url = event_url(event_hash)
|
|
82
|
+
next unless entry_url
|
|
83
|
+
|
|
84
|
+
entries << PlaylistEntry.new(
|
|
85
|
+
url: entry_url,
|
|
86
|
+
title: JsonValue.string(event_hash["title"]?),
|
|
87
|
+
id: JsonValue.string(event_hash["guid"]?),
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if entries.empty?
|
|
92
|
+
raise MediaUnavailableError.new(url.to_s, "conference has no events")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Playlist.new(
|
|
96
|
+
id: acronym,
|
|
97
|
+
title: JsonValue.string(payload["title"]?) || acronym,
|
|
98
|
+
webpage_url: url,
|
|
99
|
+
extractor: name,
|
|
100
|
+
entries: entries,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def api_payload(
|
|
105
|
+
client : HTTPClient,
|
|
106
|
+
api_url : URI,
|
|
107
|
+
url : URI,
|
|
108
|
+
subject : String,
|
|
109
|
+
) : Hash(String, JSON::Any)
|
|
110
|
+
response = client.get(api_url)
|
|
111
|
+
parse_payload(response.body, url)
|
|
112
|
+
rescue ex : HTTPError
|
|
113
|
+
if ex.status.code == 404
|
|
114
|
+
raise MediaUnavailableError.new(url.to_s, "#{subject} not found")
|
|
115
|
+
end
|
|
116
|
+
raise ex
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def host?(host : String?) : Bool
|
|
120
|
+
case host.try(&.downcase)
|
|
121
|
+
when "media.ccc.de", "www.media.ccc.de"
|
|
122
|
+
true
|
|
123
|
+
else
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private def slug_for(url : URI, pattern : Regex) : String?
|
|
129
|
+
match = pattern.match(url.path)
|
|
130
|
+
return nil unless match
|
|
131
|
+
|
|
132
|
+
slug = URI.decode(match[1])
|
|
133
|
+
slug.empty? ? nil : slug
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private def parse_payload(body : String, url : URI) : Hash(String, JSON::Any)
|
|
137
|
+
payload = JSON.parse(body).as_h?
|
|
138
|
+
unless payload
|
|
139
|
+
raise MediaUnavailableError.new(url.to_s, "metadata is not a JSON object")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
payload
|
|
143
|
+
rescue JSON::ParseException
|
|
144
|
+
raise MediaUnavailableError.new(url.to_s, "metadata is not valid JSON")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private def formats_from(recordings_value : JSON::Any?) : Array(Format)
|
|
148
|
+
recordings = recordings_value.try(&.as_a?)
|
|
149
|
+
return [] of Format unless recordings
|
|
150
|
+
|
|
151
|
+
formats = [] of Format
|
|
152
|
+
|
|
153
|
+
recordings.each do |recording|
|
|
154
|
+
recording_hash = recording.as_h?
|
|
155
|
+
next unless recording_hash
|
|
156
|
+
|
|
157
|
+
filename = JsonValue.string(recording_hash["filename"]?)
|
|
158
|
+
next unless filename
|
|
159
|
+
|
|
160
|
+
recording_url = HttpUrl.absolute(JsonValue.string(recording_hash["recording_url"]?))
|
|
161
|
+
next unless recording_url
|
|
162
|
+
|
|
163
|
+
mime_type = JsonValue.string(recording_hash["mime_type"]?) || ""
|
|
164
|
+
mime = MimeType.parse(mime_type)
|
|
165
|
+
next if mime.empty?
|
|
166
|
+
|
|
167
|
+
audio = mime.audio?
|
|
168
|
+
formats << Format.new(
|
|
169
|
+
format_id: filename,
|
|
170
|
+
url: recording_url,
|
|
171
|
+
kind: audio ? FormatKind::Audio : FormatKind::Combined,
|
|
172
|
+
container: mime.container,
|
|
173
|
+
video_codec: audio ? nil : mime.codecs,
|
|
174
|
+
audio_codec: audio ? mime.audio_codec : nil,
|
|
175
|
+
width: JsonValue.dimension(recording_hash["width"]?),
|
|
176
|
+
height: JsonValue.dimension(recording_hash["height"]?),
|
|
177
|
+
language: JsonValue.string(recording_hash["language"]?),
|
|
178
|
+
note: mime_type,
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
formats
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private def persons(value : JSON::Any?) : String?
|
|
186
|
+
array = value.try(&.as_a?)
|
|
187
|
+
return nil unless array
|
|
188
|
+
|
|
189
|
+
names = array.compact_map(&.as_s?)
|
|
190
|
+
names.empty? ? nil : names.join(", ")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private def event_url(event : Hash(String, JSON::Any)) : URI?
|
|
194
|
+
value = JsonValue.string(event["frontend_link"]?) || JsonValue.string(event["link"]?)
|
|
195
|
+
|
|
196
|
+
if value.nil?
|
|
197
|
+
slug = JsonValue.string(event["slug"]?)
|
|
198
|
+
value = "#{DEFAULT_API_BASE}/v/#{URI.encode_path_segment(slug)}" if slug
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
return nil unless value
|
|
202
|
+
value = value.sub("/public/events/", "/v/")
|
|
203
|
+
HttpUrl.absolute(value)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private def thumbnail_url(payload : Hash(String, JSON::Any)) : URI?
|
|
207
|
+
value = JsonValue.string(payload["thumb_url"]?) || JsonValue.string(payload["poster_url"]?)
|
|
208
|
+
value ? HttpUrl.absolute(value) : nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private def date_value(payload : Hash(String, JSON::Any)) : String?
|
|
212
|
+
value = JsonValue.string(payload["date"]?)
|
|
213
|
+
return nil unless value
|
|
214
|
+
return nil if value.size < 10
|
|
215
|
+
|
|
216
|
+
value[0, 10]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|