This commit is contained in:
Calvin Montgomery 2022-08-28 17:08:28 -07:00
parent fd9586e0da
commit 9e0f7b8efa
14 changed files with 139 additions and 29 deletions

View file

@ -1,6 +1,6 @@
/* /*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Calvin Montgomery and contributors Copyright (c) 2013-2022 Calvin Montgomery and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -1,3 +1,12 @@
2022-08-28
==========
This release integrates Xaekai's added support for Bandcamp, BitChute, Odysee,
and Nicovideo playback support into the main repository. The updated support
for custom fonts and audio tracks in custom media manifests is also included,
but does not work out of the box -- it requires a separate channel script; this
may be addressed in the future.
2021-08-14 2021-08-14
========== ==========

View file

@ -15,20 +15,26 @@ var order = [
'vimeo.coffee', 'vimeo.coffee',
'youtube.coffee', 'youtube.coffee',
// playerjs-based players
'playerjs.coffee', 'playerjs.coffee',
'iframechild.coffee', 'iframechild.coffee',
'odysee.coffee', 'odysee.coffee',
'streamable.coffee', 'streamable.coffee',
'embed.coffee',
'custom-embed.coffee',
'livestream.com.coffee',
'twitchclip.coffee',
'videojs.coffee',
'gdrive-player.coffee',
'hls.coffee',
'raw-file.coffee',
'rtmp.coffee',
// iframe embed-based players
'embed.coffee',
'custom-embed.coffee',
'livestream.com.coffee',
'twitchclip.coffee',
// video.js-based players
'videojs.coffee',
'gdrive-player.coffee',
'hls.coffee',
'raw-file.coffee',
'rtmp.coffee',
// mediaUpdate handler
'update.coffee' 'update.coffee'
]; ];

View file

@ -2,14 +2,14 @@
"author": "Calvin Montgomery", "author": "Calvin Montgomery",
"name": "CyTube", "name": "CyTube",
"description": "Online media synchronizer and chat", "description": "Online media synchronizer and chat",
"version": "3.82.11", "version": "3.83.0",
"repository": { "repository": {
"url": "http://github.com/calzoneman/sync" "url": "http://github.com/calzoneman/sync"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@calzoneman/jsli": "^2.0.1", "@calzoneman/jsli": "^2.0.1",
"@cytube/mediaquery": "github:CyTube/mediaquery#4f803961d72a4fc7a1e09c0babaf8ea685013b1b", "@cytube/mediaquery": "github:CyTube/mediaquery#1efa3253ead853d977564ce677f2fb94d618b49e",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",

View file

@ -23,7 +23,8 @@ window.IframeChild = class IframeChild extends PlayerJSPlayer
setupFrame: (iframe, data) -> setupFrame: (iframe, data) ->
iframe.addEventListener('load', => iframe.addEventListener('load', =>
iframe.contentWindow.VOLUME = VOLUME; # TODO: ideally, communication with the child frame should use postMessage()
iframe.contentWindow.VOLUME = VOLUME
iframe.contentWindow.loadMediaPlayer(Object.assign({}, data, { type: 'cm' } )) iframe.contentWindow.loadMediaPlayer(Object.assign({}, data, { type: 'cm' } ))
iframe.contentWindow.document.querySelector('#ytapiplayer').classList.add('vjs-16-9') iframe.contentWindow.document.querySelector('#ytapiplayer').classList.add('vjs-16-9')
adapter = iframe.contentWindow.playerjs.VideoJSAdapter(iframe.contentWindow.PLAYER.player) adapter = iframe.contentWindow.playerjs.VideoJSAdapter(iframe.contentWindow.PLAYER.player)

View file

@ -21,7 +21,7 @@ window.PeerPlayer = class PeerPlayer extends Player
site = new URL(document.URL).hostname site = new URL(document.URL).hostname
embedSrc = data.meta.embed.domain embedSrc = data.meta.embed.domain
link = "<a href=\"http://#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>" link = "<a href=\"http://#{embedSrc}\" target=\"_blank\" rel=\"noopener noreferer\"><strong>#{embedSrc}</strong></a>"
alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site), alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site),
'alert-warning') 'alert-warning')
.removeClass('col-md-12') .removeClass('col-md-12')

View file

@ -176,6 +176,14 @@ export function validate(data) {
validateSources(data.sources, data); validateSources(data.sources, data);
validateAudioTracks(data.audioTracks); validateAudioTracks(data.audioTracks);
validateTextTracks(data.textTracks); validateTextTracks(data.textTracks);
/*
* TODO: Xaekai's Octopus subtitle support uses a separate subTracks array
* in a slightly different format than textTracks. That currently requires
* a channel script to use, but if that is integrated in core then it needs
* to be validated here (and ideally merged with textTracks so there is only
* one array).
*/
validateFonts(data.fonts);
} }
function validateSources(sources, data) { function validateSources(sources, data) {
@ -198,8 +206,7 @@ function validateSources(sources, data) {
throw new ValidationError( throw new ValidationError(
`contentType "${source.contentType}" requires live: true` `contentType "${source.contentType}" requires live: true`
); );
// TODO (Xaekai): This should be allowed
// TODO: This should be allowed
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality)) if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
throw new ValidationError(`unacceptable source quality "${source.quality}"`); throw new ValidationError(`unacceptable source quality "${source.quality}"`);
@ -288,6 +295,20 @@ function validateTextTracks(textTracks) {
} }
} }
function validateFonts(fonts) {
if (typeof textTracks === 'undefined') {
return;
}
if (!Array.isArray(fonts))
throw new ValidationError('fonts must be a list of URLs');
for (let f of fonts) {
if (typeof f !== 'string')
throw new ValidationError('fonts must be a list of URLs');
}
}
function parseURL(urlstring) { function parseURL(urlstring) {
const url = urlParse(urlstring); const url = urlParse(urlstring);

View file

@ -196,7 +196,7 @@ class IOServer {
handleConnection(socket) { handleConnection(socket) {
if (!this.checkIPLimit(socket)) { if (!this.checkIPLimit(socket)) {
//return; return;
} }
patchTypecheckedFunctions(socket); patchTypecheckedFunctions(socket);

29
src/peertubelist.js Normal file
View file

@ -0,0 +1,29 @@
import { fetchPeertubeDomains, setDomains } from '@cytube/mediaquery/lib/provider/peertube';
import { stat, readFile, writeFile } from 'node:fs/promises';
import path from 'path';
const LOGGER = require('@calzoneman/jsli')('peertubelist');
const ONE_DAY = 24 * 3600 * 1000;
const FILENAME = path.join(__dirname, '..', 'peertube-hosts.json');
export async function setupPeertubeDomains() {
try {
let mtime;
try {
mtime = (await stat(FILENAME)).mtime;
} catch (_error) {
mtime = 0;
}
if (Date.now() - mtime > ONE_DAY) {
LOGGER.info('Updating peertube host list');
const hosts = await fetchPeertubeDomains();
await writeFile(FILENAME, JSON.stringify(hosts));
}
const hosts = JSON.parse(await readFile(FILENAME));
setDomains(hosts);
} catch (error) {
LOGGER.error('Failed to initialize peertube host list: %s', error.stack);
}
}

View file

@ -204,6 +204,8 @@ var Server = function () {
// background tasks init ---------------------------------------------- // background tasks init ----------------------------------------------
require("./bgtask")(self); require("./bgtask")(self);
require("./peertubelist").setupPeertubeDomains().then(() => {});
// prometheus server // prometheus server
const prometheusConfig = Config.getPrometheusConfig(); const prometheusConfig = Config.getPrometheusConfig();
if (prometheusConfig.isEnabled()) { if (prometheusConfig.isEnabled()) {

View file

@ -205,17 +205,20 @@
return "https://clips.twitch.tv/" + id; return "https://clips.twitch.tv/" + id;
case "cm": case "cm":
return id; return id;
case "pt": case "pt": {
const [domain,uuid] = id.split(';'); const [domain,uuid] = id.split(';');
return `https://${domain}/videos/watch/${uuid}`; return `https://${domain}/videos/watch/${uuid}`;
}
case "bc": case "bc":
return `https://www.bitchute.com/video/${id}/`; return `https://www.bitchute.com/video/${id}/`;
case "bn": case "bn": {
const [artist,track] = id.split(';'); const [artist,track] = id.split(';');
return `https://${artist}.bandcamp.com/track/${track}`; return `https://${artist}.bandcamp.com/track/${track}`;
case "od": }
case "od": {
const [user,video] = id.split(';'); const [user,video] = id.split(';');
return `https://odysee.com/@${user}/${video}`; return `https://odysee.com/@${user}/${video}`;
}
case "nv": case "nv":
return `https://www.nicovideo.jp/watch/${id}`; return `https://www.nicovideo.jp/watch/${id}`;
default: default:

View file

@ -233,7 +233,8 @@ describe('custom-media', () => {
contentType: 'text/vtt', contentType: 'text/vtt',
name: 'English Subtitles' name: 'English Subtitles'
} }
] ],
thumbnail: 'https://example.com/thumb.jpg',
} }
}; };
}); });
@ -321,7 +322,8 @@ describe('custom-media', () => {
contentType: 'text/vtt', contentType: 'text/vtt',
name: 'English Subtitles' name: 'English Subtitles'
} }
] ],
thumbnail: 'https://example.com/thumb.jpg',
} }
}; };

View file

@ -3,6 +3,8 @@
* Written by Xaekai * Written by Xaekai
* Copyright (c) 2022 Radiant Feather; Licensed AGPLv3 * Copyright (c) 2022 Radiant Feather; Licensed AGPLv3
* *
* Dual-licensed MIT when distributed with CyTube/sync.
*
*/ */
class NicovideoEmbed { class NicovideoEmbed {
static origin = 'https://embed.nicovideo.jp'; static origin = 'https://embed.nicovideo.jp';
@ -65,6 +67,7 @@ class NicovideoEmbed {
const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`); const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`);
source.search = new URLSearchParams({ source.search = new URLSearchParams({
jsapi: 1, jsapi: 1,
autoplay: 1,
playerId playerId
}); });
iframe.setAttribute('src', source); iframe.setAttribute('src', source);

View file

@ -3102,6 +3102,7 @@ CSEmoteList.prototype.loadPage = function (page) {
var row = document.createElement("tr"); var row = document.createElement("tr");
tbody.appendChild(row); tbody.appendChild(row);
// TODO: refactor this garbage
(function (emote, row) { (function (emote, row) {
// Add delete button // Add delete button
var tdDelete = document.createElement("td"); var tdDelete = document.createElement("td");
@ -3201,25 +3202,58 @@ CSEmoteList.prototype.loadPage = function (page) {
$(tdImage).find(".popover").remove(); $(tdImage).find(".popover").remove();
$urlDisplay.detach(); $urlDisplay.detach();
var inputGroup = document.createElement("div");
inputGroup.className = "input-group";
var editInput = document.createElement("input"); var editInput = document.createElement("input");
editInput.className = "form-control"; editInput.className = "form-control";
editInput.type = "text"; editInput.type = "text";
editInput.value = emote.image; editInput.value = emote.image;
tdImage.appendChild(editInput); inputGroup.appendChild(editInput);
var btnGroup = document.createElement("div");
btnGroup.className = "input-group-btn";
var saveBtn = document.createElement("button");
saveBtn.className = "btn btn-success";
saveBtn.textContent = "Save";
saveBtn.type = "button";
btnGroup.appendChild(saveBtn);
var cancelBtn = document.createElement("button");
cancelBtn.className = "btn btn-danger";
cancelBtn.textContent = "Cancel";
cancelBtn.type = "button";
btnGroup.appendChild(cancelBtn);
inputGroup.appendChild(btnGroup);
tdImage.appendChild(inputGroup);
editInput.focus(); editInput.focus();
function save() { function save() {
var val = editInput.value; var val = editInput.value;
tdImage.removeChild(editInput);
tdImage.appendChild(urlDisplay); if (val === emote.image) {
cleanup();
return;
}
socket.emit("updateEmote", { socket.emit("updateEmote", {
name: emote.name, name: emote.name,
image: val image: val
}); });
cleanup();
} }
editInput.onblur = save; function cleanup() {
tdImage.removeChild(inputGroup);
tdImage.appendChild(urlDisplay);
}
cancelBtn.onclick = cleanup;
saveBtn.onclick = save;
editInput.onkeyup = function (event) { editInput.onkeyup = function (event) {
if (event.keyCode === 13) { if (event.keyCode === 13) {
save(); save();