Merge pull request #1798 from mchangrh/piped-ci

Piped CI
This commit is contained in:
Ajay Ramachandran
2023-07-14 20:37:57 -04:00
committed by GitHub
13 changed files with 244 additions and 49 deletions

View File

@@ -11,9 +11,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Download instance list - name: Download instance lists
run: | run: |
wget https://api.invidious.io/instances.json -O ci/data.json wget https://api.invidious.io/instances.json -O ci/invidious_instances.json
wget https://github.com/TeamPiped/piped-uptime/raw/master/history/summary.json -O ci/piped_instances.json
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: "Run CI" - name: "Run CI"
@@ -24,7 +25,7 @@ jobs:
# v4.2.3 # v4.2.3
with: with:
commit-message: Update Invidious List commit-message: Update Invidious List
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
branch: ci/update_invidious_list branch: ci/update_invidious_list
title: Update Invidious List title: Update Invidious List
body: Automated Invidious list update body: Automated Invidious list update

3
.gitignore vendored
View File

@@ -7,5 +7,6 @@ web-ext-artifacts
dist/ dist/
tmp/ tmp/
.DS_Store .DS_Store
ci/data.json ci/invidious_instances.json
ci/piped_instances.json
test-results test-results

63
ci/generateList.ts Normal file
View File

@@ -0,0 +1,63 @@
/*
This file is only ran by GitHub Actions in order to populate the Invidious instances list
This file should not be shipped with the extension
*/
/*
Criteria for inclusion:
Invidious
- 30d uptime >= 90%
- available for at least 80/90 days
- must have been up for at least 90 days
- HTTPS only
- url includes name (this is to avoid redirects)
Piped
- 30d uptime >= 90%
- available for at least 80/90 days
- must have been up for at least 90 days
- must not be a wildcard redirect to piped.video
- must be currently up
- must have a functioning frontend
- must have a functioning API
*/
import { writeFile, existsSync } from "fs"
import { join } from "path"
import { getInvidiousList } from "./invidiousCI";
// import { getPipedList } from "./pipedCI";
const checkPath = (path: string) => existsSync(path);
const fixArray = (arr: string[]) => [...new Set(arr)].sort()
async function generateList() {
// import file from https://api.invidious.io/instances.json
const invidiousPath = join(__dirname, "invidious_instances.json");
// import file from https://github.com/TeamPiped/piped-uptime/raw/master/history/summary.json
const pipedPath = join(__dirname, "piped_instances.json");
// check if files exist
if (!checkPath(invidiousPath) || !checkPath(pipedPath)) {
console.log("Missing files")
process.exit(1);
}
// static non-invidious instances
const staticInstances = ["www.youtubekids.com"];
// invidious instances
const invidiousList = fixArray(getInvidiousList())
// piped instnaces
// const pipedList = fixArray(await getPipedList())
console.log([...staticInstances, ...invidiousList])
writeFile(
join(__dirname, "./invidiouslist.json"),
JSON.stringify([...staticInstances, ...invidiousList]),
(err) => {
if (err) return console.log(err);
}
);
}
generateList()

View File

@@ -1,55 +1,31 @@
/* import { InvidiousInstance, instanceMap } from "./invidiousType"
This file is only ran by GitHub Actions in order to populate the Invidious instances list
This file should not be shipped with the extension import * as data from "../ci/invidious_instances.json";
*/
import { writeFile, existsSync } from 'fs';
import { join } from 'path';
// import file from https://api.invidious.io/instances.json
if (!existsSync(join(__dirname, "data.json"))) {
process.exit(1);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as data from "../ci/data.json";
type instanceMap = {
name: string;
url: string;
dailyRatios: {ratio: string; label: string }[];
thirtyDayUptime: string;
}[]
// only https servers // only https servers
const mapped: instanceMap = data const mapped: instanceMap = data
// eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((i: InvidiousInstance) => i[1]?.type === "https")
.filter((i: any) => i[1]?.type === 'https') .map((instance: InvidiousInstance) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((instance: any) => {
return { return {
name: instance[0], name: instance[0],
url: instance[1].uri, url: instance[1].uri,
dailyRatios: instance[1].monitor.dailyRatios, dailyRatios: instance[1].monitor.dailyRatios,
thirtyDayUptime: instance[1]?.monitor['30dRatio'].ratio, thirtyDayUptime: instance[1]?.monitor["30dRatio"].ratio,
} }
}) });
// reliability and sanity checks // reliability and sanity checks
const reliableCheck = mapped const reliableCheck = mapped
.filter((instance) => { .filter(instance => {
// 30d uptime >= 90% // 30d uptime >= 90%
const thirtyDayUptime = Number(instance.thirtyDayUptime) >= 90 const thirtyDayUptime = Number(instance.thirtyDayUptime) >= 90;
// available for at least 80/90 days // available for at least 80/90 days
const dailyRatioCheck = instance.dailyRatios.filter(status => status.label !== "black") const dailyRatioCheck = instance.dailyRatios.filter(status => status.label !== "black");
return (thirtyDayUptime && dailyRatioCheck.length >= 80) return thirtyDayUptime && dailyRatioCheck.length >= 80;
}) })
// url includes name // url includes name
.filter(instance => instance.url.includes(instance.name)) .filter(instance => instance.url.includes(instance.name));
// finally map to array export function getInvidiousList(): string[] {
const result: string[] = reliableCheck.map(instance => instance.name).sort() return reliableCheck.map(instance => instance.name).sort()
writeFile(join(__dirname, "./invidiouslist.json"), JSON.stringify(result), (err) => { }
if (err) return console.log(err);
})

54
ci/invidiousType.ts Normal file
View File

@@ -0,0 +1,54 @@
type ratio = {
ratio: string;
label: string;
}
export type instanceMap = {
name: string;
url: string;
dailyRatios: {ratio: string; label: string }[];
thirtyDayUptime: string;
}[]
export type InvidiousInstance = [
string,
{
flag: string;
region: string;
stats: null | {
version: string;
software: {
name: string;
version: string;
branch: string;
};
openRegistrations: boolean;
usage: {
users: {
total: number;
activeHalfyear: number;
activeMonth: number;
};
};
metadata: {
updatedAt: number;
lastChannelRefreshedAt: number;
};
};
cors: boolean | null;
api: boolean | null;
type: "https" | "http" | "onion" | "i2p";
uri: string;
monitor: null | {
monitorId: number;
createdAt: number;
statusClass: string;
name: string;
url: string | null;
type: "HTTP(s)";
dailyRatios: ratio[];
"90dRatio": ratio;
"30dRatio": ratio;
};
}
]

View File

@@ -1 +1 @@
["inv.bp.projectsegfau.lt","inv.zzls.xyz","invidious.0011.lt","invidious.baczek.me","invidious.lunar.icu","invidious.privacydev.net","invidious.tiekoetter.com","iv.ggtyler.dev","iv.melmac.space","vid.puffyan.us","y.com.sb","yewtu.be","yt.artemislena.eu"] ["www.youtubekids.com","inv.bp.projectsegfau.lt","inv.tux.pizza","inv.zzls.xyz","invidious.0011.lt","invidious.lunar.icu","invidious.privacydev.net","invidious.tiekoetter.com","iv.ggtyler.dev","iv.melmac.space","vid.priv.au","vid.puffyan.us","yewtu.be","yt.artemislena.eu"]

92
ci/pipedCI.ts Normal file
View File

@@ -0,0 +1,92 @@
import * as data from "../ci/piped_instances.json";
type percent = string
type dailyMinutesDown = Record<string, number>
type PipedInstance = {
name: string;
url: string;
icon: string;
slug: string;
status: string;
uptime: percent;
uptimeDay: percent;
uptimeWeek: percent;
uptimeMonth: percent;
uptimeYear: percent;
time: number;
timeDay: number;
timeWeek: number;
timeMonth: number;
timeYear: number;
dailyMinutesDown: dailyMinutesDown
}
const percentNumber = (percent: percent) => Number(percent.replace("%", ""))
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
function dailyMinuteFilter (dailyMinutesDown: dailyMinutesDown) {
let daysDown = 0
for (const [date, minsDown] of Object.entries(dailyMinutesDown)) {
if (new Date(date) >= ninetyDaysAgo && minsDown > 1000) { // if within 90 days and down for more than 1000 minutes
daysDown++
}
}
// return true f less than 10 days down
return daysDown < 10
}
const getHost = (url: string) => new URL(url).host
const getWatchPage = async (instance: PipedInstance) =>
fetch(`https://${getHost(instance.url)}`, { redirect: "manual" })
.then(res => res.headers.get("Location"))
.catch(e => { console.log (e); return null })
const siteOK = async (instance) => {
// check if entire site is redirect
const notRedirect = await fetch(instance.url, { redirect: "manual" })
.then(res => res.status == 200)
// only allow kavin to return piped.video
// if (instance.url.startsWith("https://piped.video") && instance.slug !== "kavin-rocks-official") return false
// check if frontend is OK
const watchPageStatus = await fetch(instance.frontendUrl)
.then(res => res.ok)
// test API - stream returns ok result
const streamStatus = await fetch(`${instance.apiUrl}/streams/BaW_jenozKc`)
.then(res => res.ok)
// get startTime of monitor
const age = await fetch(instance.historyUrl)
.then(res => res.text())
.then(text => { // startTime greater than 90 days ago
const date = text.match(/startTime: (.+)/)[1]
return Date.parse(date) < ninetyDaysAgo.valueOf()
})
// console.log(notRedirect, watchPageStatus, streamStatus, age, instance.frontendUrl, instance.apiUrl)
return notRedirect && watchPageStatus && streamStatus && age
}
const staticFilters = (data as PipedInstance[])
.filter(instance => {
const isup = instance.status === "up"
const monthCheck = percentNumber(instance.uptimeMonth) >= 90
const dailyMinuteCheck = dailyMinuteFilter(instance.dailyMinutesDown)
return isup && monthCheck && dailyMinuteCheck
})
.map(async instance => {
// get frontend url
const frontendUrl = await getWatchPage(instance)
if (!frontendUrl) return null // return false if frontend doesn't resolve
// get api base
const apiUrl = instance.url.replace("/healthcheck", "")
const historyUrl = `https://raw.githubusercontent.com/TeamPiped/piped-uptime/master/history/${instance.slug}.yml`
const pass = await siteOK({ apiUrl, historyUrl, frontendUrl, url: instance.url })
const frontendHost = getHost(frontendUrl)
return pass ? frontendHost : null
})
export async function getPipedList(): Promise<string[]> {
const instances = await Promise.all(staticFilters)
.then(arr => arr.filter(i => i !== null))
return instances
}

View File

@@ -55,7 +55,7 @@
"build:watch": "npm run build:watch:chrome", "build:watch": "npm run build:watch:chrome",
"build:watch:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js --watch", "build:watch:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js --watch",
"build:watch:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js --watch", "build:watch:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js --watch",
"ci:invidious": "ts-node ci/invidiousCI.ts", "ci:invidious": "ts-node ci/generateList.ts",
"dev": "npm run build:dev && concurrently \"npm run web-run\" \"npm run build:watch\"", "dev": "npm run build:dev && concurrently \"npm run web-run\" \"npm run build:watch\"",
"dev:firefox": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox\" \"npm run build:watch:firefox\"", "dev:firefox": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox\" \"npm run build:watch:firefox\"",
"dev:firefox-android": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox-android\" \"npm run build:watch:firefox\"", "dev:firefox-android": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox-android\" \"npm run build:watch:firefox\"",

View File

@@ -524,7 +524,7 @@ function createPreviewBar(): void {
selector: ".vjs-progress-holder", selector: ".vjs-progress-holder",
isVisibleCheck: false isVisibleCheck: false
}, { }, {
// For Youtube Music // For Youtube Music and YTKids
// there are two sliders, one for volume and one for progress - both called #progressContainer // there are two sliders, one for volume and one for progress - both called #progressContainer
selector: "#progress-bar>#sliderContainer>div>#sliderBar>#progressContainer", selector: "#progress-bar>#sliderContainer>div>#sliderBar>#progressContainer",
}, { }, {

View File

@@ -97,7 +97,8 @@ class PreviewBar {
this.chapterTooltip = document.createElement("div"); this.chapterTooltip = document.createElement("div");
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip"; this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper"); // global chaper tooltip or duration tooltip
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip)") as HTMLElement; const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip)") as HTMLElement;
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return; if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;

View File

@@ -299,7 +299,8 @@ export default class Utils {
"#main-panel.ytmusic-player-page", // YouTube music "#main-panel.ytmusic-player-page", // YouTube music
"#player-container .video-js", // Invidious "#player-container .video-js", // Invidious
".main-video-section > .video-container", // Cloudtube ".main-video-section > .video-container", // Cloudtube
".shaka-video-container" // Piped ".shaka-video-container", // Piped
"#player-container.ytk-player", // YT Kids
]; ];
let referenceNode = findValidElementFromSelector(selectors) let referenceNode = findValidElementFromSelector(selectors)

View File

@@ -16,5 +16,8 @@
"dom", "dom",
"dom.iterable" "dom.iterable"
] ]
} },
"include": [
"src/**/*"
]
} }

View File

@@ -16,5 +16,8 @@
"dom", "dom",
"dom.iterable" "dom.iterable"
] ]
} },
"include": [
"src/**/*"
]
} }