Initial commit.

This commit is contained in:
Koen 2023-08-10 11:36:29 +02:00
commit 81bc6488f5
6 changed files with 868 additions and 0 deletions

9
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,9 @@
stages:
- deploy
deploy:
stage: deploy
script:
- sh deploy.sh
only:
- main

27
PeerTubeConfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "PeerTube",
"description": "A plugin that adds PeerTube as a source",
"author": "FUTO",
"authorUrl": "https://futo.org",
"sourceUrl": "https://plugins.grayjay.app/PeerTube/PeerTubeConfig.json",
"repositoryUrl": "https://futo.org",
"scriptUrl": "./PeerTubeScript.js",
"version": 13,
"iconUrl": "./peertube.png",
"id": "1c291164-294c-4c2d-800d-7bc6d31d0019",
"scriptSignature": "",
"scriptPublicKey": "",
"packages": ["Http"],
"allowEval": false,
"allowUrls": [
"everywhere"
],
"constants": {
"baseUrl": "https://peertube.futo.org"
}
}

335
PeerTubeScript.js Normal file
View file

@ -0,0 +1,335 @@
const PLATFORM = "PeerTube";
var config = {};
/**
* Build a query
* @param {{[key: string]: any}} params Query params
* @returns {String} Query string
*/
function buildQuery(params) {
let query = "";
let first = true;
for (const [key, value] of Object.entries(params)) {
if (value) {
if (first) {
first = false;
} else {
query += "&";
}
query += `${key}=${value}`;
}
}
return (query && query.length > 0) ? `?${query}` : "";
}
function getChannelPager(path, params, page) {
log(`getChannelPager page=${page}`, params)
const count = 20;
const start = (page ?? 0) * count;
params = { ... params, start, count }
const url = `${config.constants.baseUrl}${path}`;
const urlWithParams = `${url}${buildQuery(params)}`;
log("GET " + urlWithParams);
const res = http.GET(urlWithParams, {});
if (res.code != 200) {
log("Failed to get channels", res);
return new ChannelPager([], false);
}
const obj = JSON.parse(res.body);
return new PeerTubeChannelPager(obj.data.map(v => {
return new PlatformAuthorLink(new PlatformID(PLATFORM, v.name, config.id), v.displayName, v.url, v.avatar ? `${config.constants.baseUrl}${v.avatar.path}` : "");
}), obj.total > (start + count), path, params, page);
}
function getVideoPager(path, params, page) {
log(`getVideoPager page=${page}`, params)
const count = 20;
const start = (page ?? 0) * count;
params = { ... params, start, count }
const url = `${config.constants.baseUrl}${path}`;
const urlWithParams = `${url}${buildQuery(params)}`;
log("GET " + urlWithParams);
const res = http.GET(urlWithParams, {});
if (res.code != 200) {
log("Failed to get videos", res);
return new VideoPager([], false);
}
const obj = JSON.parse(res.body);
return new PeerTubeVideoPager(obj.data.map(v => {
return new PlatformVideo({
id: new PlatformID(PLATFORM, v.uuid, config.id),
name: v.name ?? "",
thumbnails: new Thumbnails([new Thumbnail(`${config.constants.baseUrl}${v.thumbnailPath}`, 0)]),
author: new PlatformAuthorLink(new PlatformID(PLATFORM, v.channel.name, config.id),
v.channel.displayName,
v.channel.url,
v.channel.avatar ? `${config.constants.baseUrl}${v.channel.avatar.path}` : ""),
datetime: Math.round((new Date(v.publishedAt)).getTime() / 1000),
duration: v.duration,
viewCount: v.views,
url: v.url,
isLive: v.isLive
});
}), obj.total > (start + count), path, params, page);
}
function getCommentPager(path, params, page) {
log(`getCommentPager page=${page}`, params)
const count = 20;
const start = (page ?? 0) * count;
params = { ... params, start, count }
const url = `${config.constants.baseUrl}${path}`;
const urlWithParams = `${url}${buildQuery(params)}`;
log("GET " + urlWithParams);
const res = http.GET(urlWithParams, {});
if (res.code != 200) {
log("Failed to get comments", res);
return new CommentPager([], false);
}
const obj = JSON.parse(res.body);
return new PeerTubeCommentPager(obj.data.map(v => {
return new Comment({
contextUrl: url,
author: new PlatformAuthorLink(new PlatformID(PLATFORM, v.account.name, config.id), v.account.displayName, `${config.constants.baseUrl}/api/v1/video-channels/${v.account.name}`, ""),
message: v.text,
rating: new RatingLikes(0),
date: Math.round((new Date(v.createdAt)).getTime() / 1000),
replyCount: v.totalReplies,
context: { id: v.id }
});
}), obj.total > (start + count), path, params, page);
}
source.enable = function (conf) {
config = conf ?? {};
};
source.getHome = function () {
return getVideoPager('/api/v1/videos', {
sort: "best"
}, 0);
};
source.searchSuggestions = function(query) {
return [];
};
source.getSearchCapabilities = () => {
return {
types: [Type.Feed.Mixed, Type.Feed.Streams, Type.Feed.Videos],
sorts: [Type.Order.Chronological, "publishedAt"]
};
};
source.search = function (query, type, order, filters) {
let sort = order;
if (sort === Type.Order.Chronological) {
sort = "-publishedAt";
}
const params = {
search: query,
sort
};
if (type == Type.Feed.Streams) {
params.isLive = true;
} else if (type == Type.Feed.Videos) {
params.isLive = false;
}
return getVideoPager('/api/v1/search/videos', params, 0);
};
source.searchChannels = function (query) {
return getChannelPager('/api/v1/search/video-channels', {
search: query
}, 0);
};
source.isChannelUrl = function(url) {
return url.startsWith(`${config.constants.baseUrl}/video-channels/`);
};
source.getChannel = function (url) {
const tokens = url.split('/');
const handle = tokens[tokens.length - 1];
const urlWithParams = `${config.constants.baseUrl}/api/v1/video-channels/${handle}`;
log("GET " + urlWithParams);
const res = http.GET(urlWithParams, {});
if (res.code != 200) {
log("Failed to get channel", res);
return null;
}
const obj = JSON.parse(res.body);
return new PlatformChannel({
id: new PlatformID(PLATFORM, obj.name, config.id),
name: obj.displayName,
thumbnail: obj.avatar ? `${config.constants.baseUrl}${obj.avatar.path}` : "",
banner: null,
subscribers: obj.followersCount,
description: obj.description ?? "",
url: obj.url,
links: {}
});
};
source.getChannelCapabilities = () => {
return {
types: [Type.Feed.Mixed, Type.Feed.Streams, Type.Feed.Videos],
sorts: [Type.Order.Chronological, "publishedAt"]
};
};
source.getChannelContents = function (url, type, order, filters) {
let sort = order;
if (sort === Type.Order.Chronological) {
sort = "-publishedAt";
}
const params = {
sort
};
if (type == Type.Feed.Streams) {
params.isLive = true;
} else if (type == Type.Feed.Videos) {
params.isLive = false;
}
const tokens = url.split('/');
const handle = tokens[tokens.length - 1];
return getVideoPager(`/api/v1/video-channels/${handle}/videos`, params, 0);
};
source.isContentDetailsUrl = function(url) {
return url.startsWith(`${config.constants.baseUrl}/videos/watch/`);
};
const supportedResolutions = {
'1080p': { width: 1920, height: 1080 },
'720p': { width: 1280, height: 720 },
'480p': { width: 854, height: 480 },
'360p': { width: 640, height: 360 },
'144p': { width: 256, height: 144 }
};
source.getContentDetails = function (url) {
const tokens = url.split('/');
const handle = tokens[tokens.length - 1];
const urlWithParams = `${config.constants.baseUrl}/api/v1/videos/${handle}`;
log("GET " + urlWithParams);
const res = http.GET(urlWithParams, {});
if (res.code != 200) {
log("Failed to get video detail", res);
return null;
}
const obj = JSON.parse(res.body);
const sources = [];
for (const streamingPlaylist of obj.streamingPlaylists) {
sources.push(new HLSSource({
name: "HLS",
url: streamingPlaylist.playlistUrl,
duration: obj.duration ?? 0,
priority: true
}));
for (const file of streamingPlaylist.files) {
let supportedResolution;
if (file.resolution.width && file.resolution.height) {
supportedResolution = { width: file.resolution.width, height: file.resolution.height };
} else {
supportedResolution = supportedResolutions[file.resolution.label];
}
if (!supportedResolution) {
continue;
}
sources.push(new VideoUrlSource({
name: file.resolution.label,
url: file.fileDownloadUrl,
width: supportedResolution.width,
height: supportedResolution.height,
duration: obj.duration,
container: "video/mp4"
}));
}
}
return new PlatformVideoDetails({
id: new PlatformID(PLATFORM, obj.uuid, config.id),
name: obj.name,
thumbnails: new Thumbnails([new Thumbnail(`${config.constants.baseUrl}${obj.thumbnailPath}`, 0)]),
author: new PlatformAuthorLink(new PlatformID(PLATFORM, obj.channel.name, config.id),
obj.channel.displayName,
obj.channel.url,
obj.channel.avatar ? `${config.constants.baseUrl}${obj.channel.avatar.path}` : ""),
datetime: Math.round((new Date(obj.publishedAt)).getTime() / 1000),
duration: obj.duration,
viewCount: obj.views,
url: obj.url,
isLive: obj.isLive,
description: obj.description,
video: new VideoSourceDescriptor(sources)
});
};
source.getComments = function (url) {
const tokens = url.split('/');
const handle = tokens[tokens.length - 1];
return getCommentPager(`/api/v1/videos/${handle}/comment-threads`, {}, 0);
}
source.getSubComments = function(comment) {
return getCommentPager(`/api/v1/videos/${comment.context.id}/comment-threads`, {}, 0);
}
class PeerTubeVideoPager extends VideoPager {
constructor(results, hasMore, path, params, page) {
super(results, hasMore, { path, params, page });
}
nextPage() {
return getVideoPager(this.context.path, this.context.params, (this.context.page ?? 0) + 1);
}
}
class PeerTubeChannelPager extends ChannelPager {
constructor(results, hasMore, path, params, page) {
super(results, hasMore, { path, params, page });
}
nextPage() {
return getChannelPager(this.context.path, this.context.params, (this.context.page ?? 0) + 1);
}
}
class PeerTubeCommentPager extends CommentPager {
constructor(results, hasMore, path, params, page) {
super(results, hasMore, { path, params, page });
}
nextPage() {
return getCommentPager(this.context.path, this.context.params, (this.context.page ?? 0) + 1);
}
}

474
autocomplete.js Normal file
View file

@ -0,0 +1,474 @@
//Reference Scriptfile
//Intended exclusively for auto-complete in your IDE, not for execution
var IS_TESTING = false;
let Type = {
Source: {
Dash: "DASH",
HLS: "HLS",
STATIC: "Static"
},
Feed: {
Videos: "VIDEOS",
Streams: "STREAMS",
Mixed: "MIXED",
Live: "LIVE"
},
Order: {
Chronological: "CHRONOLOGICAL"
},
Date: {
LastHour: "LAST_HOUR",
Today: "TODAY",
LastWeek: "LAST_WEEK",
LastMonth: "LAST_MONTH",
LastYear: "LAST_YEAR"
},
Duration: {
Short: "SHORT",
Medium: "MEDIUM",
Long: "LONG"
}
};
let Language = {
UNKNOWN: "Unknown",
ARABIC: "Arabic",
SPANISH: "Spanish",
FRENCH: "French",
HINDI: "Hindi",
INDONESIAN: "Indonesian",
KOREAN: "Korean",
PORTBRAZIL: "Portuguese Brazilian",
RUSSIAN: "Russian",
THAI: "Thai",
TURKISH: "Turkish",
VIETNAMESE: "Vietnamese",
ENGLISH: "English"
}
class ScriptException extends Error {
constructor(type, msg) {
if(arguments.length == 1) {
super(arguments[0]);
this.plugin_type = "ScriptException";
this.message = arguments[0];
}
else {
super(msg);
this.plugin_type = type ?? ""; //string
this.msg = msg ?? ""; //string
}
}
}
class TimeoutException extends ScriptException {
constructor(msg) {
super(msg);
this.plugin_type = "ScriptTimeoutException";
}
}
class Thumbnails {
constructor(thumbnails) {
this.sources = thumbnails ?? []; // Thumbnail[]
}
}
class Thumbnail {
constructor(url, quality) {
this.url = url ?? ""; //string
this.quality = quality ?? 0; //integer
}
}
class PlatformID {
constructor(platform, id, pluginId) {
this.platform = platform ?? ""; //string
this.pluginId = pluginId; //string
this.value = id; //string
}
}
class ResultCapabilities {
constructor(types, sorts, filters) {
this.types = types ?? [];
this.sorts = sorts ?? [];
this.filters = filters ?? [];
}
}
class FilterGroup {
constructor(name, filters, isMultiSelect, id) {
if(!name) throw new ScriptException("No name for filter group");
if(!filters) throw new ScriptException("No filter provided");
this.name = name
this.filters = filters
this.isMultiSelect = isMultiSelect;
this.id = id;
}
}
class FilterCapability {
constructor(name, value, id) {
if(!name) throw new ScriptException("No name for filter");
if(!value) throw new ScriptException("No filter value");
this.name = name;
this.value = value;
this.id = id;
}
}
class PlatformAuthorLink {
constructor(id, name, url, thumbnail) {
this.id = id ?? PlatformID(); //PlatformID
this.name = name ?? ""; //string
this.url = url ?? ""; //string
this.thumbnail = thumbnail; //string
}
}
class PlatformVideo {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "PlatformVideo";
this.id = obj.id ?? PlatformID(); //PlatformID
this.name = obj.name ?? ""; //string
this.thumbnails = obj.thumbnails ?? Thumbnails([]); //Thumbnail[]
this.author = obj.author ?? PlatformAuthorLink(); //PlatformAuthorLink
this.datetime = obj.uploadDate ?? 0; //OffsetDateTime (Long)
this.url = obj.url ?? ""; //String
this.duration = obj.duration ?? -1; //Long
this.viewCount = obj.viewCount ?? -1; //Long
this.isLive = obj.isLive ?? false; //Boolean
}
}
class PlatformVideoDetails extends PlatformVideo {
constructor(obj) {
super(obj);
obj = obj ?? {};
this.plugin_type = "PlatformVideoDetails";
this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource
this.hls = obj.hls ?? null; //HLSSource
this.live = obj.live ?? null; //VideoSource
this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? [];
}
}
//Sources
class VideoSourceDescriptor {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "MuxVideoSourceDescriptor";
this.isUnMuxed = false;
if(obj.constructor === Array)
this.videoSources = obj;
else
this.videoSources = obj.videoSources ?? [];
}
}
class UnMuxVideoSourceDescriptor {
constructor(videoSourcesOrObj, audioSources) {
videoSourcesOrObj = videoSourcesOrObj ?? {};
this.plugin_type = "UnMuxVideoSourceDescriptor";
this.isUnMuxed = true;
if(videoSourcesOrObj.constructor === Array) {
this.videoSources = videoSourcesOrObj;
this.audioSources = audioSources;
}
else {
this.videoSources = videoSourcesOrObj.videoSources ?? [];
this.audioSources = videoSourcesOrObj.audioSources ?? [];
}
}
}
class VideoUrlSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "VideoUrlSource";
this.width = obj.width ?? 0;
this.height = obj.height ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.duration = obj.duration ?? 0;
this.url = obj.url;
}
}
class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "VideoUrlRangeSource";
this.itagId = obj.itagId ?? null;
this.initStart = obj.initStart ?? null;
this.initEnd = obj.initEnd ?? null;
this.indexStart = obj.indexStart ?? null;
this.indexEnd = obj.indexEnd ?? null;
}
}
class AudioUrlSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "AudioUrlSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
}
}
class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "AudioUrlRangeSource";
this.itagId = obj.itagId ?? null;
this.initStart = obj.initStart ?? null;
this.initEnd = obj.initEnd ?? null;
this.indexStart = obj.indexStart ?? null;
this.indexEnd = obj.indexEnd ?? null;
this.audioChannels = obj.audioChannels ?? 2;
}
}
class HLSSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "HLSSource";
this.name = obj.name ?? "HLS";
this.duration = obj.duration ?? 0;
this.url = obj.url;
}
}
class DashSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashSource";
this.name = obj.name ?? "Dash";
this.duration = obj.duration ?? 0;
this.url = obj.url;
}
}
//Channel
class PlatformChannel {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "PlatformChannel";
this.id = obj.id ?? ""; //string
this.name = obj.name ?? ""; //string
this.thumbnail = obj.thumbnail; //string
this.banner = obj.banner; //string
this.subscribers = obj.subscribers ?? 0; //integer
this.description = obj.description; //string
this.url = obj.url ?? ""; //string
this.links = obj.links ?? { } //Map<string,string>
}
}
//Ratings
class RatingLikes {
constructor(likes) {
this.type = 1;
this.likes = likes;
}
}
class RatingLikesDislikes {
constructor(likes,dislikes) {
this.type = 2;
this.likes = likes;
this.dislikes = dislikes;
}
}
class RatingScaler {
constructor(value) {
this.type = 3;
this.value = value;
}
}
class Comment {
constructor(obj) {
this.plugin_type = "Comment";
this.contextUrl = obj.contextUrl ?? "";
this.author = obj.author ?? new PlatformAuthorLink(null, "", "", null);
this.message = obj.message ?? "";
this.rating = obj.rating ?? new RatingLikes(0);
this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {};
}
}
//Pagers
class VideoPager {
constructor(results, hasMore, context) {
this.plugin_type = "VideoPager";
this.results = results ?? [];
this.hasMore = hasMore ?? false;
this.context = context ?? {};
}
hasMorePagers() { return this.hasMore; }
nextPage() { return new Pager([], false, this.context) }
}
class ChannelPager {
constructor(results, hasMore, context) {
this.plugin_type = "ChannelPager";
this.results = results ?? [];
this.hasMore = hasMore ?? false;
this.context = context ?? {};
}
hasMorePagers() { return this.hasMore; }
nextPage() { return new Pager([], false, this.context) }
}
class CommentPager {
constructor(results, hasMore, context) {
this.plugin_type = "CommentPager";
this.results = results ?? [];
this.hasMore = hasMore ?? false;
this.context = context ?? {};
}
hasMorePagers() { return this.hasMore; }
nextPage() { return new Pager([], false, this.context) }
}
function throwException(type, message) {
throw new Error("V8EXCEPTION:" + type + "-" + message);
}
//To override by plugin
const source = {
getHome() { return new Pager([], false, {}); },
enable(config){ },
disable() {},
searchSuggestions(query){ return []; },
getSearchCapabilities(){ return { types: [], sorts: [] }; },
search(query){ return new Pager([], false, {}); }, //TODO
isChannelUrl(url){ return false; },
getChannel(url){ return null; },
getChannelCapabilities(){ return { types: [], sorts: [] }; },
getChannelVideos(url) { return new Pager([], false, {}); },
isVideoDetailsUrl(url){ return false; },
getVideoDetails(url){ }, //TODO
//getComments(url){ return new Pager([], false, {}); }, //TODO
//getSubComments(comment){ return new Pager([], false, {}); }, //TODO
//getSubscriptionsUser(){ return []; },
//getPlaylistsUser(){ return []; }
};
function parseSettings(settings) {
if(!settings)
return {};
let newSettings = {};
for(let key in settings) {
if(typeof settings[key] == "string")
newSettings[key] = JSON.parse(settings[key]);
else
newSettings[key] = settings[key];
}
return newSettings;
}
function log(str) {
if(str) {
if(typeof str == "string")
bridge.log(str);
else
bridge.log(JSON.stringify(str, null, 4));
}
}
//Package Bridge (variable: bridge)
let bridge = {
/**
* @return {Boolean}
**/
isLoggedIn: function() {},
/**
* @param {String} str
* @return {Unit}
**/
log: function(str) {},
/**
* @param {String} str
* @return {Unit}
**/
throwTest: function(str) {},
/**
* @param {String} str
* @return {Unit}
**/
toast: function(str) {},
}
//Package Http (variable: http)
let http = {
/**
* @param {String} url
* @param {Map} headers
* @param {Boolean} useAuth
* @return {BridgeHttpResponse}
**/
GET: function(url, headers, useAuth) {},
/**
* @param {String} url
* @param {String} body
* @param {Map} headers
* @param {Boolean} useAuth
* @return {BridgeHttpResponse}
**/
POST: function(url, body, headers, useAuth) {},
/**
* @return {BatchBuilder}
**/
batch: function() {},
/**
* @param {String} method
* @param {String} url
* @param {Map} headers
* @param {Boolean} useAuth
* @return {BridgeHttpResponse}
**/
request: function(method, url, headers, useAuth) {},
/**
* @param {String} method
* @param {String} url
* @param {String} body
* @param {Map} headers
* @param {Boolean} useAuth
* @return {BridgeHttpResponse}
**/
requestWithBody: function(method, url, body, headers, useAuth) {},
}

23
deploy.sh Normal file
View file

@ -0,0 +1,23 @@
#!/bin/sh
DOCUMENT_ROOT=/var/www/sources
# Take site offline
echo "Taking site offline..."
touch $DOCUMENT_ROOT/maintenance.file
# Swap over the content
echo "Deploying content..."
cp peertube.png $DOCUMENT_ROOT/
cp PeerTubeConfig.json $DOCUMENT_ROOT/
cp PeerTubeScript.js $DOCUMENT_ROOT/
# Notify Cloudflare to wipe the CDN cache
echo "Purging Cloudflare cache..."
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
# Take site back online
echo "Bringing site back online..."
rm $DOCUMENT_ROOT/maintenance.file

BIN
peertube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB