-
-
Notifications
You must be signed in to change notification settings - Fork 55
Dev: Adding video services
To support multiple video services, OpenTogetherTube uses a plugin-like system called "Service Adapters". Every service adapter is a class that handles the communication with a specific video service, like YouTube or Vimeo.
To create your own service adapter, create a new file under server/services/
which contains a class that extends ServiceAdapter
and overrides all necessary methods. If you want to see an example, take a look at server/services/vimeo.js
. It is a very simple, yet complete implementation.
Here is a template that you can use to start working on your own adapter. It includes the most common, required methods.
const ServiceAdapter = require("../serviceadapter");
class MyServiceAdapter extends ServiceAdapter {
get serviceId() {
return "";
}
canHandleURL(url) {
return false;
}
isCollectionURL(url) {
return false;
}
getVideoId(url) {
return "";
}
async fetchVideoInfo(videoId) {
return [];
}
}
module.exports = MyServiceAdapter;
The service ID is required to identify the service in client-server communication and the cache. It's a simple string.
get serviceId() {
return "myservice";
}
In order to work out which adapter has to handle a specific request, every adapter has to be able to determine whether it can resolve the URL. The method receives the URL as a string and must return a boolean that is true
if the URL points to a valid video source that this adapter can handle.
const URL = require("url");
// ...
canHandleURL(url) {
// Returns true for all URLs pointing to example.com.
const host = URL.parse(url).host;
return host.endsWith("example.com");
}
Determines whether a URL points to a collection resource like a playlist or a channel. This method receives a URL as a string. This URL was already run through canHandleURL
, so it is guaranteed to be valid. If your video service doesn't have any collections, this method should just return false
.
const URL = require("url");
// ...
isCollectionURL(url) {
const pathname = URL.parse(url).pathname;
return pathname.startsWith("/playlist/");
}
This method extracts the ID of a video from a URL and returns it. It receives a URL as a string that is guaranteed to be valid and not a collection URL as defined by canHandleURL
and isCollectionURL
.
const URL = require("url");
// ...
getVideoId(url) {
// This splits the path into parts separated by slashes and returns the
// last part. Given the URL is "https://example.com/video/abc123" it would
// return "abc123".
const pathname = URL.parse(url).pathname;
return pathname.split("/").slice(-1)[0].trim();
}
This is the heart of every service adapter. It receives a video ID, queries the API for information and returns it in the form of a Video
object.
In addition to a video ID, it receives an array with properties that are still missing. This is useful for APIs that let you query only specific bits of information in order to save quota. If a video is partially cached, this array will include only the properties that are not yet cached. Otherwise it will include all properties. You can ignore this parameter if your API doesn't support this.
const axios = require("axios");
const ServiceAdapter = require("../serviceadapter");
const Video = require("../../common/video");
class ExampleAdapter extends ServiceAdapter {
api = axios.create({
baseURL: "https://api.example.com",
});
// ...
async fetchVideoInfo(videoId) {
const result = await this.api.get(`/video/${videoId}`);
const video = new Video({
service: this.serviceId,
id: videoId,
title: result.data.title,
description: result.data.description,
thumbnail: result.data.thumbnail_url,
length: result.data.duration,
});
return video;
}
}
Implement this getter and return false to denote that video information should not be cached and instead always be fetched from the API. If you don't implement this, it will default to true
, which will usually work. One example of a non-cache-safe service would be direct links, because the videos behind URLs could arbitrarily change, while the video behind a YouTube ID can not.
get isCacheSafe() {
return false;
}
This method is only required if your implementation of isCollectionURL
can return true
.
If a URL does not simply point to only one video as denoted by isCollectionURL
, this method will be called instead of fetchVideoInfo
. It receives the URL as a string and must return an array of Video
objects.
const URL = require("url");
// ...
async resolveURL(url) {
// fetchChannelVideos, fetchPlaylistVideos, getChannelId and getPlaylistId
// are not part of the ServiceAdapter interface. They are simply part of
// this example code. You would have to implement these yourself and you
// could call them whatever you want.
const urlInfo = URL.parse(url);
if (urlInfo.pathname.startsWith("/channel/")) {
return this.fetchChannelVideos(this.getChannelId(url));
}
else if (urlInfo.pathname.startsWith("/playlist/")) {
return this.fetchPlaylistVideos(this.getPlaylistId(url));
}
else {
// This is technically not needed if you set your `canHandleURL` and
// `isCollectionURL` methods up correctly. It's still included here in
// case something goes wrong and we still receive an invalid URL.
return [];
}
}
This method is only required if your implementation of isCollectionURL
can return true
.
This method receives an array of request objects each containing a video and a missingInfo
array. Here is an example of what it could look like:
[
{
"id": "abc123",
"missingInfo": ["description", "length"]
}
]
For most APIs you will probably only need the ID. The purpose of the missingInfo
property is described in fetchVideoInfo
.
fetchManyVideoInfo
must return an array of Video
objects.
async fetchManyVideoInfo(requests) {
// If your API only allows single video queries, you can just delegate to
// fetchVideoInfo.
const videos = Promise.all(
requests.map((request) => this.fetchVideoInfo(request.id))
);
return videos;
}