diff --git a/LICENSE b/LICENSE index 731158a4..7ad52c2b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ /* 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: diff --git a/NEWS.md b/NEWS.md index c2322c97..d83b63b4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ========== diff --git a/bin/build-player.js b/bin/build-player.js index 281e926a..63e2bebc 100755 --- a/bin/build-player.js +++ b/bin/build-player.js @@ -15,20 +15,26 @@ var order = [ 'vimeo.coffee', 'youtube.coffee', + // playerjs-based players 'playerjs.coffee', - 'iframechild.coffee', - 'odysee.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', + 'iframechild.coffee', + 'odysee.coffee', + 'streamable.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' ]; diff --git a/package.json b/package.json index 99064a4a..9d3e80f0 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.82.11", + "version": "3.83.0", "repository": { "url": "http://github.com/calzoneman/sync" }, "license": "MIT", "dependencies": { "@calzoneman/jsli": "^2.0.1", - "@cytube/mediaquery": "github:CyTube/mediaquery#4f803961d72a4fc7a1e09c0babaf8ea685013b1b", + "@cytube/mediaquery": "github:CyTube/mediaquery#1efa3253ead853d977564ce677f2fb94d618b49e", "bcrypt": "^5.0.1", "bluebird": "^3.7.2", "body-parser": "^1.19.0", diff --git a/player/iframechild.coffee b/player/iframechild.coffee index 9147fded..5fdb3b10 100644 --- a/player/iframechild.coffee +++ b/player/iframechild.coffee @@ -23,7 +23,8 @@ window.IframeChild = class IframeChild extends PlayerJSPlayer setupFrame: (iframe, data) -> 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.document.querySelector('#ytapiplayer').classList.add('vjs-16-9') adapter = iframe.contentWindow.playerjs.VideoJSAdapter(iframe.contentWindow.PLAYER.player) diff --git a/player/peertube.coffee b/player/peertube.coffee index b8a07a36..de509323 100644 --- a/player/peertube.coffee +++ b/player/peertube.coffee @@ -21,7 +21,7 @@ window.PeerPlayer = class PeerPlayer extends Player site = new URL(document.URL).hostname embedSrc = data.meta.embed.domain - link = "#{embedSrc}" + link = "#{embedSrc}" alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site), 'alert-warning') .removeClass('col-md-12') diff --git a/src/custom-media.js b/src/custom-media.js index 8de50841..bb57792b 100644 --- a/src/custom-media.js +++ b/src/custom-media.js @@ -176,6 +176,14 @@ export function validate(data) { validateSources(data.sources, data); validateAudioTracks(data.audioTracks); 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) { @@ -198,8 +206,7 @@ function validateSources(sources, data) { throw new ValidationError( `contentType "${source.contentType}" requires live: true` ); - - // TODO: This should be allowed + // TODO (Xaekai): This should be allowed if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(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) { const url = urlParse(urlstring); diff --git a/src/io/ioserver.js b/src/io/ioserver.js index f23a932a..1bd9757b 100644 --- a/src/io/ioserver.js +++ b/src/io/ioserver.js @@ -196,7 +196,7 @@ class IOServer { handleConnection(socket) { if (!this.checkIPLimit(socket)) { - //return; + return; } patchTypecheckedFunctions(socket); diff --git a/src/peertubelist.js b/src/peertubelist.js new file mode 100644 index 00000000..3fe77474 --- /dev/null +++ b/src/peertubelist.js @@ -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); + } +} diff --git a/src/server.js b/src/server.js index b1a5b3b9..9974953e 100644 --- a/src/server.js +++ b/src/server.js @@ -204,6 +204,8 @@ var Server = function () { // background tasks init ---------------------------------------------- require("./bgtask")(self); + require("./peertubelist").setupPeertubeDomains().then(() => {}); + // prometheus server const prometheusConfig = Config.getPrometheusConfig(); if (prometheusConfig.isEnabled()) { diff --git a/src/utilities.js b/src/utilities.js index 92fa8cdc..250b9aaf 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -205,17 +205,20 @@ return "https://clips.twitch.tv/" + id; case "cm": return id; - case "pt": + case "pt": { const [domain,uuid] = id.split(';'); return `https://${domain}/videos/watch/${uuid}`; + } case "bc": return `https://www.bitchute.com/video/${id}/`; - case "bn": + case "bn": { const [artist,track] = id.split(';'); return `https://${artist}.bandcamp.com/track/${track}`; - case "od": + } + case "od": { const [user,video] = id.split(';'); return `https://odysee.com/@${user}/${video}`; + } case "nv": return `https://www.nicovideo.jp/watch/${id}`; default: diff --git a/test/custom-media.js b/test/custom-media.js index b432a3f5..2cb98d69 100644 --- a/test/custom-media.js +++ b/test/custom-media.js @@ -233,7 +233,8 @@ describe('custom-media', () => { contentType: 'text/vtt', name: 'English Subtitles' } - ] + ], + thumbnail: 'https://example.com/thumb.jpg', } }; }); @@ -321,7 +322,8 @@ describe('custom-media', () => { contentType: 'text/vtt', name: 'English Subtitles' } - ] + ], + thumbnail: 'https://example.com/thumb.jpg', } }; diff --git a/www/js/niconico.js b/www/js/niconico.js index d257345d..18f77402 100644 --- a/www/js/niconico.js +++ b/www/js/niconico.js @@ -3,6 +3,8 @@ * Written by Xaekai * Copyright (c) 2022 Radiant Feather; Licensed AGPLv3 * + * Dual-licensed MIT when distributed with CyTube/sync. + * */ class NicovideoEmbed { static origin = 'https://embed.nicovideo.jp'; @@ -65,6 +67,7 @@ class NicovideoEmbed { const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`); source.search = new URLSearchParams({ jsapi: 1, + autoplay: 1, playerId }); iframe.setAttribute('src', source); diff --git a/www/js/util.js b/www/js/util.js index d8ecb5d5..6d26eeff 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -3102,6 +3102,7 @@ CSEmoteList.prototype.loadPage = function (page) { var row = document.createElement("tr"); tbody.appendChild(row); + // TODO: refactor this garbage (function (emote, row) { // Add delete button var tdDelete = document.createElement("td"); @@ -3201,25 +3202,58 @@ CSEmoteList.prototype.loadPage = function (page) { $(tdImage).find(".popover").remove(); $urlDisplay.detach(); + var inputGroup = document.createElement("div"); + inputGroup.className = "input-group"; + var editInput = document.createElement("input"); editInput.className = "form-control"; editInput.type = "text"; 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(); function save() { var val = editInput.value; - tdImage.removeChild(editInput); - tdImage.appendChild(urlDisplay); + + if (val === emote.image) { + cleanup(); + return; + } socket.emit("updateEmote", { name: emote.name, image: val }); + + cleanup(); } - editInput.onblur = save; + function cleanup() { + tdImage.removeChild(inputGroup); + tdImage.appendChild(urlDisplay); + } + + cancelBtn.onclick = cleanup; + saveBtn.onclick = save; editInput.onkeyup = function (event) { if (event.keyCode === 13) { save();