Project

General

Profile

Download (11.9 KB) Statistics
| Branch: | Revision:

hydrilla-fixes-bundle / src / odysee-com-fix / odysee.js @ ecc6c218

1
/**
2
 * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
3
 *
4
 * Make video playback and search on odysee.com functional without nonfree js
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

    
44
/* use with https://odysee.com/*** */
45

    
46
/* Helper functions for ajax. */
47

    
48
function ajax_callback(xhttp, cb)
49
{
50
    cb(xhttp.response);
51
}
52

    
53
function perform_ajax(method, url, callback, err_callback, data)
54
{
55
    const xhttp = new XMLHttpRequest();
56
    xhttp.onload = () => ajax_callback(xhttp, callback);
57
    xhttp.onerror = err_callback;
58
    xhttp.onabort = err_callback;
59
    xhttp.open(method, url, true);
60
    try {
61
	xhttp.send(data);
62
    } catch(e) {
63
	err_callback();
64
    }
65
}
66

    
67
/* Helper functions for strings with HTML entities (e.g. `&quot;'). */
68
function HTML_decode(text)
69
{
70
    const tmp_span = document.createElement("span");
71
    tmp_span.innerHTML = text;
72
    return tmp_span.textContent;
73
}
74

    
75
/* Odysee API servers. */
76

    
77
const odysee_resolve_url = "https://api.na-backend.odysee.com/api/v1/proxy?m=resolve";
78
const lighthouse_search_url = "https://lighthouse.odysee.com/search";
79

    
80
/*
81
 * If we're on a video page, show the video. Use JSON data embedded in <head>
82
 * if possible. If not - fetch video data using Odysee API.
83
 */
84

    
85
let data = null;
86

    
87
function process_json_script(json_script)
88
{
89
    try {
90
	data = JSON.parse(json_script.textContent);
91
    } catch (e) {
92
	console.log("Error parsing content data", e);
93
    }
94
}
95

    
96
for (const json_script of document.querySelectorAll("head script")) {
97
    if (["blocked-type", "type"].map(a => json_script.getAttribute(a))
98
	.includes("application/ld+json"))
99
	process_json_script(json_script);
100
}
101

    
102
const body = document.createElement("body");
103
const video_container = document.createElement("div");
104

    
105
body.appendChild(video_container);
106

    
107
function show_video(content_url, title, upload_date, description)
108
{
109
    if (content_url) {
110
	const video = document.createElement("video");
111
	const source = document.createElement("source");
112

    
113
	source.src = content_url;
114

    
115
	video.setAttribute("width", "100%");
116
	video.setAttribute("height", "auto");
117
	video.setAttribute("controls", "");
118

    
119
	video.appendChild(source);
120

    
121
	video_container.appendChild(video);
122
    }
123

    
124
    if (title) {
125
	const h1 = document.createElement("h1");
126

    
127
	h1.textContent = HTML_decode(title);
128
	h1.setAttribute("style", "color: #555;");
129

    
130
	video_container.appendChild(h1);
131
    }
132

    
133
    if (upload_date) {
134
	try {
135
	    const date = new Date(upload_date).toString();
136
	    const date_div = document.createElement("div");
137

    
138
	    date_div.textContent = `Uploaded: ${date}`;
139
	    date_div.setAttribute("style", "font-size: 14px; font-weight: bold; margin-bottom: 5px;");
140

    
141
	    video_container.appendChild(date_div);
142
	} catch(e) {
143
	    console.log("Error parsing content upload date", e);
144
	}
145
    }
146

    
147
    if (description) {
148
	const description_div = document.createElement("div");
149

    
150
	description_div.textContent = HTML_decode(description);
151
	description_div.setAttribute("style", "white-space: pre;");
152

    
153
	video_container.appendChild(description_div);
154
    }
155
}
156

    
157
function show_video_from_query(response)
158
{
159
    try {
160
	var result = Object.values(JSON.parse(response).result)[0];
161

    
162
	if (result.value_type !== "stream")
163
	    return;
164

    
165
	var date = result.timestamp * 1000;
166
	var description = result.value.description;
167
	var title = result.value.title;
168
	const name = encodeURIComponent(result.name);
169
	var url = `https://odysee.com/$/stream/${name}/${result.claim_id}`;
170
    } catch (e) {
171
	return;
172
    }
173

    
174
    show_video(url, title, date, description);
175
}
176

    
177
function fetch_show_video(name, claim_id)
178
{
179
    const payload = {
180
	jsonrpc: "2.0",
181
	method: "resolve",
182
	params: {
183
	    urls: [`lbry://${decodeURIComponent(name)}#${claim_id}`],
184
	    include_purchase_receipt: true
185
	},
186
	id: Math.round(Math.random() * 10**14)
187
    };
188

    
189
    perform_ajax("POST", odysee_resolve_url, show_video_from_query,
190
		 () => null, JSON.stringify(payload));
191
}
192

    
193
if (data && typeof data === "object" && data["@type"] === "VideoObject") {
194
    show_video(data.contentUrl, data.name, data.uploadDate, data.description);
195
} else {
196
    const match = /\/([^/]+):([0-9a-f]+)$/.exec(document.URL);
197
    if (match)
198
	fetch_show_video(match[1], match[2]);
199
}
200

    
201
/* Show search. */
202

    
203
const search_input = document.createElement("input");
204
const search_submit = document.createElement("button");
205
const search_form = document.createElement("form");
206
const error_div = document.createElement("div");
207

    
208
search_submit.textContent = "Search Odysee";
209

    
210
search_form.setAttribute("style", "margin: 15px 0 0 0;");
211

    
212
search_form.appendChild(search_input);
213
search_form.appendChild(search_submit);
214

    
215
error_div.textContent = "Failed to perform search :c";
216
error_div.setAttribute("style", "display: none;");
217

    
218
body.appendChild(search_form);
219
body.appendChild(error_div);
220

    
221
/* Replace the UI. */
222

    
223
document.documentElement.replaceChild(body, document.body);
224

    
225
/* Add the logic of performing search and showing results. */
226

    
227
function show_error()
228
{
229
    error_div.setAttribute("style", "color: #b44;");
230
}
231

    
232
function clear_error()
233
{
234
    error_div.setAttribute("style", "display: none;");
235
}
236

    
237
let results_div = null;
238
const load_more_but = document.createElement("button");
239

    
240
load_more_but.textContent = "Load more";
241

    
242
function show_search_entries(new_results_div, response)
243
{
244
    try {
245
	var results = Object.values(JSON.parse(response).result);
246
    } catch (e) {
247
	console.log("Failed to parse search response :c",
248
		    "Bad response format from api.na-backend.odysee.com.");
249
	show_error();
250
	return;
251
    }
252

    
253
    for (const result of results) {
254
	try {
255
	    if (result.value_type !== "stream")
256
		continue;
257

    
258
	    let channel_specifier = "";
259
	    let channel_name = null;
260
	    try {
261
		channel_name = result.signing_channel.name;
262
		const channel_name_enc = encodeURIComponent(channel_name);
263
		const channel_digit = result.signing_channel.claim_id[0];
264
		channel_specifier = `${channel_name_enc}:${channel_digit}`;
265
	    } catch (e) {
266
	    }
267
	    const video_name = encodeURIComponent(result.name);
268
	    const video_id = result.claim_id[0];
269

    
270
	    const result_a = document.createElement("a");
271
	    const thumbnail = document.createElement("img");
272
	    const title_span = document.createElement("span");
273
	    const uploader_div = document.createElement("div");
274
	    const description_div = document.createElement("div");
275

    
276
	    thumbnail.setAttribute("style", "width: 100px; height: auto;");
277
	    thumbnail.setAttribute("alt", result.value.thumbnail.url);
278
	    thumbnail.src = result.value.thumbnail.url;
279

    
280
	    title_span.setAttribute("style", "font-weight: bold;");
281
	    title_span.textContent = result.value.title;
282

    
283
	    uploader_div.setAttribute("style", "margin-left: 5px; font-size: 21px; color: #555;");
284
	    uploader_div.textContent = channel_name;
285

    
286
	    description_div.setAttribute("style", "white-space: pre;");
287
	    description_div.textContent = result.value.description;
288

    
289
	    result_a.setAttribute("style", "display: block; width: 100%; text-decoration: none; color: #333; margin: 8px; border-style: solid; border-width: 3px 0 0 0; border-color: #7aa;");
290
	    result_a.href = `https://odysee.com/${channel_specifier}/${video_name}:${video_id}`;
291

    
292
	    if (result.value.thumbnail.url)
293
		result_a.appendChild(thumbnail);
294
	    result_a.appendChild(title_span);
295
	    if (channel_name)
296
		result_a.appendChild(uploader_div);
297
	    result_a.appendChild(description_div);
298

    
299
	    new_results_div.appendChild(result_a);
300
	}
301
	catch(e) {
302
	    console.log(e);
303
	}
304
    }
305

    
306
    clear_error();
307

    
308
    if (results_div)
309
	results_div.remove();
310

    
311
    results_div = new_results_div;
312

    
313
    body.appendChild(results_div);
314
    body.appendChild(load_more_but);
315

    
316
    enable_search_form();
317
}
318

    
319
function search_ajax_error(url)
320
{
321
    console.log(`Failed to query ${url} :c`);
322
    show_error();
323
    enable_search_form();
324
}
325

    
326
function get_detailed_search_entries(new_results_div, response)
327
{
328
    /* TODO: Simplify JSON handling using JSON schemas. */
329
    try {
330
	var response_data = JSON.parse(response);
331
	if (!Array.isArray(response_data))
332
	    throw "Bad response format from lighthouse.odysee.com.";
333
    } catch (e) {
334
	show_error();
335
	console.log("Failed to parse search response :c", e);
336
	enable_search_form();
337
	return;
338
    }
339

    
340
    const callback = r => show_search_entries(new_results_div, r);
341
    const lbry_urls = [];
342

    
343
    for (const search_result of response_data) {
344
	if (!search_result.claimId || !search_result.name)
345
	    continue;
346
	lbry_urls.push(`lbry://${search_result.name}#${search_result.claimId}`);
347
    }
348

    
349
    const payload = {
350
	jsonrpc: "2.0",
351
	method: "resolve",
352
	params: {
353
	    urls: lbry_urls,
354
	    include_purchase_receipt: true
355
	},
356
	id: Math.round(Math.random() * 10**14)
357
    };
358

    
359
    const url = odysee_resolve_url;
360

    
361
    perform_ajax("POST", url, callback, () => search_ajax_error(url),
362
		 JSON.stringify(payload));
363
}
364

    
365
function get_search_entries(new_results_div, query, from)
366
{
367
    const callback = r => get_detailed_search_entries(new_results_div, r);
368
    const url = `${lighthouse_search_url}?s=${encodeURIComponent(query)}&size=20&from=${from}&claimType=file,channel&nsfw=false&free_only=true`;
369

    
370
    new_results_div.setAttribute("data-fetched", parseInt(from) + 20);
371

    
372
    perform_ajax("GET", url, callback, () => search_ajax_error(url));
373
}
374

    
375
function search(event)
376
{
377
    if (event)
378
	event.preventDefault();
379

    
380
    if (!/[^\s]/.test(search_input.value))
381
	return;
382

    
383
    disable_search_form();
384

    
385
    const new_results_div = document.createElement("div");
386

    
387
    new_results_div.setAttribute("data-query", search_input.value);
388

    
389
    get_search_entries(new_results_div, search_input.value, 0);
390
}
391

    
392
function search_more()
393
{
394
    disable_search_form();
395

    
396
    get_search_entries(results_div, results_div.getAttribute("data-query"),
397
		       results_div.getAttribute("data-fetched"));
398
}
399

    
400
load_more_but.addEventListener("click", search_more);
401

    
402
function enable_search_form()
403
{
404
    search_form.addEventListener("submit", search);
405
    search_submit.removeAttribute("disabled");
406
    if (results_div)
407
	load_more_but.removeAttribute("disabled");
408
}
409

    
410
function disable_search_form()
411
{
412
    search_form.removeEventListener("submit", search);
413
    search_submit.setAttribute("disabled", "");
414
    load_more_but.setAttribute("disabled", "");
415
}
416

    
417

    
418
enable_search_form();
419

    
420

    
421
const match = /^[^?]*search\?q=([^&]+)/.exec(document.URL)
422
if (match) {
423
    search_input.value = decodeURIComponent(match[1]);
424
    search();
425
}
(2-2/2)