First Post
This is the first post.
Read moreThis is the first post.
Read moreThis is the second post.
Read moreThis is the third post.
Read more// A simple plugin that stores the history of scan durations
function init() {
$app.onScanCompleted((e) => {
// Store the scanning duration (in ms)
$store.set("scan-completed", e.duration)
e.next()
})
$ui.register((ctx) => {
// Callback is triggered when the value is updated
$store.watch<number>("scan-completed", (value) => {
const now = new Date().toISOString().replaceall(".", "\_")
$storage.set("scan-duration-history."+now, value)
ctx.toast.info(\`Scanning took ${value/1000} seconds!\`)
})
})
}
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui.md).
# UI
- \[Basics\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics.md): Basics of the UI context.
- \[Helpers\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers.md): Convenience helpers for common UI plugin workflows.
- \[Cron\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron.md): Schedule recurring UI-context jobs with cron expressions.
- \[User Interface\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface.md)
- \[Tray\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray.md)
- \[Webview\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview.md): Sandboxed iframes
- \[Toast\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast.md): Provide instant feedback in a popup.
- \[Screen\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen.md): Observe and control navigation within the app.
- \[Command Palette\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette.md)
- \[Action\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action.md)
- \[DOM\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom.md): API for DOM manipulation in Seanime plugins. Each DOM operation involves communication between the plugin and the browser, so understanding performance considerations is important.
- \[Anime/Library\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library.md)
- \[Anime\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime.md)
- \[Playback (External)\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external.md): Interact with the desktop media player integrations.
- \[VideoCore\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore.md): Interact with the built-in players (Denshi, Online Streaming) in Seanime.
- \[MPV\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv.md): Interact with the user's MPV instance.
- \[Continuity\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity.md): Interact with Seanime's watch history system that powers playback resuming.
- \[Scanner\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner.md)
- \[Auto Downloader\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader.md)
- \[Auto Scanner\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner.md)
- \[Filler Manager\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager.md)
- \[External Player Link\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link.md)
- \[Torrentstream\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream.md)
- \[Debridstream\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream.md)
- \[Torrent Search\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search.md)
- \[Auto Select\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select.md)
- \[Downloading\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading.md)
- \[Downloader\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader.md): OS-agnostic API for downloading files asynchronously.
- \[Torrent Client\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client.md)
- \[Debrid\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid.md)
- \[Other\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other.md)
- \[Auth\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth.md)
- \[App Settings\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/app-settings.md)
- \[Extensions\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions.md)
- \[Manga\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga.md)
- \[Discord\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord.md)
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers.md).
# Helpers
## $app
{% hint style="info" %}
Only available in the UI context.
{% endhint %}
\`\`\`typescript
$ui.register((ctx) => {
$app.getVersion() // "3.7.0"
$app.getVersionName() // "Gold"
// Invalidate certain queries to cause the client to refetch them automatically
// Find the query keys here: https://github.com/5rahim/seanime/blob/main/internal/events/endpoints.go
$app.invalidateClientQuery(\[\])
$app.getClientIds() // Returns the client IDs
$app.getClientPlatform("id") // Returns the platform, "web", "denshi"
})
\`\`\`
## Client Helpers
### getClientIds
\`$app.getClientIds()\`
Returns the IDs of the UI clients currently connected to Seanime.
This is useful for APIs that accept a \`clientId\`, such as torrent/debrid streaming when you want to target a specific client.
\*\*Example:\*\*
\`\`\`typescript
const clientIds = $app.getClientIds()
for (const clientId of clientIds) {
console.log(clientId, $app.getClientPlatform(clientId))
}
\`\`\`
### getClientPlatform
\`$app.getClientPlatform(clientId)\`
Returns the platform for a connected client.
Typical values are \`"web"\` and \`"denshi"\`. If the client is unknown, Seanime returns an empty string.
\*\*Parameters:\*\*
\* \`clientId\`: String - A client ID returned by \`$app.getClientIds()\`
\*\*Example:\*\*
\`\`\`typescript
const denshiClientIds = $app
.getClientIds()
.filter((clientId) => $app.getClientPlatform(clientId) === "denshi")
console.log(denshiClientIds)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database.md).
# Database
## Permission
{% hint style="warning" %}
\`database\` permission is required.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["database"\]
}
}
}
## Invalidate queries
After some database operations you might want to cause the client to automatically refetch certain queries. This is possible using \`$app.invalidateClientQuery\` - \[Helpers\](/seanime-extensions/plugins/apis/helpers.md)
## Local files
You can interact with the scanned file entries (also called local files).
### Get all
\`\`\`typescript
const localFiles = $database.localFiles.getAll()
\`\`\`
### Edit
\`\`\`typescript
// Get all 'One Piece' files
const onePieceLocalFiles = $database
.localFiles.findBy((lf) => {
return lf.mediaId === 21
})
// Lock all 'One piece' files
for (const lf of onePieceLocalFiles) {
lf.locked = true
}
$database.localFiles.save(onePieceLocalFiles)
\`\`\`
Note that \`save\` only works for existing entries.
### Insert
\`\`\`typescript
// Inserts a new collection of local files
// This is equivalent to doing a scan
const localFiles = $database.localFiles.insert(\[\
//...\
\])
\`\`\`
## AniList
### Get Token
{% hint style="warning" %}
\`anilist-token\` permission is required
{% endhint %}
\`\`\`typescript
$database.anilist.getToken()
\`\`\`
### Get Username
\`\`\`typescript
$database.anilist.getUsername()
\`\`\`
## Auto Downloader Rules
\`\`\`typescript
// Get all rules
$database.autoDownloaderRules.getAll()
// Get rules by media ID
const rules = $database.autoDownloaderRules.getByMediaId(21)
for (const rule of rules) {
rule.enabled = false
// Update a rule
$database.autoDownloaderRules.update(rule.dbId, rule)
}
// Remove a rule
$database.autoDownloaderRules.remove(ruleDbId)
// Insert a rule
$database.autoDownloaderRules.insert({
//...
})
\`\`\`
## Auto Downloader Items
In Seanime, an item is usually added by the auto downloader when the user has chosen not to immediately download torrents. It is shown in the queue and lets the user download that torrent later.
\`\`\`typescript
// Get all items
$database.autoDownloaderItems.getAll()
// Get items by media ID
const items = $database.autoDownloaderItems.getByMediaId(21)
for (const item of items) {
// Update an item
$database.autoDownloaderItems.update(item.dbId, item)
}
// Remove an item
$database.autoDownloaderItems.remove(itemDbId)
// Insert an item
$database.autoDownloaderItems.insert({
//...
})
\`\`\`
## Silenced media entries
\`\`\`typescript
const silencedAnimeIds = $database.silencedMediaEntries.getAllIds()
// Silence an anime
$database.silencedMediaEntries.setSilenced(21, true)
$database.silencedMediaEntries.isSilenced(21) // true
\`\`\`
## Media fillers
\`\`\`typescript
const fillerData = $database.mediaFillers.getAll()
$database.mediaFillers.get(21)
$database.mediaFillers.insert("provider", 21, "slug", \["600"\])
$database.mediaFillers.remove(21)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os.md).
# OS
Go reference: {
//...
"plugin": {
"permissions": {
"scopes": \["system"\]
}
}
}
### Allow lists
By default, all commands you may try to execute and all directories and files you may try to read to write to will be restricted. You need to explicitly declare which command and the arguments you want to execute and which directories/files you want to read or write to.
{
//...
"plugin": {
"permissions": \["system", ...\],
"systemAllowList": {
"allowReadPaths": \["$TEMP/\*"\],
"allowWritePaths": \["$TEMP/\*"\],
"commandScopes": \[\]
}
}
}
### Paths
\* \`/path/to/dir/\` - Matches only the specific directory
\* \`/path/to/dir/\*\` - Matches all files in the directory, but not subdirectories
\* \`/path/to/dir/\*\*\` - Matches all files and directories recursively
\* /\`path/to/dir/\*\*/\*\` - Same as above, matches all files and directories recursively
Here are pre-defined directory variables
\* $TEMP - The temp directory
\* $CACHE - The cache directory (LocalAppData on Windows)
\* $HOME - The home directory (%USERPROFILE% on Windows)
\* $CONFIG - The user config directory (AppData on Windows)
\* $DOWNLOAD - The download directory
\* $DOCUMENT - The document directory
\* $DESKTOP - The desktop directory
\* $SEANIME\\\_ANIME\\\_LIBRARY - Any of the user's anime library paths
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external.md).
# Playback (External)
This API allows you to control the interface between Seanime and desktop media players (MPV, IINA, VLC, MPC-HC).
## Permissions
{% hint style="warning" %}
\`playback\` permission is required
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["playback"\]
}
}
}
## Core methods
### playUsingMediaPlayer
\*\*\`playUsingMediaPlayer(filePath)\`\*\*
Plays a local file using the configured media player, with automatic tracking.
\*\*Parameters:\*\*
\* \`filePath\`: String - Path to a scanned local video file
\*\*Note:\*\* This only works with files properly scanned by Seanime. Using it with unscanned files will result in tracking errors.
\*\*Example:\*\*
\`\`\`typescript
// Play a local file with tracking
try {
await ctx.playback.playUsingMediaPlayer("/anime/One Piece/One Piece - 1015.mkv")
console.log("Playback started successfully")
} catch (error) {
console.error("Playback error:", error)
}
\`\`\`
### streamUsingMediaPlayer
\`streamUsingMediaPlayer(windowTitle, streamUrl, anime, aniDbEpisode)\`
Streams a video from a URL using the configured media player, with automatic tracking.
\*\*Parameters:\*\*
\* \`windowTitle\`: String - Title for the player window
\* \`streamUrl\`: String - URL of the video stream
\* \`anime\`: AL\\\_BaseAnime - AniList anime object
\* \`aniDbEpisode\`: String - AniDB episode number
\*\*Example:\*\*
\`\`\`typescript
// Stream an episode with proper tracking
const anime = $anilist.getAnime(21) // One Piece
try {
await ctx.playback.streamUsingMediaPlayer(
"One Piece - Episode 1015",
"https://example.com/streams/one-piece-1015.mkv",
anime,
"1015"
)
console.log("Stream started successfully")
} catch (error) {
console.error("Stream error:", error)
}
\`\`\`
### registerEventListener
\`registerEventListener(callback)\`
Registers a listener for playback events.
\*\*Parameters:\*\*
\* \`callback\`: Function(event: PlaybackEvent) - Function called when an event occurs
\*\*Example:\*\*
\`\`\`typescript
// Listen for playback events
// Callback triggered every 1-3 seconds
const unsubscribe = ctx.playback.registerEventListener((event) => {
//
// Local file playback
//
if (event.isVideoStarted || event.isVideoCompleted || event.isIsVideoStopped) {
// Video started
if (event.isVideoStated) {
console.log(event.startedEvent?.filename)
return
}
// Video completed
if (event.isVideoCompleted) {
console.log(event.completedEvent?.filename)
return
}
// Video stopped
if (event.isIsVideoStopped) {
console.log(event.stoppedEvent?.reason)
return
}
// The playback state
if (event.state) {
console.log("Media title", event.state.mediaTitle)
console.log("Episode number", event.state.episodeNumber)
console.log("Completion percentage", event.state.completionPercentage)
}
if(event.status) {
console.log("Is Playing", event.status.playing)
console.log("Current time", event.status.currentTimeInSeconds)
console.log("Duration", event.status.durationInSeconds)
}
}
//
// Stream playback
//
if (event.isStreamStarted || event.isStreamCompleted || event.isStreamStopped) {
// Stream started
if (event.isStreamStarted) {
console.log(event.startedEvent?.filename)
return
}
// Stream completed
if (event.isStreamCompleted) {
console.log(event.completedEvent?.filename)
return
}
// Stream stopped
if (event.isStreamStopped) {
console.log(event.stoppedEvent?.reason)
return
}
// The stream playback state
if (event.state) {
console.log("Media title", event.state.mediaTitle)
console.log("Episode number", event.state.episodeNumber)
console.log("Completion percentage", event.state.completionPercentage)
}
if(event.status) {
console.log("Is Playing", event.status.playing)
console.log("Current time", event.status.currentTimeInSeconds)
console.log("Duration", event.status.durationInSeconds)
}
}
})
// Later, to stop listening
unsubscribe()
\`\`\`
### pause
Pauses the current playback.
\*\*Example:\*\*
\`\`\`typescript
try {
ctx.playback.pause()
console.log("Playback paused")
} catch (error) {
console.error("Could not pause:", error)
}
\`\`\`
### resume
Resumes the paused playback.
\*\*Example:\*\*
\`\`\`typescript
// Resume after pausing
try {
ctx.playback.resume()
console.log("Playback resumed")
} catch (error) {
console.error("Could not resume:", error)
}
\`\`\`
### seekTo
Seeks to a specific position in the current playback.
\*\*Parameters:\*\*
\* \`seconds\`: Number - The position to seek to in seconds
\*\*Example:\*\*
\`\`\`typescript
// Skip ahead 30 seconds
try {
ctx.playback.seekTo(currentTimeInSeconds + 30)
console.log("Skipped forward 30 seconds")
} catch (error) {
console.error("Could not seek:", error)
}
\`\`\`
### cancel
Cancels the current playback.
\*\*Example:\*\*
\`\`\`typescript
try {
ctx.playback.cancel()
console.log("Playback canceled")
} catch (error) {
console.error("Could not cancel playback:", error)
}
\`\`\`
### startManualTracking
\`startManualTracking(opts)\`
Starts manual progress tracking for media that is not being launched through Seanime's integrated playback APIs.
If \`clientId\` is omitted, Seanime broadcasts it to all clients.
\*\*Parameters:\*\*
\* \`opts\`: \`PlaybackManualTrackingOptions\`
\* \`opts.mediaId\`: Number - AniList media ID to track
\* \`opts.episodeNumber\`: Number - Episode number to sync
\* \`opts.clientId\`: String - Optional client ID override
\*\*Example:\*\*
\`\`\`typescript
await ctx.playback.startManualTracking({
mediaId: 21,
episodeNumber: 1,
})
\`\`\`
### syncCurrentProgress
\`syncCurrentProgress()\`
Synchronizes the currently tracked progress with AniList.
This is most useful after \`startManualTracking()\` in custom player or external-player-link workflows.
\*\*Example:\*\*
\`\`\`typescript
await ctx.playback.syncCurrentProgress()
\`\`\`
### cancelManualTracking
\`cancelManualTracking()\`
Stops the current manual tracking session.
\*\*Example:\*\*
\`\`\`typescript
ctx.playback.cancelManualTracking()
\`\`\`
### getNextEpisode
Gets the next episode to play after the current one.
\*\*Example:\*\*
\`\`\`typescript
// Check if there's a next episode
try {
const nextEpisode = await ctx.playback.getNextEpisode()
if (nextEpisode) {
console.log(\`Next episode: ${nextEpisode.name}\`)
} else {
console.log("No next episode available")
}
} catch (error) {
console.error("Error getting next episode:", error)
}
\`\`\`
### playNextEpisode
Plays the next episode for the current media.
\*\*Example:\*\*
\`\`\`typescript
// Play next episode when current is almost done
ctx.playback.registerEventListener((event) => {
if (event.status && event.status.completionPercentage > 95) {
try {
ctx.playback.playNextEpisode()
} catch(e) {}
}
})
\`\`\`
## Best Practices
#### Media Tracking
The playback API is designed for tracked media files that are part of the Seanime library:
\`\`\`typescript
// Good practice: Play scanned files for proper tracking
const localFile = getScannedFile() // Get a file that's in the library
ctx.playback.playUsingMediaPlayer(localFile.path)
// Bad practice: Playing unscanned files won't track properly
ctx.playback.playUsingMediaPlayer("/random/video.mp4") // Will cause tracking errors
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/hooks.md).
# Hooks
{% hint style="danger" %}
Difficulty: Hard
\* Event-driven understanding required
{% endhint %}
## List of hooks
function init() {
$shared.define("formatters", () => {
return {
seconds(ms: number) {
return \`${Math.round(ms / 1000)}s\`
},
}
})
$app.onScanCompleted((event) => {
const formatters = $shared.use("formatters")
console.log(formatters.seconds(event.duration))
event.next()
})
$ui.register((ctx) => {
const formatters = $shared.use("formatters")
ctx.toast.info(\`Ready in ${formatters.seconds(2500)}\`)
})
}
## Methods
### define
\`$shared.define(name, factory)\`
Registers a shared factory under a unique name.
\*\*Parameters:\*\*
\* \`name\`: String - The shared module name.
\* \`factory\`: Function - A function that returns the exported value.
{% hint style="info" %}
\`$shared.define()\` should be called before registering hooks or the UI scope.
{% endhint %}
Rules:
\* Names are trimmed and must not be empty.
\* The name can only be defined once.
\* The factory must return a value.
### use
\`$shared.use(name)\`
Evaluates the shared factory inside the current runtime and returns its exported value.
\*\*Parameters:\*\*
\* \`name\`: String - The shared module name.
{% hint style="warning" %}
\`use()\` runs the shared factory in the current runtime. Treat the result as a fresh runtime-local value, not as cross-runtime shared state.
{% endhint %}
## Good to know
\* \`use()\` is available in the main runtime, hook runtimes, and the UI runtime.
\* If the module name is unknown, Seanime throws a \`TypeError\`.
\* If the factory returns \`undefined\` or \`null\`, Seanime throws a \`TypeError\`.
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity.md).
# Continuity
## Core Methods
### getWatchHistoryItem
Gets the last recorded progress for an anime.
\`\`\`typescript
const item = ctx.continuity.getWatchHistoryItem(21)
\`\`\`
### updateWatchHistoryItem
Add or update the last recorded progress for an anime.
\*\*Parameters:\*\*
\* \`opts\`: Object containing:
\* \`currentTime\`: Number - Last recorded progress in seconds
\* \`duration\` : Number - Total duration in seconds
\* \`mediaId\` : Number
\* \`episodeNumber\` : Number
\* \`filepath?\` : String
\* \`kind\` : "onlinestream" | "mediastream" | "external\\\_player"
### getWatchHistory
Gets all last recorded progress items.
\`\`\`typescript
const items = ctx.continuity.getWatchHistory()
\`\`\`
### deleteWatchHistoryItem
\*\*Parameters\*\*:
\* \`mediaId\` : Number
\`\`\`typescript
ctx.continuity.deleteWatchHistoryItem(21)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link.md).
# External Player Link
## Core Methods
### open
\`\`\`
open(url, mediaId, episodeNumber)
\`\`\`
Opens a URL in an external media player.
\*\*Parameters\*\*:
\* \`url\`: string - URL to open in the external player
\* \`mediaId\`: number - AniList media ID for tracking
\* \`episodeNumber\`: number - Episode number for tracking
Example:
\`\`\`javascript
// Open a video in an external player with tracking
ctx.externalPlayerLink.open(
"https://example.com/videos/one-piece-1015.mkv",
21, // One Piece media ID
1015 // Episode number
)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream.md).
# Debridstream
The \`ctx.debridstream\` API lets plugins stream from the configured debrid provider.
This API is only available when the plugin has both \`playback\` and \`debrid\` permissions.
## Permissions
{% hint style="warning" %}
\`playback\` and \`debrid\` permissions are required
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["playback", "debrid"\]
}
}
}
## Core Methods
### getPreviousStreamOptions
\`getPreviousStreamOptions()\`
Returns the previous debrid stream options, or \`undefined\` if no stream has been started yet.
### getStreamURL
\`getStreamURL()\`
Returns the active debrid stream URL, or \`undefined\` if no stream URL is available.
### startStream
\`startStream(options)\`
Starts a debrid stream.
If \`playbackType\` is \`"nativeplayer"\` or \`"externalPlayerLink"\`, \`clientId\` is required.
\*\*Parameters:\*\*
\* \`options.mediaId\`: Number
\* \`options.episodeNumber\`: Number - Relative episode number
\* \`options.aniDBEpisode\`: String - AniDB episode identifier
\* \`options.playbackType\`: \`"default" | "externalPlayerLink" | "nativeplayer" | "none" | "noneAndAwait"\`
\* \`options.torrent\`: \`$app.HibikeTorrent\_AnimeTorrent\` - Optional
\* \`options.fileId\`: String - Optional provider file ID
\* \`options.fileIndex\`: Number - Optional manual file index
\* \`options.userAgent\`: String - Optional
\* \`options.clientId\`: String - Optional unless required by the playback type
\* \`options.autoSelect\`: Boolean - Optional
\* \`options.batchEpisodeFiles\`: \`TorrentstreamBatchEpisodeFiles\` - Optional
\*\*Example:\*\*
\`\`\`typescript
await ctx.debridstream.startStream({
mediaId: 21,
episodeNumber: 1,
aniDBEpisode: "1",
playbackType: "default",
autoSelect: true,
})
\`\`\`
### cancelStream
\`cancelStream(options?)\`
Cancels the current debrid stream.
\*\*Parameters:\*\*
\* \`options.removeTorrent\`: Boolean - Optional. When \`true\`, Seanime also removes the torrent from the debrid service.
\*\*Example:\*\*
\`\`\`typescript
ctx.debridstream.cancelStream({
removeTorrent: true,
})
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner.md).
# Auto Scanner
The \`ctx.autoScanner\` API lets plugins inspect or trigger Seanime's automatic library scanner.
## Core Methods
### notify
Notifies the auto-scanner to check for new files.
Example:
\`\`\`javascript
// Notify the auto-scanner to check for new files
ctx.autoScanner.notify()
console.log("Auto-scanner notified to check for new files")
\`\`\`
### runNow
\`runNow()\`
Runs the auto scanner immediately.
Example:
\`\`\`typescript
ctx.autoScanner.runNow()
\`\`\`
### isEnabled
\`isEnabled()\`
Returns whether the auto scanner is enabled.
Example:
\`\`\`typescript
console.log(ctx.autoScanner.isEnabled())
\`\`\`
### isWaiting
\`isWaiting()\`
Returns whether the auto scanner is currently waiting for its debounce timer.
Example:
\`\`\`typescript
if (ctx.autoScanner.isWaiting()) {
console.log("Auto scanner is waiting before the next run")
}
\`\`\`
### isScanning
\`isScanning()\`
Returns whether the auto scanner is actively scanning.
Example:
\`\`\`typescript
console.log(ctx.autoScanner.isScanning())
\`\`\`
### getWaitTimeMs
\`getWaitTimeMs()\`
Returns the debounce wait time in milliseconds.
Example:
\`\`\`typescript
const waitTimeMs = ctx.autoScanner.getWaitTimeMs()
console.log(\`Auto scanner wait time: ${waitTimeMs}ms\`)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview.md).
# Webview
Webview Plugins can be used to create all kinds of interfaces using HTML and JS.
## Create a Webview
{% hint style="info" %}
You can only create \*\*one\*\* webview per \*\*slot\*\*.
{% endhint %}
\`\`\`typescript
// Create a panel that appears below the home screen toolbar
const panel = ctx.newWebview({
slot: "after-home-screen-toolbar",
fullWidth: true,
autoHeight: true,
})
\`\`\`
### Options
\`\`\`typescript
interface WebviewOptions {
slot: "screen" |
"fixed" |
"after-home-screen-toolbar" |
"home-screen-bottom" |
"schedule-screen-top" |
"schedule-screen-bottom" |
"anime-screen-bottom" |
"after-anime-entry-episode-list" |
"after-anime-episode-list" |
"before-anime-entry-episode-list" |
"manga-screen-bottom" |
"manga-entry-screen-bottom" |
"after-manga-entry-chapter-list" |
"after-discover-screen-header" |
"after-media-entry-details" |
"after-media-entry-form"
// Iframe options
className?: string
style?: string
width?: string
height?: string
maxWidth?: string
maxHeight?: string
zIndex?: number
// Iframe height is automatically adjusted to fit the webview content
autoHeight?: boolean
// Iframe width takes the entire available width
fullWidth?: boolean
hidden?: boolean
// Applies when slot = "screen"
sidebar?: {
label: string,
icon: string,
}
// Applies when slot = "fixed"
window?: {
draggable?: boolean
defaultX?: number
defaultY?: number
defaultPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right"
frameless?: boolean
}
}
\`\`\`
### Screen Slot
A Webview created with the slot \`screen\` will be rendered in its own page.
// Create a screen webview with a sidebar button
const webview = ctx.newWebview({
slot: "screen",
fullWidth: true,
autoHeight: true,
sidebar: {
label: "Notepad",
icon: \`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>\`,
},
})
### Fixed Slot
// Renders iframe with transparent background
webview.setContent(() => \`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
html {
color-scheme: dark;
overflow: hidden;
}
body {
background: transparent;
font-family: -apple-system, system-ui, sans-serif;
margin: 0;
}
</style>
</head>
<body>
<script>
</script>
</body>
</html>
\`)
### Messages
$ui.register((ctx) => {
const notes = ctx.state<Array<{ id: string, text: string, checked: boolean }>>(\[\
{ id: "2", text: "Check discussion for latest episode", checked: true },\
{ id: "1", text: "Watch the next season", checked: false },\
\])
// Sync notes state with webview
panel.channel.sync("notes", notes)
panel.channel.on("toggle-note", (id) => {
notes.set(prev => prev.map(n => n.id === id ? { ...n, checked: !n.checked } : n))
})
panel.setContent(() => \`
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<script>
window.webview.on("notes", (newNotes) => console.log(newNotes))
function toggleNote(id) {
window.webview.send("toggle-note", id)
}
</script>
</body>
</html>
\`)
})
### Example
This example uses Preact, a lightweight React alternative suitable for webviews.
\`\`\`typescript
function init() {
$ui.register((ctx) => {
// In a real plugin, you'd likely load this from whatever resource
const currentMedia = ctx.state({
id: 101,
title: "Frieren: Beyond Journey's End",
cover: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx154587-qQTzQnEJJ3oB.jpg"
})
const notes = ctx.state{
//...
"plugin": {
"permissions": {
"scopes": \["storage"\]
}
}
}
## Usage
### API
Unlike \`store\` , \`storage\` handles nested values out of the box.
\`\`\`typescript
$storage.set("foo.bar", 1)
$storage.set("foo.baz", "2")
$storage.has("foo") // true
$storage.get{
//...
"plugin": {
"permissions": {
"scopes": \["anilist"\]
}
}
}
## Refresh collections
This is needed if you edit the user's collection.
\`\`\`typescript
$anilist.refreshAnimeCollection()
$anilist.refreshMangaCollection()
\`\`\`
## Empty cache
Clears the cache for fetched anime/manga entries.
\`\`\`typescript
$anilist.clearCache()
\`\`\`
## Update entry
\`\`\`typescript
$anilist.updateEntry(
mediaId: number,
status: $app.AL\_MediaListStatus | undefined,
scoreRaw: number | undefined,
progress: number | undefined,
startedAt: $app.AL\_FuzzyDateInput | undefined,
completedAt: $app.AL\_FuzzyDateInput | undefined,
): void
\`\`\`
## Update entry progress
\`\`\`typescript
$anilist.updateEntryProgress(
mediaId: number,
progress: number,
status: $app.AL\_MediaListStatus | undefined,
): void
\`\`\`
## Update entry repeat
\`\`\`typescript
$anilist.updateEntryRepeat(mediaId: number, repeat: number): void
\`\`\`
## Delete entry
\`\`\`typescript
$anilist.deleteEntry(mediaListEntryId: number): void
\`\`\`
## Add media to collection
\`\`\`typescript
/\*\*
\* Add media to collection.
\*
\* This will add the media to the collection with the status "PLANNING".
\*
\* The anime/manga collection should be refreshed after adding the media.
\*/
$anilist.addMediaToCollection(mediaIds: number\[\]): void
\`\`\`
## Get collections
\`\`\`typescript
/\*\*
\* Get the user's anime collection.
\* This collection does not include lists with no status.
\*/
$anilist.getAnimeCollection(bypassCache: boolean): $app.AL\_AnimeCollection
/\*\*
\* Get the raw anime collection data.
\* This collection includes lists with no status.
\*/
$anilist.getRawAnimeCollection(bypassCache: boolean): $app.AL\_AnimeCollection
/\*\*
\* Get the user's manga collection.
\* This collection does not include lists with no status.
\*/
$anilist.getMangaCollection(bypassCache: boolean): $app.AL\_MangaCollection
/\*\*
\* Get the raw manga collection data.
\* This collection includes lists with no status.
\*/
$anilist.getRawMangaCollection(bypassCache: boolean): $app.AL\_MangaCollection
/\*\*
\* Get anime collection with relations
\*/
$anilist.getAnimeCollectionWithRelations(): $app.AL\_AnimeCollectionWithRelations
\`\`\`
## Get anime/manga data
\`\`\`typescript
/\*\*
\* Get anime by ID
\*/
$anilist.getAnime(id: number): $app.AL\_BaseAnime
/\*\*
\* Get manga by ID
\*/
$anilist.getManga(id: number): $app.AL\_BaseManga
/\*\*
\* Get detailed anime info by ID
\*/
$anilist.getAnimeDetails(id: number): $app.AL\_AnimeDetailsById\_Media
/\*\*
\* Get detailed manga info by ID
\*/
$anilist.getMangaDetails(id: number): $app.AL\_MangaDetailsById\_Media
/\*\*
\* Get studio details
\*/
$anilist.getStudioDetails(studioId: number): $app.AL\_StudioDetails
\`\`\`
## Search / List
\`\`\`typescript
/\*\*
\* List anime based on search criteria
\*/
$anilist.listAnime(
page: number | undefined,
search: string | undefined,
perPage: number | undefined,
sort: $app.AL\_MediaSort\[\] | undefined,
status: $app.AL\_MediaStatus\[\] | undefined,
genres: string\[\] | undefined,
averageScoreGreater: number | undefined,
season: $app.AL\_MediaSeason | undefined,
seasonYear: number | undefined,
format: $app.AL\_MediaFormat | undefined,
isAdult: boolean | undefined,
): $app.AL\_ListAnime
/\*\*
\* List manga based on search criteria
\*/
$anilist.listManga(
page: number | undefined,
search: string | undefined,
perPage: number | undefined,
sort: $app.AL\_MediaSort\[\] | undefined,
status: $app.AL\_MediaStatus\[\] | undefined,
genres: string\[\] | undefined,
averageScoreGreater: number | undefined,
startDateGreater: string | undefined,
startDateLesser: string | undefined,
format: $app.AL\_MediaFormat | undefined,
countryOfOrigin: string | undefined,
isAdult: boolean | undefined,
): $app.AL\_ListManga
/\*\*
\* List recent anime
\*/
$anilist.listRecentAnime(
page: number | undefined,
perPage: number | undefined,
airingAtGreater: number | undefined,
airingAtLesser: number | undefined,
notYetAired: boolean | undefined,
): $app.AL\_ListRecentAnime
\`\`\`
## Custom GraphQL query
\`\`\`typescript
$anilist.customQuery{
// ...
"plugin": {
"permissions": {
"allow": {
"readPaths": \["$DOWNLOAD"\],
"writePaths": \["$DOWNLOAD"\],
"commandScopes": \[\
{\
"command": "ls",\
"args": \[{ "value": "-la" }, { "validator": "$PATH" }\]\
},\
{\
"command": "grep",\
"args": \[{ "value": "Hello" }, { "validator": "$PATH" }\]\
},\
{\
"command": "sort",\
"args": \[\]\
},\
{\
"command": "echo",\
"args": \[{ "validator": "$ARGS" }\]\
},\
{\
"command": "open",\
"args": \[{ "validator": "^https?://.\*$" }\]\
}\
\]
}
}
}
}
This example shows:
\* The \`ls\` command can be executed with the \`-la\` argument followed by a valid file/directory path \`$PATH\`. This path must be in the allow list for \`write\` . \`$PATH\` is an alternative to writing the regex.
\* The \`grep\` command is allowed with the "Hello" argument and a similar path validation.
\* The \`sort\` command is permitted without any additional arguments.
\* The \`echo\` command is allowed with any argument or list of arguments.
\* The \`open\` command is allowed with any valid URLs
## Command (sync)
The code below shows how to run a command with the caveat that this approach will block the plugin's UI context thread until the command finishes running.
\`\`\`typescript
const tempDir = $os.tempDir();
try {
// Create a command to list files
const cmd = $os.cmd("ls", "-la", tempDir);
// Set up stdout capture
const stdoutPipe = cmd.stdoutPipe();
// Start the command
cmd.start();
// Read the output
const output = $io.readAll(stdoutPipe);
console.log($toString(output));
// Wait for the command to complete
cmd.wait();
// Check exit code
const exitCode = cmd.processState.exitCode();
console.log("Command exit code:", exitCode); // Command exit code: 0
} catch (e) {
console.log("Command execution error:", e.message);
}
\`\`\`
## Command (async)
If you need to run a command without blocking the plugin's UI context thread, you should use \`$osExtra.asyncCmd\` .
{% hint style="info" %}
Do not use both sync and async commands in the same plugin as this can cause some data race issues.
{% endhint %}
\`\`\`typescript
const tempDir = $os.tempDir();
try {
// Create a command to list files
const cmd = $osExtra.asyncCmd("ls", "-la", tempDir);
// The callback function will fire for each new line of the stdout, stderr
// and when the command finishes executing.
cmd.run((data, err, exitCode, signal) => {
// Stdout
if (data) {
console.log("Data:", $toString(data));
}
// Stderr
if (err) {
console.log("Error:", $toString(err));
}
// Command exited
if (exitCode !== undefined) {
console.log("Exited:", exitCode, signal);
}
});
console.log("Doesn't wait for the command to finish!")
// You still have access to the underlying command
const \_cmd = cmd.getCommand()
} catch (e) {
console.log("Command execution error:", e.message);
}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore.md).
# VideoCore
VideoCore is the built-in video player used by the Denshi desktop app and the online streaming web player.
## Permissions
{% hint style="warning" %}
\`playback\` permission is required
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["playback"\]
}
}
}
## Code methods
### addEventListener
\`addEventListener(eventId, callback)\`
Registers a listener for playback events.
Parameters:
\* \`eventId\`: String - The identifier of the event to listen for (e.g., \`"video-paused"\`, \`"video-seeked"\`, \`"video-loaded"\`).
\* \`callback\`: Function - The function to execute when the event triggers. Receives the event object.
\*Example:\*
\`\`\`typescript
ctx.videoCore.addEventListener("video-loaded", (event) => {
console.log("Playback info received by video player", event)
})
ctx.videoCore.addEventListener("video-loaded-metadata", (event) => {
console.log("Metadata loaded, initializing modules", event)
})
ctx.videoCore.addEventListener("video-can-play", (event) => {
console.log("First frame is ready", event)
})
\`\`\`
### removeEventListener
\`removeEventListener(eventId)\`
Removes a previously registered event listener.
Parameters:
\* \`eventId\`: String - The identifier of the event to remove.
\*Example:\*
\`\`\`typescript
ctx.videoCore.removeEventListener("video-paused")
\`\`\`
### playEpisodeFromPlaylist
\`playEpisodeFromPlaylist(which)\`
Instructs the media player to play a specific episode from the current playlist.
The playlist can be a custom playlist or the list of episodes for the current media being played.
Parameters:
\* \`which\`: "previous" | "next" | string (The AniDB Episode ID)
\*Example:\*
\`\`\`typescript
ctx.videoCore.playEpisodeFromPlaylist("next")
\`\`\`
### playStream
\`playStream(streamUrl, anidbEpisode, media)\`
Starts playback of a URL in the built-in Denshi player with progress tracking.
The promise resolves once the stream has been initiated, not when playback completes.
This method automatically targets the connected Denshi client.
Parameters:
\* \`streamUrl\`: String - The URL to stream
\* \`anidbEpisode\`: String - AniDB episode identifier used for progress tracking
\* \`media\`: \`AL\_BaseAnime\` - AniList anime object used for progress tracking
\*Example:\*
\`\`\`typescript
const media = $anilist.getAnime(21)
await ctx.videoCore.playStream(
"https://example.com/streams/one-piece-1.m3u8",
"1",
media,
)
\`\`\`
### playLocalFile
\`playLocalFile(path)\`
Starts playback of a scanned local file in the built-in Denshi player with progress tracking.
The promise resolves once playback has been initiated, not when playback completes.
The file must already exist in Seanime's scanned library.
Parameters:
\* \`path\`: String - Absolute path to the local file
\*Example:\*
\`\`\`typescript
await ctx.videoCore.playLocalFile("/anime/One Piece/One Piece - 1015.mkv")
\`\`\`
### pause
\`pause()\`
Pauses the current playback.
\*Example:\*
\`\`\`typescript
ctx.videoCore.pause()
\`\`\`
### resume
\`resume()\`
Resumes playback if it is currently paused.
\*Example:\*
\`\`\`typescript
ctx.videoCore.resume()
\`\`\`
### seek
\`seek(seconds)\`
Seeks the video by a relative amount of seconds.
Parameters:
\* \`seconds\`: Number - The number of seconds to seek forward (positive) or backward (negative).
\*Example:\*
\`\`\`typescript
// Skip forward 10 seconds
ctx.videoCore.seek(10)
\`\`\`
### seekTo
\`seekTo(seconds)\`
Seeks to a specific timestamp in the video.
Parameters:
\* \`seconds\`: Number - The absolute timestamp to seek to.
\*Example:\*
\`\`\`typescript
// Go to the 1-minute mark
ctx.videoCore.seekTo(60)
\`\`\`
### terminate
\`terminate()\`
Stops playback and terminates the media player instance.
\*Example:\*
\`\`\`typescript
ctx.videoCore.terminate()
\`\`\`
### setFullscreen
\`setFullscreen(fullscreen)\`
Toggles or sets the fullscreen state of the player.
Parameters:
\* \`fullscreen\`: Boolean - \`true\` to enter fullscreen, \`false\` to exit.
\*Example:\*
\`\`\`typescript
ctx.videoCore.setFullscreen(true)
\`\`\`
### setPip
\`setPip(enabled)\`
Toggles or sets the Picture-in-Picture (PiP) state of the player.
Parameters:
\* \`enabled\`: Boolean - \`true\` to enable PiP, \`false\` to disable.
\*Example:\*
\`\`\`typescript
ctx.videoCore.setPip(true)
\`\`\`
### showMessage
\`showMessage(message, duration)\`
Displays a temporary OSD message on the media player.
Parameters:
\* \`message\`: String - The text to display.
\* \`duration\`: Number - Duration in milliseconds, default is 2000.
\*Example:\*
\`\`\`typescript
ctx.videoCore.showMessage("Hello World", 2000)
\`\`\`
### setSkipData
Overrides skip data.
\`setSkipData(skipData)\`
Parameters:
\* \`skipData\` : \`$videocore.SkipData\` - The data
\*Example:\*
\`\`\`typescript
ctx.videoCore.setSkipData({ op: { interval: { startTime: 0, endTime: 1500 } }, ed: null })
\`\`\`
### clearSkipData
\`clearSkipData()\`
Removes skip data.
### getTextTracks
\`getTextTracks()\`
Asynchronously retrieves the list of available subtitle/caption tracks.
\*Example:\*
\`\`\`typescript
const tracks = ctx.videoCore.getTextTracks()
for(const track of tracks) {
console.log(track.type) // "subtitles" or "captions"
console.log(track.index)
}
\`\`\`
### setSubtitleTrack
\`setSubtitleTrack(trackNumber)\`
Selects a specific subtitle track.
Parameters:
\* \`trackNumber\`: Number - The ID/index of the subtitle track to select.
\*Example:\*
\`\`\`typescript
ctx.videoCore.setSubtitleTrack(1)
\`\`\`
### setMediaCaptionTrack
\`setMediaCaptionTrack(trackIndex)\`
Selects a specific media caption track.
Parameters:
\* \`trackIndex\`: Number - The index of the caption track to select.
\*Example:\*
\`\`\`typescript
ctx.videoCore.setMediaCaptionTrack(0)
\`\`\`
### addExternalSubtitleTrack
\`addExternalSubtitleTrack(track)\`
Adds an external subtitle file as a track and selects it.
If "Convert Soft Subs to ASS" is enabled, the track will be converted to ASS, else it will be converted to WebVTT.
Parameters:
\* \`track\`: Object - A \`VideoSubtitleTrack\` object
\*Example:\*
\`\`\`typescript
ctx.videoCore.addExternalSubtitleTrack({
url: "https://example.com/subs.ass",
label: "English (ASS)",
language: "en",
type: "ass".
})
ctx.videoCore.addExternalSubtitleTrack({
label: "English (VTT)",
language: "eng",
content: "WEBVTT\\n\\n00:00:00.000 --> 00:00:50.000\\nHello World",
type: "vtt",
})
\`\`\`
### setAudioTrack
\`setAudioTrack(trackNumber)\`
Selects a specific audio track.
Parameters:
\* \`trackNumber\`: Number - The ID/index of the audio track to select.
\*Example:\*
\`\`\`typescript
ctx.videoCore.setAudioTrack(2)
\`\`\`
### getPlaybackStatus
\`getPlaybackStatus()\`
Synchronously retrieves the current status of the playback
\*Example:\*
\`\`\`typescript
const status = ctx.videoCore.getPlaybackStatus()
if (status.paused) {
// ...
}
\`\`\`
### getPlaybackState
\`getPlaybackState()\`
Synchronously retrieves the comprehensive state object of the player.
\*Example:\*
\`\`\`typescript
const state = ctx.videoCore.getPlaybackState()
console.log(state.clientId, state.playerType, state.playbackInfo)
\`\`\`
### getCurrentMedia
\`getCurrentMedia()\`
Synchronously retrieves information about the media currently being played.
\*Example:\*
\`\`\`typescript
const media = ctx.videoCore.getCurrentMedia()
console.log(media.id)
\`\`\`
### getPlaylist
\`getPlaylist()\`
Asynchronously retrieves the current playlist.
\*Example:\*
\`\`\`typescript
const playlist = await ctx.videoCore.getPlaylist()
console.log(playlist.nextEpisode)
\`\`\`
### pullStatus
\`pullStatus()\`
Asynchronously forces a status update from the player and returns the result.
\*Example:\*
\`\`\`typescript
const status = await ctx.videoCore.pullStatus()
\`\`\`
### getCurrentPlaybackInfo
\`getCurrentPlaybackInfo()\`
Synchronously retrieves the current playback information
\*Example:\*
\`\`\`typescript
const info = ctx.videoCore.getCurrentPlaybackInfo()
\`\`\`
### getCurrentClientId
\`getCurrentClientId()\`
Synchronously retrieves the unique identifier of the connected client/player.
\*Returns:\* \`String\`
\*Example:\*
\`\`\`typescript
const clientId = ctx.videoCore.getCurrentClientId()
\`\`\`
### getCurrentPlayerType
\`getCurrentPlayerType()\`
Synchronously retrieves the type of player currently active (e.g., "native", "web").
\*Returns:\* \`String\`
\*Example:\*
\`\`\`typescript
const type = ctx.videoCore.getCurrentPlayerType()
\`\`\`
### getCurrentPlaybackType
\`getCurrentPlaybackType()\`
Synchronously retrieves the type of playback being performed (e.g., "torrent", "debrid", "file", "onlinestream").
\*Returns:\* \`String\`
\*Example:\*
\`\`\`typescript
const type = ctx.videoCore.getCurrentPlaybackType()
\`\`\`
### getSkipData
\`getSkipData()\`
Asynchronously retrieve existing skip data (from AniSkip) if it exists.
\*Returns:\* \`$videocore.SkipData\`
\*Example:\*
\`\`\`typescript
const skipData = ctx.videoCore.getSkipData()
\`\`\`
### State Request Methods
The following methods are used to request specific state updates from the media player. These functions trigger an event listener response with the requested data.
#### sendGetFullscreen
\`sendGetFullscreen()\` Requests the current fullscreen state.
#### sendGetPip
\`sendGetPip()\` Requests the current Picture-in-Picture state.
#### sendGetAnime4K
\`sendGetAnime4K()\` Requests the current Anime4K configuration/state.
#### sendGetSubtitleTrack
\`sendGetSubtitleTrack()\` Requests the currently selected subtitle track.
#### sendGetAudioTrack
\`sendGetAudioTrack()\` Requests the currently selected audio track.
#### sendGetMediaCaptionTrack
\`sendGetMediaCaptionTrack()\` Requests the currently selected media caption track.
#### sendGetPlaybackState
\`sendGetPlaybackState()\` Requests the full playback state.
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath.md).
# Filepath
## $filepath
\`$filepath\` implements utility routines for manipulating filename paths in a way compatible with the target operating system-defined file paths.
Go reference: {
//...
"plugin": {
"permissions": {
"scopes": \["playback"\]
}
}
}
## Core Methods
### isEnabled
\`isEnabled()\`
Returns whether torrentstream is enabled in Seanime.
### getPreviousStreamOptions
\`getPreviousStreamOptions()\`
Returns the previous torrentstream start options, or \`undefined\` if no stream has been started yet.
### getBatchHistory
\`getBatchHistory(mediaId)\`
Returns saved batch history for a media entry, or \`undefined\` if none exists.
\*\*Parameters:\*\*
\* \`mediaId\`: Number - AniList media ID
### startStream
\`startStream(options)\`
Starts a torrent stream.
If \`playbackType\` is \`"nativeplayer"\` or \`"externalPlayerLink"\`, \`clientId\` is required.
\*\*Parameters:\*\*
\* \`options.mediaId\`: Number
\* \`options.episodeNumber\`: Number - Relative episode number
\* \`options.aniDbEpisode\`: String - AniDB episode identifier
\* \`options.playbackType\`: \`"default" | "externalPlayerLink" | "nativeplayer" | "none" | "noneAndAwait"\`
\* \`options.autoSelect\`: Boolean - Optional
\* \`options.torrent\`: \`$app.HibikeTorrent\_AnimeTorrent\` - Optional manual selection
\* \`options.fileIndex\`: Number - Optional manual file index
\* \`options.userAgent\`: String - Optional
\* \`options.clientId\`: String - Optional unless required by the playback type
\* \`options.batchEpisodeFiles\`: \`TorrentstreamBatchEpisodeFiles\` - Optional
\*\*Example:\*\*
\`\`\`typescript
await ctx.torrentstream.startStream({
mediaId: 21,
episodeNumber: 1,
aniDbEpisode: "1",
playbackType: "default",
autoSelect: true,
})
\`\`\`
### stopStream
\`stopStream()\`
Stops the active torrent stream.
\*\*Example:\*\*
\`\`\`typescript
await ctx.torrentstream.stopStream()
\`\`\`
### cancelPreparedStream
\`cancelPreparedStream()\`
Cancels a prepared torrent stream, if one exists.
\*\*Example:\*\*
\`\`\`typescript
ctx.torrentstream.cancelPreparedStream()
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select.md).
# Auto Select
The \`ctx.autoSelect\` API manages the saved auto-select profile Seanime uses for torrent and debrid workflows.
There is a single saved profile.
## Profile Shape
\`\`\`typescript
type AutoSelectProfile = {
providers?: string\[\]
releaseGroups?: string\[\]
resolutions?: string\[\]
excludeTerms?: string\[\]
preferredLanguages?: string\[\]
preferredCodecs?: string\[\]
preferredSources?: string\[\]
multipleAudioPreference?: "neutral" | "prefer" | "avoid" | "only" | "never"
multipleSubsPreference?: "neutral" | "prefer" | "avoid" | "only" | "never"
batchPreference?: "neutral" | "prefer" | "avoid" | "only" | "never"
bestReleasePreference?: "neutral" | "prefer" | "avoid" | "only" | "never"
requireLanguage?: boolean
requireCodec?: boolean
requireSource?: boolean
minSeeders?: number
minSize?: string
maxSize?: string
}
\`\`\`
## Core Methods
### getProfile
\`getProfile()\`
Returns the saved auto-select profile, or \`undefined\` if no profile has been saved yet.
Example:
\`\`\`typescript
const profile = ctx.autoSelect.getProfile()
console.log(profile)
\`\`\`
### saveProfile
\`saveProfile(profile)\`
Saves the auto-select profile and returns the stored value.
\*\*Parameters:\*\*
\* \`profile\`: \`AutoSelectProfile\`
Example:
\`\`\`typescript
const savedProfile = ctx.autoSelect.saveProfile({
providers: \["nyaa"\],
resolutions: \["1080p"\],
preferredLanguages: \["japanese"\],
batchPreference: "avoid",
bestReleasePreference: "prefer",
minSeeders: 5,
})
console.log(savedProfile)
\`\`\`
### deleteProfile
\`deleteProfile()\`
Deletes the saved auto-select profile.
Example:
\`\`\`typescript
ctx.autoSelect.deleteProfile()
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search.md).
# Torrent Search
The \`ctx.torrentSearch\` API provides access to the configured anime torrent provider extensions.
## Core Methods
### getProviderIds
\`getProviderIds()\`
Returns all available anime torrent provider IDs.
Example:
\`\`\`typescript
const providerIds = ctx.torrentSearch.getProviderIds()
console.log(providerIds)
\`\`\`
### getDefaultProviderId
\`getDefaultProviderId()\`
Returns the default torrent provider ID, or \`undefined\` if none is configured.
Example:
\`\`\`typescript
const providerId = ctx.torrentSearch.getDefaultProviderId()
console.log(providerId)
\`\`\`
### searchAnime
\`searchAnime(options)\`
Searches anime torrents using the configured provider extensions.
The current implementation requires \`Media\` and \`Type\`.
\*\*Parameters:\*\*
\* \`options\`: \`$app.Torrent\_AnimeSearchOptions\`
\* \`options.Provider\`: String - Provider ID
\* \`options.Type\`: \`"smart" | "simple"\`
\* \`options.Media\`: \`$app.AL\_BaseAnime\`
\* \`options.Query\`: String
\* \`options.Batch\`: Boolean
\* \`options.EpisodeNumber\`: Number
\* \`options.BestReleases\`: Boolean
\* \`options.Resolution\`: String
\* \`options.IncludeSpecialProviders\`: Boolean
\* \`options.SkipPreviews\`: Boolean
\*\*Returns:\*\* \`Promise<$app.Torrent\_SearchData>\`
\*\*Example:\*\*
\`\`\`typescript
const providerId = ctx.torrentSearch.getDefaultProviderId()
const media = $anilist.getAnime(21)
if (providerId) {
const results = await ctx.torrentSearch.searchAnime({
Provider: providerId,
Type: "smart",
Media: media,
Query: media.title?.userPreferred ?? "",
Batch: false,
EpisodeNumber: 1,
BestReleases: false,
Resolution: "",
IncludeSpecialProviders: false,
SkipPreviews: false,
})
console.log(results.torrents)
}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system.md).
# System
{% hint style="warning" %}
As of Seanime \`v3.8.0\`, when the user enables Extension Secure Mode, sensitive system actions may trigger approval prompts.
If the user rejects the prompt, the action throws. If prompts cannot be displayed, such as during app startup before a UI client is connected, the action also fails immediately and can break plugin startup.
{% endhint %}
{% content-ref url="/pages/iY4xp8JhVNA2YKHpDoXw" %}
\[Permissions\](/seanime-extensions/plugins/apis/system/permissions.md)
{% endcontent-ref %}
{% content-ref url="/pages/KcrYQcNEdAA9WXUUZW5Y" %}
\[OS\](/seanime-extensions/plugins/apis/system/os.md)
{% endcontent-ref %}
{% content-ref url="/pages/26H6qWSVWf8xgaVyx2bH" %}
\[Filepath\](/seanime-extensions/plugins/apis/system/filepath.md)
{% endcontent-ref %}
{% content-ref url="/pages/6fb4QniIOewrrMtMky8u" %}
\[Buffers, I/O\](/seanime-extensions/plugins/apis/system/buffers-i-o.md)
{% endcontent-ref %}
{% content-ref url="/pages/sgw4fthFaY8EmiiJnizK" %}
\[MIME\](/seanime-extensions/plugins/apis/system/mime.md)
{% endcontent-ref %}
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen.md).
# Screen
{% hint style="warning" %}
\*\*Pitfall\*\*
Users having multiple tabs open can lead to \*\*unexpected behavior\*\*.
This happens because navigation events are received from all connected clients.
{% endhint %}
## Listen to navigation
\`\`\`typescript
// Listen to navigation
ctx.screen.onNavigate(e => {
// User navigated to the 'One Piece' anime page
console.log(e.pathname) // /entry
console.log(e.searchParams) // { "id": "21" }
})
// Or as a state
const screen = ctx.screen.getState()
const isAnimeEntry = ctx.computed(() => screen.get().current === "entry", \[screen\])
isAnimeEntry.get()
\`\`\`
## Navigate
\`\`\`typescript
// Navigate to the 'Sakamoto Days' anime page
ctx.screen.navigateTo("/entry", { "id": "177709" })
\`\`\`
## Reload the screen
\`\`\`typescript
// Hard reload the webapp/desktop client screen.
ctx.screen.reload()
\`\`\`
## Get the current screen
Calling this will trigger \`onNavigate\`
This is useful if you want to know the current screen without having to wait for the user to navigate.
\`\`\`typescript
ctx.screen.loadCurrent()
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader.md).
# Auto Downloader
The \`ctx.autoDownloader\` API lets plugins inspect or trigger Seanime's auto-downloader.
## Core Methods
### run
\`run(isSimulation?)\`
Schedules a non-blocking auto-downloader run.
\*\*Parameters:\*\*
\* \`isSimulation\`: Boolean - Optional. When \`true\`, Seanime runs the auto-downloader in simulation mode.
Example:
\`\`\`typescript
ctx.autoDownloader.run(true)
\`\`\`
### runNow
\`runNow()\`
Runs the auto-downloader immediately with real downloads enabled.
This call is non-blocking.
Example:
\`\`\`typescript
ctx.autoDownloader.runNow()
\`\`\`
### runCheck
\`runCheck(options?)\`
Runs a focused auto-downloader check and returns simulation results.
Seanime clears any previous simulation results before this runs.
\*\*Parameters:\*\*
\* \`options\`: \`AutoDownloaderRunCheckOptions\`
\* \`options.isSimulation\`: Boolean - Optional
\* \`options.ruleIds\`: Number\\\[\] - Optional list of rule IDs to check
Example:
\`\`\`typescript
const results = await ctx.autoDownloader.runCheck({
isSimulation: true,
ruleIds: \[12, 18\],
})
console.log(results)
\`\`\`
### getSimulationResults
\`getSimulationResults()\`
Returns the stored results from the last \`runCheck()\` call.
Example:
\`\`\`typescript
const results = ctx.autoDownloader.getSimulationResults()
console.log(results.length)
\`\`\`
### clearSimulationResults
\`clearSimulationResults()\`
Clears the stored auto-downloader simulation results.
Example:
\`\`\`typescript
ctx.autoDownloader.clearSimulationResults()
\`\`\`
### getSettings
\`getSettings()\`
Returns the current auto-downloader settings, or \`undefined\` if the feature is not configured.
\*\*Returns:\*\* \`$app.Models\_AutoDownloaderSettings | undefined\`
Example:
\`\`\`typescript
const settings = ctx.autoDownloader.getSettings()
console.log(settings)
\`\`\`
### isEnabled
\`isEnabled()\`
Returns whether Seanime's auto-downloader is currently enabled.
Example:
\`\`\`typescript
if (ctx.autoDownloader.isEnabled()) {
ctx.autoDownloader.runNow()
}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray.md).
# Tray
Example
const count = ctx.state(0);
ctx.setInterval(() => {
count.set(c => c + 1);
}, 1000);
tray.render(() => {
return tray.stack({
items: \[\
tray.text(\`Count: ${count.get()}\`),\
// Show the button when the count reaches 5\
count.get() >= 5\
? tray.button("Click me", { onClick: "button-clicked" })\
: trat.text("Nothing here")\
\],
})
})
## Tray events
\`\`\`typescript
tray.onOpen(() => {
// User opened the tray content
})
tray.onClose(() => {
// User closed the tray content
})
tray.onClick(() => {
// User clicked the tray icon
})
// Open the tray
// This will not work on the first page load or if the plugin is not pinned
tray.open()
// Close the tray
tray.close()
// Force the tray content to update
// Not useful is most cases because the tray updates when states change
tray.update()
\`\`\`
## Event handlers
You can register functions to specific event triggers like \`onClick\` using \`ctx.registerEventHandler()\` in order to define custom behaviors based on user action.
//..
ctx.registerEventHandler("reset-counter", () => {
count.set(0)
})
tray.render(() => {
return tray.stack({
items: \[\
tray.text(\`Count: ${count.get()}\`),\
tray.button("Reset counter", { onClick: "reset-counter" }),\
\],
})
})
#### Tips
You can register inline event handlers. Make sure the first argument is unique to that element.
\`\`\`typescript
tray.stack(allItems.map((item) => {
return tray.flex(\[\
tray.text(key),\
tray.button({ \
label: "Open", \
size: "sm", \
// It takes a unique ID key as first argument!\
onClick: ctx.eventHandler(item.id, () => {\
// Do something...\
}),\
intent: "gray-subtle"\
}),\
\], { gap: 1, style: { alignItems: "center" } })
}))
\`\`\`
## Base components
\`\`\`typescript
tray.div(\[\], { style: {} })
tray.stack(\[\], { style: {} })
tray.flex(\[\], { style: {} })
tray.text(...)
tray.anchor(...)
tray.a(...)
tray.p(...)
tray.span(...)
tray.css(...)
tray.badge(...)
tray.alert(...)
tray.img(...)
\`\`\`
## Fields/Forms
Field components:
\* input
\* button
\* select
\* radioGroup
\* checkbox
\* switch
Use \`ctx.fieldRef\` to get and set a field's value synchronously.
const textInputRef = ctx.fieldRef<string>("Default value")
// When the form is submitted
ctx.registerEventHandler("submit-form", () => {
// We can get the value of the text input
console.log(textInputRef.current)
// We can change the value of the text input
textInputRef.setValue("")
})
tray.render(() => tray.stack(\[\
text.input("A text field", { fieldRef: textInputRef }),\
tray.button("Submit", { onClick: "submit-form" }),\
\]))
### Select, RadioGroup
You can create forms easily with \`ctx.fieldRef\` and the available field components.
\`\`\`typescript
const selectRef = ctx.fieldRef//...
const count = ctx.state(0)
ctx.setInterval(() => {
count.set(c => c+1)
}, 1000)
function resetCount() {
count.set(0)
}
// Tray will update each time count changes
tray.render(() => tray.text(\`Count: ${count.get()}\`))
### Computed
\`\`\`typescript
const count = ctx.state(0)
const text = ctx.computed(() => \`Count is ${count.get()}\`, \[count\])
text.get()
\`\`\`
### Effects
// Effect registers a callback that runs each time count changes
ctx.effect(() => {
console.log("count changed, " + count.get())
}, \[count\])
### Example
In this example, we fetch some info from an external API each time the user navigates to an anime page.
{% code title="Example" %}
\`\`\`typescript
const currentMediaId = ctx.state{
//...
"plugin": {
"permissions": {
"scopes": \["playback"\]
}
}
}
## Core Methods
### openAndPlay
Opens and plays a file with MPV (without tracking).
\*\*Parameters:\*\*
\* \`filePath\`: String - Path to a video file
\*\*Example:\*\*
\`\`\`typescript
// Play a file directly with MPV without tracking
try {
await ctx.mpv.openAndPlay("/path/to/video.mkv")
console.log("MPV playback started")
} catch (error) {
console.error("MPV playback error:", error)
}
\`\`\`
### onEvent
Registers a listener for MPV player events (fires frequently).
\*\*Parameters:\*\*
\* \`callback\`: Function(event, closed) - Callback function for events
\*\*Example:\*\*
\`\`\`typescript
// Monitor MPV events (use carefully - fires multiple times per second)
const unsubscribe = ctx.mpv.onEvent((event, closed) => {
if (closed) {
console.log("MPV connection closed")
return
}
console.log("MPV loaded file:", event.data)
})
// Unsubscribe anytime
unsubscribe()
\`\`\`
### getConnection
Returns the underlying connection object to the MPV instance.
\*\*Returns:\*\* MpvConnection | undefined - The MPV connection if available
\`\`\`typescript
const conn = ctx.mpv.getConnection()
// Check the connection first
if (conn && !conn.isClosed()) {
// shortcut to call("set\_property", property, value)
conn.set("time-pos", 90)
// shortcut call("get\_property", property)
conn.get("time-pos")
// This works but you should use ctx.mpv.close() instead
conn.close()
}
\`\`\`
### stop
Stops the MPV player.
\*\*Example:\*\*
\`\`\`typescript
// Stop playback
try {
ctx.mpv.stop()
} catch (e) {
console.log("Failed to stop player", e)
}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader.md).
# Downloader
## Permission
{% hint style="warning" %}
\`system\` permission is required.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["system"\],
"allow": {
"writePaths": \["$DOWNLOAD/\*\*/\*"\]
}
}
}
}
\`\`\`typescript
//...
// Destination file
// Note that $DOWNLOAD is in the allow list
const filePath = $filepath.Join($osExtra.downloadDir(), "file.zip")
const downloadUrl = "http://example.com/download/file.zip"
// Start a download
const downloadID = ctx.downloader.download(downloadUrl, filePath);
// Track progress
const cancelWatch = ctx.downloader.watch(downloadID, (progress) => {
console.log("Download progress:",
progress.percentage.toFixed(2), "%, ",
"Speed:", (progress.speed / 1024).toFixed(2), "KB/s, ",
"Downloaded:", (progress.totalBytes / 1024).toFixed(2), "KB"
);
if (progress.status === "completed") {
// download completed
} else if (progress.status === "error") {
// something went wrong
}
});
// Cancel at any time
ctx.downloader.cancel(downloadID)
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron.md).
# Cron
\`ctx.cron\` lets UI plugins register recurring jobs.
## Permissions
{% hint style="warning" %}
\`cron\` permission is required.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["cron"\]
}
}
}
## Quick example
\`\`\`typescript
$ui.register((ctx) => {
ctx.cron.add("refresh-anime-cache", "\*/15 \* \* \* \*", () => {
console.log("refreshing anime cache")
})
ctx.cron.start()
})
\`\`\`
## Expressions
Seanime accepts either one of the supported macros or a five-part cron expression:
\`minute hour day-of-month month day-of-week\`
Supported segment formats:
\* \`\*\`
\* \`1-5\`
\* \`\*/10\`
\* \`1-30/5\`
\* \`1,2,10-20/2\`
Supported macros:
\* \`@yearly\` / \`@annually\`
\* \`@monthly\`
\* \`@weekly\`
\* \`@daily\` / \`@midnight\`
\* \`@hourly\`
\* \`@30min\`
\* \`@15min\`
\* \`@10min\`
\* \`@5min\`
## Methods
### add
\`ctx.cron.add(jobId, cronExpr, callback)\`
Registers a job.
If the same \`jobId\` already exists, Seanime replaces it.
\*\*Parameters:\*\*
\* \`jobId\`: String - Unique job identifier.
\* \`cronExpr\`: String - Cron macro or five-part expression.
\* \`callback\`: Function - Job body.
### remove
\`ctx.cron.remove(jobId)\`
Removes one job by ID.
### removeAll
\`ctx.cron.removeAll()\`
Removes every registered job.
### total
\`ctx.cron.total()\`
Returns the number of registered jobs.
### start
\`ctx.cron.start()\`
Starts the scheduler.
Calling \`start()\` again restarts it.
### stop
\`ctx.cron.stop()\`
Stops the scheduler.
### hasStarted
\`ctx.cron.hasStarted()\`
Returns whether the scheduler is currently running.
## Notes
\* Adding jobs does not start the scheduler automatically.
\* Jobs run asynchronously when they become due.
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth.md).
# Auth
The \`ctx.auth\` API lets a UI plugin log the user in or out of AniList after the user approves a prompt.
## Permissions
{% hint style="warning" %}
\`auth\` permission is required.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["auth"\]
}
}
}
{% hint style="info" %}
This API is not bound in secure mode.
{% endhint %}
## Core Methods
### login
\`ctx.auth.login(token)\`
Logs the user in to AniList with the provided token after the user approves the prompt.
\*\*Parameters:\*\*
\* \`token\`: String - The AniList token to save.
\*\*Returns:\*\* \`Promise{
//...
"plugin": {
"permissions": {
"scopes": \["extensions"\]
}
}
}
{% hint style="info" %}
This API is not bound in secure mode.
{% endhint %}
## Core Methods
### enable
\`ctx.extensions.enable(extensionId)\`
Enables the extension with that ID.
### disable
\`ctx.extensions.disable(extensionId)\`
Disables the extension with that ID.
### setDisabled
\`ctx.extensions.setDisabled(extensionId, disabled)\`
Sets the disabled state directly.
### Example
\`\`\`typescript
$ui.register((ctx) => {
const tray = ctx.newTray({
tooltipText: "Extension manager",
withContent: true,
})
const extensionIdRef = ctx.fieldRef("my-other-plugin")
ctx.registerEventHandler("disable-extension", async () => {
try {
await ctx.extensions.disable(extensionIdRef.current ?? "")
ctx.toast.warning("Extension disabled")
} catch (error) {
ctx.toast.alert(\`Disable failed: ${error.message}\`)
}
})
ctx.registerEventHandler("enable-extension", async () => {
try {
await ctx.extensions.enable(extensionIdRef.current ?? "")
ctx.toast.success("Extension enabled")
} catch (error) {
ctx.toast.alert(\`Enable failed: ${error.message}\`)
}
})
tray.render(() => tray.stack(\[\
tray.input("Extension ID", { fieldRef: extensionIdRef }),\
tray.button("Disable", { onClick: "disable-extension" }),\
tray.button("Enable", { onClick: "enable-extension" }),\
\]))
})
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime.md).
# MIME
\`\`\`typescript
try {
$mime.parse("text/html; charset=utf-8")
// => { mediaType: "text/html", parameters: { charset: "utf-8" } }
$mime.format("text/html", { charset: "utf-8" })
// => text/html; charset=utf-8
} catch {}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client.md).
# Torrent Client
## Permission
{% hint style="warning" %}
\`torrent-client\` permission is required.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["torrent-client"\],
}
}
}
## Core Methods
### getTorrents
\`\`\`
getTorrents()
\`\`\`
Retrieves a list of all torrents in the torrent client.
Example:
\`\`\`javascript
// Get all torrents from the client
try {
const torrents = await ctx.torrentClient.getTorrents()
console.log("Retrieved torrents:", torrents)
} catch (error) {
console.error("Error getting torrents:", error)
}
\`\`\`
### getActiveTorrents
\`\`\`
getActiveTorrents()
\`\`\`
Retrieves a list of active torrents (downloading/uploading) from the torrent client.
Example:
\`\`\`javascript
// Get only active torrents
try {
const activeTorrents = await ctx.torrentClient.getActiveTorrents()
console.log("Active torrents:", activeTorrents)
} catch (error) {
console.error("Error getting active torrents:", error)
}
\`\`\`
### addMagnets
\`\`\`
addMagnets(magnets, dest)
\`\`\`
Adds magnet links to the torrent client.
\*\*Parameters\*\*:
\* \`magnets\`: string\\\[\] - Array of magnet links
\* \`dest\`: string - Destination path for downloaded files
Example:
\`\`\`javascript
// Add magnet links to the torrent client
try {
await ctx.torrentClient.addMagnets(
\["magnet:?xt=urn:btih:xxxxxx", "magnet:?xt=urn:btih:yyyyyy"\],
"/downloads/anime"
)
console.log("Magnets added successfully")
} catch (error) {
console.error("Error adding magnets:", error)
}
\`\`\`
### removeTorrents
\`\`\`
removeTorrents(hashes)
\`\`\`
Removes torrents from the client.
\*\*Parameters\*\*:
\* \`hashes\`: string\\\[\] - Array of torrent hashes to remove
Example:
\`\`\`javascript
// Remove torrents from the client
try {
await ctx.torrentClient.removeTorrents(\["abc123def456", "xyz789uvw"\])
console.log("Torrents removed successfully")
} catch (error) {
console.error("Error removing torrents:", error)
}
\`\`\`
### pauseTorrents
\`\`\`
pauseTorrents(hashes)
\`\`\`
Pauses specified torrents.
\*\*Parameters\*\*:
\* \`hashes\`: string\\\[\] - Array of torrent hashes to pause
Example:
\`\`\`javascript
// Pause specific torrents
try {
await ctx.torrentClient.pauseTorrents(\["abc123def456", "xyz789uvw"\])
console.log("Torrents paused successfully")
} catch (error) {
console.error("Error pausing torrents:", error)
}
\`\`\`
### resumeTorrents
\`\`\`
resumeTorrents(hashes)
\`\`\`
Resumes specified torrents.
\*\*Parameters\*\*:
\* \`hashes\`: string\\\[\] - Array of torrent hashes to resume
Example:
\`\`\`javascript
// Resume specific torrents
try {
await ctx.torrentClient.resumeTorrents(\["abc123def456", "xyz789uvw"\])
console.log("Torrents resumed successfully")
} catch (error) {
console.error("Error resuming torrents:", error)
}
\`\`\`
### deselectFiles
\`\`\`
deselectFiles(hash, indices)
\`\`\`
Deselects specific files within a torrent.
\*\*Parameters\*\*:
\* \`hash\`: string - Hash of the torrent
\* \`indices\`: number\\\[\] - Array of file indices to deselect
Example:
\`\`\`javascript
// Deselect specific files in a torrent
try {
await ctx.torrentClient.deselectFiles("abc123def456", \[0, 2, 5\])
console.log("Files deselected successfully")
} catch (error) {
console.error("Error deselecting files:", error)
}
\`\`\`
### getFiles
\`\`\`
getFiles(hash)
\`\`\`
Retrieves all files within a specific torrent.
\*\*Parameters\*\*:
\* \`hash\`: string - Hash of the torrent
Example:
\`\`\`javascript
// Get all files in a torrent
try {
const files = await ctx.torrentClient.getFiles("abc123def456")
console.log("Torrent files:", files)
} catch (error) {
console.error("Error getting files:", error)
}
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord.md).
# Discord
## Permissions
{% hint style="warning" %}
\`discord\` permission is required
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["discord"\]
}
}
}
## ctx.discord
The \`ctx.discord\` API allows your plugin to integrate with Discord Rich Presence, displaying what users are watching or reading in their Discord status.
## Core Methods
### setAnimeActivity
Sets Discord Rich Presence to show anime activity.
\*\*Parameters:\*\*
\* \`opts\`: Object containing:
\* \`id\`: Number - AniList media ID
\* \`title\`: String - Anime title to display
\* \`image\`: String - Image URL for the anime
\* \`isMovie\`: Boolean - Whether the anime is a movie
\* \`episodeNumber\`: Number - Current episode number
\* \`progress\` : Number - Progress in seconds
\* \`duration\` : Number - Duration in seconds
\* \`totalEpisodes?\` : Number - Number of episodes of the anime
\* \`currentEpisodeCount?\` : Number - Number of playable episodes
\* \`episodeTitle?\` : String - Episode title
### updateAnimeActivity
Update the current anime activity progress set by \`setAnimeActivity\` .
This is safe to call every second, Seanime will take care of batching updates
\*\*Parameters\*\*:
\* \`progress\` : Number - Progress in seconds
\* \`duration\` : Number - Duration in seconds
\* \`paused\` : Boolean
### setMangaActivity
Sets Discord Rich Presence to show manga reading activity.
\*\*Parameters:\*\*
\* \`opts\`: Object containing:
\* \`id\`: Number - AniList media ID
\* \`title\`: String - Manga title to display
\* \`image\`: String - Image URL for the manga
\* \`chapter\`: String - Current chapter number or range
### cancel
The \`cancel()\` function terminates any ongoing Discord Rich Presence activity.
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast.md).
# Toast
## Info
\`\`\`typescript
ctx.toast.info("Info!")
\`\`\`
## Alert
\`\`\`typescript
ctx.toast.alert("Alert!")
\`\`\`
## Warning
\`\`\`typescript
ctx.toast.warning("Warning!")
\`\`\`
## Success
\`\`\`typescript
ctx.toast.success("Success!")
\`\`\`
---
# Unknown
\> For the complete documentation index, see \[llms.txt\](https://seanime.gitbook.io/seanime-extensions/llms.txt). Markdown versions of documentation pages are available by appending \`.md\` to page URLs; this page is available as \[Markdown\](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid.md).
# Debrid
The \`ctx.debrid\` API lets plugins inspect debrid state, add torrents, and manage local debrid downloads.
## Permissions
{% hint style="warning" %}
\`debrid\` permission is required. If you use \`addAndQueueTorrent()\` or \`downloadTorrent()\`, the destination must also be covered by \`allow.writePaths\`.
{% endhint %}
{
//...
"plugin": {
"permissions": {
"scopes": \["debrid"\],
"allow": {
"writePaths": \["$DOWNLOAD/\*\*/\*"\]
}
}
}
}
## Core Methods
### hasProvider
\`hasProvider()\`
Returns whether a debrid provider is currently configured.
### getSettings
\`getSettings()\`
Returns the current debrid settings, or \`undefined\` if debrid is not configured.
### getQueuedDownloads
\`getQueuedDownloads()\`
Returns debrid downloads that Seanime has queued for local download.
### addTorrent
\`addTorrent(options)\`
Adds a torrent to the configured debrid provider.
At least one of \`torrent\`, \`magnetLink\`, or \`infoHash\` must be provided.
If \`selectFileId\` is omitted, Seanime defaults it to \`"all"\`.
\*\*Returns:\*\* \`Promise