Skip to content

Plugin System Introduction

AN Player supports a plugin-based architecture that lets developers add new content sources to the app. Plugins are Node.js/TypeScript modules that act as bridges between AN Player and any data source or protocol.

TIP

The full plugin model definition is available at xeinebiu/android_anplayer_plugin. This is the best place to understand all available interfaces and models.

What is a Plugin?

A plugin is a TypeScript module that you write, compile into a single bundled JavaScript file using ncc, and distribute as a manifest file. AN Player runs this script inside an embedded Node.js runtime, so your plugin can connect to any server using any protocol, parse its responses, and return structured media data, all without any native Android code.

AN Player <-> Your Plugin <-> Your Source

When a user installs your plugin, AN Player copies the script to the device and runs it on demand inside the embedded runtime. Your plugin is never "installed" in the Android sense, just JavaScript evaluated in Node.js.

What Can a Plugin Do?

A plugin can implement any combination of the following capabilities:

CapabilityDescription
FeedReturn a list of items shown on the Discover screen
SearchAccept a search query and return matching results
Auto-completeReturn live suggestions as the user types
Browse albumsNavigate folder/category/playlist hierarchies
Browse authorsList content by creator
Browse categoriesList content by genre or topic
Stream playbackResolve a media item URI to a playable stream URL

You only need to implement the capabilities your data source supports. Everything else defaults to ExtractorNotImplementedError, which AN Player handles gracefully.

Every plugin is a factory function that receives a context object (including config and logger) and returns an object containing the extractor factories and classification helpers.

typescript
import { AnPlayerExtractor } from '@xeinebiu/anplayer-plugin-core';

const MyPlugin: AnPlayerExtractor = ({ config, logger }) => {
  return {
    async isSupported(uri) {
      return uri.hostname === 'myapi.example.com';
    },
    async isAlbum(uri) {
      return uri.pathname === '/album';
    },
    async isMediaTrack(uri) {
      return uri.pathname === '/media';
    },
    async getFeedExtractor() {
      // return your feed extractor here
    },
    // ... implement other extractors
  };
};

export default MyPlugin;

The factory pattern ensures that your plugin receives its unique configuration (like API keys or server URLs)

  • config: A string containing any user-supplied configuration (API keys, custom tokens, or serialized settings). The app doesn't enforce a format; it just stores the string and passes it back to your plugin. and a dedicated logger directly through its constructor. All methods are asynchronous, allowing you to perform I/O or other setup as needed.

IMPORTANT

Your plugin must be exported as the default export. AN Player expects the script to return a single factory function compatible with the AnPlayerExtractor signature.

AN Player identifies content by URIs. Your plugin's classification methods decide:

  1. async isSupported(uri): does this plugin own this URI at all?
  2. async isAlbum(uri): is this URI a browseable container (folder, playlist, category)?
  3. async isMediaTrack(uri): is this URI a playable media item?

You construct these URIs yourself when returning data from your feed or album extractor, so you have full control over the scheme.

Error Handling

When a capability is not implemented, throw ExtractorNotImplementedError:

typescript
import { ExtractorNotImplementedError } from '@xeinebiu/anplayer-plugin-core';

// Inside an extractor method you chose not to implement:
throw new ExtractorNotImplementedError();

AN Player catches this and either hides the feature or shows an appropriate message to the user.

Next Steps