diff --git a/docs/Example Plugin.md b/docs/Example Plugin.md new file mode 100644 index 0000000000000000000000000000000000000000..a4831f05d1ec3a1d55fd1105fbb0aa81536a1dcd --- /dev/null +++ b/docs/Example Plugin.md @@ -0,0 +1,244 @@ +# Example plugin + +Note that this is just a starting point, plugins can also implement optional features such as login, importing playlists/subscriptions, etc. For full examples please see in-house developed plugins (click [here](https://gitlab.futo.org/videostreaming/plugins)). + +```js +source.enable = function (conf) { + /** + * @param conf: SourceV8PluginConfig (the SomeConfig.js) + */ +} + +source.getHome = function(continuationToken) { + /** + * @param continuationToken: any? + * @returns: VideoPager + */ + const videos = []; // The results (PlatformVideo) + const hasMore = false; // Are there more pages? + const context = { continuationToken: continuationToken }; // Relevant data for the next page + return new SomeHomeVideoPager(videos, hasMore, context); +} + +source.searchSuggestions = function(query) { + /** + * @param query: string + * @returns: string[] + */ + + const suggestions = []; //The suggestions for a specific search query + return suggestions; +} + +source.getSearchCapabilities = function() { + //This is an example of how to return search capabilities like available sorts, filters and which feed types are available (see source.js for more details) + return { + types: [Type.Feed.Mixed], + sorts: [Type.Order.Chronological, "^release_time"], + filters: [ + { + id: "date", + name: "Date", + isMultiSelect: false, + filters: [ + { id: Type.Date.Today, name: "Last 24 hours", value: "today" }, + { id: Type.Date.LastWeek, name: "Last week", value: "thisweek" }, + { id: Type.Date.LastMonth, name: "Last month", value: "thismonth" }, + { id: Type.Date.LastYear, name: "Last year", value: "thisyear" } + ] + }, + ] + }; +} + +source.search = function (query, type, order, filters, continuationToken) { + /** + * @param query: string + * @param type: string + * @param order: string + * @param filters: Map<string, Array<string>> + * @param continuationToken: any? + * @returns: VideoPager + */ + const videos = []; // The results (PlatformVideo) + const hasMore = false; // Are there more pages? + const context = { query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page + return new SomeSearchVideoPager(videos, hasMore, context); +} + +source.getSearchChannelContentsCapabilities = function () { + //This is an example of how to return search capabilities on a channel like available sorts, filters and which feed types are available (see source.js for more details) + return { + types: [Type.Feed.Mixed], + sorts: [Type.Order.Chronological], + filters: [] + }; +} + +source.searchChannelContents = function (url, query, type, order, filters, continuationToken) { + /** + * @param url: string + * @param query: string + * @param type: string + * @param order: string + * @param filters: Map<string, Array<string>> + * @param continuationToken: any? + * @returns: VideoPager + */ + + const videos = []; // The results (PlatformVideo) + const hasMore = false; // Are there more pages? + const context = { channelUrl: channelUrl, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page + return new SomeSearchChannelVideoPager(videos, hasMore, context); +} + +source.searchChannels = function (query, continuationToken) { + /** + * @param query: string + * @param continuationToken: any? + * @returns: ChannelPager + */ + + const channels = []; // The results (PlatformChannel) + const hasMore = false; // Are there more pages? + const context = { query: query, continuationToken: continuationToken }; // Relevant data for the next page + return new SomeChannelPager(channels, hasMore, context); +} + +source.isChannelUrl = function(url) { + /** + * @param url: string + * @returns: boolean + */ + + return REGEX_CHANNEL_URL.test(url); +} + +source.getChannel = function(url) { + return new PlatformChannel({ + //... see source.js for more details + }); +} + +source.getChannelContents = function(url, type, order, filters, continuationToken) { + /** + * @param url: string + * @param type: string + * @param order: string + * @param filters: Map<string, Array<string>> + * @param continuationToken: any? + * @returns: VideoPager + */ + + const videos = []; // The results (PlatformVideo) + const hasMore = false; // Are there more pages? + const context = { url: url, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page + return new SomeChannelVideoPager(videos, hasMore, context); +} + +source.isContentDetailsUrl = function(url) { + /** + * @param url: string + * @returns: boolean + */ + + return REGEX_DETAILS_URL.test(url); +} + +source.getContentDetails = function(url) { + /** + * @param url: string + * @returns: PlatformVideoDetails + */ + + return new PlatformVideoDetails({ + //... see source.js for more details + }); +} + +source.getComments = function (url, continuationToken) { + /** + * @param url: string + * @param continuationToken: any? + * @returns: CommentPager + */ + + const comments = []; // The results (Comment) + const hasMore = false; // Are there more pages? + const context = { url: url, continuationToken: continuationToken }; // Relevant data for the next page + return new SomeCommentPager(comments, hasMore, context); + +} +source.getSubComments = function (comment) { + /** + * @param comment: Comment + * @returns: SomeCommentPager + */ + + if (typeof comment === 'string') { + comment = JSON.parse(comment); + } + + return getCommentsPager(comment.context.claimId, comment.context.claimId, 1, false, comment.context.commentId); +} + +class SomeCommentPager extends CommentPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.getComments(this.context.url, this.context.continuationToken); + } +} + +class SomeHomeVideoPager extends VideoPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.getHome(this.context.continuationToken); + } +} + +class SomeSearchVideoPager extends VideoPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.search(this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); + } +} + +class SomeSearchChannelVideoPager extends VideoPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.searchChannelContents(this.context.channelUrl, this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); + } +} + +class SomeChannelPager extends ChannelPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.searchChannelContents(this.context.query, this.context.continuationToken); + } +} + +class SomeChannelVideoPager extends VideoPager { + constructor(results, hasMore, context) { + super(results, hasMore, context); + } + + nextPage() { + return source.getChannelContents(this.context.url, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); + } +} +``` \ No newline at end of file diff --git a/docs/Script Signing.md b/docs/Script Signing.md new file mode 100644 index 0000000000000000000000000000000000000000..5e593e356ee9713c6b5a36910327649e08045286 --- /dev/null +++ b/docs/Script Signing.md @@ -0,0 +1,31 @@ +# Script signing + +The `scriptSignature` and `scriptPublicKey` should be set whenever you deploy your script (NOT REQUIRED DURING DEVELOPMENT). The purpose of these fields is to verify that a plugin update was made by the same individual that developed the original plugin. This prevents somebody from hijacking your plugin without having access to your public private keypair. When this value is not present, you can still use this plugin, however the user will be informed that these values are missing and that this is a security risk. Here is an example script showing you how to generate these values. See below for more details. + +You can use this script to generate the `scriptSignature` and `scriptPublicKey` fields above: + +`sign-script.sh` +```sh +#!/bin/sh +#Example usage: +#cat script.js | sign-script.sh +#sh sign-script.sh script.js + +#Set your key paths here +PRIVATE_KEY_PATH=~/.ssh/id_rsa +PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub + +PUBLIC_KEY_PKCS8=$(ssh-keygen -f "$PUBLIC_KEY_PATH" -e -m pkcs8 | tail -n +2 | head -n -1 | tr -d '\n') +echo "This is your public key: '$PUBLIC_KEY_PKCS8'" + +if [ $# -eq 0 ]; then + # No parameter provided, read from stdin + DATA=$(cat) +else + # Parameter provided, read from file + DATA=$(cat "$1") +fi + +SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha512 -sign ~/.ssh/id_rsa | base64 -w 0) +echo "This is your signature: '$SIGNATURE'" +``` \ No newline at end of file diff --git a/plugin-development.md b/plugin-development.md index 8330a53ff9b2e1c63606535edabf8b611a2f0975..6f49269e4e997383a1d23ab9f9f68ebbc88e4c64 100644 --- a/plugin-development.md +++ b/plugin-development.md @@ -3,363 +3,142 @@ ## Table of Contents - [Introduction](#introduction) -- [Grayjay App Overview](#grayjay-app-overview) -- [Plugin Development Overview](#plugin-development-overview) -- [Setting up the Development Environment](#setting-up-the-development-environment) -- [Using the Developer Interface](#using-the-developer-interface) +- [Quick Start](#quick-start) +- [Configuration file](#configuration-file) +- [Packages](#packages) +- [Authentication](#authentication) +- [Content Types](#content-types) +- [Example plugin](#example-plugin) +- [Pagination](#pagination) +- [Script signing](#script-signing) - [Plugin Deployment](#plugin-deployment) - [Common Issues and Troubleshooting](#common-issues-and-troubleshooting) -- [Additional Resources](#additional-resources) - [Support and Contact](#support-and-contact) - ## Introduction -Welcome to the Grayjay App plugin development documentation. This guide will provide an overview of Grayjay's plugin system and guide you through the steps necessary to create, test, debug, and deploy plugins. +Welcome to the Grayjay App plugin development documentation. Plugins are additional components that you can create to extend the functionality of the Grayjay app, for example a YouTube or Odysee plugin. This guide will provide an overview of Grayjay's plugin system and guide you through the steps necessary to create, test, debug, and deploy plugins. + +## Quick Start + +### Download GrayJay: + +- Download the GrayJay app for Android [here](https://grayjay.app/). + +### Enable GrayJay Developer Mode: -## Grayjay App Overview +- Enable developer mode in the GrayJay app (not Android settings app) by tapping the “More†tab, tapping “Settingsâ€, scrolling all the way to the bottom, and tapping the “Version Code†multiple times. -Grayjay is a unique media application that aims to revolutionize the relationship between content creators and their audiences. By shifting the focus from platforms to creators, Grayjay democratizes the content delivery process, empowering creators to retain full ownership of their content and directly monetize their work. +### Run the GrayJay DevServer: -For users, Grayjay offers a more privacy-focused and personalized content viewing experience. Rather than being manipulated by opaque algorithms, users can decide what they want to watch, thus enhancing their engagement and enjoyment of the content. +- At the bottom of the Settings page in the GrayJay app, Click the purple “Developer Settings†button. Then click the “Start Server†button to start the DevServer. -Our ultimate goal is to create the best media app, merging content and features that users love with a strong emphasis on user and creator empowerment and privacy. + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/07fc4919b0a8446c4cdf5335565c0611/image.png" width="200"> -By developing Grayjay, we strive to make a stride toward a more open, interconnected, and equitable media ecosystem. This ecosystem fosters a thriving community of creators who are supported by their audiences, all facilitated through a platform that respects and prioritizes privacy and ownership. +### Open the GrayJay DevServer on your computer: -## Plugin Development Overview +- Open the Android settings app and search for “IP addressâ€. The IP address should look like `192.168.X.X`. +- Open `http://<phone-ip>:11337/dev` in your web browser. + + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/72885c3bc51b8efe9462ee68d47e3b51/image.png" width="600"> -Plugins are additional components that you can create to extend the functionality of the Grayjay app. +### Create and host your plugin: -## Setting up the Developer Environment +- Clone the [Odysee plugin](https://gitlab.futo.org/videostreaming/plugins/odysee) as an example +- `cd` into the project folder and serve with `npx serve` (if you have [Node.js](https://nodejs.org/en/)) or any other HTTP Server you desire. +- `npx serve` should give you a Network url (not the localhost one) that looks like `http://192.168.X.X:3000`. Your config file URL will be something like `http://192.168.X.X:3000/OdyseeConfig.json`. + + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/cc266da0a0b85c5770abca22c0b03b3b/image.png" width="600"> -Before you start developing plugins, it is necessary to set up a suitable developer environment. Here's how to do it: +### Test your plugin: -1. Create a plugin, the minimal starting point is the following. +- When the DevServer is open in your browser, enter the config file URL and click “Load Pluginâ€. This will NOT inject the plugin into the app, for that you need to click "Inject Plugin" on the Integration tab. + + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/386a562f30a60cfcbb8a8a1345a788e5/image.png" width="600"> + +- On the Testing tab, you can individually test the methods in your plugin. To reload once you make changes on the plugin, click the top-right refresh button. *Note: While testing, the custom domParser package is overwritten with the browser's implementation, so it may behave differently than once it is loaded into the app.* + + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/08830eb8cc56cc55ba445dd49db86235/image.png" width="600"> + +- On the Integration tab you can test your plugin end-to-end in the GrayJay app and monitor device logs. You can click "Inject Plugin" in order to inject the plugin into the app. Your plugin should show up on the Sources tab in the GrayJay app. If you make changes and want to reload the plugin, click "Inject Plugin" again. + + <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/74813fbf37dcfc63055595061e41c48b/image.png" width="600"> -`SomeConfig.js` -```json +## Configuration file + +Create a configuration file for your plugin. + +`SomeConfig.json` +```js { "name": "Some name", "description": "A description for your plugin", "author": "Your author name", "authorUrl": "https://yoursite.com", + // The `sourceUrl` field should contain the URL where your plugin will be publically accessible in the future. This allows the app to scan this location to see if there are any updates available. "sourceUrl": "https://yoursite.com/SomeConfig.json", "repositoryUrl": "https://github.com/someuser/someproject", "scriptUrl": "./SomeScript.js", "version": 1, "iconUrl": "./someimage.png", + + // The `id` field should be a uniquely generated UUID like from [https://www.uuidgenerator.net/](https://www.uuidgenerator.net/). This will be used to distinguish your plugin from others. "id": "309b2e83-7ede-4af8-8ee9-822bc4647a24", - "scriptSignature": "<ommitted>", - "scriptPublicKey": "<ommitted>", + // See the "Script Signing" section for details + "scriptSignature": "<omitted>", + "scriptPublicKey": "<omitted>", + + // See the "Packages" section for details, currently allowed values are: ["Http", "DOMParser", "Utilities"] "packages": ["Http"], "allowEval": false, + + // The `allowUrls` field is allowed to be `everywhere`, this means that the plugin is allowed to access all URLs. However, this will popup a warning for the user that this is the case. Therefore, it is recommended to narrow the scope of the accessible URLs only to the URLs that you actually need. Other requests will be blocked. During development it can be convenient to use `everywhere`. Possible values are `odysee.com`, `api.odysee.com`, etc. "allowUrls": [ "everywhere" ] } ``` -The `sourceUrl` field should contain the URL where your plugin will be publically accessible in the future. This allows the app to scan this location to see if there are any updates available. - -The `id` field should be a uniquely generated UUID like from [https://www.uuidgenerator.net/](https://www.uuidgenerator.net/). This will be used to distinguish your plugin from others. - -The `allowUrls` field is allowed to be `everywhere`, this means that the plugin is allowed to access all URLs. However, this will popup a warning for the user that this is the case. Therefore, it is recommended to narrow the scope of the accessible URLs only to the URLs that you actually need. Other requests will be blocked. During development it can be convenient to use `everywhere`. Possible values are `odysee.com`, `api.odysee.com`, etc. - -The `scriptSignature` and `scriptPublicKey` should be set whenever you deploy your script (NOT REQUIRED DURING DEVELOPMENT). The purpose of these fields is to verify that a plugin update was made by the same individual that developed the original plugin. This prevents somebody from hijacking your plugin without having access to your public private keypair. When this value is not present, you can still use this plugin, however the user will be informed that these values are missing and that this is a security risk. Here is an example script showing you how to generate these values. - -`sign-script.sh` -```sh -#!/bin/sh -#Example usage: -#cat script.js | sign-script.sh -#sh sign-script.sh script.js - -#Set your key paths here -PRIVATE_KEY_PATH=~/.ssh/id_rsa -PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub - -PUBLIC_KEY_PKCS8=$(ssh-keygen -f "$PUBLIC_KEY_PATH" -e -m pkcs8 | tail -n +2 | head -n -1 | tr -d '\n') -echo "This is your public key: '$PUBLIC_KEY_PKCS8'" - -if [ $# -eq 0 ]; then - # No parameter provided, read from stdin - DATA=$(cat) -else - # Parameter provided, read from file - DATA=$(cat "$1") -fi - -SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha512 -sign ~/.ssh/id_rsa | base64 -w 0) -echo "This is your signature: '$SIGNATURE'" - -``` +## Packages The `packages` field allows you to specify which packages you want to use, current available packages are: -- `Http`: for performing HTTP requests (see [docs](TODO)) -- `DOMParser`: for parsing a DOM (see [docs](TODO)) -- `Utilities`: for various utility functions like generating random UUIDs or converting to Base64 (see [docs](TODO)) +- `Http`: for performing HTTP requests (see [docs](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/packages/packageHttp.md)) +- `DOMParser`: for parsing a DOM (no docs yet, see [source code](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt)) +- `Utilities`: for various utility functions like generating UUIDs or converting to Base64 (no docs yet, see [source code](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/app/src/main/java/com/futo/platformplayer/engine/packages/PackageUtilities.kt)) -Note that this is just a starting point, plugins can also implement optional features such as login, importing playlists/subscriptions, etc. For full examples please see in-house developed plugins (click [here](TODO)). +## Authentication -`SomeScript.js` -```js -source.enable = function (conf) { - /** - * @param conf: SourceV8PluginConfig (the SomeConfig.js) - */ -} +Authentication is sometimes required by plugins to access user data and premium content, for example on YouTube or Patreon. -source.getHome = function(continuationToken) { - /** - * @param continuationToken: any? - * @returns: VideoPager - */ - const videos = []; // The results (PlatformVideo) - const hasMore = false; // Are there more pages? - const context = { continuationToken: continuationToken }; // Relevant data for the next page - return new SomeHomeVideoPager(videos, hasMore, context); -} - -source.searchSuggestions = function(query) { - /** - * @param query: string - * @returns: string[] - */ - - const suggestions = []; //The suggestions for a specific search query - return suggestions; -} +See [Authentication.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Authentication.md) -source.getSearchCapabilities = function() { - //This is an example of how to return search capabilities like available sorts, filters and which feed types are available (see source.js for more details) - return { - types: [Type.Feed.Mixed], - sorts: [Type.Order.Chronological, "^release_time"], - filters: [ - { - id: "date", - name: "Date", - isMultiSelect: false, - filters: [ - { id: Type.Date.Today, name: "Last 24 hours", value: "today" }, - { id: Type.Date.LastWeek, name: "Last week", value: "thisweek" }, - { id: Type.Date.LastMonth, name: "Last month", value: "thismonth" }, - { id: Type.Date.LastYear, name: "Last year", value: "thisyear" } - ] - }, - ] - }; -} +## Content Types -source.search = function (query, type, order, filters, continuationToken) { - /** - * @param query: string - * @param type: string - * @param order: string - * @param filters: Map<string, Array<string>> - * @param continuationToken: any? - * @returns: VideoPager - */ - const videos = []; // The results (PlatformVideo) - const hasMore = false; // Are there more pages? - const context = { query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page - return new SomeSearchVideoPager(videos, hasMore, context); -} +Docs for data structures like PlatformVideo your plugin uses to communicate with the GrayJay app. -source.getSearchChannelContentsCapabilities = function () { - //This is an example of how to return search capabilities on a channel like available sorts, filters and which feed types are available (see source.js for more details) - return { - types: [Type.Feed.Mixed], - sorts: [Type.Order.Chronological], - filters: [] - }; -} +See [Content Types.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Content%20Types.md) -source.searchChannelContents = function (url, query, type, order, filters, continuationToken) { - /** - * @param url: string - * @param query: string - * @param type: string - * @param order: string - * @param filters: Map<string, Array<string>> - * @param continuationToken: any? - * @returns: VideoPager - */ - - const videos = []; // The results (PlatformVideo) - const hasMore = false; // Are there more pages? - const context = { channelUrl: channelUrl, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page - return new SomeSearchChannelVideoPager(videos, hasMore, context); -} +## Example plugin -source.searchChannels = function (query, continuationToken) { - /** - * @param query: string - * @param continuationToken: any? - * @returns: ChannelPager - */ - - const channels = []; // The results (PlatformChannel) - const hasMore = false; // Are there more pages? - const context = { query: query, continuationToken: continuationToken }; // Relevant data for the next page - return new SomeChannelPager(channels, hasMore, context); -} +See the example plugin to better understand the plugin API e.g. `getHome` and `search`. -source.isChannelUrl = function(url) { - /** - * @param url: string - * @returns: boolean - */ +See [Example Plugin.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Example%20Plugin.md) - return REGEX_CHANNEL_URL.test(url); -} +## Pagination -source.getChannel = function(url) { - return new PlatformChannel({ - //... see source.js for more details - }); -} +Plugins use "Pagers" to send paginated data to the GrayJay app. -source.getChannelContents = function(url, type, order, filters, continuationToken) { - /** - * @param url: string - * @param type: string - * @param order: string - * @param filters: Map<string, Array<string>> - * @param continuationToken: any? - * @returns: VideoPager - */ - - const videos = []; // The results (PlatformVideo) - const hasMore = false; // Are there more pages? - const context = { url: url, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page - return new SomeChannelVideoPager(videos, hasMore, context); -} +See [Pagers.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Pagers.md) -source.isContentDetailsUrl = function(url) { - /** - * @param url: string - * @returns: boolean - */ +## Script signing - return REGEX_DETAILS_URL.test(url); -} +When you deploy your plugin, you'll need to add code signing for security. -source.getContentDetails = function(url) { - /** - * @param url: string - * @returns: PlatformVideoDetails - */ - - return new PlatformVideoDetails({ - //... see source.js for more details - }); -} - -source.getComments = function (url, continuationToken) { - /** - * @param url: string - * @param continuationToken: any? - * @returns: CommentPager - */ - - const comments = []; // The results (Comment) - const hasMore = false; // Are there more pages? - const context = { url: url, continuationToken: continuationToken }; // Relevant data for the next page - return new SomeCommentPager(comments, hasMore, context); - -} -source.getSubComments = function (comment) { - /** - * @param comment: Comment - * @returns: SomeCommentPager - */ - - if (typeof comment === 'string') { - comment = JSON.parse(comment); - } - - return getCommentsPager(comment.context.claimId, comment.context.claimId, 1, false, comment.context.commentId); -} - -class SomeCommentPager extends CommentPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.getComments(this.context.url, this.context.continuationToken); - } -} - -class SomeHomeVideoPager extends VideoPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.getHome(this.context.continuationToken); - } -} - -class SomeSearchVideoPager extends VideoPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.search(this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); - } -} - -class SomeSearchChannelVideoPager extends VideoPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.searchChannelContents(this.context.channelUrl, this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); - } -} - -class SomeChannelPager extends ChannelPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.searchChannelContents(this.context.query, this.context.continuationToken); - } -} - -class SomeChannelVideoPager extends VideoPager { - constructor(results, hasMore, context) { - super(results, hasMore, context); - } - - nextPage() { - return source.getChannelContents(this.context.url, this.context.type, this.context.order, this.context.filters, this.context.continuationToken); - } -} -``` - -2. Configure a web server to host the plugin. This can be something as simple as a NGINX server where you just place the files in the wwwroot or a simple dotnet/npm program that hosts the file for you. The important part is that the webserver and the phone are on the same network and the phone can access the files hosted by the development machine. An example of what this would look like is [here](https://plugins.grayjay.app/Odysee/OdyseeConfig.json). Alternatively, you could simply point to a Github/Gitlab raw file if you do not want to host it yourself. Note that the URL is not required to be publically accessible during development and HTTPS is NOT required. -3. Enable developer mode on the mobile application by going to settings, clicking on the version code multiple times. Once enabled, click on developer settings and then in the developer settings enable the webserver. -4. You are now able to access the developer interface on the phone via `http://<phone-ip>:11337/dev`. - -## Using the Developer Interface - -Once in the web portal you will see several tabs and a form allowing you to load a plugin. - -1. Lets load your plugin. Take the URL that your plugin config is available at (like http://192.168.1.196:5000/Some/SomeConfig.json) and enter it in the `Plugin Config Json Url` field. Once entered, click load plugin. -*The package override domParser will override the domParser with the browser implementation. This is useful when you quickly want to iterate on plugins that parse the DOM, but it is less accurate to what the plugin will behave like once in-app.* -2. Once the plugin is loaded, you can click on the `Testing` tab and call individual methods. This allows you to quickly iterate, test methods and make sure they are returning the proper values. To reload once you make changes on the plugin, click the top-right refresh button. -3. After you are sure everything is working properly, click the `Integration` tab in order to perform integration testing on your plugin. You can click the `Inject Plugin` button in order to inject the plugin into the app. On the sources page in your app you should see your source and you are able to test it and make sure everything works. If you make changes and want to reload the plugin, click the `Inject Plugin` button again. +See [Script Signing.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Script%Signing.md) ## Plugin Deployment @@ -403,12 +182,6 @@ Ensure the QR code correctly points to the plugin config URL. The URL must be pu Make sure the signature is correctly generated and added. Also, ensure the version number in the config matches the new version number. -## Additional Resources - -Here are some additional resources that might help you with your plugin development: - -Please - ## Support and Contact If you have any issues or need further assistance, feel free to reach out to us at: