9 Commits

5 changed files with 232 additions and 131 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,109 +1,122 @@
// Api request lib
import fetch, { FetchError, Headers } from "node-fetch";
import { URL, URLSearchParams } from "url";
namespace PeerTubeRequester {
export type Config = {
domain_name: string | URL;
username: string;
password: string;
};
}
type UploadInstruction = {
[key: string]: string;
channelId: string;
targetUrl: string;
};
class PeerTubeRequester {
readonly domain_name: URL;
readonly username: string;
readonly password: string;
constructor(readonly config: PeerTubeRequester.Config) {
this.domain_name = new URL("/", config.domain_name);
this.username = config.username;
this.password = config.password;
}
private async requestAuthToken(): Promise<any> {
let response = await fetch(
new URL(`/api/v1/oauth-clients/local`, this.domain_name)
);
if (!response.ok) {
throw new Error(response.statusText); // CRASH
}
const { client_id, client_secret } = await response.json();
const client_info: { [key: string]: string } = {
client_id,
client_secret,
grant_type: "password",
response_type: "code",
username: this.username,
password: this.password,
};
let myParams = new URLSearchParams();
for (const key in client_info) myParams.append(key, client_info[key]);
response = await fetch(new URL(`/api/v1/users/token`, this.domain_name), {
method: "post",
body: myParams,
});
if (!response.ok) {
throw new Error(response.statusText); // CRASH
}
const { access_token: accessToken } = await response.json();
return accessToken;
}
async uploadFromUrl(message: UploadInstruction): Promise<void> {
const accessToken = await this.requestAuthToken();
const myUploadForm = new URLSearchParams();
const myHeader = new Headers();
myHeader.append("Authorization", `Bearer ${accessToken}`);
for (const key in message) myUploadForm.append(key, message[key]);
const response = await fetch(
new URL(`/api/v1/videos/imports`, this.domain_name),
{
method: "post",
headers: myHeader,
body: myUploadForm,
}
);
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error(
`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:
throw new Error(response.statusText);
break;
case 409:
throw new Error(
`Oops, your instance did not allowed the HTTPS import.\
Contact your administrator.
${response.statusText}`
);
break;
default:
throw new Error(
`Oh, you encountered an undocumented issues.\
Please create an issue to the plugin project.
ERROR: ${response.statusText}`
);
break;
}
}
}
}
export { PeerTubeRequester };
// Api request lib
import fetch, { Headers } from "node-fetch";
import { URL, URLSearchParams } from "url";
namespace PeerTubeRequester {
export type Config = {
domainName: string | URL;
username: string;
password: string;
};
}
type UploadInstruction = {
[key: string]: string;
channelId: string;
targetUrl: string;
};
class PeerTubeRequester {
readonly domainName: URL;
readonly username: string;
readonly password: string;
constructor(readonly config: PeerTubeRequester.Config) {
this.domainName = new URL("/", config.domainName);
this.username = config.username;
this.password = config.password;
}
async requestAuthToken(): Promise<any> {
let response = await fetch(
new URL(`/api/v1/oauth-clients/local`, this.domainName)
);
if (!response.ok) {
throw new Error("Cannot get client credentials : " + response.statusText); // CRASH
}
const { client_id: clientId, client_secret: clientSecret } =
await response.json();
const clientInfo: { [key: string]: string } = {
client_id: clientId,
client_secret: clientSecret,
grant_type: "password",
response_type: "code",
username: this.username,
password: this.password,
};
let myParams = new URLSearchParams();
for (const key in clientInfo) myParams.append(key, clientInfo[key]);
response = await fetch(new URL(`/api/v1/users/token`, this.domainName), {
method: "post",
body: myParams,
});
if (!response.ok) {
throw new Error("Cannot get access Token : " + response.statusText); // CRASH
}
const { access_token: accessToken } = await response.json();
return accessToken;
}
async uploadFromUrl(message: UploadInstruction): Promise<void> {
const accessToken = await this.requestAuthToken();
const myUploadForm = new URLSearchParams();
const myHeader = new Headers();
myHeader.append("Authorization", `Bearer ${accessToken}`);
for (const key in message) myUploadForm.append(key, message[key]);
const response = await fetch(
new URL("/api/v1/videos/imports", this.domainName),
{
method: "post",
headers: myHeader,
body: myUploadForm,
}
);
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error(
`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:
throw new Error(response.statusText);
break;
case 409:
throw new Error(
`Oops, your instance did not allowed the HTTPS import.\
Contact your administrator.
${response.statusText}`
);
break;
default:
throw new Error(
`Oh, you encountered an undocumented issues.\
Please create an issue to the plugin project.
ERROR: ${response.statusText}`
);
break;
}
}
}
async getChannelId(channelName: string): Promise<number> {
const response = await fetch(
new URL(`/api/v1/videos-channels/${channelName}`, this.domainName),
{
method: "gett"
}
);
const { id } = await response.json();
return id;
}
}
export { PeerTubeRequester };

1
package-lock.json generated
View File

@ -9,6 +9,7 @@
"dependencies": {
"@types/node-fetch": "^2.5.11",
"form-data": "^4.0.0",
"listener-rss": "^0.0.3",
"listener-rss-aggregator": "^0.0.5",
"node-fetch": "^2.6.1"
},

View File

@ -1,9 +1,9 @@
{
"name": "peertube-plugin-auto-import-ytb",
"description": "PeerTube plugin quickstart",
"version": "0.0.1",
"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"
@ -34,6 +34,7 @@
"@types/node-fetch": "^2.5.11",
"form-data": "^4.0.0",
"listener-rss-aggregator": "^0.0.5",
"listener-rss": "^0.0.3",
"node-fetch": "^2.6.1"
}
}

View File

@ -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";
@ -29,6 +30,18 @@ async function register({
type: "input-textarea",
});
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"), "");
@ -37,20 +50,36 @@ async function register({
);
myManager = new ListenerRssAggregator(configAggregator);
peertube = new PeerTubeRequester({
domain_name: "http://localhost:9000",
username: "root",
password: "test",
});
logger.debug("Aggregator created");
const inputs = await settingsManager.getSetting("ytb-urls");
if (inputs) await addListeners(inputs);
const settingYtbUrls = await settingsManager.getSetting("ytb-urls");
if (settingYtbUrls) await addListeners(settingYtbUrls);
logger.debug("Config loaded");
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.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"]);
});
@ -64,13 +93,31 @@ async function register({
JSON.stringify(entries)
);
for (const item of entries.items)
await peertube.uploadFromUrl({
channelId: datas.channelId,
targetUrl: item.link,
});
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 {
@ -96,9 +143,9 @@ async function addListeners(listenerInput: string) {
myManager.stopAll();
await myManager.saveOverride(listeners);
if (logger) logger.warn("Configuration changed: " + listenerInput);
if (logger) logger.debug("Configuration changed: " + listenerInput);
myManager.startAll();
if (goodPeertubeCredential) myManager.startAll();
}
async function unregister() {