Compare commits

...

14 Commits

5 changed files with 251 additions and 180 deletions

View File

@ -1,3 +1,42 @@
# PeerTube plugin Quickstart
# Auto Import YouTube
See https://docs.joinpeertube.org/#/contribute-plugins?id=write-a-plugintheme
## Config
To use this plugin, you need to give some valid admins credential inside the plugin's settings.
After this you have to specify some YouTube Channel who need to be bind with a PeerTube Channel.
For this you have to use this format inside the plugins's setting. Into `URL list of Youtube channel to synchronize`'s text area. :
```json
[
{
"address":"https://www.youtube.com/feeds/videos.xml?channel_id=${MyYouTubeChannelID}",
"ChannelId":"MyPeertubeChannelId"
} ,
...
]
```
This an array of object who's in this format :
```ts
type SettingsContent = {
address: string;
channelId: string;
timeloop?: number;
};
```
### address
For exemple, to the Youtube channel : `https://www.youtube.com/channel/YouTube`, the channel id is `UCBR8-60-B28hp2BmDPdntcQ`. You can get it when you're clicking in the channel button on the video player. (He's not Highlighted by YouTube. Cheer Up !)
So you need to specify the following address inside your configuration : `https://www.youtube.com/feeds/videos.xml?channel_id=UCBR8-60-B28hp2BmDPdntcQ`.
### channelId
The peertube's channel id is a number associated to a peertube's channel. He's unique per channel inside a same instance.
### timeloop
It's represent the time between two update of the videos list.
It's not needful to specify the timeloop. The default value is set to 5 minutes.

View File

@ -1,13 +1,10 @@
// import { ImplementableApi } from './implementableApi';
// Api request lib
import fetch, { FetchError, Headers } from "node-fetch";
import fetch, { Headers } from "node-fetch";
import { URL, URLSearchParams } from "url";
import FormData from "form-data";
// import dedent from "ts-dedent";
namespace PeerTubeRequester {
export type Config = {
domain_name: string | URL;
domainName: string | URL;
username: string;
password: string;
};
@ -19,45 +16,30 @@ type UploadInstruction = {
targetUrl: string;
};
type ClientToken = {
client_id: string;
client_secret: string;
grant_type: "password";
response_type: "code";
username: string;
password: string;
};
type UserToken = {
access_token: string;
token_type: string;
expires_in: string;
refresh_token: string;
};
class PeerTubeRequester {
readonly domain_name: URL;
readonly domainName: URL;
readonly username: string;
readonly password: string;
constructor(readonly config: PeerTubeRequester.Config) {
this.domain_name = new URL("/", config.domain_name);
this.domainName = new URL("/", config.domainName);
this.username = config.username;
this.password = config.password;
}
async apiRequest(message: UploadInstruction): Promise<void> {
async requestAuthToken(): Promise<any> {
let response = await fetch(
new URL(`/api/v1/oauth-clients/local`, this.domain_name)
new URL(`/api/v1/oauth-clients/local`, this.domainName)
);
if (!response.ok) {
throw new Error(response.statusText); // CRASH
throw new Error("Cannot get client credentials : " + response.statusText); // CRASH
}
const { client_id, client_secret } = await response.json();
const { client_id: clientId, client_secret: clientSecret } =
await response.json();
const client_info: { [key: string]: string } = {
client_id,
client_secret,
const clientInfo: { [key: string]: string } = {
client_id: clientId,
client_secret: clientSecret,
grant_type: "password",
response_type: "code",
username: this.username,
@ -65,25 +47,28 @@ class PeerTubeRequester {
};
let myParams = new URLSearchParams();
for (const key in client_info) myParams.append(key, client_info[key]);
for (const key in clientInfo) myParams.append(key, clientInfo[key]);
response = await fetch(new URL(`/api/v1/users/token`, this.domain_name), {
response = await fetch(new URL(`/api/v1/users/token`, this.domainName), {
method: "post",
body: myParams,
});
if (!response.ok) {
throw new Error(response.statusText); // CRASH
throw new Error("Cannot get access Token : " + response.statusText); // CRASH
}
const { access_token: accessToken } = await response.json();
return accessToken;
}
const { access_token } = await response.json();
// Upload
async uploadFromUrl(message: UploadInstruction): Promise<void> {
const accessToken = await this.requestAuthToken();
const myUploadForm = new URLSearchParams();
const myHeader = new Headers();
myHeader.append("Authorization", `Bearer ${access_token}`);
myHeader.append("Authorization", `Bearer ${accessToken}`);
for (const key in message) myUploadForm.append(key, message[key]);
response = await fetch(
new URL(`/api/v1/videos/imports`, this.domain_name),
const response = await fetch(
new URL("/api/v1/videos/imports", this.domainName),
{
method: "post",
headers: myHeader,
@ -95,9 +80,9 @@ class PeerTubeRequester {
switch (response.status) {
case 400:
throw new Error(
`Your target URL was not accepted by the API.\
Actualy it's : ${message.targetUrl}
${response.statusText}`
`Bad or malformed request. Probably because your target URL (from Youtube?) was not accepted by the API.\
The target URL you attempted to pass: ${message.targetUrl}.
Response from the server: ${response.statusText}`
);
break;
case 403:
@ -105,15 +90,15 @@ class PeerTubeRequester {
break;
case 409:
throw new Error(
`Oops, your instance had not allowed the HTTPS import.\
`Oops, your instance did not allowed the HTTPS import.\
Contact your administrator.
${response.statusText}`
);
break;
default:
throw new Error(
`Oh, you resolved an undocumented issues.\
Please report this on the git if you have the time.
`Oh, you encountered an undocumented issues.\
Please create an issue to the plugin project.
ERROR: ${response.statusText}`
);
break;

35
package-lock.json generated
View File

@ -1,15 +1,16 @@
{
"name": "peertube-plugin-auto-import-ytb",
"version": "0.0.2",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.0.2",
"version": "0.0.1",
"dependencies": {
"@types/node-fetch": "^2.5.11",
"form-data": "^4.0.0",
"listener-rss-agregator": "git+https://zeteo.me/gitea/Outils-PeerTube/listener-rss-agregators#1b36afbb34",
"listener-rss": "^0.0.3",
"listener-rss-aggregator": "^0.0.5",
"node-fetch": "^2.6.1"
},
"devDependencies": {
@ -398,11 +399,10 @@
"rss-parser": "^3.11.0"
}
},
"node_modules/listener-rss-agregator": {
"version": "0.0.3",
"resolved": "git+https://zeteo.me/gitea/Outils-PeerTube/listener-rss-agregators#1b36afbb344112c3827b5bcea92e1cc60bd8cf6a",
"hasInstallScript": true,
"license": "MIT",
"node_modules/listener-rss-aggregator": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/listener-rss-aggregator/-/listener-rss-aggregator-0.0.5.tgz",
"integrity": "sha512-0QE7kkzurjsWr4gNAJ4X+C7UFSyaVuYq2mKYXZ3shvyj81kULpBgFZSg/70MZkUoqixgWQ5P8oxyztRDOP78tw==",
"dependencies": {
"@databases/sqlite": "^3.0.0",
"listener-rss": "^0.0.3"
@ -799,9 +799,9 @@
}
},
"node_modules/tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"version": "4.4.14",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.14.tgz",
"integrity": "sha512-ouN3XcSWYOAHmXZ+P4NEFJvqXL50To9OZBSQNNP30vBUFJFZZ0PLX15fnwupv6azfxMUfUDUr2fhYw4zGAEPcg==",
"dependencies": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
@ -1270,9 +1270,10 @@
"rss-parser": "^3.11.0"
}
},
"listener-rss-agregator": {
"version": "git+https://zeteo.me/gitea/Outils-PeerTube/listener-rss-agregators#1b36afbb344112c3827b5bcea92e1cc60bd8cf6a",
"from": "listener-rss-agregator@git+https://zeteo.me/gitea/Outils-PeerTube/listener-rss-agregators#1b36afbb34",
"listener-rss-aggregator": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/listener-rss-aggregator/-/listener-rss-aggregator-0.0.5.tgz",
"integrity": "sha512-0QE7kkzurjsWr4gNAJ4X+C7UFSyaVuYq2mKYXZ3shvyj81kULpBgFZSg/70MZkUoqixgWQ5P8oxyztRDOP78tw==",
"requires": {
"@databases/sqlite": "^3.0.0",
"listener-rss": "^0.0.3"
@ -1604,9 +1605,9 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"version": "4.4.14",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.14.tgz",
"integrity": "sha512-ouN3XcSWYOAHmXZ+P4NEFJvqXL50To9OZBSQNNP30vBUFJFZZ0PLX15fnwupv6azfxMUfUDUr2fhYw4zGAEPcg==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",

View File

@ -1,9 +1,9 @@
{
"name": "peertube-plugin-auto-import-ytb",
"description": "PeerTube plugin quickstart",
"description": "Peertube plugin to auto import videos from a youtube channel to a local peertube channel",
"version": "0.0.2",
"author": "AmauryJOLY",
"bugs": "https://framagit.org/framasoft/peertube/peertube-plugin-quickstart/issues",
"bugs": "https://zeteo.me/gitea/Outils-PeerTube/peertube-plugin-auto-import-ytb/issues",
"clientScripts": [],
"css": [],
"devDependencies": {
@ -13,7 +13,7 @@
"engine": {
"peertube": ">=3.2.0"
},
"homepage": "https://framagit.org/framasoft/peertube/peertube-plugin-quickstart",
"homepage": "https://zeteo.me/gitea/Outils-PeerTube/peertube-plugin-auto-import-ytb",
"keywords": [
"peertube",
"plugin"
@ -33,7 +33,8 @@
"dependencies": {
"@types/node-fetch": "^2.5.11",
"form-data": "^4.0.0",
"listener-rss-agregator": "git+https://zeteo.me/gitea/Outils-PeerTube/listener-rss-agregators#1b36afbb34",
"listener-rss-aggregator": "^0.0.5",
"listener-rss": "^0.0.3",
"node-fetch": "^2.6.1"
}
}

View File

@ -1,4 +1,4 @@
import { ListenerRssAggregator } from "listener-rss-agregator";
import { ListenerRssAggregator } from "listener-rss-aggregator";
import { ListenerRss } from "listener-rss";
import { PeerTubeRequester } from "../lib/peertubeRequester";
@ -10,7 +10,8 @@ type ListenerData = ListenerRss.Config & {
let myManager: ListenerRssAggregator;
let listenersDataBinding = new Map<string, ListenerData>();
let logger: any;
let peertube: PeerTubeRequester;
let peertube: PeerTubeRequester | undefined = undefined;
let goodPeertubeCredential: boolean = false;
import * as path from "path";
import fs from "fs";
@ -25,11 +26,23 @@ async function register({
registerSetting({
name: "ytb-urls",
label: "liste des urls youtube a auto-importer",
label: "URL list of Youtube channel to synchronize",
type: "input-textarea",
});
logger.warn("setting register");
registerSetting({
name: "admin-name",
label: "Admin Username",
type: "input",
});
registerSetting({
name: "admin-password",
label: "Admin Password",
type: "input-password",
});
logger.debug("setting register");
fs.appendFileSync(path.join(basePath, "/storage.bd"), "");
const configAggregator = await ListenerRssAggregator.instantiateAggregator(
@ -37,47 +50,80 @@ async function register({
);
myManager = new ListenerRssAggregator(configAggregator);
peertube = new PeerTubeRequester({
domain_name: "http://localhost:9000",
username: "root",
password: "test",
logger.debug("Aggregator created");
const settingYtbUrls = await settingsManager.getSetting("ytb-urls");
if (settingYtbUrls) await addListeners(settingYtbUrls);
const settingCredentials: any = await settingsManager.getSettings([
"admin-name",
"admin-password",
]);
if (settingCredentials["admin-name"] && settingCredentials["admin-password"])
apiRequestInitializer({
domainName: peertubeHelpers.config.getWebserverUrl(),
username: settingCredentials["admin-name"],
password: settingCredentials["admin-password"],
});
logger.warn("Aggregator created");
const inputs = await settingsManager.getSetting("ytb-urls");
if (inputs) await addListeners(inputs);
logger.warn("Config loaded");
logger.debug("Actual config loaded");
settingsManager.onSettingsChange(async (settings: any) => {
if (
!peertube ||
peertube.username != settings["admin-name"] ||
peertube.password != settings["admin-password"]
)
apiRequestInitializer({
domainName: peertubeHelpers.config.getWebserverUrl(),
username: settings["admin-name"],
password: settings["admin-password"],
});
await addListeners(settings["ytb-urls"]);
});
myManager.on("newEntries", (entries: any) => {
myManager.on("newEntries", async (entries: any) => {
const datas = listenersDataBinding.get(entries.addressListener);
if (!datas) return;
logger.warn(
"Nouvelles entrées détéctées: " +
JSON.stringify(entries) +
" de " +
datas.channelId
logger.debug(
"New entries detected from channel #%i: %s",
datas.channelId,
JSON.stringify(entries)
);
for (const item of entries.items)
peertube.apiRequest({
if (peertube)
await peertube.uploadFromUrl({
channelId: datas.channelId,
targetUrl: item.link,
});
else {
logger.warn("Bad credential provides. New entries Skipped.");
}
});
}
async function apiRequestInitializer(data: PeerTubeRequester.Config) {
peertube = new PeerTubeRequester(data);
try {
await peertube.requestAuthToken();
goodPeertubeCredential = true;
logger.debug("credential ok");
} catch (error) {
logger.warn("Error during the credential validation : " + error);
peertube = undefined;
goodPeertubeCredential = false;
}
}
async function addListeners(listenerInput: string) {
let listeners: ListenerData[];
try {
listeners = JSON.parse(listenerInput);
} catch {
logger.warn("Erreur: malformé");
logger.error("Malformed URL");
return;
}
let newListeners = listeners.filter(
@ -97,14 +143,13 @@ async function addListeners(listenerInput: string) {
myManager.stopAll();
await myManager.saveOverride(listeners);
if (logger) logger.warn("Configuration modifiée: " + listenerInput);
if (logger) logger.debug("Configuration changed: " + listenerInput);
myManager.startAll();
if (goodPeertubeCredential) myManager.startAll();
}
async function unregister() {
myManager.stopAll();
return;
}
module.exports = {