# Table of Contents - [Getting started | Seanime Extensions](#getting-started-seanime-extensions) - [Anime Torrent Provider | Seanime Extensions](#anime-torrent-provider-seanime-extensions) - [Manga Provider | Seanime Extensions](#manga-provider-seanime-extensions) - [Core APIs | Seanime Extensions](#core-apis-seanime-extensions) - [Changelog | Seanime Extensions](#changelog-seanime-extensions) - [Online Streaming Provider | Seanime Extensions](#online-streaming-provider-seanime-extensions) - [Custom Source | Seanime Extensions](#custom-source-seanime-extensions) - [Introduction | Seanime Extensions](#introduction-seanime-extensions) - [Write, test, share | Seanime Extensions](#write-test-share-seanime-extensions) - [Write, test, share | Seanime Extensions](#write-test-share-seanime-extensions) - [Permissions | Seanime Extensions](#permissions-seanime-extensions) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [APIs | Seanime Extensions](#apis-seanime-extensions) - [Helpers | Seanime Extensions](#helpers-seanime-extensions) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Store | Seanime Extensions](#store-seanime-extensions) - [Database | Seanime Extensions](#database-seanime-extensions) - [Debug | Seanime Extensions](#debug-seanime-extensions) - [Unknown](#unknown) - [Permissions | Seanime Extensions](#permissions-seanime-extensions) - [Feature requests | Seanime Extensions](#feature-requests-seanime-extensions) - [Hooks | Seanime Extensions](#hooks-seanime-extensions) - [UI | Seanime Extensions](#ui-seanime-extensions) - [OS | Seanime Extensions](#os-seanime-extensions) - [Commands | Seanime Extensions](#commands-seanime-extensions) - [Basics | Seanime Extensions](#basics-seanime-extensions) - [Downloading | Seanime Extensions](#downloading-seanime-extensions) - [Storage | Seanime Extensions](#storage-seanime-extensions) - [AniList | Seanime Extensions](#anilist-seanime-extensions) - [User Interface | Seanime Extensions](#user-interface-seanime-extensions) - [Downloader | Seanime Extensions](#downloader-seanime-extensions) - [MIME | Seanime Extensions](#mime-seanime-extensions) - [Auth | Seanime Extensions](#auth-seanime-extensions) - [Example | Seanime Extensions](#example-seanime-extensions) - [Other | Seanime Extensions](#other-seanime-extensions) - [Cron | Seanime Extensions](#cron-seanime-extensions) - [Discord | Seanime Extensions](#discord-seanime-extensions) - [Extensions | Seanime Extensions](#extensions-seanime-extensions) - [Manga | Seanime Extensions](#manga-seanime-extensions) - [Toast | Seanime Extensions](#toast-seanime-extensions) - [Torrent Client | Seanime Extensions](#torrent-client-seanime-extensions) - [Debrid | Seanime Extensions](#debrid-seanime-extensions) - [Helpers | Seanime Extensions](#helpers-seanime-extensions) - [Screen | Seanime Extensions](#screen-seanime-extensions) - [Command Palette | Seanime Extensions](#command-palette-seanime-extensions) - [Anime/Library | Seanime Extensions](#anime-library-seanime-extensions) - [Tray | Seanime Extensions](#tray-seanime-extensions) - [Auto Scanner | Seanime Extensions](#auto-scanner-seanime-extensions) - [Scanner | Seanime Extensions](#scanner-seanime-extensions) - [Continuity | Seanime Extensions](#continuity-seanime-extensions) - [MPV | Seanime Extensions](#mpv-seanime-extensions) - [Auto Downloader | Seanime Extensions](#auto-downloader-seanime-extensions) - [Webview | Seanime Extensions](#webview-seanime-extensions) - [Debridstream | Seanime Extensions](#debridstream-seanime-extensions) - [Anime | Seanime Extensions](#anime-seanime-extensions) - [External Player Link | Seanime Extensions](#external-player-link-seanime-extensions) - [Auto Select | Seanime Extensions](#auto-select-seanime-extensions) - [Filler Manager | Seanime Extensions](#filler-manager-seanime-extensions) - [Torrentstream | Seanime Extensions](#torrentstream-seanime-extensions) - [Torrent Search | Seanime Extensions](#torrent-search-seanime-extensions) - [System | Seanime Extensions](#system-seanime-extensions) - [Filepath | Seanime Extensions](#filepath-seanime-extensions) - [Playback (External) | Seanime Extensions](#playback-external-seanime-extensions) - [Buffers, I/O | Seanime Extensions](#buffers-i-o-seanime-extensions) - [Shared Modules | Seanime Extensions](#shared-modules-seanime-extensions) - [VideoCore | Seanime Extensions](#videocore-seanime-extensions) - [DOM | Seanime Extensions](#dom-seanime-extensions) - [Action | Seanime Extensions](#action-seanime-extensions) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Feature requests | Seanime Extensions](#feature-requests-seanime-extensions) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) - [Unknown](#unknown) --- # Getting started | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/seanime/readme.md) . Seanime has an embedded JavaScript (ES5) engine which allows you to write various types of extensions with minimal effort. Seanime's JavaScript engine does NOT support **Node.JS** or **Browser** APIs but offers its own APIs for convenience. Check out the [Core APIs](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis) . [Core APIs](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis) Content Providers[](https://seanime.gitbook.io/seanime-extensions#content-providers) ------------------------------------------------------------------------------------- [Write, test, share](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share) Plugins[](https://seanime.gitbook.io/seanime-extensions#plugins) ----------------------------------------------------------------- [Introduction](https://seanime.gitbook.io/seanime-extensions/plugins/introduction) [Basics](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics) [NextCore APIs](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis) Last updated 3 months ago --- # Anime Torrent Provider | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider.md) . Difficulty: Easy Use bootstrapping command[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#use-bootstrapping-command) You can use this third-party tool to help you quickly bootstrap a folder locally Copy npx seanime-tool g-template Types[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#types) ------------------------------------------------------------------------------------------------------ anime-torrent-provider.d.ts Copy declare type AnimeProviderSmartSearchFilter = "batch" | "episodeNumber" | "resolution" | "query" | "bestReleases" declare type AnimeProviderType = "main" | "special" declare interface AnimeProviderSettings { // Indicates whether the extension supports smart search. canSmartSearch: boolean // Filters that can be used in smart search. smartSearchFilters: AnimeProviderSmartSearchFilter[] // Indicates whether the extension supports adult content. supportsAdult: boolean // Type of the provider. type: AnimeProviderType } // Media object passed to 'search' and 'smartSearch' methods. declare interface Media { // AniList ID of the media. id: number // MyAnimeList ID of the media. idMal?: number // e.g. "FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS" // This will be set to "NOT_YET_RELEASED" if the status is unknown. status?: string // e.g. "TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC" // This will be set to "TV" if the format is unknown. format?: string // e.g. "Attack on Titan" englishTitle?: string // e.g. "Shingeki no Kyojin" romajiTitle?: string // TotalEpisodes is total number of episodes of the media. // This will be -1 if the total number of episodes is unknown / not applicable. episodeCount?: number // Absolute offset of the media's season. // This will be 0 if the media is not seasonal or the offset is unknown. absoluteSeasonOffset?: number // All alternative titles of the media. synonyms: string[] // Whether the media is NSFW. isAdult: boolean // Start date of the media. // This will be undefined if it has no start date. startDate?: FuzzyDate } declare interface FuzzyDate { year: number month?: number day?: number } declare interface AnimeSearchOptions { // The media object. media: Media // The user search query. query: string } declare interface AnimeSmartSearchOptions { // The media object. media: Media // The user search query. // This will be empty if your extension does not support custom queries. query: string // Indicates whether the user wants to search for batch torrents. // This will be false if your extension does not support batch torrents. batch: boolean // The episode number the user wants to search for. // This will be 0 if your extension does not support episode number filtering. episodeNumber: number // The resolution the user wants to search for. // This will be empty if your extension does not support resolution filtering. resolution: string // AniDB Anime ID of the media. anidbAID: number // AniDB Episode ID of the media. anidbEID: number // Indicates whether the user wants to search for the best releases. // This will be false if your extension does not support filtering by best releases. bestReleases: boolean } declare interface AnimeTorrent { name: string // Date of the torrent. // The date should have RFC3339 format. e.g. "2006-01-02T15:04:05Z07:00" date: string // Size of the torrent in bytes. size: number // Formatted size of the torrent. e.g. "1.2 GB" // Leave this empty if you want Seanime to format the size. formattedSize: string // Number of seeders of the torrent. seeders: number // Number of leechers of the torrent. leechers: number // Number of downloads of the torrent. downloadCount: number // Link to the torrent page. link: string // Download URL of the torrent. // Leave this empty if you cannot provide a direct download URL. downloadUrl?: string // Magnet link of the torrent. // Set this to null if you cannot provide a magnet link without scraping. magnetLink?: string // Info hash of the torrent. // Set this to null if you cannot provide an info hash without scraping. infoHash?: string // The resolution of the torrent. // Leave this empty if you want Seanime to parse the resolution from the name. resolution?: string // Set this to true if you can confirm that the torrent is a batch. // Else, Seanime will parse the torrent name to determine if it's a batch. isBatch?: boolean // Episode number of the torrent. // Return -1 if unknown / unable to determine and Seanime will parse the torrent name. episodeNumber: number // Release group of the torrent. // Leave this empty if you want Seanime to parse the release group from the name. releaseGroup?: string // Set this to true if you can confirm that the torrent is the best release. isBestRelease: boolean // Set this to true if you can confirm that the torrent matches the anime the user is searching for. // e.g. If the torrent was found using the AniDB anime or episode ID confirmed: boolean } Code[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#code) ---------------------------------------------------------------------------------------------------- Do not change the name of the class. It must be Provider. Copy /// class Provider { private api = "https://example.com" // Returns the provider settings. getSettings(): AnimeProviderSettings { // TODO: Edit this return { canSmartSearch: true, smartSearchFilters: ["batch", "episodeNumber", "resolution"], supportsAdult: false, type: "main", } } // Returns the search results depending on the query. async search(opts: AnimeSearchOptions): Promise { // TODO return [] } // Returns the search results depending on the search options. async smartSearch(opts: AnimeSmartSearchOptions): Promise { // TODO return [] } // Scrapes the torrent page to get the info hash. // If already present in AnimeTorrent, this should just return the info hash without scraping. async getTorrentInfoHash(torrent: AnimeTorrent): Promise { return torrent.infoHash } // Scrapes the torrent page to get the magnet link. // If already present in AnimeTorrent, this should just return the magnet link without scraping. async getTorrentMagnetLink(torrent: AnimeTorrent): Promise { return torrent.magnetLink } // Returns the latest torrents. // Note that this is only used by "main" providers. async getLatest(): Promise { // TODO return [] } } ### Settings[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#settings) #### type[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#type) * `main`: Your extension can be used as **default provider** for torrent search and the Auto Downloader. * `special`: Your extension can **ONLY** be used for torrent search. #### canSmartSearch / smartSearchFilters[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#cansmartsearch-smartsearchfilters) ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FqGAduQs9DgfN9RhR15yL%252Fimg-2025-03-16-16-13-14.png%3Falt%3Dmedia%26token%3D27690df9-db8d-47ad-814c-151a9cac6116&width=768&dpr=3&quality=100&sign=9f338f69&sv=2) * `batch` : Your extension can look for batches * `episodeNumber` : Your extension can look for specific episode numbers * `resolution` : Your extension can filter by resolution * `query`: Allow the user to change the smart search title * `bestReleases` : Your extension can find highest-quality torrents Example[](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider#example) ---------------------------------------------------------------------------------------------------------- Copy /// /// class Provider { api = "https://feed.animetosho.org/json" getSettings(): AnimeProviderSettings { return { canSmartSearch: true, smartSearchFilters: ["batch", "episodeNumber", "resolution"], supportsAdult: false, type: "main", } } async search(opts: AnimeSearchOptions): Promise { const query = `?q=${encodeURIComponent(opts.query)}&only_tor=1` console.log(query) const torrents = await this.fetchTorrents(query) return torrents.map(t => this.toAnimeTorrent(t)) } async smartSearch(opts: AnimeSmartSearchOptions): Promise { const ret: AnimeTorrent[] = [] if (opts.batch) { if (!opts.anidbAID) return [] let torrents = await this.searchByAID(opts.anidbAID, opts.resolution) if (!(opts.media.format == "MOVIE" || opts.media.episodeCount == 1)) { torrents = torrents.filter(t => t.num_files > 1) } for (const torrent of torrents) { const t = this.toAnimeTorrent(torrent) t.isBatch = true ret.push(t) } return ret } if (!opts.anidbEID) return [] const torrents = await this.searchByEID(opts.anidbEID, opts.resolution) for (const torrent of torrents) { ret.push(this.toAnimeTorrent(torrent)) } return ret } async getTorrentInfoHash(torrent: AnimeTorrent): Promise { return torrent.infoHash || "" } async getTorrentMagnetLink(torrent: AnimeTorrent): Promise { return torrent.magnetLink || "" } async getLatest(): Promise { const query = `?q=&only_tor=1` const torrents = await this.fetchTorrents(query) return torrents.map(t => this.toAnimeTorrent(t)) } async searchByAID(aid: number, quality: string): Promise { const q = encodeURIComponent(this.formatQuality(quality)) const query = `?order=size-d&aid=${aid}&q=${q}` return this.fetchTorrents(query) } async searchByEID(eid: number, quality: string): Promise { const q = encodeURIComponent(this.formatQuality(quality)) const query = `?eid=${eid}&q=${q}` return this.fetchTorrents(query) } async fetchTorrents(url: string): Promise { const furl = `${this.api}${url}` try { const response = await fetch(furl) if (!response.ok) { throw new Error(`Failed to fetch torrents, ${response.statusText}`) } const torrents: ToshoTorrent[] = await response.json() return torrents.map(t => { if (t.seeders > 30000) { t.seeders = 0 } if (t.leechers > 30000) { t.leechers = 0 } return t }) } catch (error) { throw new Error(`Error fetching torrents: ${error}`) } } formatQuality(quality: string): string { return quality.replace(/p$/, "") } toAnimeTorrent(torrent: ToshoTorrent): AnimeTorrent { return { name: torrent.title, date: new Date(torrent.timestamp * 1000).toISOString(), size: torrent.total_size, formattedSize: "", seeders: torrent.seeders, leechers: torrent.leechers, downloadCount: torrent.torrent_download_count, link: torrent.link, downloadUrl: torrent.torrent_url, magnetLink: torrent.magnet_uri, infoHash: torrent.info_hash, resolution: "", isBatch: false, episodeNumber: -1, isBestRelease: false, confirmed: true, } } } type ToshoTorrent = { id: number title: string link: string timestamp: number status: string tosho_id?: number nyaa_id?: number nyaa_subdom?: any anidex_id?: number torrent_url: string info_hash: string info_hash_v2?: string magnet_uri: string seeders: number leechers: number torrent_download_count: number tracker_updated?: any nzb_url?: string total_size: number num_files: number anidb_aid: number anidb_eid: number anidb_fid: number article_url: string article_title: string website_url: string } [PreviousWrite, test, share](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share) [NextManga Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider) Last updated 3 months ago --- # Manga Provider | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider.md) . Difficulty: Easy Use bootstrapping command[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#use-bootstrapping-command) You can use this third-party tool to help you quickly bootstrap a folder locally Copy npx seanime-tool g-template Types[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#types) ---------------------------------------------------------------------------------------------- manga-provider.d.ts Copy declare type SearchResult = { id: string title: string synonyms?: string[] year?: number image?: string } declare type ChapterDetails = { id: string url: string title: string chapter: string index: number scanlator?: string language?: string rating?: number updatedAt?: string } declare type ChapterPage = { url: string index: number headers: { [key: string]: string } } declare type QueryOptions = { query: string year?: number } declare type Settings = { supportsMultiLanguage?: boolean supportsMultiScanlator?: boolean } declare abstract class MangaProvider { search(opts: QueryOptions): Promise findChapters(id: string): Promise findChapterPages(id: string): Promise getSettings(): Settings } Code[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#code) -------------------------------------------------------------------------------------------- Do not change the name of the class. It must be Provider. Copy /// class Provider { private api = "https://example.com" getSettings(): Settings { return { supportsMultiLanguage: false, supportsMultiScanlator: false, } } // Returns the search results based on the query. async search(opts: QueryOptions): Promise { // TODO return [{\ id: "999",\ title: "Manga Title",\ synonyms: ["Synonym 1", "Synonym 2"],\ year: 2021,\ image: "https://example.com/image.jpg",\ }] } // Returns the chapters based on the manga ID. // The chapters should be sorted in ascending order (0, 1, ...). async findChapters(mangaId: string): Promise { // TODO return [{\ id: `999-chapter-1`,\ url: "https://example.com/manga/999-chapter-1",\ title: "Chapter 1",\ chapter: "1",\ index: 0,\ }] } // Returns the chapter pages based on the chapter ID. // The pages should be sorted in ascending order (0, 1, ...). async findChapterPages(chapterId: string): Promise { // TODO return [{\ url: "https://example.com/manga/999-chapter-1/page-1.jpg",\ index: 0,\ headers: {\ "Referer": "https://example.com/manga/999/chapter-1",\ },\ }] } } ### Workflow[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#workflow) `search` is called twice when the user opens the manga page. Each time with a different manga title as query (English, Romaji). The best match will automatically be selected and `findChapters` will be called with the manga ID from the search result to get the list of chapters. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FUF1kuTZ5qQrX9OL247aY%252Fimage.png%3Falt%3Dmedia%26token%3D75ef0a74-5eb2-4c77-936c-c22386727abe&width=768&dpr=3&quality=100&sign=5b030f98&sv=2) `findChapterPages` is called when the user requests to read or download the chapter. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FLIDGusZF6pPtwd1q3Sxy%252Fimage.png%3Falt%3Dmedia%26token%3D2e50044c-e6c1-4a26-98b4-bd36a7525dc2&width=768&dpr=3&quality=100&sign=ecf9ed93&sv=2) ### Manga ID, Chapter ID[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#manga-id-chapter-id) Depending on the source website you’re getting the data from, the URLs might get a little complex. For example, if a manga’s chapter page is: [`https://example.com/manga/999/chapter-1`](https://example.com/manga/999/chapter-1) consisting of 2 URL sections (in this case, the manga ID and the chapter ID), you can construct the Seanime chapter ID by combining the two parts and splitting them in `findChapterPages` . ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FSizqGEViam7MUer2BGdA%252Fimage.png%3Falt%3Dmedia%26token%3D30a9cb41-3c50-4000-8d63-969a85415e24&width=768&dpr=3&quality=100&sign=33f80bb3&sv=2) ### Settings[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#settings) * If your manga source supports multiple languages for chapters and you want your extension to give this option to the users, set `supportsMultiLanguage` to `true` and set the `language` property for each of the `ChapterDetails`. Preferably [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) . * Similarly, you can also give the option to choose a scanlator by setting `supportsMultiScanlator` to `true` and setting the `scanlator` property for each of the `ChapterDetails`. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FjJGEyZuLpm8gvDUiPDKl%252Fimage.png%3Falt%3Dmedia%26token%3Ded43ab5d-9c3f-47ad-8550-3bac01d08582&width=768&dpr=3&quality=100&sign=13015550&sv=2) Example[](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider#example) -------------------------------------------------------------------------------------------------- Copy /// class Provider { private api = "https://api.comick.fun" getSettings(): Settings { return { supportsMultiLanguage: false, supportsMultiScanlator: false, } } async search(opts: QueryOptions): Promise { console.log(this.api, opts.query) const requestRes = await fetch(`${this.api}/v1.0/search?q=${encodeURIComponent(opts.query)}&limit=25&page=1`, { method: "get", }) const comickRes = await requestRes.json() as ComickSearchResult[] const ret: SearchResult[] = [] for (const res of comickRes) { let cover: any = res.md_covers ? res.md_covers[0] : null if (cover && cover.b2key != undefined) { cover = "https://meo.comick.pictures/" + cover.b2key } ret.push({ id: res.hid, title: res.title ?? res.slug, synonyms: res.md_titles?.map(t => t.title) ?? {}, year: res.year ?? 0, image: cover, }) } return ret } async findChapters(id: string): Promise { console.log("Fetching chapters", id) const chapterList: ChapterDetails[] = [] const data = (await (await fetch(`${this.api}/comic/${id}/chapters?lang=en&page=0&limit=1000000`))?.json()) as { chapters: ComickChapter[] } const chapters: ChapterDetails[] = [] for (const chapter of data.chapters) { if (!chapter.chap) { continue } let title = "Chapter " + this.padNum(chapter.chap, 2) + " " if (title.length === 0) { if (!chapter.title) { title = "Oneshot" } else { title = chapter.title } } let canPush = true for (let i = 0; i < chapters.length; i++) { if (chapters[i].title?.trim() === title?.trim()) { canPush = false } } if (canPush) { if (chapter.lang === "en") { chapters.push({ url: `${this.api}/comic/${id}/chapter/${chapter.hid}`, index: 0, id: chapter.hid, title: title?.trim(), chapter: chapter.chap, rating: chapter.up_count - chapter.down_count, updatedAt: chapter.updated_at, }) } } } chapters.reverse() for (let i = 0; i < chapters.length; i++) { chapters[i].index = i } return chapters } async findChapterPages(id: string): Promise { const data = (await (await fetch(`${this.api}/chapter/${id}`))?.json()) as { chapter: { md_images: { vol: any; w: number; h: number; b2key: string }[] } } const pages: ChapterPage[] = [] data.chapter.md_images.map((image, index: number) => { pages.push({ url: `https://meo.comick.pictures/${image.b2key}?width=${image.w}`, index: index, headers: {}, }) }) return pages } padNum(number: string, places: number): string { let range = number.split("-") range = range.map((chapter) => { chapter = chapter.trim() const digits = chapter.split(".")[0].length return "0".repeat(Math.max(0, places - digits)) + chapter }) return range.join("-") } } interface ComickSearchResult { title: string; id: number; hid: string; slug: string; year?: number; rating: string; rating_count: number; follow_count: number; user_follow_count: number; content_rating: string; created_at: string; demographic: number; md_titles: { title: string }[]; md_covers: { vol: any; w: number; h: number; b2key: string }[]; highlight: string; } interface Comic { id: number; hid: string; title: string; country: string; status: number; links: { al: string; ap: string; bw: string; kt: string; mu: string; amz: string; cdj: string; ebj: string; mal: string; raw: string; }; last_chapter: any; chapter_count: number; demographic: number; hentai: boolean; user_follow_count: number; follow_rank: number; comment_count: number; follow_count: number; desc: string; parsed: string; slug: string; mismatch: any; year: number; bayesian_rating: any; rating_count: number; content_rating: string; translation_completed: boolean; relate_from: Array; mies: any; md_titles: { title: string }[]; md_comic_md_genres: { md_genres: { name: string; type: string | null; slug: string; group: string } }[]; mu_comics: { licensed_in_english: any; mu_comic_categories: { mu_categories: { title: string; slug: string }; positive_vote: number; negative_vote: number; }[]; }; md_covers: { vol: any; w: number; h: number; b2key: string }[]; iso639_1: string; lang_name: string; lang_native: string; } interface ComickChapter { id: number; chap: string; title: string; vol: string | null; lang: string; created_at: string; updated_at: string; up_count: number; down_count: number; group_name: any; hid: string; identities: any; md_chapter_groups: { md_groups: { title: string; slug: string } }[]; } [PreviousAnime Torrent Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider) [NextOnline Streaming Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider) Last updated 3 months ago --- # Core APIs | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis.md) . Types[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#types) ------------------------------------------------------------------------------- Create a `core.d.ts` file containing the following content: [https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension\_repo/goja\_plugin\_types/core.d.tsraw.githubusercontent.com](https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension_repo/goja_plugin_types/core.d.ts) core.d.ts Examples[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#examples) ------------------------------------------------------------------------------------- ### User preference[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#user-preference) Returns the user preference value from [Add user configuration (optional)](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#add-user-configuration-optional) Copy $getUserPreference("apiToken") ### Console[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#console) Copy console.log() console.warn() console.error() ### Fetch[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#fetch) Example Copy /// const res = await fetch("https://jsonplaceholder.typicode.com/todos/1") const data = res.json() ### Doc[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#doc) Parse HTML Copy const $ = LoadDoc(`

First Post

This is the first post.

Read more

Second Post

This is the second post.

Read more

Third Post

This is the third post.

Read more
`); const titles = $("section") .children("article.post") .filter((i, e) => e.attr("data-id") !== "1") .map((i, e) => e.children("h2").text()) console.log(titles) // [Second Post, Third Post] ### ChromeDP[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#chromedp) Headless browser powered by the user's installed Chrome/Chromium binary. ChromeDP is experimental and is not guaranteed to work. Always call `close()` to avoid leaking resources. Copy // -------- Content Providers ---------- const browser = await ChromeDP.newBrowser(); await browser.navigate("https://example.com"); // Wait for the dynamic item to appear await browser.waitVisible("#dynamic-item"); // Get the text of the dynamic item const itemText = await browser.text("#dynamic-item"); await browser.close(); // ------------- Plugins --------------- function init() { $ui.register((ctx: $ui.Context) => { async function doSomething() { const browser = await ctx.chromeDP.newBrowser() // ... await browser.close() } }) } ### CryptoJS[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#cryptojs) Copy let message = "seanime"; let key = CryptoJS.enc.Utf8.parse("secret key"); console.log("Message:", message); let encrypted = CryptoJS.AES.encrypt(message, key); console.log("Encrypted:", encrypted); // map[iv toString] console.log("Encrypted.toString():", encrypted.toString()); // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk= console.log("Encrypted.toString(CryptoJS.enc.Base64):", encrypted.toString(CryptoJS.enc.Base64)); // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk= let decrypted = CryptoJS.AES.decrypt(encrypted, key); console.log("Decrypted:", decrypted.toString(CryptoJS.enc.Utf8)); let iv = CryptoJS.enc.Utf8.parse("3134003223491201"); encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv }); console.log("Encrypted:", encrypted); // map[iv toString] decrypted = CryptoJS.AES.decrypt(encrypted, key); console.log("Decrypted without IV:", decrypted.toString(CryptoJS.enc.Utf8)); // "" <- Nothing decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv: iv }); console.log("Decrypted with IV:", decrypted.toString(CryptoJS.enc.Utf8)); // seanime let a = CryptoJS.enc.Utf8.parse("Hello, World!"); console.log(a); // // Uint8Array [72 101 108 108 ...] let b = CryptoJS.enc.Base64.stringify(a); console.log(b); // SGVsbG8sIFdvcmxkIQ== let c = CryptoJS.enc.Base64.parse(b); console.log(c); // Uint8Array [72 101 108 108 ...] let d = CryptoJS.enc.Utf8.stringify(c); console.log(d); // Hello, World! ### $habari[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdhabari) Filename parser Copy data := $habari.parse("Hyouka (2012) S1-2 [BD 1080p HEVC OPUS] [Dual-Audio]") console.log(data.title) // Hyouka console.log(data.formatted_title) // Hyouka (2012) console.log(data.year) // 2012 console.log(data.season_number) // ["1", "2"] console.log(data.video_resolution) // 1080p ### $clone[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdclone) Useful for safely handling events. Copy function init() { $app.onPreUpdateEntryEvent(e => { $store.set("onPreUpdateEntry", $clone(e)) }) } ### $replace[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdreplace) The `$replace` function is used to overwrite properties of an object within an event. This only works on values that are not undefined and on values that are references under the hood. How it works Copy $app.onGetAnime((e) => { if(e.anime.id === 130003) { console.log(e.anime.title) // { // "english": "Bocchi the Rock!", // "romaji": "Bocchi the Rock!", // "userPreferred": "Bocchi the Rock!" // } e.anime.title = { "english": "The One Piece is Real" } console.log(e.anime.title) // { // "english": "The One Piece is Real", // "romaji": "Bocchi the Rock!", // "userPreferred": "Bocchi the Rock!" // } // ✅ Overwrite the entire 'title' object $replace(e.anime.title, { "english": "The One Piece is Real" }) console.log(e.anime.title) // { // "english": "The One Piece is Real", // "romaji": undefined, // "userPreferred": undefined // } e.anime.synonyms[0] = "The One Piece" // ✅ Works $replace(e.anime.synonyms[0], "The One Piece") // ✅ Works // ⛔️ Doesn't work because 'id' is not a reference under the hood $replace(e.anime.id, 22) // ⛔️ Doesn't work if 'bannerImage' is undefined $replace(e.anime.bannerImage, "abc") } e.next(); }) ### $torrentUtils[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdtorrentutils) Copy // Get a magnet link from a torrent file content const res = await fetch("http://[...].torrent") const content = res.text() $torrentUtils.getMagnetLinkFromTorrentData(content) ### $toString[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdtostring) Converts binary data to string. Copy const uint8Array = new Uint8Array(new ArrayBuffer(5)); uint8Array[0] = 104; uint8Array[1] = 101; uint8Array[2] = 108; uint8Array[3] = 108; uint8Array[4] = 111; console.log($toString(uint8Array)); // hello ### $toBytes[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdtobytes) Similar to the Web API `TextEncoder.encode` Copy const b = $toBytes("hello") console.log(b); // Uint8Array [104, 101, 108, 108, 111] console($toString(b)); // hello ### $sleep[](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis#usdsleep) Use carefully Copy // sleeps for 1s $sleep(1000) [PreviousGetting started](https://seanime.gitbook.io/seanime-extensions) [NextChangelog](https://seanime.gitbook.io/seanime-extensions/seanime/changelog) Last updated 3 months ago --- # Changelog | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/seanime/changelog.md) . v3.5.0[](https://seanime.gitbook.io/seanime-extensions/seanime/changelog#v3.5.0) --------------------------------------------------------------------------------- * Added `readme` to manifest v3.2.0[](https://seanime.gitbook.io/seanime-extensions/seanime/changelog#v3.2.0) --------------------------------------------------------------------------------- * `VideoCore` API introduced for UI context * `ctx.playback.seek` renamed to `ctx.playback.seekTo` v3.0.0[](https://seanime.gitbook.io/seanime-extensions/seanime/changelog#v3.0.0) --------------------------------------------------------------------------------- * Custom source extensions * `isDrawer` prop to `tray.newTray` v2.8.4[](https://seanime.gitbook.io/seanime-extensions/seanime/changelog#v2.8.4) --------------------------------------------------------------------------------- * `app.d.ts`, `plugin.d.ts` have been fixed and updated * `ctx.fieldRef` can now take a default value * New `ctx.eventHandler(callback)` for inlined event handler registration * New `className` prop for tray components [PreviousCore APIs](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis) [NextWrite, test, share](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share) Last updated 3 months ago --- # Online Streaming Provider | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider.md) . Difficulty: Moderate Use bootstrapping command[](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider#use-bootstrapping-command) You can use this third-party tool to help you quickly bootstrap a folder locally Copy npx seanime-tool g-template Types[](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider#types) --------------------------------------------------------------------------------------------------------- online-streaming-provider.d.ts Copy declare type SearchResult = { id: string title: string url: string subOrDub: SubOrDub } declare type SubOrDub = "sub" | "dub" | "both" declare type EpisodeDetails = { id: string number: number url: string title?: string } declare type EpisodeServer = { server: string headers: { [key: string]: string } videoSources: VideoSource[] } declare type VideoSourceType = "mp4" | "m3u8" | "unknown" declare type VideoSource = { url: string type: VideoSourceType // Quality or label of the video source, should be unique (e.g. "1080p", "1080p - English") quality: string // Secondary label of the video source (e.g. "English") label?: string subtitles: VideoSubtitle[] } declare type VideoSubtitle = { id: string url: string language: string isDefault: boolean } declare interface Media { id: number idMal?: number status?: string format?: string englishTitle?: string romajiTitle?: string episodeCount?: number absoluteSeasonOffset?: number synonyms: string[] isAdult: boolean startDate?: FuzzyDate } declare interface FuzzyDate { year: number month?: number day?: number } declare type SearchOptions = { media: Media query: string dub: boolean year?: number } declare type Settings = { episodeServers: string[] supportsDub: boolean } declare abstract class AnimeProvider { search(opts: SearchOptions): Promise findEpisodes(id: string): Promise findEpisodeServer(episode: EpisodeDetails, server: string): Promise getSettings(): Settings } Code[](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider#code) ------------------------------------------------------------------------------------------------------- Do not change the name of the class. It must be Provider. Copy /// class Provider { getSettings(): Settings { return { episodeServers: ["server1", "server2"], supportsDub: true, } } async search(query: SearchOptions): Promise { return [{\ id: "1",\ title: "Anime Title",\ url: "https://example.com/anime/1",\ subOrDub: "both",\ }] } async findEpisodes(id: string): Promise { return [{\ id: "1",\ number: 1,\ url: "https://example.com/episode/1",\ title: "Episode title",\ }] } async findEpisodeServer(episode: EpisodeDetails, _server: string): Promise { let server = "server1" if (_server !== "default") server = _server return { server: server, headers: {}, videoSources: [{\ url: "https://example.com/.../stream.m3u8",\ type: "m3u8",\ quality: "1080p",\ subtitles: [{\ id: "1",\ url: "https://example.com/.../subs.vtt",\ language: "en",\ isDefault: true,\ }],\ }], } } } Example[](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider#example) ------------------------------------------------------------------------------------------------------------- Copy /// /// type EpisodeData = { id: number; episode: number; title: string; snapshot: string; filler: number; session: string; created_at?: string } type AnimeData = { id: number; title: string; type: string; year: number; poster: string; session: string } class Provider { api = "https://example.com" headers = { Referer: "https://example.com" } getSettings(): Settings { return { episodeServers: ["kwik"], supportsDub: false, } } async search(opts: SearchOptions): Promise { const req = await fetch(`${this.api}/api?m=search&q=${encodeURIComponent(opts.query)}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }) if (!req.ok) { return [] } const data = (await req.json()) as { data: AnimeData[] } const results: SearchResult[] = [] if (!data?.data) { return [] } data.data.map((item: AnimeData) => { results.push({ subOrDub: "sub", id: item.session, title: item.title, url: "", }) }) return results } async findEpisodes(id: string): Promise { let episodes: EpisodeDetails[] = [] const req = await fetch( `${this.api}${id.includes("-") ? `/anime/${id}` : `/a/${id}`}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }, ) const html = await req.text() function pushData(data: EpisodeData[]) { for (const item of data) { episodes.push({ id: item.session + "$" + id, number: item.episode, title: item.title && item.title.length > 0 ? item.title : "Episode " + item.episode, url: req.url, }) } } const $ = LoadDoc(html) const tempId = $("head > meta[property='og:url']").attr("content")!.split("/").pop()! const { last_page, data } = (await ( await fetch(`${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=1`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }) ).json()) as { last_page: number; data: EpisodeData[] } pushData(data) const pageNumbers = Array.from({ length: last_page - 1 }, (_, i) => i + 2) const promises = pageNumbers.map((pageNumber) => fetch(`${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=${pageNumber}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }).then((res) => res.json()), ) const results = (await Promise.all(promises)) as { data: EpisodeData[] }[] results.forEach((showData) => { for (const data of showData.data) { if (data) { pushData([data]) } } }); (data as any[]).sort((a, b) => a.number - b.number) if (episodes.length === 0) { throw new Error("No episodes found.") } const lowest = episodes[0].number if (lowest > 1) { for (let i = 0; i < episodes.length; i++) { episodes[i].number = episodes[i].number - lowest + 1 } } // Remove episode with decimal numbers (those aren't supported) episodes = episodes.filter((episode) => Number.isInteger(episode.number)) return episodes } async findEpisodeServer(episode: EpisodeDetails, _server: string): Promise { const episodeId = episode.id.split("$")[0] const animeId = episode.id.split("$")[1] console.log(`${this.api}/play/${animeId}/${episodeId}`) const req = await fetch( `${this.api}/play/${animeId}/${episodeId}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }, ) const html = await req.text() const regex = /https:\/\/kwik\.si\/e\/\w+/g const matches = html.match(regex) if (matches === null) { throw new Error("Failed to fetch episode server.") } const $ = LoadDoc(html) const result: EpisodeServer = { videoSources: [], headers: this.headers ?? {}, server: "kwik", } $("button[data-src]").each(async (_, el) => { let videoSource: VideoSource = { url: "", type: "m3u8", quality: "", subtitles: [], } videoSource.url = el.data("src")! if (!videoSource.url) { return } const fansub = el.data("fansub")! const quality = el.data("resolution")! videoSource.quality = `${quality}p - ${fansub}` if (el.data("audio") === "eng") { videoSource.quality += " (Eng)" } if (videoSource.url === matches[0]) { videoSource.quality += " (default)" } result.videoSources.push(videoSource) }) const queries = result.videoSources.map(async (videoSource) => { try { const src_req = await fetch(videoSource.url, { headers: { Referer: this.headers.Referer, "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.56", }, }) const src_html = await src_req.text() const scripts = src_html.match(/eval\(f.+?\}\)\)/g) if (!scripts) { return } for (const _script of scripts) { const scriptMatch = _script.match(/eval(.+)/) if (!scriptMatch || !scriptMatch[1]) { continue } try { const decoded = eval(scriptMatch[1]) const link = decoded.match(/source='(.+?)'/) if (!link || !link[1]) { continue } videoSource.url = link[1] } catch (e) { console.error("Failed to extract kwik link", e) } } } catch (e) { console.error("Failed to fetch kwik link", e) } }) await Promise.all(queries) return result } } [PreviousManga Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider) [NextCustom Source](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source) Last updated 3 months ago --- # Custom Source | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source.md) . Note: You cannot test custom sources in the playground. Load them in development mode ([like here](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-2.-create-a-manifest-file) ). Difficulty: Moderate Type Definitions[](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source#type-definitions) ------------------------------------------------------------------------------------------------------------------- custom-source.d.ts Copy /// declare type Settings = { supportsAnime: boolean supportsManga: boolean } declare type ListResponse = { media: T[] page: number totalPages: number total: number } declare abstract class CustomSource { getSettings(): Settings async getAnime(ids: number[]): Promise<$app.AL_BaseAnime[]> async getAnimeMetadata(id: number): Promise<$app.Metadata_AnimeMetadata | null> async getAnimeWithRelations(id: number): Promise<$app.AL_CompleteAnime> async getAnimeDetails(id: number): Promise<$app.AL_AnimeDetailsById_Media | null> async getManga(ids: number[]): Promise<$app.AL_BaseManga[]> async listAnime(search: string, page: number, perPage: number): Promise> async getMangaDetails(id: number): Promise<$app.AL_MangaDetailsById_Media | null> async listManga(search: string, page: number, perPage: number): Promise> } Keyword search the various $app types used here: [https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension\_repo/goja\_plugin\_types/app.d.tsraw.githubusercontent.com](https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension_repo/goja_plugin_types/app.d.ts) Code[](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source#code) ------------------------------------------------------------------------------------------- Do not change the name of the class. It must be Provider. You can define the media objects in an external API and use fetch to retrieve them dynamically. ### Media objects[](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source#media-objects) Under the hood, custom source media are treated like AniList media, which is the reason why you need to return objects following AniList's JSON schemas. For the media `id`s, you're free to use any number starting from 1. Under the hood, Seanime will automatically convert these IDs to unique numbers to avoid conflicts. Copy /// const anime: Record = {} const animeMetadata: Record = {} const manga: Record = {} class Provider implements CustomSource { getSettings(): Settings { return { supportsAnime: true, supportsManga: true, } } // Returns all requested anime objects. async getAnime(ids: number[]): Promise<$app.AL_BaseAnime[]> { let ret: $app.AL_BaseAnime[] = [] for (const id of ids) { if (anime[id]) { // Here we make a deep copy and remove the 'relations' attribute // this turn AL_CompleteAnime into AL_BaseAnime const a = $clone(media[id]) as $app.AL_CompleteAnime delete a["relations"] ret.push(a) } } return ret } // Optionally returns the details for an anime (genres, trailer, etc.) // Note that not all the fields are used by the client. async getAnimeDetails(id: number): Promise<$app.AL_AnimeDetailsById_Media | null> { return null } // Returns the metadata for an anime. // This is used for episodes. async getAnimeMetadata(id: number): Promise<$app.Metadata_AnimeMetadata | null> { return animeMetadata[id] } // Returns the anime object with its 'relations'. // This is only used by the library scanner to build a relation tree. async getAnimeWithRelations(id: number): Promise<$app.AL_CompleteAnime> { if (media[id]) { return media[id] as $app.AL_CompleteAnime } throw new Error("not found.") } // Returns all requested manga objects. async getManga(ids: number[]): Promise<$app.AL_BaseManga[]> { let ret: $app.AL_BaseManga[] = [] for (const id of ids) { if (manga[id]) { ret.push(manga[id]) } } return ret } // Optionally returns the manga details. // Similarly to getAnimeDetails, not all fields will be used by the client. async getMangaDetails(id: number): Promise<$app.AL_MangaDetailsById_Media | null> { return null } // Returns all anime available on the extension. async listAnime(search: string, page: number, perPage: number): Promise> { return { media: Object.values(media), total: 1, page: 1, totalPages: 1, } } // Returns all manga available on the extension. async listManga(search: string, page: number, perPage: number): Promise> { return { media: Object.values(manga), total: 1, page: 1, totalPages: 1, } } } [PreviousOnline Streaming Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider) [NextIntroduction](https://seanime.gitbook.io/seanime-extensions/plugins/introduction) Last updated 3 months ago --- # Introduction | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/introduction.md) . A plugin is a type of extension that allows for more in-depth customization and addition of new features through multiple APIs. What can plugins do?[](https://seanime.gitbook.io/seanime-extensions/plugins/introduction#what-can-plugins-do) --------------------------------------------------------------------------------------------------------------- With the right permissions, a lot. Here is a high overview of what they're capable of: * Create a tray icon and display dynamic content in the tray * Add buttons, context menu items, dropdown items to specific places * Create a dynamic command palette * Register hooks to modify server-side behavior * Run commands, access the file system, create, read, edit files etc. * Communicate with AniList and other APIs * Fetch and store data * Manipulate the DOM * And more The plugin system is largely inspired by PocketBase. [PreviousCustom Source](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source) [NextWrite, test, share](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share) Last updated 3 months ago --- # Write, test, share | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share.md) . 1\. Create a JS/TS file[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-1.-create-a-js-ts-file) ---------------------------------------------------------------------------------------------------------------------------- Shortcut: Use bootstrapping command[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#shortcut-use-bootstrapping-command) You can use this third-party tool to help you quickly bootstrap a plugin folder locally Copy npx seanime-tool g-template my-plugin.ts Copy function init() { // This function is called when the plugin is loaded // There is no guarantee as to when exactly the plugin will be loaded at startup } 2\. Create a manifest file[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-2.-create-a-manifest-file) ---------------------------------------------------------------------------------------------------------------------------------- The ID should be unique, in case of a conflict with another extension, your plugin might not be loaded. The name of the file should be the same as the ID. Here we're going with Typescript. We're setting `isDevelopment`to `true` in order to be able to quickly reload it when we make changes. `payloadURI` in this case is the path to the plugin code, it must be an absolute path. Obviously, before sharing the extension we'll change the `payloadURI` to the URL of the file containing the code and remove `isDevelopment`. my-plugin.json Copy { "id": "my-plugin", "name": "My Plugin", "version": "1.0.0", "manifestURI": "", "language": "typescript", "type": "plugin", "description": "An example plugin", "author": "Seanime", "icon": "", "website": "", "readme": "", "notes": "", "lang": "multi", "payloadURI": "C:/path/to/my-plugin/code.ts", "plugin": { "version": "1", "permissions": {} }, "isDevelopment": true } 3\. Quick overview[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-3.-quick-overview) ------------------------------------------------------------------------------------------------------------------ ### a. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#a.-permissions) Some APIs require specific permissions in order to function. The user of your plugin will need to **grant** them after the installation. ### b. Hooks[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#b.-hooks) You can register hook callbacks to listen to various types of events happening on the server, modify them or execute custom logic. Learn more about hooks in later sections. **In a nutshell** Hooks can be used to listen to or edit server-side events. For example: Example Copy function init() { // This hook is triggered before Seanime formats the library data of an anime // The event contains the variables that Seanime will use, and you can modify them $app.onAnimeEntryLibraryDataRequested((e) => { // Setting this to an empty array will cause Seanime to think that the anime // has not been downloaded. e.entryLocalFiles = [] e.next() // Continue hook chain }) } Each hook handler must call `e.next()` in order for the hook chain listening to that event to proceed. Not calling it will impact other plugins listening to that event. ### c. UI Context[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#c.-ui-context) Hooks are great for customizing server-side behavior but most **business logic** and interface interactions will be done in the UI context. **In a nutshell** Think of the UI context as the "main thread" of your plugin and hooks can be thought of as "worker threads". Hooks and UI Context can be used alongside each other. In the later section you will learn how communication is done between them. Copy // 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("scan-completed", (value) => { const now = new Date().toISOString().replaceall(".", "_") // Add the value to the history // Note that this could have been done in the hook callback BUT // the UI context is better suited for business logic $storage.set("scan-duration-history."+now, value) ctx.toast.info(`Scanning took ${value/1000} seconds!`) }) }) } ### d. Javascript restrictions[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#d.-javascript-restrictions) The UI context and each hook callback are run in isolated environments (called runtimes), and thus, cannot share state easily or read global variables. Copy const globalVar = 42; function init() { const value = 42; $app.onGetAnime((e) => { console.log(globalVar) // undefined console.log(value) // undefined }) $ui.register((ctx) => { console.log(globalVar) // undefined console.log(value) // undefined }) } However, you can still share variables between hooks and the UI context using [$store](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store) . [Store](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store) ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252F7LbQgNfwe5gmmv7eDKqQ%252Fimg-2025-04-28-10-02-09%25402x.png%3Falt%3Dmedia%26token%3D407552a2-42e6-40b3-bb7e-84415c133faf&width=768&dpr=3&quality=100&sign=1c4b30c2&sv=2) Diagram of plugin ### e. Types[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#e.-types) Add the type definition files located here, in addition to `core.d.ts` [https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension\_repo/goja\_plugin\_types/app.d.tsraw.githubusercontent.com](https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension_repo/goja_plugin_types/app.d.ts) app.d.ts [https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension\_repo/goja\_plugin\_types/plugin.d.tsraw.githubusercontent.com](https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension_repo/goja_plugin_types/plugin.d.ts) plugin.d.ts [https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension\_repo/goja\_plugin\_types/system.d.tsraw.githubusercontent.com](https://raw.githubusercontent.com/5rahim/seanime/refs/heads/main/internal/extension_repo/goja_plugin_types/system.d.ts) system.d.ts my-plugin.ts Copy /// /// /// /// function init() { // Everything is magically typed! $ui.register((ctx) => { ctx.dom.onReady(() => { console.log("Page loaded!") }) }) } 4\. Write and test[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-4.-write-and-test) ------------------------------------------------------------------------------------------------------------------ You're good to go! ### Code the extension[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#code-the-extension) [APIs](https://seanime.gitbook.io/seanime-extensions/plugins/apis) [UI](https://seanime.gitbook.io/seanime-extensions/plugins/ui) [Hooks](https://seanime.gitbook.io/seanime-extensions/plugins/hooks) ### Test it live[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#test-it-live) In order to test your plugin, add the manifest file inside the `extensions` directory which is inside your [data directory](https://seanime.rahim.app/docs/config#data-directory) . Because you've set `isDevelopement` to true in your manifest file, you will be able to manually reload the extension without having to restart the app. It's recommended to test your plugin with the web-app version of Seanime for convenience. 5\. Share[](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share#id-5.-share) ------------------------------------------------------------------------------------------------ If you want to share your plugin with others, you can host both the code and manifest file on GitHub and [share](https://seanime.rahim.app/community/extensions) the link to the file. Make sure to replace `payloadURI` with the URL of the hosted file containing the code. Also, remove `isDevelopment` . [PreviousIntroduction](https://seanime.gitbook.io/seanime-extensions/plugins/introduction) [NextPermissions](https://seanime.gitbook.io/seanime-extensions/plugins/permissions) Last updated 3 months ago --- # Write, test, share | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share.md) . Content providers are a type of extension used to add more sources to existing features in Seanime. * Anime torrent providers * Manga providers * Online streaming providers * Custom sources 1\. Write and test[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-1.-write-and-test) ---------------------------------------------------------------------------------------------------------------------------- ### Code the extension[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#code-the-extension) [Anime Torrent Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider) [Manga Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider) [Online Streaming Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider) [Custom Source](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source) ### Test in the playground[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#test-in-the-playground) 1. Go to the `Extensions` page in Seanime. 2. Click on the `Playground` dropdown option. ![Playground](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2Fi.postimg.cc%2Ffyy9xGmG%2FClean-Shot-2024-08-25-at-14-30-362x.webp&width=300&dpr=3&quality=100&sign=8f6270b3&sv=2) 1. Select which type of extension you want to test and enter the code. You will be able to select the **method (function)** you want to test. Different methods have different **simulation parameters** based on real in-app usage. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FkxxyvzknBvrL73Uspr8A%252Fimage.png%3Falt%3Dmedia%26token%3D36258759-33d0-4c2b-9b7d-3747b5eb6fd5&width=768&dpr=3&quality=100&sign=1c9dd187&sv=2) 2\. Create a manifest file[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-2.-create-a-manifest-file) -------------------------------------------------------------------------------------------------------------------------------------------- ### Create the file[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#create-the-file) Make the ID unique in order to avoid conflicts. The name of the file should be the same as the ID. * `id`: ID of your extension. * `name`: The name of the extension. * `description`: A short description of the extension. * `manifestURI`: The URI where the manifest file is hosted. Used by Seanime to check for updates. This can be empty if you don’t plan on hosting and sharing your extension. * `version`: The version of the extension. `x.x.x` (e.g. 0.1.0) * `author`: The author of the extension. * `type`: The type of extension. See below for the available types. * `anime-torrent-provider`, `manga-provider`, `onlinestream-provider` , `custom-source` * `language`: The **programming language** of the extension. * Can be `**typescript**`**, or** `**javascript**`. * `lang`: **ISO 639-1** language of the extension’s content (e.g. “en”, “fr” etc.). * Set it to `**multi**` if your extension supports multiple languages. * `readme`: URL to documentation * `notes`: Additional info ### Paste the payload[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#paste-the-payload) You have two options: 1. Paste the code of your extension in the `payload` field. 2. Paste a URL to the code of your extension in the `payloadURI` field and remove `payload` empty. 3\. Share[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-3.-share) ---------------------------------------------------------------------------------------------------------- If you want to share your extension with others, you can host the manifest file on GitHub and [share](https://seanime.rahim.app/community/extensions) the link to the file. If you just want to use it for yourself, just place the JSON file in the `extensions` directory in your [data directory](https://seanime.rahim.app/docs/config#data-directory) . 4\. Update your extension[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-4.-update-your-extension) ------------------------------------------------------------------------------------------------------------------------------------------ This is a simple process. Just update the `version` field in the JSON file and paste the new code in the `payload` field. Your extension might become incompatible with a later version of Seanime. Check the [Extension Changelog](https://seanime.gitbook.io/seanime-extensions/seanime/changelog) for breaking changes and update your code accordingly. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2Fi.postimg.cc%2FRVzjPvNQ%2FClean-Shot-2024-08-27-at-18-49-172x.webp&width=768&dpr=3&quality=100&sign=81cccbfd&sv=2) Do not change your extension ID between updates Add user configuration (optional)[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#add-user-configuration-optional) ------------------------------------------------------------------------------------------------------------------------------------------------------ You can make it so users can enter arbitrary values that you can use in variables inside your code. This is useful when your extension needs to use a personal API key for example. Guide[](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#guide) ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FChRoJaG3DImgeDNpyyCm%252Fimage.png%3Falt%3Dmedia%26token%3Da059d8d0-25b0-4454-8936-0544a2cfac4a&width=300&dpr=3&quality=100&sign=a53a9309&sv=2) * Declare any number of **string** variables containing the configuration field keys you want to accept in the format `{{key}}`. These variables will be replaced with the values the user entered when the extension is loaded. * In your manifest file, add a `userConfig` field. The field's 'name' should be the same as the key between the double curly brackets in your code. * `requiresConfig`: Set to `true` to force the user to validate the configuration before the extension is loaded. * `version`: The version of the configuration. Increment this number when you make changes to the configuration fields of your extension. [PreviousChangelog](https://seanime.gitbook.io/seanime-extensions/seanime/changelog) [NextAnime Torrent Provider](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider) Last updated 3 months ago * [1\. Write and test](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-1.-write-and-test) * [Code the extension](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#code-the-extension) * [Test in the playground](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#test-in-the-playground) * [2\. Create a manifest file](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-2.-create-a-manifest-file) * [Create the file](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#create-the-file) * [Paste the payload](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#paste-the-payload) * [3\. Share](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-3.-share) * [4\. Update your extension](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#id-4.-update-your-extension) * [Add user configuration (optional)](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share#add-user-configuration-optional) my-original-extension-id.json Copy { "id": "my-original-extension-id", "name": "My Extension Name", "description": "My Extension Description", "manifestURI": "", "version": "1.0.0", "author": "Author Name", "type": "", "language": "", "lang": "", "payload": "" } Copy { //... "userConfig": { "requiresConfig": true, "version": 1, "fields": [\ {\ "name": "api",\ "label": "API URL",\ "type": "text",\ "default": "https://feed.animetosho.org/json"\ },\ {\ "name": "withSmartSearch",\ "label": "Enable Smart Search",\ "type": "switch",\ "default": "true"\ },\ {\ "name": "type",\ "label": "Provider Type",\ "type": "select",\ "default": "main",\ "options": [\ {\ "label": "Main",\ "value": "main"\ },\ {\ "label": "Special",\ "value": "special"\ }\ ]\ }\ ] } } --- # Permissions | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/permissions.md) . Network Requests[](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#network-requests) ------------------------------------------------------------------------------------------------------- For `reasoning`, provide a brief explanation for the access scope permitted by `allowedDomains` . This will be displayed to the users. Copy { //... "plugin": { "version": "1", "permissions": { "scopes": [], "allow": { "networkAccess": { "allowedDomains": ["example.com", "*.example.com"], "reasoning": "This is mandatory" } } } } } Unsafe[](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#unsafe) ----------------------------------------------------------------------------------- ### DOM Script Manipulation[](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#dom-script-manipulation) This flag is required if you want to manipulate script tags or inject custom javascript in the DOM. ### DOM Link Manipulation[](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#dom-link-manipulation) [PreviousWrite, test, share](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share) [NextAPIs](https://seanime.gitbook.io/seanime-extensions/plugins/apis) Last updated 3 months ago * [Network Requests](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#network-requests) * [Unsafe](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#unsafe) * [DOM Script Manipulation](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#dom-script-manipulation) * [DOM Link Manipulation](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#dom-link-manipulation) Copy { //... "plugin": { "version": "1", "permissions": { "scopes": [], "allow": { "unsafeFlags": [\ {\ "flag": "dom-script-manipulation",\ "reason": "Enter your reason here, this will be shown to the user."\ }\ ] } } } } --- # Unknown \# Seanime Extensions ## Seanime Extensions - \[Getting started\](https://seanime.gitbook.io/seanime-extensions/seanime/readme.md) - \[Core APIs\](https://seanime.gitbook.io/seanime-extensions/seanime/core-apis.md): These APIs are shared between all types of extension: content providers and plugins. - \[Changelog\](https://seanime.gitbook.io/seanime-extensions/seanime/changelog.md): Important changes are logged here. - \[Write, test, share\](https://seanime.gitbook.io/seanime-extensions/content-providers/write-test-share.md) - \[Anime Torrent Provider\](https://seanime.gitbook.io/seanime-extensions/content-providers/anime-torrent-provider.md) - \[Manga Provider\](https://seanime.gitbook.io/seanime-extensions/content-providers/manga-provider.md) - \[Online Streaming Provider\](https://seanime.gitbook.io/seanime-extensions/content-providers/online-streaming-provider.md) - \[Custom Source\](https://seanime.gitbook.io/seanime-extensions/content-providers/custom-source.md): This extension type allows you to add custom media to Seanime. - \[Introduction\](https://seanime.gitbook.io/seanime-extensions/plugins/introduction.md) - \[Write, test, share\](https://seanime.gitbook.io/seanime-extensions/plugins/write-test-share.md): How to start coding - \[Permissions\](https://seanime.gitbook.io/seanime-extensions/plugins/permissions.md) - \[APIs\](https://seanime.gitbook.io/seanime-extensions/plugins/apis.md) - \[Helpers\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers.md) - \[Store\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store.md): Key-value store. - \[Storage\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage.md): Persistent storage. - \[Database\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database.md): Interact with parts of Seanime's database. - \[AniList\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist.md): Interact with the user's AniList account. - \[Shared Modules\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared.md): Reuse helper factories across plugin runtimes. - \[Debug\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug.md): Development-only logging helpers for plugin runtimes. - \[System\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system.md): The System APIs give you access to a set of methods for file operations, downloading and more. - \[Permissions\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions.md): The System APIs give you access to a set of methods for file operations, downloading and more. - \[OS\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os.md): OS-agonistic APIs for operating system functionality, such as interacting with the filesystem. - \[Filepath\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath.md) - \[Commands\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands.md) - \[Buffers, I/O\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o.md) - \[MIME\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime.md) - \[UI\](https://seanime.gitbook.io/seanime-extensions/plugins/ui.md) - \[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) - \[Hooks\](https://seanime.gitbook.io/seanime-extensions/plugins/hooks.md) - \[Example\](https://seanime.gitbook.io/seanime-extensions/plugins/example.md) - \[Feature requests\](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests.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/write-test-share.md). # Write, test, share ## 1. Create a JS/TS file Shortcut: Use bootstrapping command You can use this third-party tool to help you quickly bootstrap a plugin folder locally \`\`\`bash npx seanime-tool g-template \`\`\` {% code title="my-plugin.ts" %} \`\`\`typescript function init() { // This function is called when the plugin is loaded // There is no guarantee as to when exactly the plugin will be loaded at startup } \`\`\` {% endcode %} ## 2. Create a manifest file {% hint style="warning" %} The ID should be unique, in case of a conflict with another extension, your plugin might not be loaded. The name of the file should be the same as the ID. {% endhint %} Here we're going with Typescript. We're setting \`isDevelopment\`to \`true\` in order to be able to quickly reload it when we make changes. \`payloadURI\` in this case is the path to the plugin code, it must be an absolute path. Obviously, before sharing the extension we'll change the \`payloadURI\` to the URL of the file containing the code and remove \`isDevelopment\`. { "id": "my-plugin", "name": "My Plugin", "version": "1.0.0", "manifestURI": "", "language": "typescript", "type": "plugin", "description": "An example plugin", "author": "Seanime", "icon": "", "website": "", "readme": "", "notes": "", "lang": "multi", "payloadURI": "C:/path/to/my-plugin/code.ts", "plugin": { "version": "1", "permissions": {} }, "isDevelopment": true } \## 3. Quick overview ### a. Permissions Some APIs require specific permissions in order to function. The user of your plugin will need to \*\*grant\*\* them after the installation. ### b. Hooks You can register hook callbacks to listen to various types of events happening on the server, modify them or execute custom logic. Learn more about hooks in later sections. {% hint style="info" %} \*\*In a nutshell\*\* Hooks can be used to listen to or edit server-side events. {% endhint %} For example: {% code title="Example" overflow="wrap" %} \`\`\`typescript function init() { // This hook is triggered before Seanime formats the library data of an anime // The event contains the variables that Seanime will use, and you can modify them $app.onAnimeEntryLibraryDataRequested((e) => { // Setting this to an empty array will cause Seanime to think that the anime // has not been downloaded. e.entryLocalFiles = \[\] e.next() // Continue hook chain }) } \`\`\` {% endcode %} {% hint style="warning" %} Each hook handler must call \`e.next()\` in order for the hook chain listening to that event to proceed. Not calling it will impact other plugins listening to that event. {% endhint %} ### c. UI Context Hooks are great for customizing server-side behavior but most \*\*business logic\*\* and interface interactions will be done in the UI context. {% hint style="info" %} \*\*In a nutshell\*\* Think of the UI context as the "main thread" of your plugin and hooks can be thought of as "worker threads". {% endhint %} Hooks and UI Context can be used alongside each other. In the later section you will learn how communication is done between them. \`\`\`typescript // 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("scan-completed", (value) => { const now = new Date().toISOString().replaceall(".", "\_") // Add the value to the history // Note that this could have been done in the hook callback BUT // the UI context is better suited for business logic $storage.set("scan-duration-history."+now, value) ctx.toast.info(\`Scanning took ${value/1000} seconds!\`) }) }) } \`\`\` ### d. Javascript restrictions The UI context and each hook callback are run in isolated environments (called runtimes), and thus, cannot share state easily or read global variables. {% code overflow="wrap" %} \`\`\`typescript const globalVar = 42; function init() { const value = 42; $app.onGetAnime((e) => { console.log(globalVar) // undefined console.log(value) // undefined }) $ui.register((ctx) => { console.log(globalVar) // undefined console.log(value) // undefined }) } \`\`\` {% endcode %} However, you can still share variables between hooks and the UI context using \[$store\](/seanime-extensions/plugins/apis/store.md). {% content-ref url="/pages/eOtc0rCPGHayShUyimol" %} \[Store\](/seanime-extensions/plugins/apis/store.md) {% endcontent-ref %} ![](https://seanime.gitbook.io/files/r0YqTD9HG25hokoigNnO) Diagram of plugin \### e. Types Add the type definition files located here, in addition to \`core.d.ts\` {% embed url="" %} app.d.ts {% endembed %} {% embed url="" %} plugin.d.ts {% endembed %} {% embed url="" %} system.d.ts {% endembed %} {% code title="my-plugin.ts" %} \`\`\`typescript /// /// /// /// function init() { // Everything is magically typed! $ui.register((ctx) => { ctx.dom.onReady(() => { console.log("Page loaded!") }) }) } \`\`\` {% endcode %} ## 4. Write and test You're good to go! ### Code the extension {% content-ref url="/pages/GPwSadMQcB1MBO4U6RHf" %} \[APIs\](/seanime-extensions/plugins/apis.md) {% endcontent-ref %} {% content-ref url="/pages/tCMfSxrXGfjuywgytCU2" %} \[UI\](/seanime-extensions/plugins/ui.md) {% endcontent-ref %} {% content-ref url="/pages/qyfCjgRpNaZm6PkKeADL" %} \[Hooks\](/seanime-extensions/plugins/hooks.md) {% endcontent-ref %} ### Test it live In order to test your plugin, add the manifest file inside the \`extensions\` directory which is inside your \[data directory\](https://seanime.rahim.app/docs/config#data-directory). Because you've set \`isDevelopement\` to true in your manifest file, you will be able to manually reload the extension without having to restart the app. It's recommended to test your plugin with the web-app version of Seanime for convenience. ## 5. Share If you want to share your plugin with others, you can host both the code and manifest file on GitHub and \[share\](https://seanime.rahim.app/community/extensions) the link to the file. {% hint style="info" %} Make sure to replace \`payloadURI\` with the URL of the hosted file containing the code. Also, remove \`isDevelopment\` . {% endhint %} --- # 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/seanime/core-apis.md). # Core APIs ## Types Create a \`core.d.ts\` file containing the following content: {% embed url="" %} core.d.ts {% endembed %} ## Examples ### User preference Returns the user preference value from \[Write, test, share\](/seanime-extensions/content-providers/write-test-share.md#add-user-configuration-optional) \`\`\`typescript $getUserPreference("apiToken") \`\`\` ### Console \`\`\`typescript console.log() console.warn() console.error() \`\`\` ### Fetch {% code title="Example" %} \`\`\`typescript /// const res = await fetch("https://jsonplaceholder.typicode.com/todos/1") const data = res.json() \`\`\` {% endcode %} ### Doc Parse HTML \`\`\`typescript const $ = LoadDoc(\` First Post ---------- This is the first post. [Read more](https://example.com/first-post) Second Post ----------- This is the second post. [Read more](https://example.com/second-post) Third Post ---------- This is the third post. [Read more](https://example.com/third-post) \`); const titles = $("section") .children("article.post") .filter((i, e) => e.attr("data-id") !== "1") .map((i, e) => e.children("h2").text()) console.log(titles) // \[Second Post, Third Post\] \`\`\` ### ChromeDP Headless browser powered by the user's installed Chrome/Chromium binary. {% hint style="warning" %} ChromeDP is experimental and is not guaranteed to work. Always call \`close()\` to avoid leaking resources. {% endhint %} \`\`\`typescript // -------- Content Providers ---------- const browser = await ChromeDP.newBrowser(); await browser.navigate("https://example.com"); // Wait for the dynamic item to appear await browser.waitVisible("#dynamic-item"); // Get the text of the dynamic item const itemText = await browser.text("#dynamic-item"); await browser.close(); // ------------- Plugins --------------- function init() { $ui.register((ctx: $ui.Context) => { async function doSomething() { const browser = await ctx.chromeDP.newBrowser() // ... await browser.close() } }) } \`\`\` ### CryptoJS \`\`\`typescript let message = "seanime"; let key = CryptoJS.enc.Utf8.parse("secret key"); console.log("Message:", message); let encrypted = CryptoJS.AES.encrypt(message, key); console.log("Encrypted:", encrypted); // map\[iv toString\] console.log("Encrypted.toString():", encrypted.toString()); // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk= console.log("Encrypted.toString(CryptoJS.enc.Base64):", encrypted.toString(CryptoJS.enc.Base64)); // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk= let decrypted = CryptoJS.AES.decrypt(encrypted, key); console.log("Decrypted:", decrypted.toString(CryptoJS.enc.Utf8)); let iv = CryptoJS.enc.Utf8.parse("3134003223491201"); encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv }); console.log("Encrypted:", encrypted); // map\[iv toString\] decrypted = CryptoJS.AES.decrypt(encrypted, key); console.log("Decrypted without IV:", decrypted.toString(CryptoJS.enc.Utf8)); // "" <- Nothing decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv: iv }); console.log("Decrypted with IV:", decrypted.toString(CryptoJS.enc.Utf8)); // seanime let a = CryptoJS.enc.Utf8.parse("Hello, World!"); console.log(a); // // Uint8Array \[72 101 108 108 ...\] let b = CryptoJS.enc.Base64.stringify(a); console.log(b); // SGVsbG8sIFdvcmxkIQ== let c = CryptoJS.enc.Base64.parse(b); console.log(c); // Uint8Array \[72 101 108 108 ...\] let d = CryptoJS.enc.Utf8.stringify(c); console.log(d); // Hello, World! \`\`\` ### $habari Filename parser \`\`\`typescript data := $habari.parse("Hyouka (2012) S1-2 \[BD 1080p HEVC OPUS\] \[Dual-Audio\]") console.log(data.title) // Hyouka console.log(data.formatted\_title) // Hyouka (2012) console.log(data.year) // 2012 console.log(data.season\_number) // \["1", "2"\] console.log(data.video\_resolution) // 1080p \`\`\` ### $clone Useful for safely handling events. \`\`\`typescript function init() { $app.onPreUpdateEntryEvent(e => { $store.set("onPreUpdateEntry", $clone(e)) }) } \`\`\` ### $replace The \`$replace\` function is used to overwrite properties of an object within an event. {% hint style="warning" %} This only works on values that are not undefined and on values that are references under the hood. {% endhint %} {% tabs %} {% tab title="How it works" %} {% code overflow="wrap" %} \`\`\`typescript $app.onGetAnime((e) => { if(e.anime.id === 130003) { console.log(e.anime.title) // { // "english": "Bocchi the Rock!", // "romaji": "Bocchi the Rock!", // "userPreferred": "Bocchi the Rock!" // } e.anime.title = { "english": "The One Piece is Real" } console.log(e.anime.title) // { // "english": "The One Piece is Real", // "romaji": "Bocchi the Rock!", // "userPreferred": "Bocchi the Rock!" // } // ✅ Overwrite the entire 'title' object $replace(e.anime.title, { "english": "The One Piece is Real" }) console.log(e.anime.title) // { // "english": "The One Piece is Real", // "romaji": undefined, // "userPreferred": undefined // } e.anime.synonyms\[0\] = "The One Piece" // ✅ Works $replace(e.anime.synonyms\[0\], "The One Piece") // ✅ Works // ⛔️ Doesn't work because 'id' is not a reference under the hood $replace(e.anime.id, 22) // ⛔️ Doesn't work if 'bannerImage' is undefined $replace(e.anime.bannerImage, "abc") } e.next(); }) \`\`\` {% endcode %} {% endtab %} {% endtabs %} ### $torrentUtils \`\`\`typescript // Get a magnet link from a torrent file content const res = await fetch("http://\[...\].torrent") const content = res.text() $torrentUtils.getMagnetLinkFromTorrentData(content) \`\`\` ### $toString Converts binary data to string. \`\`\`typescript const uint8Array = new Uint8Array(new ArrayBuffer(5)); uint8Array\[0\] = 104; uint8Array\[1\] = 101; uint8Array\[2\] = 108; uint8Array\[3\] = 108; uint8Array\[4\] = 111; console.log($toString(uint8Array)); // hello \`\`\` ### $toBytes Similar to the Web API \`TextEncoder.encode\` \`\`\`typescript const b = $toBytes("hello") console.log(b); // Uint8Array \[104, 101, 108, 108, 111\] console($toString(b)); // hello \`\`\` ### $sleep {% hint style="warning" %} Use carefully {% endhint %} \`\`\`typescript // sleeps for 1s $sleep(1000) \`\`\` --- # 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/introduction.md). # Introduction A plugin is a type of extension that allows for more in-depth customization and addition of new features through multiple APIs. ## What can plugins do? With the right permissions, a lot. Here is a high overview of what they're capable of: \* Create a tray icon and display dynamic content in the tray \* Add buttons, context menu items, dropdown items to specific places \* Create a dynamic command palette \* Register hooks to modify server-side behavior \* Run commands, access the file system, create, read, edit files etc. \* Communicate with AniList and other APIs \* Fetch and store data \* Manipulate the DOM \* And more The plugin system is largely inspired by PocketBase. --- # 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/permissions.md). # Permissions ## Network Requests For \`reasoning\`, provide a brief explanation for the access scope permitted by \`allowedDomains\` . This will be displayed to the users. { //... "plugin": { "version": "1", "permissions": { "scopes": [], "allow": { "networkAccess": { "allowedDomains": ["example.com", "*.example.com"], "reasoning": "This is mandatory" } } } } } \## Unsafe ### DOM Script Manipulation This flag is required if you want to manipulate script tags or inject custom javascript in the DOM. { //... "plugin": { "version": "1", "permissions": { "scopes": [], "allow": { "unsafeFlags": [\ {\ "flag": "dom-script-manipulation",\ "reason": "Enter your reason here, this will be shown to the user."\ }\ ] } } } } \### DOM Link Manipulation --- # 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/content-providers/manga-provider.md). # Manga Provider {% hint style="success" %} Difficulty: Easy {% endhint %} Use bootstrapping command You can use this third-party tool to help you quickly bootstrap a folder locally \`\`\`bash npx seanime-tool g-template \`\`\` \## Types {% code title="manga-provider.d.ts" %} \`\`\`typescript declare type SearchResult = { id: string title: string synonyms?: string\[\] year?: number image?: string } declare type ChapterDetails = { id: string url: string title: string chapter: string index: number scanlator?: string language?: string rating?: number updatedAt?: string } declare type ChapterPage = { url: string index: number headers: { \[key: string\]: string } } declare type QueryOptions = { query: string year?: number } declare type Settings = { supportsMultiLanguage?: boolean supportsMultiScanlator?: boolean } declare abstract class MangaProvider { search(opts: QueryOptions): Promise findChapters(id: string): Promise findChapterPages(id: string): Promise getSettings(): Settings } \`\`\` {% endcode %} ## Code {% hint style="warning" %} Do not change the name of the class. It must be Provider. {% endhint %} \`\`\`typescript /// class Provider { private api = "https://example.com" getSettings(): Settings { return { supportsMultiLanguage: false, supportsMultiScanlator: false, } } // Returns the search results based on the query. async search(opts: QueryOptions): Promise { // TODO return \[{ id: "999", title: "Manga Title", synonyms: \["Synonym 1", "Synonym 2"\], year: 2021, image: "https://example.com/image.jpg", }\] } // Returns the chapters based on the manga ID. // The chapters should be sorted in ascending order (0, 1, ...). async findChapters(mangaId: string): Promise { // TODO return \[{ id: \`999-chapter-1\`, url: "https://example.com/manga/999-chapter-1", title: "Chapter 1", chapter: "1", index: 0, }\] } // Returns the chapter pages based on the chapter ID. // The pages should be sorted in ascending order (0, 1, ...). async findChapterPages(chapterId: string): Promise { // TODO return \[{ url: "https://example.com/manga/999-chapter-1/page-1.jpg", index: 0, headers: { "Referer": "https://example.com/manga/999/chapter-1", }, }\] } } \`\`\` ### Workflow \`search\` is called twice when the user opens the manga page. Each time with a different manga title as query (English, Romaji). The best match will automatically be selected and \`findChapters\` will be called with the manga ID from the search result to get the list of chapters. ![](https://seanime.gitbook.io/files/DUN6kwPi86xeDpr8O8g1) \`findChapterPages\` is called when the user requests to read or download the chapter. ![](https://seanime.gitbook.io/files/hym5smMeeXX8lBXgUDK6) \### Manga ID, Chapter ID Depending on the source website you’re getting the data from, the URLs might get a little complex. For example, if a manga’s chapter page is: \[\`https://example.com/manga/999/chapter-1\`\](https://example.com/manga/999/chapter-1) consisting of 2 URL sections (in this case, the manga ID and the chapter ID), you can construct the Seanime chapter ID by combining the two parts and splitting them in \`findChapterPages\` . ![](https://seanime.gitbook.io/files/xly88IEwx8VVpRDNkvpA) \### Settings \* If your manga source supports multiple languages for chapters and you want your extension to give this option to the users, set \`supportsMultiLanguage\` to \`true\` and set the \`language\` property for each of the \`ChapterDetails\`. Preferably \[ISO 639-1\](https://en.wikipedia.org/wiki/ISO\_639-1). \* Similarly, you can also give the option to choose a scanlator by setting \`supportsMultiScanlator\` to \`true\` and setting the \`scanlator\` property for each of the \`ChapterDetails\`. ![](https://seanime.gitbook.io/files/UbYNcrjUI0UJyr3R4j73) \## Example \`\`\`typescript /// class Provider { private api = "https://api.comick.fun" getSettings(): Settings { return { supportsMultiLanguage: false, supportsMultiScanlator: false, } } async search(opts: QueryOptions): Promise { console.log(this.api, opts.query) const requestRes = await fetch(\`${this.api}/v1.0/search?q=${encodeURIComponent(opts.query)}&limit=25&page=1\`, { method: "get", }) const comickRes = await requestRes.json() as ComickSearchResult\[\] const ret: SearchResult\[\] = \[\] for (const res of comickRes) { let cover: any = res.md\_covers ? res.md\_covers\[0\] : null if (cover && cover.b2key != undefined) { cover = "https://meo.comick.pictures/" + cover.b2key } ret.push({ id: res.hid, title: res.title ?? res.slug, synonyms: res.md\_titles?.map(t => t.title) ?? {}, year: res.year ?? 0, image: cover, }) } return ret } async findChapters(id: string): Promise { console.log("Fetching chapters", id) const chapterList: ChapterDetails\[\] = \[\] const data = (await (await fetch(\`${this.api}/comic/${id}/chapters?lang=en&page=0&limit=1000000\`))?.json()) as { chapters: ComickChapter\[\] } const chapters: ChapterDetails\[\] = \[\] for (const chapter of data.chapters) { if (!chapter.chap) { continue } let title = "Chapter " + this.padNum(chapter.chap, 2) + " " if (title.length === 0) { if (!chapter.title) { title = "Oneshot" } else { title = chapter.title } } let canPush = true for (let i = 0; i < chapters.length; i++) { if (chapters\[i\].title?.trim() === title?.trim()) { canPush = false } } if (canPush) { if (chapter.lang === "en") { chapters.push({ url: \`${this.api}/comic/${id}/chapter/${chapter.hid}\`, index: 0, id: chapter.hid, title: title?.trim(), chapter: chapter.chap, rating: chapter.up\_count - chapter.down\_count, updatedAt: chapter.updated\_at, }) } } } chapters.reverse() for (let i = 0; i < chapters.length; i++) { chapters\[i\].index = i } return chapters } async findChapterPages(id: string): Promise { const data = (await (await fetch(\`${this.api}/chapter/${id}\`))?.json()) as { chapter: { md\_images: { vol: any; w: number; h: number; b2key: string }\[\] } } const pages: ChapterPage\[\] = \[\] data.chapter.md\_images.map((image, index: number) => { pages.push({ url: \`https://meo.comick.pictures/${image.b2key}?width=${image.w}\`, index: index, headers: {}, }) }) return pages } padNum(number: string, places: number): string { let range = number.split("-") range = range.map((chapter) => { chapter = chapter.trim() const digits = chapter.split(".")\[0\].length return "0".repeat(Math.max(0, places - digits)) + chapter }) return range.join("-") } } interface ComickSearchResult { title: string; id: number; hid: string; slug: string; year?: number; rating: string; rating\_count: number; follow\_count: number; user\_follow\_count: number; content\_rating: string; created\_at: string; demographic: number; md\_titles: { title: string }\[\]; md\_covers: { vol: any; w: number; h: number; b2key: string }\[\]; highlight: string; } interface Comic { id: number; hid: string; title: string; country: string; status: number; links: { al: string; ap: string; bw: string; kt: string; mu: string; amz: string; cdj: string; ebj: string; mal: string; raw: string; }; last\_chapter: any; chapter\_count: number; demographic: number; hentai: boolean; user\_follow\_count: number; follow\_rank: number; comment\_count: number; follow\_count: number; desc: string; parsed: string; slug: string; mismatch: any; year: number; bayesian\_rating: any; rating\_count: number; content\_rating: string; translation\_completed: boolean; relate\_from: Array; mies: any; md\_titles: { title: string }\[\]; md\_comic\_md\_genres: { md\_genres: { name: string; type: string | null; slug: string; group: string } }\[\]; mu\_comics: { licensed\_in\_english: any; mu\_comic\_categories: { mu\_categories: { title: string; slug: string }; positive\_vote: number; negative\_vote: number; }\[\]; }; md\_covers: { vol: any; w: number; h: number; b2key: string }\[\]; iso639\_1: string; lang\_name: string; lang\_native: string; } interface ComickChapter { id: number; chap: string; title: string; vol: string | null; lang: string; created\_at: string; updated\_at: string; up\_count: number; down\_count: number; group\_name: any; hid: string; identities: any; md\_chapter\_groups: { md\_groups: { title: string; slug: string } }\[\]; } \`\`\` --- # 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/content-providers/custom-source.md). # Custom Source Note: You cannot test custom sources in the playground. Load them in development mode (\[like here\](https://seanime.gitbook.io/seanime-extensions/content-providers/pages/MjcdvXTd5UlAZTG2Soio#id-2.-create-a-manifest-file)). {% hint style="warning" %} Difficulty: Moderate {% endhint %} ## Type Definitions {% code title="custom-source.d.ts" %} \`\`\`typescript /// declare type Settings = { supportsAnime: boolean supportsManga: boolean } declare type ListResponse = { media: T\[\] page: number totalPages: number total: number } declare abstract class CustomSource { getSettings(): Settings async getAnime(ids: number\[\]): Promise<$app.AL\_BaseAnime\[\]> async getAnimeMetadata(id: number): Promise<$app.Metadata\_AnimeMetadata | null> async getAnimeWithRelations(id: number): Promise<$app.AL\_CompleteAnime> async getAnimeDetails(id: number): Promise<$app.AL\_AnimeDetailsById\_Media | null> async getManga(ids: number\[\]): Promise<$app.AL\_BaseManga\[\]> async listAnime(search: string, page: number, perPage: number): Promise\> async getMangaDetails(id: number): Promise<$app.AL\_MangaDetailsById\_Media | null> async listManga(search: string, page: number, perPage: number): Promise\> } \`\`\` {% endcode %} Keyword search the various $app types used here: {% embed url="" %} ## Code {% hint style="warning" %} Do not change the name of the class. It must be Provider. {% endhint %} {% hint style="info" %} You can define the media objects in an external API and use fetch to retrieve them dynamically. {% endhint %} ### Media objects Under the hood, custom source media are treated like AniList media, which is the reason why you need to return objects following AniList's JSON schemas. For the media \`id\`s, you're free to use any number starting from 1. Under the hood, Seanime will automatically convert these IDs to unique numbers to avoid conflicts. \`\`\`typescript /// const anime: Record = {} const animeMetadata: Record = {} const manga: Record = {} class Provider implements CustomSource { getSettings(): Settings { return { supportsAnime: true, supportsManga: true, } } // Returns all requested anime objects. async getAnime(ids: number\[\]): Promise<$app.AL\_BaseAnime\[\]> { let ret: $app.AL\_BaseAnime\[\] = \[\] for (const id of ids) { if (anime\[id\]) { // Here we make a deep copy and remove the 'relations' attribute // this turn AL\_CompleteAnime into AL\_BaseAnime const a = $clone(media\[id\]) as $app.AL\_CompleteAnime delete a\["relations"\] ret.push(a) } } return ret } // Optionally returns the details for an anime (genres, trailer, etc.) // Note that not all the fields are used by the client. async getAnimeDetails(id: number): Promise<$app.AL\_AnimeDetailsById\_Media | null> { return null } // Returns the metadata for an anime. // This is used for episodes. async getAnimeMetadata(id: number): Promise<$app.Metadata\_AnimeMetadata | null> { return animeMetadata\[id\] } // Returns the anime object with its 'relations'. // This is only used by the library scanner to build a relation tree. async getAnimeWithRelations(id: number): Promise<$app.AL\_CompleteAnime> { if (media\[id\]) { return media\[id\] as $app.AL\_CompleteAnime } throw new Error("not found.") } // Returns all requested manga objects. async getManga(ids: number\[\]): Promise<$app.AL\_BaseManga\[\]> { let ret: $app.AL\_BaseManga\[\] = \[\] for (const id of ids) { if (manga\[id\]) { ret.push(manga\[id\]) } } return ret } // Optionally returns the manga details. // Similarly to getAnimeDetails, not all fields will be used by the client. async getMangaDetails(id: number): Promise<$app.AL\_MangaDetailsById\_Media | null> { return null } // Returns all anime available on the extension. async listAnime(search: string, page: number, perPage: number): Promise\> { return { media: Object.values(media), total: 1, page: 1, totalPages: 1, } } // Returns all manga available on the extension. async listManga(search: string, page: number, perPage: number): Promise\> { return { media: Object.values(manga), total: 1, page: 1, totalPages: 1, } } } \`\`\` --- # APIs | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis.md) . [Helpers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers) [Store](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store) [Storage](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage) [Database](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database) [AniList](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist) [Shared Modules](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared) [Debug](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug) [System](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system) [PreviousPermissions](https://seanime.gitbook.io/seanime-extensions/plugins/permissions) [NextHelpers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers) Last updated 3 months ago --- # Helpers | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers.md) . $app[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#usdapp) ---------------------------------------------------------------------------------- Only available in the UI context. Copy $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[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#client-helpers) ---------------------------------------------------------------------------------------------------- ### getClientIds[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/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:** ### getClientPlatform[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#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:** [PreviousAPIs](https://seanime.gitbook.io/seanime-extensions/plugins/apis) [NextStore](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store) Last updated 3 months ago * [$app](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#usdapp) * [Client Helpers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#client-helpers) * [getClientIds](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#getclientids) * [getClientPlatform](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers#getclientplatform) Copy const clientIds = $app.getClientIds() for (const clientId of clientIds) { console.log(clientId, $app.getClientPlatform(clientId)) } Copy 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/content-providers/write-test-share.md). # Write, test, share Content providers are a type of extension used to add more sources to existing features in Seanime. \* Anime torrent providers \* Manga providers \* Online streaming providers \* Custom sources ## 1. Write and test ### Code the extension {% content-ref url="/pages/99zRHzdo6j2AQHQtBph6" %} \[Anime Torrent Provider\](/seanime-extensions/content-providers/anime-torrent-provider.md) {% endcontent-ref %} {% content-ref url="/pages/VknVhKLu357cUCzkis9Z" %} \[Manga Provider\](/seanime-extensions/content-providers/manga-provider.md) {% endcontent-ref %} {% content-ref url="/pages/kCikKP13hdklt5hmJFWF" %} \[Online Streaming Provider\](/seanime-extensions/content-providers/online-streaming-provider.md) {% endcontent-ref %} {% content-ref url="/pages/v8hRcPA70OoxCXw8YH1m" %} \[Custom Source\](/seanime-extensions/content-providers/custom-source.md) {% endcontent-ref %} ### Test in the playground 1. Go to the \`Extensions\` page in Seanime. 2. Click on the \`Playground\` dropdown option. ![Playground](https://i.postimg.cc/fyy9xGmG/Clean-Shot-2024-08-25-at-14-30-362x.webp) 3. Select which type of extension you want to test and enter the code. You will be able to select the \*\*method (function)\*\* you want to test. Different methods have different \*\*simulation parameters\*\* based on real in-app usage. ![](https://seanime.gitbook.io/files/lDnqYqTcVzKsYnASwFua) \## 2. Create a manifest file ### Create the file {% hint style="warning" %} Make the ID unique in order to avoid conflicts. The name of the file should be the same as the ID. {% endhint %} {% code title="my-original-extension-id.json" %} \`\`\`json { "id": "my-original-extension-id", "name": "My Extension Name", "description": "My Extension Description", "manifestURI": "", "version": "1.0.0", "author": "Author Name", "type": "", "language": "", "lang": "", "payload": "" } \`\`\` {% endcode %} \* \`id\`: ID of your extension. \* \`name\`: The name of the extension. \* \`description\`: A short description of the extension. \* \`manifestURI\`: The URI where the manifest file is hosted. Used by Seanime to check for updates. This can be empty if you don’t plan on hosting and sharing your extension. \* \`version\`: The version of the extension. \`x.x.x\` (e.g. 0.1.0) \* \`author\`: The author of the extension. \* \`type\`: The type of extension. See below for the available types. \* \`anime-torrent-provider\`, \`manga-provider\`, \`onlinestream-provider\` , \`custom-source\` \* \`language\`: The \*\*programming language\*\* of the extension. \* Can be \*\*\`typescript\`, or \`javascript\`\*\*. \* \`lang\`: \*\*ISO 639-1\*\* language of the extension’s content (e.g. “en”, “fr” etc.). \* Set it to \*\*\`multi\`\*\* if your extension supports multiple languages. \* \`readme\`: URL to documentation \* \`notes\`: Additional info ### Paste the payload You have two options: 1. Paste the code of your extension in the \`payload\` field. 2. Paste a URL to the code of your extension in the \`payloadURI\` field and remove \`payload\` empty. ## 3. Share If you want to share your extension with others, you can host the manifest file on GitHub and \[share\](https://seanime.rahim.app/community/extensions) the link to the file. If you just want to use it for yourself, just place the JSON file in the \`extensions\` directory in your \[data directory\](https://seanime.rahim.app/docs/config#data-directory). ## 4. Update your extension This is a simple process. Just update the \`version\` field in the JSON file and paste the new code in the \`payload\` field. {% hint style="warning" %} Your extension might become incompatible with a later version of Seanime. Check the \[Extension Changelog\](/seanime-extensions/seanime/changelog.md) for breaking changes and update your code accordingly. {% endhint %} ![](https://i.postimg.cc/RVzjPvNQ/Clean-Shot-2024-08-27-at-18-49-172x.webp) {% hint style="warning" %} Do not change your extension ID between updates {% endhint %} ## Add user configuration (optional) You can make it so users can enter arbitrary values that you can use in variables inside your code. This is useful when your extension needs to use a personal API key for example. Guide ![](https://seanime.gitbook.io/files/LjKz3x2ZGcCqmB1Jhzzp) \* Declare any number of \*\*string\*\* variables containing the configuration field keys you want to accept in the format \`{{key}}\`. These variables will be replaced with the values the user entered when the extension is loaded. \* In your manifest file, add a \`userConfig\` field. {% hint style="info" %} The field's 'name' should be the same as the key between the double curly brackets in your code. {% endhint %} \`\`\`json { //... "userConfig": { "requiresConfig": true, "version": 1, "fields": \[ { "name": "api", "label": "API URL", "type": "text", "default": "https://feed.animetosho.org/json" }, { "name": "withSmartSearch", "label": "Enable Smart Search", "type": "switch", "default": "true" }, { "name": "type", "label": "Provider Type", "type": "select", "default": "main", "options": \[ { "label": "Main", "value": "main" }, { "label": "Special", "value": "special" } \] } \] } } \`\`\` \* \`requiresConfig\`: Set to \`true\` to force the user to validate the configuration before the extension is loaded. \* \`version\`: The version of the configuration. Increment this number when you make changes to the configuration fields of your 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/content-providers/online-streaming-provider.md). # Online Streaming Provider {% hint style="warning" %} Difficulty: Moderate {% endhint %} Use bootstrapping command You can use this third-party tool to help you quickly bootstrap a folder locally \`\`\`bash npx seanime-tool g-template \`\`\` \## Types {% code title="online-streaming-provider.d.ts" %} \`\`\`typescript declare type SearchResult = { id: string title: string url: string subOrDub: SubOrDub } declare type SubOrDub = "sub" | "dub" | "both" declare type EpisodeDetails = { id: string number: number url: string title?: string } declare type EpisodeServer = { server: string headers: { \[key: string\]: string } videoSources: VideoSource\[\] } declare type VideoSourceType = "mp4" | "m3u8" | "unknown" declare type VideoSource = { url: string type: VideoSourceType // Quality or label of the video source, should be unique (e.g. "1080p", "1080p - English") quality: string // Secondary label of the video source (e.g. "English") label?: string subtitles: VideoSubtitle\[\] } declare type VideoSubtitle = { id: string url: string language: string isDefault: boolean } declare interface Media { id: number idMal?: number status?: string format?: string englishTitle?: string romajiTitle?: string episodeCount?: number absoluteSeasonOffset?: number synonyms: string\[\] isAdult: boolean startDate?: FuzzyDate } declare interface FuzzyDate { year: number month?: number day?: number } declare type SearchOptions = { media: Media query: string dub: boolean year?: number } declare type Settings = { episodeServers: string\[\] supportsDub: boolean } declare abstract class AnimeProvider { search(opts: SearchOptions): Promise findEpisodes(id: string): Promise findEpisodeServer(episode: EpisodeDetails, server: string): Promise getSettings(): Settings } \`\`\` {% endcode %} ## Code {% hint style="warning" %} Do not change the name of the class. It must be Provider. {% endhint %} \`\`\`typescript /// class Provider { getSettings(): Settings { return { episodeServers: \["server1", "server2"\], supportsDub: true, } } async search(query: SearchOptions): Promise { return \[{ id: "1", title: "Anime Title", url: "https://example.com/anime/1", subOrDub: "both", }\] } async findEpisodes(id: string): Promise { return \[{ id: "1", number: 1, url: "https://example.com/episode/1", title: "Episode title", }\] } async findEpisodeServer(episode: EpisodeDetails, \_server: string): Promise { let server = "server1" if (\_server !== "default") server = \_server return { server: server, headers: {}, videoSources: \[{ url: "https://example.com/.../stream.m3u8", type: "m3u8", quality: "1080p", subtitles: \[{ id: "1", url: "https://example.com/.../subs.vtt", language: "en", isDefault: true, }\], }\], } } } \`\`\` ## Example \`\`\`typescript /// /// type EpisodeData = { id: number; episode: number; title: string; snapshot: string; filler: number; session: string; created\_at?: string } type AnimeData = { id: number; title: string; type: string; year: number; poster: string; session: string } class Provider { api = "https://example.com" headers = { Referer: "https://example.com" } getSettings(): Settings { return { episodeServers: \["kwik"\], supportsDub: false, } } async search(opts: SearchOptions): Promise { const req = await fetch(\`${this.api}/api?m=search&q=${encodeURIComponent(opts.query)}\`, { headers: { Cookie: "\_\_ddg1\_=;\_\_ddg2\_=;", }, }) if (!req.ok) { return \[\] } const data = (await req.json()) as { data: AnimeData\[\] } const results: SearchResult\[\] = \[\] if (!data?.data) { return \[\] } data.data.map((item: AnimeData) => { results.push({ subOrDub: "sub", id: item.session, title: item.title, url: "", }) }) return results } async findEpisodes(id: string): Promise { let episodes: EpisodeDetails\[\] = \[\] const req = await fetch( \`${this.api}${id.includes("-") ? \`/anime/${id}\` : \`/a/${id}\`}\`, { headers: { Cookie: "\_\_ddg1\_=;\_\_ddg2\_=;", }, }, ) const html = await req.text() function pushData(data: EpisodeData\[\]) { for (const item of data) { episodes.push({ id: item.session + "$" + id, number: item.episode, title: item.title && item.title.length > 0 ? item.title : "Episode " + item.episode, url: req.url, }) } } const $ = LoadDoc(html) const tempId = $("head > meta\[property='og:url'\]").attr("content")!.split("/").pop()! const { last\_page, data } = (await ( await fetch(\`${this.api}/api?m=release&id=${tempId}&sort=episode\_asc&page=1\`, { headers: { Cookie: "\_\_ddg1\_=;\_\_ddg2\_=;", }, }) ).json()) as { last\_page: number; data: EpisodeData\[\] } pushData(data) const pageNumbers = Array.from({ length: last\_page - 1 }, (\_, i) => i + 2) const promises = pageNumbers.map((pageNumber) => fetch(\`${this.api}/api?m=release&id=${tempId}&sort=episode\_asc&page=${pageNumber}\`, { headers: { Cookie: "\_\_ddg1\_=;\_\_ddg2\_=;", }, }).then((res) => res.json()), ) const results = (await Promise.all(promises)) as { data: EpisodeData\[\] }\[\] results.forEach((showData) => { for (const data of showData.data) { if (data) { pushData(\[data\]) } } }); (data as any\[\]).sort((a, b) => a.number - b.number) if (episodes.length === 0) { throw new Error("No episodes found.") } const lowest = episodes\[0\].number if (lowest > 1) { for (let i = 0; i < episodes.length; i++) { episodes\[i\].number = episodes\[i\].number - lowest + 1 } } // Remove episode with decimal numbers (those aren't supported) episodes = episodes.filter((episode) => Number.isInteger(episode.number)) return episodes } async findEpisodeServer(episode: EpisodeDetails, \_server: string): Promise { const episodeId = episode.id.split("$")\[0\] const animeId = episode.id.split("$")\[1\] console.log(\`${this.api}/play/${animeId}/${episodeId}\`) const req = await fetch( \`${this.api}/play/${animeId}/${episodeId}\`, { headers: { Cookie: "\_\_ddg1\_=;\_\_ddg2\_=;", }, }, ) const html = await req.text() const regex = /https:\\/\\/kwik\\.si\\/e\\/\\w+/g const matches = html.match(regex) if (matches === null) { throw new Error("Failed to fetch episode server.") } const $ = LoadDoc(html) const result: EpisodeServer = { videoSources: \[\], headers: this.headers ?? {}, server: "kwik", } $("button\[data-src\]").each(async (\_, el) => { let videoSource: VideoSource = { url: "", type: "m3u8", quality: "", subtitles: \[\], } videoSource.url = el.data("src")! if (!videoSource.url) { return } const fansub = el.data("fansub")! const quality = el.data("resolution")! videoSource.quality = \`${quality}p - ${fansub}\` if (el.data("audio") === "eng") { videoSource.quality += " (Eng)" } if (videoSource.url === matches\[0\]) { videoSource.quality += " (default)" } result.videoSources.push(videoSource) }) const queries = result.videoSources.map(async (videoSource) => { try { const src\_req = await fetch(videoSource.url, { headers: { Referer: this.headers.Referer, "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.56", }, }) const src\_html = await src\_req.text() const scripts = src\_html.match(/eval\\(f.+?\\}\\)\\)/g) if (!scripts) { return } for (const \_script of scripts) { const scriptMatch = \_script.match(/eval(.+)/) if (!scriptMatch || !scriptMatch\[1\]) { continue } try { const decoded = eval(scriptMatch\[1\]) const link = decoded.match(/source='(.+?)'/) if (!link || !link\[1\]) { continue } videoSource.url = link\[1\] } catch (e) { console.error("Failed to extract kwik link", e) } } } catch (e) { console.error("Failed to fetch kwik link", e) } }) await Promise.all(queries) return result } } \`\`\` --- # 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/seanime/readme.md). # Getting started Seanime has an embedded JavaScript (ES5) engine which allows you to write various types of extensions with minimal effort. {% hint style="warning" %} Seanime's JavaScript engine does NOT support \*\*Node.JS\*\* or \*\*Browser\*\* APIs but offers its own APIs for convenience. Check out the \[Core APIs\](/seanime-extensions/seanime/core-apis.md). {% endhint %} {% content-ref url="/pages/rDy7WgrX1WK9CgErLiJz" %} \[Core APIs\](/seanime-extensions/seanime/core-apis.md) {% endcontent-ref %} ## Content Providers {% content-ref url="/pages/QtHPbZTobIHLeiuElH6h" %} \[Write, test, share\](/seanime-extensions/content-providers/write-test-share.md) {% endcontent-ref %} ## Plugins {% content-ref url="/pages/X2tPRNil5U45n1Hf0AXY" %} \[Introduction\](/seanime-extensions/plugins/introduction.md) {% endcontent-ref %} {% content-ref url="/pages/qdYztvAjGhAm0iu4Uylg" %} \[Basics\](/seanime-extensions/plugins/ui/basics.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/apis.md). # APIs - \[Helpers\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers.md) - \[Store\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store.md): Key-value store. - \[Storage\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage.md): Persistent storage. - \[Database\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database.md): Interact with parts of Seanime's database. - \[AniList\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist.md): Interact with the user's AniList account. - \[Shared Modules\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared.md): Reuse helper factories across plugin runtimes. - \[Debug\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug.md): Development-only logging helpers for plugin runtimes. - \[System\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system.md): The System APIs give you access to a set of methods for file operations, downloading and more. - \[Permissions\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions.md): The System APIs give you access to a set of methods for file operations, downloading and more. - \[OS\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os.md): OS-agonistic APIs for operating system functionality, such as interacting with the filesystem. - \[Filepath\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath.md) - \[Commands\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands.md) - \[Buffers, I/O\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o.md) - \[MIME\](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime.md) --- # Store | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store.md) . When to use[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#when-to-use) -------------------------------------------------------------------------------------------- * Create a cache * Share values or functions between hooks * Share values or functions between hooks and UI context The values aren't persisted when your plugin is reloaded. How to use[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#how-to-use) ------------------------------------------------------------------------------------------ `$store` makes state sharing between runtimes possible. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FDZdoRFTYErtDifNxpCZu%252Fimg-2025-04-28-11-15-31%25402x.png%3Falt%3Dmedia%26token%3Db1aa101f-6a2d-43f4-b5f8-9c99ac3f1451&width=768&dpr=3&quality=100&sign=da7b1e15&sv=2) ### Unsafe access[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#unsafe-access) Available from Seanime `v3.7.2`. These helpers skip the defensive clone performed by the safe methods: * `$store.getUnsafe(key)` * `$store.getAllUnsafe()` * `$store.valuesUnsafe()` Use them only for read-only access in performance-sensitive paths. Do not mutate data returned by the unsafe methods. Mutating shared references can bypass watcher notifications and may lead to concurrent map write panics. ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#example) [PreviousHelpers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers) [NextStorage](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage) Last updated 3 months ago * [When to use](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#when-to-use) * [How to use](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#how-to-use) * [Unsafe access](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#unsafe-access) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store#example) Copy $store.set("foo", "bar") $store.get("foo") $store.watch("foo", (value) => {}) $store.getAll() // { "foo": "bar" } $store.remove("foo") $store.removeAll() $store.has("foo") // false $store.getOrSet("foo", () => { return "bar" }) $store.values() // ["bar"] Copy const rawValue = $store.getUnsafe("foo") const rawEntries = $store.getAllUnsafe() const rawValues = $store.valuesUnsafe() my-plugin.ts Copy // 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("scan-completed", (value) => { const now = new Date().toISOString().replaceall(".", "_") $storage.set("scan-duration-history."+now, value) ctx.toast.info(`Scanning took ${value/1000} seconds!`) }) }) } --- # Database | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database.md) . Permission[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#permission) --------------------------------------------------------------------------------------------- `database` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["database"] } } } Invalidate queries[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/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](https://seanime.gitbook.io/seanime-extensions/plugins/apis/helpers) Local files[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#local-files) ----------------------------------------------------------------------------------------------- You can interact with the scanned file entries (also called local files). ### Get all[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-all) ### Edit[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#edit) Note that `save` only works for existing entries. ### Insert[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#insert) AniList[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#anilist) --------------------------------------------------------------------------------------- ### Get Token[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-token) `anilist-token` permission is required ### Get Username[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-username) Auto Downloader Rules[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#auto-downloader-rules) ------------------------------------------------------------------------------------------------------------------- Auto Downloader Items[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#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. Silenced media entries[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#silenced-media-entries) --------------------------------------------------------------------------------------------------------------------- Media fillers[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#media-fillers) --------------------------------------------------------------------------------------------------- [PreviousStorage](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage) [NextAniList](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist) Last updated 3 months ago * [Permission](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#permission) * [Invalidate queries](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#invalidate-queries) * [Local files](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#local-files) * [Get all](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-all) * [Edit](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#edit) * [Insert](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#insert) * [AniList](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#anilist) * [Get Token](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-token) * [Get Username](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#get-username) * [Auto Downloader Rules](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#auto-downloader-rules) * [Auto Downloader Items](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#auto-downloader-items) * [Silenced media entries](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#silenced-media-entries) * [Media fillers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database#media-fillers) Copy const localFiles = $database.localFiles.getAll() Copy // 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) Copy // Inserts a new collection of local files // This is equivalent to doing a scan const localFiles = $database.localFiles.insert([\ //...\ ]) Copy $database.anilist.getToken() Copy $database.anilist.getUsername() Copy // 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({ //... }) Copy // 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({ //... }) Copy const silencedAnimeIds = $database.silencedMediaEntries.getAllIds() // Silence an anime $database.silencedMediaEntries.setSilenced(21, true) $database.silencedMediaEntries.isSilenced(21) // true Copy const fillerData = $database.mediaFillers.getAll() $database.mediaFillers.get(21) $database.mediaFillers.insert("provider", 21, "slug", ["600"]) $database.mediaFillers.remove(21) --- # Debug | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug.md) . `$debug` is available in all plugin runtimes. These methods are only active for development plugins. Outside development mode, `$debug.enabled` is `false` and every method becomes a no-op. Quick example[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#quick-example) ------------------------------------------------------------------------------------------------ Copy $app.onScanCompleted((event) => { $debug.info("scan completed", { duration: event.duration, mediaCount: event.stats?.animeCount, }) event.next() }) $ui.register((ctx) => { $debug.mark("ui thread started") }) Properties[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#properties) ------------------------------------------------------------------------------------------ ### enabled[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#enabled) `$debug.enabled` Returns `true` when the plugin runs in development mode. Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#methods) ------------------------------------------------------------------------------------ ### log[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#log) `$debug.log(...values)` Logs values with the default `log` level. ### info[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#info) `$debug.info(...values)` Logs values with the `info` level. ### warn[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#warn) `$debug.warn(...values)` Logs values with the `warn` level. ### error[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#error) `$debug.error(...values)` Logs values with the `error` level. ### debug[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#debug) `$debug.debug(...values)` Logs values with the `debug` level. ### clear[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#clear) `$debug.clear()` Clears the current debug output. ### time[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#time) `$debug.time(label?)` Starts a named timer. If you omit the label, Seanime uses `"default"`. ### timeEnd[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#timeend) `$debug.timeEnd(label?)` Stops a named timer and logs the elapsed duration in milliseconds. Notes[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#notes) -------------------------------------------------------------------------------- * Errors are serialized with `name`, `message`, and `stack` when available. * Plain objects and arrays are serialized as structured values, so you can inspect them in your plugin's debug panel. [PreviousShared Modules](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared) [NextSystem](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system) Last updated 3 months ago * [Quick example](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#quick-example) * [Properties](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#properties) * [enabled](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#enabled) * [Methods](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#methods) * [log](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#log) * [info](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#info) * [warn](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#warn) * [error](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#error) * [debug](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#debug) * [clear](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#clear) * [time](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#time) * [timeEnd](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#timeend) * [Notes](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug#notes) Copy $debug.time("sync") await doWork() $debug.timeEnd("sync") --- # 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/content-providers/anime-torrent-provider.md). # Anime Torrent Provider {% hint style="success" %} Difficulty: Easy {% endhint %} Use bootstrapping command You can use this third-party tool to help you quickly bootstrap a folder locally \`\`\`bash npx seanime-tool g-template \`\`\` \## Types {% code title="anime-torrent-provider.d.ts" %} \`\`\`typescript declare type AnimeProviderSmartSearchFilter = "batch" | "episodeNumber" | "resolution" | "query" | "bestReleases" declare type AnimeProviderType = "main" | "special" declare interface AnimeProviderSettings { // Indicates whether the extension supports smart search. canSmartSearch: boolean // Filters that can be used in smart search. smartSearchFilters: AnimeProviderSmartSearchFilter\[\] // Indicates whether the extension supports adult content. supportsAdult: boolean // Type of the provider. type: AnimeProviderType } // Media object passed to 'search' and 'smartSearch' methods. declare interface Media { // AniList ID of the media. id: number // MyAnimeList ID of the media. idMal?: number // e.g. "FINISHED", "RELEASING", "NOT\_YET\_RELEASED", "CANCELLED", "HIATUS" // This will be set to "NOT\_YET\_RELEASED" if the status is unknown. status?: string // e.g. "TV", "TV\_SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC" // This will be set to "TV" if the format is unknown. format?: string // e.g. "Attack on Titan" englishTitle?: string // e.g. "Shingeki no Kyojin" romajiTitle?: string // TotalEpisodes is total number of episodes of the media. // This will be -1 if the total number of episodes is unknown / not applicable. episodeCount?: number // Absolute offset of the media's season. // This will be 0 if the media is not seasonal or the offset is unknown. absoluteSeasonOffset?: number // All alternative titles of the media. synonyms: string\[\] // Whether the media is NSFW. isAdult: boolean // Start date of the media. // This will be undefined if it has no start date. startDate?: FuzzyDate } declare interface FuzzyDate { year: number month?: number day?: number } declare interface AnimeSearchOptions { // The media object. media: Media // The user search query. query: string } declare interface AnimeSmartSearchOptions { // The media object. media: Media // The user search query. // This will be empty if your extension does not support custom queries. query: string // Indicates whether the user wants to search for batch torrents. // This will be false if your extension does not support batch torrents. batch: boolean // The episode number the user wants to search for. // This will be 0 if your extension does not support episode number filtering. episodeNumber: number // The resolution the user wants to search for. // This will be empty if your extension does not support resolution filtering. resolution: string // AniDB Anime ID of the media. anidbAID: number // AniDB Episode ID of the media. anidbEID: number // Indicates whether the user wants to search for the best releases. // This will be false if your extension does not support filtering by best releases. bestReleases: boolean } declare interface AnimeTorrent { name: string // Date of the torrent. // The date should have RFC3339 format. e.g. "2006-01-02T15:04:05Z07:00" date: string // Size of the torrent in bytes. size: number // Formatted size of the torrent. e.g. "1.2 GB" // Leave this empty if you want Seanime to format the size. formattedSize: string // Number of seeders of the torrent. seeders: number // Number of leechers of the torrent. leechers: number // Number of downloads of the torrent. downloadCount: number // Link to the torrent page. link: string // Download URL of the torrent. // Leave this empty if you cannot provide a direct download URL. downloadUrl?: string // Magnet link of the torrent. // Set this to null if you cannot provide a magnet link without scraping. magnetLink?: string // Info hash of the torrent. // Set this to null if you cannot provide an info hash without scraping. infoHash?: string // The resolution of the torrent. // Leave this empty if you want Seanime to parse the resolution from the name. resolution?: string // Set this to true if you can confirm that the torrent is a batch. // Else, Seanime will parse the torrent name to determine if it's a batch. isBatch?: boolean // Episode number of the torrent. // Return -1 if unknown / unable to determine and Seanime will parse the torrent name. episodeNumber: number // Release group of the torrent. // Leave this empty if you want Seanime to parse the release group from the name. releaseGroup?: string // Set this to true if you can confirm that the torrent is the best release. isBestRelease: boolean // Set this to true if you can confirm that the torrent matches the anime the user is searching for. // e.g. If the torrent was found using the AniDB anime or episode ID confirmed: boolean } \`\`\` {% endcode %} ## Code {% hint style="warning" %} Do not change the name of the class. It must be Provider. {% endhint %} \`\`\`typescript /// class Provider { private api = "https://example.com" // Returns the provider settings. getSettings(): AnimeProviderSettings { // TODO: Edit this return { canSmartSearch: true, smartSearchFilters: \["batch", "episodeNumber", "resolution"\], supportsAdult: false, type: "main", } } // Returns the search results depending on the query. async search(opts: AnimeSearchOptions): Promise { // TODO return \[\] } // Returns the search results depending on the search options. async smartSearch(opts: AnimeSmartSearchOptions): Promise { // TODO return \[\] } // Scrapes the torrent page to get the info hash. // If already present in AnimeTorrent, this should just return the info hash without scraping. async getTorrentInfoHash(torrent: AnimeTorrent): Promise { return torrent.infoHash } // Scrapes the torrent page to get the magnet link. // If already present in AnimeTorrent, this should just return the magnet link without scraping. async getTorrentMagnetLink(torrent: AnimeTorrent): Promise { return torrent.magnetLink } // Returns the latest torrents. // Note that this is only used by "main" providers. async getLatest(): Promise { // TODO return \[\] } } \`\`\` ### Settings #### type \* \`main\`: Your extension can be used as \*\*default provider\*\* for torrent search and the Auto Downloader. \* \`special\`: Your extension can \*\*ONLY\*\* be used for torrent search. #### canSmartSearch / smartSearchFilters ![](https://seanime.gitbook.io/files/bLywxchowQETw79CvCDI) \* \`batch\` : Your extension can look for batches \* \`episodeNumber\` : Your extension can look for specific episode numbers \* \`resolution\` : Your extension can filter by resolution \* \`query\`: Allow the user to change the smart search title \* \`bestReleases\` : Your extension can find highest-quality torrents ## Example \`\`\`typescript /// /// class Provider { api = "https://feed.animetosho.org/json" getSettings(): AnimeProviderSettings { return { canSmartSearch: true, smartSearchFilters: \["batch", "episodeNumber", "resolution"\], supportsAdult: false, type: "main", } } async search(opts: AnimeSearchOptions): Promise { const query = \`?q=${encodeURIComponent(opts.query)}&only\_tor=1\` console.log(query) const torrents = await this.fetchTorrents(query) return torrents.map(t => this.toAnimeTorrent(t)) } async smartSearch(opts: AnimeSmartSearchOptions): Promise { const ret: AnimeTorrent\[\] = \[\] if (opts.batch) { if (!opts.anidbAID) return \[\] let torrents = await this.searchByAID(opts.anidbAID, opts.resolution) if (!(opts.media.format == "MOVIE" || opts.media.episodeCount == 1)) { torrents = torrents.filter(t => t.num\_files > 1) } for (const torrent of torrents) { const t = this.toAnimeTorrent(torrent) t.isBatch = true ret.push(t) } return ret } if (!opts.anidbEID) return \[\] const torrents = await this.searchByEID(opts.anidbEID, opts.resolution) for (const torrent of torrents) { ret.push(this.toAnimeTorrent(torrent)) } return ret } async getTorrentInfoHash(torrent: AnimeTorrent): Promise { return torrent.infoHash || "" } async getTorrentMagnetLink(torrent: AnimeTorrent): Promise { return torrent.magnetLink || "" } async getLatest(): Promise { const query = \`?q=&only\_tor=1\` const torrents = await this.fetchTorrents(query) return torrents.map(t => this.toAnimeTorrent(t)) } async searchByAID(aid: number, quality: string): Promise { const q = encodeURIComponent(this.formatQuality(quality)) const query = \`?order=size-d&aid=${aid}&q=${q}\` return this.fetchTorrents(query) } async searchByEID(eid: number, quality: string): Promise { const q = encodeURIComponent(this.formatQuality(quality)) const query = \`?eid=${eid}&q=${q}\` return this.fetchTorrents(query) } async fetchTorrents(url: string): Promise { const furl = \`${this.api}${url}\` try { const response = await fetch(furl) if (!response.ok) { throw new Error(\`Failed to fetch torrents, ${response.statusText}\`) } const torrents: ToshoTorrent\[\] = await response.json() return torrents.map(t => { if (t.seeders > 30000) { t.seeders = 0 } if (t.leechers > 30000) { t.leechers = 0 } return t }) } catch (error) { throw new Error(\`Error fetching torrents: ${error}\`) } } formatQuality(quality: string): string { return quality.replace(/p$/, "") } toAnimeTorrent(torrent: ToshoTorrent): AnimeTorrent { return { name: torrent.title, date: new Date(torrent.timestamp \* 1000).toISOString(), size: torrent.total\_size, formattedSize: "", seeders: torrent.seeders, leechers: torrent.leechers, downloadCount: torrent.torrent\_download\_count, link: torrent.link, downloadUrl: torrent.torrent\_url, magnetLink: torrent.magnet\_uri, infoHash: torrent.info\_hash, resolution: "", isBatch: false, episodeNumber: -1, isBestRelease: false, confirmed: true, } } } type ToshoTorrent = { id: number title: string link: string timestamp: number status: string tosho\_id?: number nyaa\_id?: number nyaa\_subdom?: any anidex\_id?: number torrent\_url: string info\_hash: string info\_hash\_v2?: string magnet\_uri: string seeders: number leechers: number torrent\_download\_count: number tracker\_updated?: any nzb\_url?: string total\_size: number num\_files: number anidb\_aid: number anidb\_eid: number anidb\_fid: number article\_url: string article\_title: string website\_url: string } \`\`\` --- # Permissions | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions.md) . Difficulty: Moderate * Some knowledge of the filesystem, platform differences is required You can check out the type definition file to see the exhaustive list of methods available and use Go's documentation to learn how to use them. The examples may use hardcoded paths but this is not recommended. Seanime is a cross-platform app, keep that in mind. As of Seanime `v3.8.0`, if the user enables Extension Secure Mode, sensitive system actions such as file reads, writes, directory inspection, and command execution can prompt for confirmation. If the user rejects a prompt, the call throws. If the prompt cannot be shown, such as during app startup before a UI client is available, the call fails immediately. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#permissions) --------------------------------------------------------------------------------------------------------- `system` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["system"] } } } ### Allow lists[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#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. ### Paths[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#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 [PreviousSystem](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system) [NextOS](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#permissions) * [Allow lists](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#allow-lists) * [Paths](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions#paths) Copy { //... "plugin": { "permissions": ["system", ...], "systemAllowList": { "allowReadPaths": ["$TEMP/*"], "allowWritePaths": ["$TEMP/*"], "commandScopes": [] } } } --- # Feature requests | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests.md) . Feature requests on GitHub pertain to features that will be integrated in the source code **only**. ### Why was this feature request closed?[](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests#why-was-this-feature-request-closed) As of `v2.8.0` , Seanime supports [Plugins](https://seanime.gitbook.io/seanime-extensions/plugins/introduction) , which can be developed entirely in JavaScript. A feature request will be closed as not planned with the label `status: plugin-suitable` if: * The feature can be reasonably added via plugin * The feature will not benefit a majority of users * The feature is mostly subjective or cosmetic (e.g. removing elements, changing layout, etc.) This is done to: * Reduce development time and update cycles * Avoid bloat by offloading noncritical features * Improve contribution ### What if I can't develop a plugin?[](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests#what-if-i-cant-develop-a-plugin) Join the Discord server and make a request in the `#extension-proposals` channel, someone might make it for you. [PreviousExample](https://seanime.gitbook.io/seanime-extensions/plugins/example) Last updated 3 months ago * [Why was this feature request closed?](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests#why-was-this-feature-request-closed) * [What if I can't develop a plugin?](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests#what-if-i-cant-develop-a-plugin) --- # Hooks | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/hooks.md) . Difficulty: Hard * Event-driven understanding required List of hooks[](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#list-of-hooks) ------------------------------------------------------------------------------------------- [https://seanime.rahim.app/docs/hooks](https://seanime.rahim.app/docs/hooks) Usage[](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#usage) --------------------------------------------------------------------------- Hooks should be used carefully as they can introduce undefined behavior and even slow down the app. Some hooks, like `onGetAnime` , are triggered very often, so it's a good habit to start by logging the event in order to figure out its frequency. You should also avoid expensive calculations or fetch calls in hook handlers unless you can guarantee that the hook is not triggered often. Any error/exception that happens in a hook handler will result in a server and client error. Test your code carefully. Example Copy function init() { // This hook is triggered before Seanime formats the library data of an anime // The event contains the variables that Seanime will use, and you can modify them $app.onAnimeEntryLibraryDataRequested((e) => { // Setting this to an empty array will cause Seanime to think that the anime // has not been downloaded. e.entryLocalFiles = [] e.next() // Continue hook chain }) } Each hook handler must call `e.next()` in order for the hook chain listening to that event to proceed. Not calling it will impact other plugins listening to that event. Best Practices[](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#best-practices) --------------------------------------------------------------------------------------------- ### Editing events[](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#editing-events) Let's say we want your plugin to change anime banner images based on what custom banner image has been set for that anime. However you want to do it without manipulating the DOM and before the page is even loaded. We can use `onGetAnimeCollection` and `onGetRawAnimeCollection` since these are triggered when Seanime fetches the user's anime collection from AniList. Note that this will not change banner images for the same anime if it's fetched using another query (e.g. discover, search). ### Listening to events[](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#listening-to-events) Let's say we want to make a plugin that stores the history of scanning durations. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FDZdoRFTYErtDifNxpCZu%252Fimg-2025-04-28-11-15-31%25402x.png%3Falt%3Dmedia%26token%3Db1aa101f-6a2d-43f4-b5f8-9c99ac3f1451&width=768&dpr=3&quality=100&sign=da7b1e15&sv=2) [PreviousDiscord](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord) [NextExample](https://seanime.gitbook.io/seanime-extensions/plugins/example) Last updated 3 months ago * [List of hooks](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#list-of-hooks) * [Usage](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#usage) * [Best Practices](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#best-practices) * [Editing events](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#editing-events) * [Listening to events](https://seanime.gitbook.io/seanime-extensions/plugins/hooks#listening-to-events) Copy // Triggers the app loads the user's AniList anime collection $app.onGetAnimeCollection((e) => { // 1. Get all the custom banner images const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } // 2. Go through all anime in the collection for (let i = 0; i < e.animeCollection!.MediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.MediaListCollection!.lists![i]!.entries!.length; j++) { const mediaId = e.animeCollection!.MediaListCollection!.lists![i]!.entries![j]!.media!.id // 3. If this anime has a custom image, change it const bannerImage = bannerImages[mediaId.toString()] if (!!bannerImage) { e.animeCollection!.MediaListCollection!.lists![i]!.entries![j]!.media!.bannerImage = bannerImage } } } // 4. Continue e.next() }) // Do the same with $app.onGetRawAnimeCollection Copy // ⚠️ Not recommended: Doing unnecessary work in the hook callback function init() { $app.onScanCompleted((e) => { const now = new Date().toISOString().replaceall(".", "_") // Add the value to the history // NOTE: In reality this operation is very fast $storage.set("scan-duration-history."+now, e.duration) e.next() }) $ui.register((ctx) => { }) } // ✅ Good practice: Defer business logic to the UI context function init() { $app.onScanCompleted((e) => { // Send a copy of the event $store.set("scan-completed", $clone(e)) e.next() }) // Let the UI context "listen" to the event and execute business logic $ui.register((ctx) => { // Callback is triggered anytime 'set' is called on that key $store.watch("scan-completed", (e) => { const now = new Date().toISOString().replaceall(".", "_") // Add the value to the history $storage.set("scan-duration-history."+now, e.duration) ctx.toast.info(`Scanning took ${e.duration/1000} seconds!`) }) }) } --- # UI | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui.md) . [Basics](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics) [Helpers](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers) [Cron](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron) [User Interface](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface) [Anime/Library](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library) [Downloading](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading) [Other](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other) [PreviousMIME](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime) [NextBasics](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics) Last updated 3 months ago --- # OS | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os.md) . Go reference: [https://pkg.go.dev/os](https://pkg.go.dev/os) As of Seanime `v3.8.0`, if the user enables Extension Secure Mode, sensitive filesystem actions can prompt for confirmation. If the user rejects the prompt, reads, writes, directory listing, creation, rename, delete, move, and extraction calls can throw. If prompts cannot be shown, such as during app startup, these calls fail immediately. $os[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#usdos) ---------------------------------------------------------------------------------- ### Info[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#info) Copy console.log("Platform:", $os.platform); // darwin console.log("Arch:", $os.arch); // arm64 ### Directories[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#directories) Copy $os.tempDir() // $TEMP must be in the allow list $os.cacheDir() // $CACHE must be in the allow list $os.configDir() // $CONFIG must be in the allow list $os.homeDir() // $HOME must be in the allow list ### File operations[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#file-operations) Always call `close()` once you're done manipulating a file. Example Copy // C:\Users\user\Downloads\test.txt Hello World! // my-plugin.ts // "C:/Users/user/Downloads/**/*" and "$TEMP/**/*" have been added to 'allowReadPaths' and 'allowWritePaths' // Access the temp directory const tempDirPath = $os.tempDir(); console.log("Temp dir:", tempDirPath); // Read files const content = $os.readFile("C:\Users\user\Downloads\test.txt"); console.log("File content:", $toString(content)); // Hello World! // Write/create files $os.writeFile("C:\Users\user\Downloads\test.txt.new", $toBytes("New content"), 0644); const newContent = $os.readFile("C:\Users\user\Downloads\test.txt.new"); console.log("New file content:", $toString(newContent)); // New content // Read directories const entries = $os.readDir("C:\Users\user\Downloads"); for (const entry of entries) { console.log(entry.name()); // test.txt, test.txt.new } // Create directories $os.mkdir("C:\Users\user\Downloads\newdir", 0755); const newEntries = $os.readDir("C:\Users\user\Downloads"); for (const entry of newEntries) { console.log(entry.name()); // test.txt, test.txt.new, newdir } // Rename files $os.rename("C:\Users\user\Downloads\test.txt.new", "C:\Users\user\Downloads\test.txt.renamed"); let renameSuccess = true try { // File exists, no error thrown $os.stat("C:\Users\user\Downloads\test.txt.renamed"); } catch(e) { renameSuccess = false } console.log(renameSuccess); // true // Remove files $os.remove("C:\Users\user\Downloads\test.txt.renamed"); let removeSuccess = true; try { $os.stat("C:\Users\user\Downloads\test.txt.renamed"); removeSuccess = false; } catch (e) { // Error thrown becuase file should not exist removeSuccess = true; } console.log(removeSuccess); // true $osExtra[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#usdosextra) -------------------------------------------------------------------------------------------- This API gives you additional functionalities ### Directories[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#directories-1) ### Unarchive files[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#unarchive-files) * unzipFile, unrarFile [PreviousPermissions](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions) [NextFilepath](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath) Last updated 3 months ago * [$os](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#usdos) * [Info](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#info) * [Directories](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#directories) * [File operations](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#file-operations) * [$osExtra](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#usdosextra) * [Directories](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#directories-1) * [Unarchive files](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os#unarchive-files) Copy $osExtra.desktopDir() // $DESKTOP must be in the allow list $osExtra.documentDir() // $DOCUMENT must be in the allow list $osExtra.downloadDir() // $DOWNLOAD must be in the allow list Copy // If "file.zip" contains `folder > file.text` $osExtra.unzipFile("/path/to/downloaded/file.zip", "/path/to/dest") // -> "/path/to/dest/folder/file.txt" // If "file.rar" contains `file.txt` $osExtra.unrarFile("/path/to/downloaded/file.zip", "/path/to/dest") // -> "/path/to/dest/file.txt" --- # Commands | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands.md) . As of Seanime `v3.8.0`, if the user enables Extension Secure Mode, command execution can prompt for confirmation. If the user rejects the prompt, the command call throws. If prompts cannot be shown, such as during app startup before a UI client is connected, the command call fails immediately. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#permissions) ------------------------------------------------------------------------------------------------------ By default, Seanime disallows running commands, you must manually defines the command and arguments your plugin will want to run using `commandScopes` . ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#example) Copy { // ... "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)[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#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. Command (async)[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#command-async) ------------------------------------------------------------------------------------------------------------ If you need to run a command without blocking the plugin's UI context thread, you should use `$osExtra.asyncCmd` . Do not use both sync and async commands in the same plugin as this can cause some data race issues. [PreviousFilepath](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath) [NextBuffers, I/O](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#permissions) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#example) * [Command (sync)](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#command-sync) * [Command (async)](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands#command-async) Copy 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); } Copy 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); } --- # Basics | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics.md) . Difficulty: Moderate * Some basic knowledge of reactive UIs and state management is recommended. What is $ui.register?[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#what-is-usdui.register) ---------------------------------------------------------------------------------------------------------------- You can interact with the user interface and execute business logic via APIs provided to your plugin when you register a UI context. Copy function init() { // This function registers the UI context for your plugin, allowing it to // have access to UI APIs $ui.register((ctx) => { // The 'ctx' objects contains all the APIs }) } Unlike hooks which are called every time a specific event is triggered, the function inside `$ui.register` is called only once during the lifetime of your plugin, right after `init(),` in other words, each time your plugin is loaded. You cannot register hook handlers inside the UI callback. Fetch[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#fetch) ------------------------------------------------------------------------------- As of v3.3.0, network requests require you to whitelist domains [Network Requests](https://seanime.gitbook.io/seanime-extensions/plugins/permissions#network-requests) In the UI context, `ctx.fetch` should be used instead of simply `fetch` . States[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#states) --------------------------------------------------------------------------------- State management allows you to keep track of dynamic data within your plugin. This approach not only helps maintain a clear separation of concerns but also enables reactive programming, where UI components like the `Tray` automatically update in response to changes in states. ### Computed[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#computed) ### Effects[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#effects) ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#example) In this example, we fetch some info from an external API each time the user navigates to an anime page. [PreviousUI](https://seanime.gitbook.io/seanime-extensions/plugins/ui) [NextHelpers](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers) Last updated 3 months ago * [What is $ui.register?](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#what-is-usdui.register) * [Fetch](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#fetch) * [States](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#states) * [Computed](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#computed) * [Effects](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#effects) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics#example) Copy $ui.register(async (ctx) => { const res = await ctx.fetch("https://jsonplaceholder.typicode.com/todos/1") const data = res.json() }) Copy //... 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()}`)) Copy const count = ctx.state(0) const text = ctx.computed(() => `Count is ${count.get()}`, [count]) text.get() Copy // Effect registers a callback that runs each time count changes ctx.effect(() => { console.log("count changed, " + count.get()) }, [count]) Example Copy const currentMediaId = ctx.state(null) const fetchedData = ctx.state([]) // When the user navigates to an anime, get the media ID ctx.screen.onNavigate((e) => { if (e.pathname === "/entry" && !!e.searchParams.id) { const id = parseInt(e.searchParams.id); currentMediaId.set(id); } else { currentMediaId.set(null); } }); // Trigger 'ctx.screen.onNavigate' when the plugin loads ctx.screen.loadCurrent() // Fetch data each time the media ID changes. ctx.effect(async () => { if (!currentMediaId.get()) return const res = ctx.fetch(`https://example.com/anilistId?=${currentMediaId.get()}`) // Store the results fetchedData.set(res.json()) }, [currentMediaId]) --- # Downloading | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading.md) . [Downloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader) [Torrent Client](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client) [Debrid](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid) [PreviousAuto Select](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select) [NextDownloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader) Last updated 3 months ago --- # Storage | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage.md) . Permission[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#permission) -------------------------------------------------------------------------------------------- `storage` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["storage"] } } } Usage[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#usage) ---------------------------------------------------------------------------------- ### API[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#api) Unlike `store` , `storage` handles nested values out of the box. Copy $storage.set("foo.bar", 1) $storage.set("foo.baz", "2") $storage.has("foo") // true $storage.get("foo.bar") // 1 $storage.get>("foo") // { "bar": 1, "baz": "2" } $storage.set("foo", "bar") $storage.get("foo") // bar $storage.watch("foo", (value) => {}) ### Unsafe access[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#unsafe-access) Available from Seanime `v3.7.2`. `$storage.getUnsafe(key)` returns the raw stored reference without cloning it first. Use it only when you need to avoid the cloning cost for large values and you can treat the result as read-only. Do not mutate objects returned by `getUnsafe()`. The safe `get()` method exists to avoid accidental shared-reference writes and concurrent map write panics. Example[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#example) -------------------------------------------------------------------------------------- Make sure your storage doesn't grow too big by doing some cleanup. Good to know[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#good-to-know) ------------------------------------------------------------------------------------------------ The plugin storage is deleted when the plugin is uninstalled. [PreviousStore](https://seanime.gitbook.io/seanime-extensions/plugins/apis/store) [NextDatabase](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database) Last updated 3 months ago * [Permission](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#permission) * [Usage](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#usage) * [API](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#api) * [Unsafe access](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#unsafe-access) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#example) * [Good to know](https://seanime.gitbook.io/seanime-extensions/plugins/apis/storage#good-to-know) Copy const rawHistory = $storage.getUnsafe>("scan-duration-history") my-plugin.ts Copy // 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("scan-completed", (value) => { const date = new Date() const now = date.toISOString().replaceall(".", "_") // Add the value to the history $storage.set("scan-duration-history."+now, { duration: value, durationInSeconds: value/1000, addedAt: date, }) ctx.toast.info(`Scanning took ${value/1000} seconds!`) }) function deleteHistory() { $storage.remove("scan-duration-history") } }) } --- # AniList | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist.md) . Difficulty: Easy Permission[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#permission) -------------------------------------------------------------------------------------------- `anilist` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["anilist"] } } } Refresh collections[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#refresh-collections) -------------------------------------------------------------------------------------------------------------- This is needed if you edit the user's collection. Copy $anilist.refreshAnimeCollection() $anilist.refreshMangaCollection() Empty cache[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#empty-cache) ---------------------------------------------------------------------------------------------- Clears the cache for fetched anime/manga entries. Update entry[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry) ------------------------------------------------------------------------------------------------ Update entry progress[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry-progress) ------------------------------------------------------------------------------------------------------------------ Update entry repeat[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry-repeat) -------------------------------------------------------------------------------------------------------------- Delete entry[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#delete-entry) ------------------------------------------------------------------------------------------------ Add media to collection[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#add-media-to-collection) ---------------------------------------------------------------------------------------------------------------------- Get collections[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#get-collections) ------------------------------------------------------------------------------------------------------ Get anime/manga data[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#get-anime-manga-data) ---------------------------------------------------------------------------------------------------------------- Search / List[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#search-list) ------------------------------------------------------------------------------------------------ Custom GraphQL query[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#custom-graphql-query) ---------------------------------------------------------------------------------------------------------------- [PreviousDatabase](https://seanime.gitbook.io/seanime-extensions/plugins/apis/database) [NextShared Modules](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared) Last updated 3 months ago * [Permission](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#permission) * [Refresh collections](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#refresh-collections) * [Empty cache](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#empty-cache) * [Update entry](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry) * [Update entry progress](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry-progress) * [Update entry repeat](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#update-entry-repeat) * [Delete entry](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#delete-entry) * [Add media to collection](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#add-media-to-collection) * [Get collections](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#get-collections) * [Get anime/manga data](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#get-anime-manga-data) * [Search / List](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#search-list) * [Custom GraphQL query](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist#custom-graphql-query) Copy $anilist.clearCache() Copy $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 Copy $anilist.updateEntryProgress( mediaId: number, progress: number, status: $app.AL_MediaListStatus | undefined, ): void Copy $anilist.updateEntryRepeat(mediaId: number, repeat: number): void Copy $anilist.deleteEntry(mediaListEntryId: number): void Copy /** * 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 Copy /** * 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 Copy /** * 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 Copy /** * 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 Copy $anilist.customQuery(body: Record, token: string): T --- # User Interface | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface.md) . [Tray](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray) [Webview](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview) [Toast](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast) [Screen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen) [Command Palette](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette) [Action](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action) [DOM](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom) [PreviousCron](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron) [NextTray](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray) Last updated 3 months ago --- # Downloader | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader.md) . Permission[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader#permission) --------------------------------------------------------------------------------------------------------- `system` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["system"], "allow": { "writePaths": ["$DOWNLOAD/**/*"] } } } } Copy //... // 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) [PreviousDownloading](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading) [NextTorrent Client](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client) Last updated 3 months ago --- # MIME | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime.md) . Copy 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 {} [PreviousBuffers, I/O](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o) [NextUI](https://seanime.gitbook.io/seanime-extensions/plugins/ui) Last updated 3 months ago --- # Auth | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth.md) . The `ctx.auth` API lets a UI plugin log the user in or out of AniList after the user approves a prompt. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#permissions) ----------------------------------------------------------------------------------------------- `auth` permission is required. Copy { //... "plugin": { "permissions": { "scopes": ["auth"] } } } This API is not bound in secure mode. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#core-methods) ------------------------------------------------------------------------------------------------- ### login[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#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` ### logout[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#logout) `ctx.auth.logout()` Logs the user out of AniList after the user approves the prompt. **Returns:** `Promise` ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#example) [PreviousOther](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other) [NextApp Settings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/app-settings) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#core-methods) * [login](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#login) * [logout](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#logout) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth#example) Copy $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "AniList auth", withContent: true, }) const tokenRef = ctx.fieldRef("") ctx.registerEventHandler("login-anilist", async () => { try { await ctx.auth.login(tokenRef.current ?? "") tokenRef.setValue("") ctx.toast.success("AniList login updated") } catch (error) { ctx.toast.alert(`Login failed: ${error.message}`) } }) ctx.registerEventHandler("logout-anilist", async () => { try { await ctx.auth.logout() ctx.toast.info("AniList logged out") } catch (error) { ctx.toast.alert(`Logout failed: ${error.message}`) } }) tray.render(() => tray.stack([\ tray.input("AniList token", { fieldRef: tokenRef }),\ tray.button("Log in", { onClick: "login-anilist" }),\ tray.button("Log out", { onClick: "logout-anilist" }),\ ])) }) --- # Example | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/example.md) . Let's create a plugin that allows users to change banner images of anime that are in the AniList collection. We'll use both the UI and hooks APIs. my-plugin.ts Copy /// /// function init() { $ui.register((ctx) => { // Create the tray icon const tray = ctx.newTray({ tooltipText: "Anime banner image", iconUrl: "https://seanime.rahim.app/logo_2.png", withContent: true, }) // Keep track of the current media ID const currentMediaId = ctx.state(0) // Create a field ref for the URL input const inputRef = ctx.fieldRef() // When the plugin loads, fetch the current screen and set the badge to 0 ctx.screen.loadCurrent() // Triggers onNavigate tray.updateBadge({ number: 0 }) // Also fetch current screen when tray is open tray.onOpen(() => { ctx.screen.loadCurrent() }) // Updates the field's value and badge based on the current anime page function updateState() { // Reset the badge and input if the user currently isn't on an anime page if (!currentMediaId.get()) { inputRef.setValue("") tray.updateBadge({ number: 0 }) } // Get the stored banner image URL for this anime const url = $storage.get("bannerImages." + currentMediaId.get()) if (url) { // If there's a URL, set the value of the input inputRef.setValue(url) // Add a badge tray.updateBadge({ number: 1, intent: "info" }) } else { inputRef.setValue("") tray.updateBadge({ number: 0 }) } } // Run the function when the plugin loads updateState() // Update currentMediaId when the user navigates ctx.screen.onNavigate((e) => { // If the user navigates to an anime page if (e.pathname === "/entry" && !!e.searchParams.id) { // Get the ID from the URL const id = parseInt(e.searchParams.id) currentMediaId.set(id) } else { currentMediaId.set(0) } }) // This effect will update the state each time currentMediaId changes ctx.effect(() => { updateState() }, [currentMediaId]) // Create a handler to store the custom banner image URL ctx.registerEventHandler("save", () => { if (!!inputRef.current) { $storage.set(`bannerImages.${currentMediaId.get()}`, inputRef.current) } else { $storage.remove(`bannerImages.${currentMediaId.get()}`) } ctx.toast.success("Banner image saved") updateState() // Update the state // Updates the data on the client // This is better than calling ctx.screen.reload() $anilist.refreshAnimeCollection() }); // Tray content tray.render(() => { return tray.stack([\ currentMediaId.get() === 0 \ ? tray.text("Open an anime") \ : tray.stack([\ tray.text(`Current media ID: ${currentMediaId.get()}`),\ tray.input({ fieldRef: inputRef }),\ tray.button({ label: "Save", onClick: "save" }),\ ])\ ]) }) }) // Register hook handlers to listen and modify the anime collection. // Triggers the app fetches the user's AniList anime collection $app.onGetAnimeCollection((e) => { const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } for (let i = 0; i < e.animeCollection!.mediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.mediaListCollection!.lists![i]!.entries!.length; j++) { const mediaId = e.animeCollection!.mediaListCollection!.lists![i]!.entries![j]!.media!.id const bannerImage = bannerImages[mediaId.toString()] if (!!bannerImage) { e.animeCollection!.mediaListCollection!.lists![i]!.entries![j]!.media!.bannerImage = bannerImage } } } e.next() }) // Same as onGetAnimeCollection but also includes custom lists. $app.onGetRawAnimeCollection((e) => { const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } for (let i = 0; i < e.animeCollection!.mediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.mediaListCollection!.lists![i]!.entries!.length; j++) { const mediaId = e.animeCollection!.mediaListCollection!.lists![i]!.entries![j]!.media!.id const bannerImage = bannerImages[mediaId.toString()] if (!!bannerImage) { e.animeCollection!.mediaListCollection!.lists![i]!.entries![j]!.media!.bannerImage = bannerImage } } } e.next() }) } [PreviousHooks](https://seanime.gitbook.io/seanime-extensions/plugins/hooks) [NextFeature requests](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests) Last updated 3 months ago --- # Other | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other.md) . [Auth](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth) [App Settings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/app-settings) [Extensions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions) [Manga](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga) [Discord](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord) [PreviousDebrid](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid) [NextAuth](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/auth) Last updated 3 months ago --- # Cron | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron.md) . `ctx.cron` lets UI plugins register recurring jobs. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#permissions) ----------------------------------------------------------------------------------------- `cron` permission is required. Copy { //... "plugin": { "permissions": { "scopes": ["cron"] } } } Quick example[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#quick-example) --------------------------------------------------------------------------------------------- Copy $ui.register((ctx) => { ctx.cron.add("refresh-anime-cache", "*/15 * * * *", () => { console.log("refreshing anime cache") }) ctx.cron.start() }) Expressions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#methods) --------------------------------------------------------------------------------- ### add[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#remove) `ctx.cron.remove(jobId)` Removes one job by ID. ### removeAll[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#removeall) `ctx.cron.removeAll()` Removes every registered job. ### total[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#total) `ctx.cron.total()` Returns the number of registered jobs. ### start[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#start) `ctx.cron.start()` Starts the scheduler. Calling `start()` again restarts it. ### stop[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#stop) `ctx.cron.stop()` Stops the scheduler. ### hasStarted[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#hasstarted) `ctx.cron.hasStarted()` Returns whether the scheduler is currently running. Notes[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#notes) ----------------------------------------------------------------------------- * Adding jobs does not start the scheduler automatically. * Jobs run asynchronously when they become due. [PreviousHelpers](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers) [NextUser Interface](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#permissions) * [Quick example](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#quick-example) * [Expressions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#expressions) * [Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#methods) * [add](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#add) * [remove](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#remove) * [removeAll](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#removeall) * [total](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#total) * [start](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#start) * [stop](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#stop) * [hasStarted](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#hasstarted) * [Notes](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron#notes) --- # Discord | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord.md) . Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#permissions) -------------------------------------------------------------------------------------------------- `discord` permission is required Copy { //... "plugin": { "permissions": { "scopes": ["discord"] } } } ctx.discord[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#core-methods) ---------------------------------------------------------------------------------------------------- ### setAnimeActivity[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#cancel) The `cancel()` function terminates any ongoing Discord Rich Presence activity. [PreviousManga](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga) [NextHooks](https://seanime.gitbook.io/seanime-extensions/plugins/hooks) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#permissions) * [ctx.discord](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#ctx.discord) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#core-methods) * [setAnimeActivity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#setanimeactivity) * [updateAnimeActivity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#updateanimeactivity) * [setMangaActivity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#setmangaactivity) * [cancel](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord#cancel) --- # Extensions | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions.md) . The `ctx.extensions` API lets a plugin enable or disable other extensions after the user approves the prompt. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#permissions) ----------------------------------------------------------------------------------------------------- `extensions` permission is required. Copy { //... "plugin": { "permissions": { "scopes": ["extensions"] } } } This API is not bound in secure mode. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#core-methods) ------------------------------------------------------------------------------------------------------- ### enable[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#enable) `ctx.extensions.enable(extensionId)` Enables the extension with that ID. ### disable[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#disable) `ctx.extensions.disable(extensionId)` Disables the extension with that ID. ### setDisabled[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#setdisabled) `ctx.extensions.setDisabled(extensionId, disabled)` Sets the disabled state directly. ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#example) [PreviousApp Settings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/app-settings) [NextManga](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#core-methods) * [enable](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#enable) * [disable](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#disable) * [setDisabled](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#setdisabled) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions#example) Copy $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" }),\ ])) }) --- # Manga | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga.md) . The `ctx.manga` API provides methods to interact with the manga system in Seanime. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#core-methods) -------------------------------------------------------------------------------------------------- ### getProviders[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getproviders) Gets all provider extensions Copy const providers = ctx.manga.getProviders() for (const providerId in providers) { console.log("ID:", providerId, "Name:", providers[providerId]) } ### getChapterContainer[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getchaptercontainer) Gets a chapter container for a specific manga, using the cache if available. **Parameters:** * `opts`: Object containing: * `mediaId`: Number - The AniList media ID * `provider`: String - The manga provider identifier * `titles`: String\[\] (Optional) - Alternative titles to help find the manga * `year`: Number (Optional) - Release year to help with identification **Example:** ### getDownloadedChapters[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getdownloadedchapters) Retrieves all downloaded manga chapters grouped by provider and manga ID. **Example:** ### getCollection[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getcollection) Retrieves the user's manga collection with all media list data. **Example:** ### refreshChapters[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#refreshchapters) Deletes all cached chapters and refetches them based on the selected provider for each manga. **Parameters:** * `selectedProviderMap`: Record - A map of manga IDs to provider IDs **Example:** ### emptyCache[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#emptycache) Empties cached chapters for a specific manga. **Parameters:** * `mediaId`: Number - The AniList media ID **Example:** [PreviousExtensions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/extensions) [NextDiscord](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/discord) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#core-methods) * [getProviders](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getproviders) * [getChapterContainer](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getchaptercontainer) * [getDownloadedChapters](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getdownloadedchapters) * [getCollection](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#getcollection) * [refreshChapters](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#refreshchapters) * [emptyCache](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/manga#emptycache) Copy // Get chapter container for a manga from MangaDex const mangaContainer = await ctx.manga.getChapterContainer({ mediaId: 000000, provider: "mangadex", titles: ["Kimetsu no Yaiba", "Demon Slayer"], year: 2016 }) if (mangaContainer) { console.log(`Found ${mangaContainer.chapters.length} chapters from ${mangaContainer.provider}`) // Process chapters for (const chapter of mangaContainer.chapters) { console.log(`Chapter ${chapter.chapter}: ${chapter.title}`) } } Copy // Get all downloaded chapters const downloadedChapters = await ctx.manga.getDownloadedChapters() // Count chapters per manga const chaptersByManga = {} for (const container of downloadedChapters) { if (!chaptersByManga[container.mediaId]) { chaptersByManga[container.mediaId] = 0 } chaptersByManga[container.mediaId] += container.chapters.length } console.log("Downloaded chapters by manga:", chaptersByManga) Copy // Get the user's manga collection const mangaCollection = await ctx.manga.getCollection() // Process each list in the collection for (const list of mangaCollection.lists) { console.log(`List ${list.status}: ${list.entries.length} entries`) // Process each manga in the list for (const entry of list.entries) { const manga = entry.media const progress = entry.listData?.progress || 0 console.log(`${manga.title.userPreferred}: ${progress}/${manga.chapters || '?'} chapters read`) } } Copy // Refresh chapters for specific manga using selected providers const providerSelections = { 30013: "mangadex", 21: "mangasee", 31706: "manganato" } // Refresh all chapters based on these provider preferences await ctx.manga.refreshChapters(providerSelections) console.log("Chapter data refreshed for selected manga") Copy // Clear cached chapters for a manga (e.g., after a major update) await ctx.manga.emptyCache(30013) console.log("Cache cleared for Demon Slayer") // Refetch immediately to get fresh data const freshData = await ctx.manga.getChapterContainer({ mediaId: 30013, provider: "mangadex" }) --- # Toast | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast.md) . Info[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#info) ------------------------------------------------------------------------------------------- Copy ctx.toast.info("Info!") Alert[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#alert) --------------------------------------------------------------------------------------------- Copy ctx.toast.alert("Alert!") Warning[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#warning) ------------------------------------------------------------------------------------------------- Copy ctx.toast.warning("Warning!") Success[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#success) ------------------------------------------------------------------------------------------------- Copy ctx.toast.success("Success!") [PreviousWebview](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview) [NextScreen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen) Last updated 3 months ago * [Info](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#info) * [Alert](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#alert) * [Warning](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#warning) * [Success](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast#success) --- # Torrent Client | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client.md) . Permission[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#permission) ------------------------------------------------------------------------------------------------------------- `torrent-client` permission is required. my-plugin.json Copy { //... "plugin": { "permissions": { "scopes": ["torrent-client"], } } } Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#core-methods) ----------------------------------------------------------------------------------------------------------------- ### getTorrents[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#gettorrents) Copy getTorrents() Retrieves a list of all torrents in the torrent client. Example: Copy // 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#getactivetorrents) Retrieves a list of active torrents (downloading/uploading) from the torrent client. Example: ### addMagnets[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#addmagnets) Adds magnet links to the torrent client. **Parameters**: * `magnets`: string\[\] - Array of magnet links * `dest`: string - Destination path for downloaded files Example: ### removeTorrents[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#removetorrents) Removes torrents from the client. **Parameters**: * `hashes`: string\[\] - Array of torrent hashes to remove Example: ### pauseTorrents[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#pausetorrents) Pauses specified torrents. **Parameters**: * `hashes`: string\[\] - Array of torrent hashes to pause Example: ### resumeTorrents[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#resumetorrents) Resumes specified torrents. **Parameters**: * `hashes`: string\[\] - Array of torrent hashes to resume Example: ### deselectFiles[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#deselectfiles) Deselects specific files within a torrent. **Parameters**: * `hash`: string - Hash of the torrent * `indices`: number\[\] - Array of file indices to deselect Example: ### getFiles[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#getfiles) Retrieves all files within a specific torrent. **Parameters**: * `hash`: string - Hash of the torrent Example: [PreviousDownloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/downloader) [NextDebrid](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid) Last updated 3 months ago * [Permission](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#permission) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#core-methods) * [getTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#gettorrents) * [getActiveTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#getactivetorrents) * [addMagnets](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#addmagnets) * [removeTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#removetorrents) * [pauseTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#pausetorrents) * [resumeTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#resumetorrents) * [deselectFiles](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#deselectfiles) * [getFiles](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client#getfiles) Copy getActiveTorrents() Copy // 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) } Copy addMagnets(magnets, dest) Copy // 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) } Copy removeTorrents(hashes) Copy // 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) } Copy pauseTorrents(hashes) Copy // Pause specific torrents try { await ctx.torrentClient.pauseTorrents(["abc123def456", "xyz789uvw"]) console.log("Torrents paused successfully") } catch (error) { console.error("Error pausing torrents:", error) } Copy resumeTorrents(hashes) Copy // Resume specific torrents try { await ctx.torrentClient.resumeTorrents(["abc123def456", "xyz789uvw"]) console.log("Torrents resumed successfully") } catch (error) { console.error("Error resuming torrents:", error) } Copy deselectFiles(hash, indices) Copy // 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) } Copy getFiles(hash) Copy // 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) } --- # Debrid | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid.md) . The `ctx.debrid` API lets plugins inspect debrid state, add torrents, and manage local debrid downloads. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#permissions) ------------------------------------------------------------------------------------------------------- `debrid` permission is required. If you use `addAndQueueTorrent()` or `downloadTorrent()`, the destination must also be covered by `allow.writePaths`. Copy { //... "plugin": { "permissions": { "scopes": ["debrid"], "allow": { "writePaths": ["$DOWNLOAD/**/*"] } } } } Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#core-methods) --------------------------------------------------------------------------------------------------------- ### hasProvider[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#hasprovider) `hasProvider()` Returns whether a debrid provider is currently configured. ### getSettings[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#getsettings) `getSettings()` Returns the current debrid settings, or `undefined` if debrid is not configured. ### getQueuedDownloads[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#getqueueddownloads) `getQueuedDownloads()` Returns debrid downloads that Seanime has queued for local download. ### addTorrent[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#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` - The debrid torrent item ID **Example:** ### addAndQueueTorrent[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#addandqueuetorrent) `addAndQueueTorrent(options)` Adds a torrent to the configured debrid provider and queues it for local download. The destination must be an absolute path and must be authorized by the plugin's `allow.writePaths`. **Returns:** `Promise` - The debrid torrent item ID **Example:** ### getTorrentInfo[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrentinfo) `getTorrentInfo(options)` Gets torrent info from the configured debrid provider. **Parameters:** * `options`: `DebridGetTorrentInfoOptions` * `options.magnetLink`: String - Optional * `options.infoHash`: String - Optional ### getTorrents[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrents) `getTorrents()` Gets torrents from the configured debrid provider. **Returns:** `Promise` ### deleteTorrent[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#deletetorrent) `deleteTorrent(torrentId)` Deletes a torrent from the configured debrid provider. **Parameters:** * `torrentId`: String - Debrid torrent item ID ### cancelDownload[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#canceldownload) `cancelDownload(itemId)` Cancels an active local debrid download. **Parameters:** * `itemId`: String - Queued download item ID ### downloadTorrent[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#downloadtorrent) `downloadTorrent(options)` Downloads a debrid torrent locally. The destination must be an absolute path and must be authorized by the plugin's `allow.writePaths`. **Parameters:** * `options.torrentItem`: `DebridTorrentItem` * `options.destination`: String - Absolute local path **Example:** ### getTorrentFilePreviews[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrentfilepreviews) `getTorrentFilePreviews(options)` Returns parsed file previews for a torrent before manual selection. **Parameters:** * `options.torrent`: `$app.HibikeTorrent_AnimeTorrent` * `options.episodeNumber`: Number * `options.media`: `$app.AL_BaseAnime` **Returns:** `Promise` **Example:** [PreviousTorrent Client](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/torrent-client) [NextOther](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#core-methods) * [hasProvider](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#hasprovider) * [getSettings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#getsettings) * [getQueuedDownloads](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#getqueueddownloads) * [addTorrent](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#addtorrent) * [addAndQueueTorrent](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#addandqueuetorrent) * [getTorrentInfo](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrentinfo) * [getTorrents](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrents) * [deleteTorrent](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#deletetorrent) * [cancelDownload](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#canceldownload) * [downloadTorrent](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#downloadtorrent) * [getTorrentFilePreviews](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading/debrid#gettorrentfilepreviews) Copy const torrentItemId = await ctx.debrid.addTorrent({ magnetLink: "magnet:?xt=urn:btih:...", }) console.log(torrentItemId) Copy const torrentItemId = await ctx.debrid.addAndQueueTorrent({ magnetLink: "magnet:?xt=urn:btih:...", destination: "/Users/rahim/Downloads/Anime", mediaId: 21, }) console.log(torrentItemId) Copy const torrents = await ctx.debrid.getTorrents() if (torrents[0]) { await ctx.debrid.downloadTorrent({ torrentItem: torrents[0], destination: "/Users/rahim/Downloads/Anime", }) } Copy const previews = await ctx.debrid.getTorrentFilePreviews({ torrent, episodeNumber: 1, media: $anilist.getAnime(21), }) console.log(previews) --- # Helpers | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers.md) . The UI context includes some helper groups for common plugin workflows: * `ctx.cache` * `ctx.settings` * `ctx.jobs` cache[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cache) -------------------------------------------------------------------------------- `ctx.cache` stores runtime-local values in memory. ### get[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#get) `ctx.cache.get(key, fallback?)` Returns the cached value, or the fallback if the key does not exist. ### set[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#set) `ctx.cache.set(key, value, ttl?)` Stores a value and optionally expires it after a number of milliseconds. You can pass either a raw number or an object like `{ ttl: 5000 }`. ### has[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#has) `ctx.cache.has(key)` Returns whether the key exists and has not expired. ### remove[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#remove) `ctx.cache.remove(key)` Deletes a cached entry and returns whether it existed. `ctx.cache.delete(key)` is an alias. ### clear[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#clear) `ctx.cache.clear()` Removes every cached entry. ### size[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#size) `ctx.cache.size()` Returns the current number of cached entries. ### getOrSet[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#getorset) `ctx.cache.getOrSet(key, loader, ttl?)` Returns the cached value when present. Otherwise it runs `loader`, stores the result, and returns it. If `loader` returns a promise, concurrent calls with the same key share the same in-flight promise. `ctx.cache.getOrLoad(...)` and `ctx.cache.remember(...)` are aliases. Example: settings[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#settings) -------------------------------------------------------------------------------------- `ctx.settings.define(name, defaults)` creates a settings helper scoped to one namespace. `ctx.settings` stores plugin-local UI settings. To read or edit Seanime's app settings, use [App Settings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/other/app-settings) . Settings are always mirrored in `$store`. If the plugin also has the `storage` permission, they are persisted in `$storage` as well. The returned object exposes these fields and methods. ### key[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#key) `settings.key` The internal storage key, prefixed with `settings:`. ### defaults[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#defaults) `settings.defaults` The default object passed to `define()`. ### get[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#get-1) `settings.get(path?, fallback?)` Returns the whole settings object, or one dot-path value. ### set[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#set-1) `settings.set(path, value)` Updates a single dot-path value. `settings.set(object)` merges the provided object into the current settings. ### save[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#save) `settings.save(value?)` Saves a full value after merging it with defaults. If you omit the value, Seanime re-saves the current settings. ### reset[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#reset) `settings.reset()` Restores the settings to their defaults. ### fieldRef[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#fieldref) `settings.fieldRef(path?)` Returns a field reference for the whole settings object or for a nested path. ### watch[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#watch) `settings.watch(callback)` Registers a callback that receives the full settings object every time it changes. Returns an unsubscribe function. Example: jobs[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#jobs) ------------------------------------------------------------------------------ `ctx.jobs` helps coordinate repeated or overlapping UI work. ### singleflight[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#singleflight) `ctx.jobs.singleflight(key, callback)` Runs only one job for the same key at a time. If another call starts while the first one is still running, Seanime returns the existing promise. This is useful for deduplicating button clicks or repeated fetches. ### debounce[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#debounce) `ctx.jobs.debounce(key, callback, delayMs)` Schedules a callback after `delayMs`. Calling it again with the same key resets the timer. Returns a cancel function. ### poll[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#poll) `ctx.jobs.poll(key, callback, intervalMs, options?)` Runs a callback on an interval until canceled. **Options:** * `options.immediate`: Boolean - Optional. When `true`, runs once immediately before the interval starts. Returns a cancel function. ### cancel[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cancel) `ctx.jobs.cancel(key)` Cancels a debounced or polling job for the key. Returns `true` when a cancelable job existed. ### cancelAll[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cancelall) `ctx.jobs.cancelAll()` Cancels every debounced or polling job. ### isRunning[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#isrunning) `ctx.jobs.isRunning(key)` Returns whether a `singleflight()` job with this key is currently in progress. Example: [PreviousBasics](https://seanime.gitbook.io/seanime-extensions/plugins/ui/basics) [NextCron](https://seanime.gitbook.io/seanime-extensions/plugins/ui/cron) Last updated 3 months ago * [cache](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cache) * [get](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#get) * [set](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#set) * [has](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#has) * [remove](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#remove) * [clear](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#clear) * [size](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#size) * [getOrSet](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#getorset) * [settings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#settings) * [key](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#key) * [defaults](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#defaults) * [get](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#get-1) * [set](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#set-1) * [save](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#save) * [reset](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#reset) * [fieldRef](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#fieldref) * [watch](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#watch) * [jobs](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#jobs) * [singleflight](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#singleflight) * [debounce](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#debounce) * [poll](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#poll) * [cancel](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cancel) * [cancelAll](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#cancelall) * [isRunning](https://seanime.gitbook.io/seanime-extensions/plugins/ui/helpers#isrunning) Copy $ui.register((ctx) => { const currentMediaId = ctx.state(null) const currentTitle = ctx.state("Open an anime entry") ctx.screen.onNavigate((event) => { if (event.pathname === "/entry" && event.searchParams.id) { currentMediaId.set(parseInt(event.searchParams.id)) return } currentMediaId.set(null) currentTitle.set("Open an anime entry") ctx.cache.remove("current-entry") }) ctx.effect(async () => { const mediaId = currentMediaId.get() if (!mediaId) { return } const animeEntry = await ctx.cache.getOrSet( `anime-entry:${mediaId}`, () => ctx.anime.getAnimeEntry(mediaId), { ttl: 60_000 }, ) currentTitle.set(animeEntry?.media?.title?.userPreferred ?? "Unknown title") ctx.cache.set("current-entry", { mediaId, loadedAt: Date.now(), }, 5_000) console.log("cache size", ctx.cache.size()) }, [currentMediaId]) ctx.screen.loadCurrent() }) Copy const settings = ctx.settings.define("banner-images", { enabled: true, opacity: 0.8, }) Copy $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "DX settings example", withContent: true, }) const settings = ctx.settings.define("entry-filters", { query: "", onlyUnwatched: false, sort: "title", }) const queryRef = settings.fieldRef("query") queryRef.onValueChange((value) => { settings.set("query", value) }) const stopWatching = settings.watch((nextSettings) => { console.log("filters updated", nextSettings) }) ctx.registerEventHandler("toggle-unwatched", () => { settings.set("onlyUnwatched", !settings.get("onlyUnwatched", false)) }) ctx.registerEventHandler("reset-filters", () => { settings.reset() queryRef.setValue(settings.get("query", "")) ctx.toast.success("Filters reset") }) ctx.registerEventHandler("stop-filter-watch", () => { stopWatching() ctx.toast.info("Stopped watching settings changes") }) tray.render(() => tray.stack([\ tray.text(`Sort: ${settings.get("sort", "title")}`),\ tray.text(`Only unwatched: ${settings.get("onlyUnwatched", false) ? "yes" : "no"}`),\ tray.input("Search", { fieldRef: queryRef }),\ tray.button("Toggle unwatched", { onClick: "toggle-unwatched" }),\ tray.button("Reset filters", { onClick: "reset-filters" }),\ tray.button("Stop watch", { onClick: "stop-filter-watch" }),\ ])) }) Copy $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "Jobs example", withContent: true, }) const currentMediaId = ctx.state(null) const status = ctx.state("idle") const lastRefreshAt = ctx.state(null) async function refreshEntryInfo() { const mediaId = currentMediaId.get() if (!mediaId) { return } status.set("refreshing") await ctx.jobs.singleflight("entry-info-refresh", () => { return ctx.anime.getEntryDownloadInfo(mediaId) }) lastRefreshAt.set(Date.now()) status.set("ready") } ctx.screen.onNavigate((event) => { if (event.pathname === "/entry" && event.searchParams.id) { currentMediaId.set(parseInt(event.searchParams.id)) ctx.jobs.debounce("entry-info-debounce", () => { return refreshEntryInfo() }, 400) return } currentMediaId.set(null) status.set("idle") ctx.jobs.cancel("entry-info-debounce") }) const cancelPolling = ctx.jobs.poll("entry-info-poll", () => { if (!currentMediaId.get()) { return } return refreshEntryInfo() }, 30_000, { immediate: true }) ctx.registerEventHandler("refresh-now", () => { return refreshEntryInfo() }) ctx.registerEventHandler("stop-polling", () => { cancelPolling() ctx.toast.info("Stopped background refresh") }) tray.render(() => tray.stack([\ tray.text(`Status: ${status.get()}`),\ tray.text(`Running: ${ctx.jobs.isRunning("entry-info-refresh") ? "yes" : "no"}`),\ tray.text(`Last refresh: ${lastRefreshAt.get() ?? "never"}`),\ tray.button("Refresh now", { onClick: "refresh-now" }),\ tray.button("Stop polling", { onClick: "stop-polling" }),\ ])) ctx.screen.loadCurrent() }) --- # Screen | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen.md) . **Pitfall** Users having multiple tabs open can lead to **unexpected behavior**. This happens because navigation events are received from all connected clients. Listen to navigation[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#listen-to-navigation) ---------------------------------------------------------------------------------------------------------------------------- Copy // 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#navigate) ---------------------------------------------------------------------------------------------------- Copy // Navigate to the 'Sakamoto Days' anime page ctx.screen.navigateTo("/entry", { "id": "177709" }) Reload the screen[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#reload-the-screen) ---------------------------------------------------------------------------------------------------------------------- Get the current screen[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#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. [PreviousToast](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast) [NextCommand Palette](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette) Last updated 3 months ago * [Listen to navigation](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#listen-to-navigation) * [Navigate](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#navigate) * [Reload the screen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#reload-the-screen) * [Get the current screen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen#get-the-current-screen) Copy // Hard reload the webapp/desktop client screen. ctx.screen.reload() Copy ctx.screen.loadCurrent() --- # Command Palette | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette.md) . Create a command palette[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#create-a-command-palette) --------------------------------------------------------------------------------------------------------------------------------------------- You can only create one command palette in your plugin. Copy // ... const cmd = ctx.newCommandPalette({ placeholder: "Search for something", // The command palette will open when the user presses 't' // You can choose to not have a keyboard shortcut keyboardShortcut: "t", }) // Open the command palette when the tray icon is clicked tray.onClick(() => { cmd.open() }) ### Keyboard shortcut[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#keyboard-shortcut) You can set a keyboard shortcut for your command palette. Read this documentation to learn how to format it: [https://craig.is/killing/mice](https://craig.is/killing/mice) . Items[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#items) ------------------------------------------------------------------------------------------------------- [PreviousScreen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/screen) [NextAction](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action) Last updated 3 months ago * [Create a command palette](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#create-a-command-palette) * [Keyboard shortcut](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#keyboard-shortcut) * [Items](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette#items) Copy async function fetchTodos() { // Fetch the todos const res = await ctx.fetch("https://jsonplaceholder.typicode.com/todos") const todos = res.json<{ title: string }[]>() // Set the items // Calling `setItems` will automatically re-render the command palette cmd.setItems(todos.map((todo) => ({ label: todo.title, value: todo.title, // This is used for filtering, should be unique! // Optional filtering for when the user writes something in the input filterType: "includes", // or "contains" onSelect: () => { ctx.toast.info(`Todo ${todo.title} selected`) }, }))) } // Default item cmd.setItems([\ {\ label: "Fetch Todos",\ value: "fetch todos",\ onSelect: async () => {\ await fetchTodos()\ },\ },\ ]) --- # Anime/Library | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library.md) . [Anime](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime) [Playback (External)](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external) [VideoCore](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore) [MPV](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv) [Continuity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity) [Scanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner) [Auto Downloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader) [Auto Scanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner) [Filler Manager](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager) [External Player Link](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link) [Torrentstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream) [Debridstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream) [Torrent Search](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search) [Auto Select](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select) [PreviousDOM](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom) [NextAnime](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime) Last updated 3 months ago --- # Tray | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray.md) . ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FyfYaPiEkChyI8oOCqTDh%252Fimage.png%3Falt%3Dmedia%26token%3D3d1d5b98-1ad2-4389-a2cb-97d8be07410a&width=768&dpr=3&quality=100&sign=a4c6bc96&sv=2) Example Create a tray icon[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#create-a-tray-icon) ---------------------------------------------------------------------------------------------------------------------- You can only create one tray icon in your plugin Add a badge[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#add-a-badge) -------------------------------------------------------------------------------------------------------- * `intent`: "alert" | "info" | "warning" | "success" Rendering content[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#rendering-content) -------------------------------------------------------------------------------------------------------------------- `withContent` should be set to 'true'. The `state` is a mechanism to keep track of variable data over time, enabling components to re-render automatically when the data changes. The `render()` function is used to define how content should be presented in the tray popover. It takes a callback function that returns a tree of components. Components, like `tray.text` and `tray.button`, are reusable building blocks of the UI, each responsible for rendering a piece of the interface according to the current state. The tray will be re-rendered anytime there is a state change even if the state isn't in the render function. Tray events[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#tray-events) -------------------------------------------------------------------------------------------------------- Event handlers[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#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. #### Tips[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#tips) You can register inline event handlers. Make sure the first argument is unique to that element. Base components[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#base-components) ---------------------------------------------------------------------------------------------------------------- Fields/Forms[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#fields-forms) ---------------------------------------------------------------------------------------------------------- Field components: * input * button * select * radioGroup * checkbox * switch Use `ctx.fieldRef` to get and set a field's value synchronously. ### Select, RadioGroup[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#select-radiogroup) You can create forms easily with `ctx.fieldRef` and the available field components. ### Checkbox, Switch[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#checkbox-switch) Complex components[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#complex-components) ---------------------------------------------------------------------------------------------------------------------- ### CSS[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#css) ### Tabs, Dropdown, Modal[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#tabs-dropdown-modal) [PreviousUser Interface](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface) [NextWebview](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview) Last updated 3 months ago * [Create a tray icon](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#create-a-tray-icon) * [Add a badge](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#add-a-badge) * [Rendering content](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#rendering-content) * [Tray events](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#tray-events) * [Event handlers](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#event-handlers) * [Base components](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#base-components) * [Fields/Forms](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#fields-forms) * [Select, RadioGroup](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#select-radiogroup) * [Checkbox, Switch](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#checkbox-switch) * [Complex components](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#complex-components) * [CSS](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#css) * [Tabs, Dropdown, Modal](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray#tabs-dropdown-modal) Copy const tray = ctx.newTray({ tooltipText: "My plugin", iconUrl: "https://seanime.rahim.app/logo_2.png", withContent: true, isDrawer: false, // Choose whether the tray contents are displayed in a drawer }) Copy tray.updateBadge({ number: 1, intent: "alert" }) // Remove the badge tray.updateBadge({ number: 0 }) Copy 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")\ ], }) }) Copy 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() Copy //.. 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" }),\ ], }) }) Copy 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" } }) })) Copy 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(...) Copy const textInputRef = ctx.fieldRef("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" }),\ ])) Copy const selectRef = ctx.fieldRef() const radioGroupRef = ctx.fieldRef() tray.render(() => tray.stack([\ tray.select("Label", { \ placeholder: "Select...",\ options: [\ { label: "One Piece", value: "21" },\ { label: "Sakamoto Days", value: "177709" },\ ],\ fieldRef: selectRef,\ }),\ tray.radioGroup("Label", { \ options: [\ { label: "One Piece", value: "21" },\ { label: "Sakamoto Days", value: "177709" },\ ],\ fieldRef: radioGroupRef,\ }),\ ])) Copy const checkboxRef = ctx.fieldRef() const switchRef = ctx.fieldRef() tray.render(() => tray.stack([\ tray.checkbox("Do something", { \ fieldRef: checkboxRef\ }),\ tray.switch("Do something else", { \ fieldRef: switchRef\ }),\ ])) Copy tray.div([\ tray.stack([\ tray.css(`\ .red { background-color: red; }\ `),\ // Red square\ tray.tooltip(tray.div([], { className: "square red relative" }), { text: "Test tooltip" }),\ ]),\ tray.stack([\ // No red\ tray.tooltip(tray.div([], { className: "square red relative" }), { text: "Test tooltip" }),\ ]),\ ]) Copy tray.tabs([\ tray.tabsList([\ tray.tabsTrigger(tray.span("Item 1"), { value: "1" }),\ tray.tabsTrigger(tray.span("Item 2"), { value: "2" }),\ ]),\ tray.tabsContent([\ tray.text("Hello, World!"),\ tray.a([\ tray.span("A "),\ tray.span("link", { className: "font-bold" }),\ ], { href: "#" }),\ tray.p([\ tray.span("This is a paragraph."),\ ]),\ tray.modal({\ trigger: tray.button("Open modal"),\ open: modalOpen.get(),\ onOpenChange: ctx.eventHandler("modal-open-change", ({ open }) => {\ console.log(open)\ modalOpen.set(open)\ }),\ items: [\ tray.text("Hello, World!"),\ ],\ }),\ tray.dropdownMenu({\ trigger: tray.button("Open dropdown"),\ items: [\ tray.dropdownMenuItem(tray.span("Item 1")),\ tray.dropdownMenuItem(tray.span("Item 2")),\ tray.dropdownMenuItem(tray.span("Item 3")),\ ],\ }),\ ], { value: "1" }),\ tray.tabsContent([\ tray.text("Item 2 content"),\ ], { value: "2" }),\ ], { defaultValue: "1" }), --- # Auto Scanner | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner.md) . The `ctx.autoScanner` API lets plugins inspect or trigger Seanime's automatic library scanner. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#core-methods) ----------------------------------------------------------------------------------------------------------------- ### notify[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#notify) Notifies the auto-scanner to check for new files. Example: Copy // Notify the auto-scanner to check for new files ctx.autoScanner.notify() console.log("Auto-scanner notified to check for new files") ### runNow[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#runnow) `runNow()` Runs the auto scanner immediately. Example: Copy ctx.autoScanner.runNow() ### isEnabled[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#isenabled) `isEnabled()` Returns whether the auto scanner is enabled. Example: ### isWaiting[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#iswaiting) `isWaiting()` Returns whether the auto scanner is currently waiting for its debounce timer. Example: ### isScanning[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#isscanning) `isScanning()` Returns whether the auto scanner is actively scanning. Example: ### getWaitTimeMs[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#getwaittimems) `getWaitTimeMs()` Returns the debounce wait time in milliseconds. Example: [PreviousAuto Downloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader) [NextFiller Manager](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#core-methods) * [notify](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#notify) * [runNow](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#runnow) * [isEnabled](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#isenabled) * [isWaiting](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#iswaiting) * [isScanning](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#isscanning) * [getWaitTimeMs](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner#getwaittimems) Copy console.log(ctx.autoScanner.isEnabled()) Copy if (ctx.autoScanner.isWaiting()) { console.log("Auto scanner is waiting before the next run") } Copy console.log(ctx.autoScanner.isScanning()) Copy const waitTimeMs = ctx.autoScanner.getWaitTimeMs() console.log(`Auto scanner wait time: ${waitTimeMs}ms`) --- # Scanner | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner.md) . The `ctx.scanner` API runs a library scan immediately and persists the result to Seanime's database. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner#core-methods) ------------------------------------------------------------------------------------------------------------ ### scan[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner#scan) `scan(options?)` Runs a library scan immediately and resolves to the scanned local files. If no local files are found, Seanime resolves this call with an empty array. **Parameters:** * `options`: `ScannerScanOptions` * `options.enhanced`: Boolean - Optional * `options.enhanceWithOfflineDatabase`: Boolean - Optional * `options.skipLockedFiles`: Boolean - Optional * `options.skipIgnoredFiles`: Boolean - Optional **Returns:** `Promise<$app.Anime_LocalFile[]>` **Example:** Copy const localFiles = await ctx.scanner.scan({ enhanced: true, enhanceWithOfflineDatabase: true, skipLockedFiles: true, skipIgnoredFiles: true, }) console.log(`Scanned ${localFiles.length} files`) [PreviousContinuity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity) [NextAuto Downloader](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner#core-methods) * [scan](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner#scan) --- # Continuity | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity.md) . Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#core-methods) --------------------------------------------------------------------------------------------------------------- ### getWatchHistoryItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#getwatchhistoryitem) Gets the last recorded progress for an anime. Copy const item = ctx.continuity.getWatchHistoryItem(21) ### updateWatchHistoryItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#getwatchhistory) Gets all last recorded progress items. ### deleteWatchHistoryItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#deletewatchhistoryitem) **Parameters**: * `mediaId` : Number [PreviousMPV](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv) [NextScanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#core-methods) * [getWatchHistoryItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#getwatchhistoryitem) * [updateWatchHistoryItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#updatewatchhistoryitem) * [getWatchHistory](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#getwatchhistory) * [deleteWatchHistoryItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity#deletewatchhistoryitem) Copy const items = ctx.continuity.getWatchHistory() Copy ctx.continuity.deleteWatchHistoryItem(21) --- # MPV | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv.md) . Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#permissions) ------------------------------------------------------------------------------------------------------ `playback` permission is required Copy { //... "plugin": { "permissions": { "scopes": ["playback"] } } } Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#core-methods) -------------------------------------------------------------------------------------------------------- ### openAndPlay[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#openandplay) Opens and plays a file with MPV (without tracking). **Parameters:** * `filePath`: String - Path to a video file **Example:** ### onEvent[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#onevent) Registers a listener for MPV player events (fires frequently). **Parameters:** * `callback`: Function(event, closed) - Callback function for events **Example:** ### getConnection[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#getconnection) Returns the underlying connection object to the MPV instance. **Returns:** MpvConnection | undefined - The MPV connection if available ### stop[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#stop) Stops the MPV player. **Example:** [PreviousVideoCore](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore) [NextContinuity](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/continuity) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#core-methods) * [openAndPlay](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#openandplay) * [onEvent](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#onevent) * [getConnection](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#getconnection) * [stop](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv#stop) Copy // 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) } Copy // 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() Copy 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() } Copy // Stop playback try { ctx.mpv.stop() } catch (e) { console.log("Failed to stop player", e) } --- # Auto Downloader | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader.md) . The `ctx.autoDownloader` API lets plugins inspect or trigger Seanime's auto-downloader. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#core-methods) -------------------------------------------------------------------------------------------------------------------- ### run[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#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: Copy ctx.autoDownloader.run(true) ### runNow[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#runnow) `runNow()` Runs the auto-downloader immediately with real downloads enabled. This call is non-blocking. Example: Copy ctx.autoDownloader.runNow() ### runCheck[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#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: ### getSimulationResults[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#getsimulationresults) `getSimulationResults()` Returns the stored results from the last `runCheck()` call. Example: ### clearSimulationResults[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#clearsimulationresults) `clearSimulationResults()` Clears the stored auto-downloader simulation results. Example: ### getSettings[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#getsettings) `getSettings()` Returns the current auto-downloader settings, or `undefined` if the feature is not configured. **Returns:** `$app.Models_AutoDownloaderSettings | undefined` Example: ### isEnabled[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#isenabled) `isEnabled()` Returns whether Seanime's auto-downloader is currently enabled. Example: [PreviousScanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/scanner) [NextAuto Scanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#core-methods) * [run](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#run) * [runNow](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#runnow) * [runCheck](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#runcheck) * [getSimulationResults](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#getsimulationresults) * [clearSimulationResults](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#clearsimulationresults) * [getSettings](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#getsettings) * [isEnabled](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-downloader#isenabled) Copy const results = await ctx.autoDownloader.runCheck({ isSimulation: true, ruleIds: [12, 18], }) console.log(results) Copy const results = ctx.autoDownloader.getSimulationResults() console.log(results.length) Copy ctx.autoDownloader.clearSimulationResults() Copy const settings = ctx.autoDownloader.getSettings() console.log(settings) Copy if (ctx.autoDownloader.isEnabled()) { ctx.autoDownloader.runNow() } --- # Webview | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview.md) . Webview Plugins can be used to create all kinds of interfaces using HTML and JS. Create a Webview[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#create-a-webview) --------------------------------------------------------------------------------------------------------------------- You can only create **one** webview per **slot**. Copy // Create a panel that appears below the home screen toolbar const panel = ctx.newWebview({ slot: "after-home-screen-toolbar", fullWidth: true, autoHeight: true, }) ### Options[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#options) Copy 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#screen-slot) A Webview created with the slot `screen` will be rendered in its own page. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FeZVZIkSEqmbYdm4oQK8R%252Fimage.png%3Falt%3Dmedia%26token%3Dbfc2668c-cf81-445b-be04-793101cff11a&width=768&dpr=3&quality=100&sign=29ab692e&sv=2) ### Fixed Slot[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#fixed-slot) ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252Fw6S3Q2bDVPv2fLADdauP%252FUntitled.webp%3Falt%3Dmedia%26token%3Decee1bb7-c218-481f-8c85-a0c20f8850c1&width=768&dpr=3&quality=100&sign=66ef46df&sv=2) Events[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#events) ------------------------------------------------------------------------------------------------- Rendering[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#rendering) ------------------------------------------------------------------------------------------------------- ### HTML[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#html) ### Messages[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#messages) ### Example[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#example) This example uses Preact, a lightweight React alternative suitable for webviews. [PreviousTray](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/tray) [NextToast](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/toast) Last updated 3 months ago * [Create a Webview](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#create-a-webview) * [Options](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#options) * [Screen Slot](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#screen-slot) * [Fixed Slot](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#fixed-slot) * [Events](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#events) * [Rendering](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#rendering) * [HTML](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#html) * [Messages](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#messages) * [Example](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/webview#example) Copy // Create a screen webview with a sidebar button const webview = ctx.newWebview({ slot: "screen", fullWidth: true, autoHeight: true, sidebar: { label: "Notepad", icon: ``, }, }) Copy const webview = ctx.newWebview({ slot: "fixed", width: "100%", maxWidth: "1200px", height: "500px", window: { draggable: true, defaultPosition: "bottom-right", }, hidden: true, }) button.onClick(() => { webview.show() }) Copy webview.onMount(() => { // Webview has been mounted on the screen }) webview.onUnmount(() => { // Webview has been unmounted }) webview.onLoad(() => { // Webview content has been loaded (after mount) }) // Force the webview content to update webview.update() // Returns the path to the webview screen webview.getScreenPath() // /webview?id=my-plugin webview.hide() webview.show() Copy // Renders iframe with transparent background webview.setContent(() => ` `) Copy $ui.register((ctx) => { const notes = ctx.state>([\ { 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(() => ` ... `) }) Copy 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>([\ { id: "2", text: "Check discussion for latest episode", checked: true },\ { id: "1", text: "Watch the next season", checked: false },\ ]) // Create the Webview const panel = ctx.newWebview({ slot: "screen", fullWidth: true, autoHeight: true, sidebar: { label: "Notepad", icon: ``, }, }) // Setup Communication // Automatically keep 'notes' and 'currentMedia' variables in sync with the webview panel.channel.sync("notes", notes) panel.channel.sync("media", currentMedia) // Handle events sent from the webview panel.channel.on("add-note", (text) => { console.log("Received note:", text) const newNote = { id: Date.now().toString(), text, checked: false } // Updating this state automatically sends the new value to the webview thanks to .sync() notes.set([...notes.get(), newNote]) ctx.toast.success("Note added") }) panel.channel.on("toggle-note", (id) => { notes.set(prev => prev.map(n => n.id === id ? { ...n, checked: !n.checked } : n)) }) panel.channel.on("delete-note", (id) => { notes.set(prev => prev.filter(n => n.id !== id)) }) // Render the UI panel.setContent(() => `
`) }) } --- # Debridstream | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream.md) . 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#permissions) --------------------------------------------------------------------------------------------------------------- `playback` and `debrid` permissions are required Copy { //... "plugin": { "permissions": { "scopes": ["playback", "debrid"] } } } Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#core-methods) ----------------------------------------------------------------------------------------------------------------- ### getPreviousStreamOptions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#getpreviousstreamoptions) `getPreviousStreamOptions()` Returns the previous debrid stream options, or `undefined` if no stream has been started yet. ### getStreamURL[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#getstreamurl) `getStreamURL()` Returns the active debrid stream URL, or `undefined` if no stream URL is available. ### startStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#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:** ### cancelStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#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:** [PreviousTorrentstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream) [NextTorrent Search](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#core-methods) * [getPreviousStreamOptions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#getpreviousstreamoptions) * [getStreamURL](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#getstreamurl) * [startStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#startstream) * [cancelStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream#cancelstream) Copy await ctx.debridstream.startStream({ mediaId: 21, episodeNumber: 1, aniDBEpisode: "1", playbackType: "default", autoSelect: true, }) Copy ctx.debridstream.cancelStream({ removeTorrent: true, }) --- # Anime | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime.md) . The `ctx.anime` API provides methods to interact with the anime system in Seanime. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#core-methods) ---------------------------------------------------------------------------------------------------------- ### getAnimeEntry[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getanimeentry) Gets a an anime entry, using the cache if available. **Parameters:** * `mediaId`: Number - The AniList media ID **Example:** Copy const animeEntry = await ctx.anime.getAnimeEntry(21) ### getAnimeMetadata[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getanimemetadata) Gets raw anime metadata from the metadata provider **Parameters:** * `from` : "anilist" | "mal" | "kitsu" | "anidb" * `mediaId`: Number - The media ID Example: Copy const metadata = await ctx.anime.getAnimeMetadata("anilist", 21) ### getEntryDownloadInfo[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getentrydownloadinfo) `getEntryDownloadInfo(mediaId)` Builds download information for an anime entry using Seanime's local files, AniList collection progress/status, and metadata provider data. **Parameters:** * `mediaId`: Number - The AniList media ID **Returns:** `Promise<$app.Anime_EntryDownloadInfo>` **Example:** ### getEpisodeCollection[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getepisodecollection) `getEpisodeCollection(mediaId)` Returns the normalized episode collection for an anime. This is the same shape used by entry episode tabs and other Seanime library views. **Parameters:** * `mediaId`: Number - The AniList media ID **Returns:** `Promise<$app.Anime_EpisodeCollection>` **Example:** ### clearEpisodeMetadataCache[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#clearepisodemetadatacache) Empties the episode metadata cache ### registerEntryEpisodeTab[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#registerentryepisodetab) Adds a custom episode tab. ![](https://seanime.gitbook.io/seanime-extensions/~gitbook/image?url=https%3A%2F%2F266901462-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F7Mat9fLDAotSl6o8o4P3%252Fuploads%252FvzPUESkk03qz9vG8jhXy%252Fimage.png%3Falt%3Dmedia%26token%3D8a31f58f-3952-43a5-86e0-5a03d66efb75&width=768&dpr=3&quality=100&sign=cb817ab4&sv=2) **Example:** [PreviousAnime/Library](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library) [NextPlayback (External)](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#core-methods) * [getAnimeEntry](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getanimeentry) * [getAnimeMetadata](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getanimemetadata) * [getEntryDownloadInfo](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getentrydownloadinfo) * [getEpisodeCollection](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#getepisodecollection) * [clearEpisodeMetadataCache](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#clearepisodemetadatacache) * [registerEntryEpisodeTab](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime#registerentryepisodetab) Copy const info = await ctx.anime.getEntryDownloadInfo(21) console.log(info) Copy const episodeCollection = await ctx.anime.getEpisodeCollection(21) console.log(episodeCollection.episodes) Copy ctx.anime.clearEpisodeMetadataCache() Copy const tab = ctx.anime.registerEntryEpisodeTab({ shouldShow: ({ mediaId }) => true, icon: ``, name: "Test", onEpisodeCollection: ({ episodeCollection }) => { // You can edit the default episode collection return episodeCollection }, onSelectEpisode: ({ episodeNumber }) => { ctx.toast.info(`Episode ${episodeNumber} selected`) }, }) const isOpen = tab.getIsOpen() ctx.effect(() => { console.log(isOpen.get()) }, [isOpen]) --- # External Player Link | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link.md) . Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link#core-methods) ------------------------------------------------------------------------------------------------------------------------- ### open[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link#open) Copy 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: Copy // 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 ) [PreviousFiller Manager](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager) [NextTorrentstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link#core-methods) * [open](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link#open) --- # Auto Select | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select.md) . 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#profile-shape) ------------------------------------------------------------------------------------------------------------------ Copy 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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#core-methods) ---------------------------------------------------------------------------------------------------------------- ### getProfile[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#getprofile) `getProfile()` Returns the saved auto-select profile, or `undefined` if no profile has been saved yet. Example: ### saveProfile[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#saveprofile) `saveProfile(profile)` Saves the auto-select profile and returns the stored value. **Parameters:** * `profile`: `AutoSelectProfile` Example: ### deleteProfile[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#deleteprofile) `deleteProfile()` Deletes the saved auto-select profile. Example: [PreviousTorrent Search](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search) [NextDownloading](https://seanime.gitbook.io/seanime-extensions/plugins/ui/downloading) Last updated 3 months ago * [Profile Shape](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#profile-shape) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#core-methods) * [getProfile](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#getprofile) * [saveProfile](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#saveprofile) * [deleteProfile](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select#deleteprofile) Copy const profile = ctx.autoSelect.getProfile() console.log(profile) Copy const savedProfile = ctx.autoSelect.saveProfile({ providers: ["nyaa"], resolutions: ["1080p"], preferredLanguages: ["japanese"], batchPreference: "avoid", bestReleasePreference: "prefer", minSeeders: 5, }) console.log(savedProfile) Copy ctx.autoSelect.deleteProfile() --- # Filler Manager | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager.md) . Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#core-methods) ------------------------------------------------------------------------------------------------------------------- ### getFillerEpisodes[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#getfillerepisodes) Retrieves filler episode data for an anime. **Parameters**: * `mediaId`: number - AniList media ID Returns: string\[\] | undefined - List of filler episode numbers or undefined if not found Example: Copy // Get filler episodes for One Piece const fillerEpisodes = ctx.fillerManager.getFillerEpisodes(21) if (fillerEpisodes) { console.log("Filler episodes:", fillerEpisodes) } else { console.log("No filler data found") } ### removeFillerData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#removefillerdata) Removes filler episode data for an anime. **Parameters**: * `mediaId`: number - AniList media ID Example: ### setFillerEpisodes[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#setfillerepisodes) Sets custom filler episode data for an anime. **Parameters**: * `mediaId`: number - AniList media ID * `fillerEpisodes`: string\[\] - List of episode numbers that are filler Example: ### isEpisodeFiller[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#isepisodefiller) Checks if a specific episode is marked as filler. **Parameters**: * `mediaId`: number - AniList media ID * `episodeNumber`: number - Episode number to check Returns: boolean - True if the episode is filler, false otherwise Example: ### hydrateFillerData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#hydratefillerdata) Updates a library entry with filler episode data. **Parameters**: * `entry`: Entry - Anime library entry object Example: ### hydrateOnlinestreamFillerData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#hydrateonlinestreamfillerdata) Updates online stream episodes with filler episode data. **Parameters**: * `mediaId`: number - AniList media ID * `episodes`: Episode\[\] - Array of online stream episodes Example: [PreviousAuto Scanner](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-scanner) [NextExternal Player Link](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#core-methods) * [getFillerEpisodes](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#getfillerepisodes) * [removeFillerData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#removefillerdata) * [setFillerEpisodes](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#setfillerepisodes) * [isEpisodeFiller](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#isepisodefiller) * [hydrateFillerData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#hydratefillerdata) * [hydrateOnlinestreamFillerData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/filler-manager#hydrateonlinestreamfillerdata) Copy // Remove filler data for One Piece ctx.fillerManager.removeFillerData(21) console.log("Filler data removed") Copy // Set custom filler episodes for One Piece ctx.fillerManager.setFillerEpisodes(21, ["50", "51", "52", "99", "100"]) console.log("Custom filler data set") Copy isEpisodeFiller(mediaId, episodeNumber) Copy // Check if episode 99 of One Piece is filler const isFiller = ctx.fillerManager.isEpisodeFiller(21, 99) console.log("Episode 99 is filler:", isFiller) Copy hydrateFillerData(entry) Copy // Hydrate filler data for a library entry const entry = getAnimeEntry(21) // One Piece ctx.fillerManager.hydrateFillerData(entry) console.log("Filler data added to entry") Copy hydrateOnlinestreamFillerData(mediaId, episodes) Copy // Hydrate filler data for online stream episodes const episodes = getOnlineStreamEpisodes(21) // One Piece episodes ctx.fillerManager.hydrateOnlinestreamFillerData(21, episodes) console.log("Filler data added to online stream episodes") --- # Torrentstream | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream.md) . The `ctx.torrentstream` API lets plugins stream torrents through Seanime. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#permissions) ---------------------------------------------------------------------------------------------------------------- `playback` permission is required Copy { //... "plugin": { "permissions": { "scopes": ["playback"] } } } Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#core-methods) ------------------------------------------------------------------------------------------------------------------ ### isEnabled[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#isenabled) `isEnabled()` Returns whether torrentstream is enabled in Seanime. ### getPreviousStreamOptions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#getpreviousstreamoptions) `getPreviousStreamOptions()` Returns the previous torrentstream start options, or `undefined` if no stream has been started yet. ### getBatchHistory[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#getbatchhistory) `getBatchHistory(mediaId)` Returns saved batch history for a media entry, or `undefined` if none exists. **Parameters:** * `mediaId`: Number - AniList media ID ### startStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#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:** ### stopStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#stopstream) `stopStream()` Stops the active torrent stream. **Example:** ### cancelPreparedStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#cancelpreparedstream) `cancelPreparedStream()` Cancels a prepared torrent stream, if one exists. **Example:** [PreviousExternal Player Link](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/external-player-link) [NextDebridstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#permissions) * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#core-methods) * [isEnabled](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#isenabled) * [getPreviousStreamOptions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#getpreviousstreamoptions) * [getBatchHistory](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#getbatchhistory) * [startStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#startstream) * [stopStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#stopstream) * [cancelPreparedStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrentstream#cancelpreparedstream) Copy await ctx.torrentstream.startStream({ mediaId: 21, episodeNumber: 1, aniDbEpisode: "1", playbackType: "default", autoSelect: true, }) Copy await ctx.torrentstream.stopStream() Copy ctx.torrentstream.cancelPreparedStream() --- # Torrent Search | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search.md) . The `ctx.torrentSearch` API provides access to the configured anime torrent provider extensions. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#core-methods) ------------------------------------------------------------------------------------------------------------------- ### getProviderIds[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#getproviderids) `getProviderIds()` Returns all available anime torrent provider IDs. Example: Copy const providerIds = ctx.torrentSearch.getProviderIds() console.log(providerIds) ### getDefaultProviderId[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#getdefaultproviderid) `getDefaultProviderId()` Returns the default torrent provider ID, or `undefined` if none is configured. Example: Copy const providerId = ctx.torrentSearch.getDefaultProviderId() console.log(providerId) ### searchAnime[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#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:** [PreviousDebridstream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/debridstream) [NextAuto Select](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/auto-select) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#core-methods) * [getProviderIds](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#getproviderids) * [getDefaultProviderId](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#getdefaultproviderid) * [searchAnime](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/torrent-search#searchanime) Copy 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) } --- # System | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system.md) . 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. [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions) [OS](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os) [Filepath](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath) [Buffers, I/O](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o) [MIME](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime) [PreviousDebug](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug) [NextPermissions](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/permissions) Last updated 3 months ago --- # Filepath | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath.md) . $filepath[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#usdfilepath) ---------------------------------------------------------------------------------------------------- `$filepath` implements utility routines for manipulating filename paths in a way compatible with the target operating system-defined file paths. Go reference: [https://pkg.go.dev/path/filepath](https://pkg.go.dev/path/filepath) ### Helpers[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#helpers) Copy const baseName = $filepath.base("C:\Users\user\Downloads\file.mkv"); console.log(baseName); // file.mkv const dirName = $filepath.dir("C:\Users\user\Downloads\file.mkv"); console.log(dirName); // C:\Users\user\Donwloads const extName = $filepath.ext("C:\Users\user\Downloads\file.mkv"); console.log(extName); // .mkv const joinedPath = $filepath.join("C:", "Users", "user", "subdir", "file.txt"); console.log(joinedPath); // C:\Users\user\subdir\file.txt const [dir, file] = $filepath.split("C:\Users\user\Downloads\file.mkv"); console.log(dir, file); // C:\Users\user\Downloads, file.mkv const globResults = $filepath.glob("C:\Users\user\Downloads", "*.txt"); console.log(globResults); // test.txt, test2.txt const isMatch = $filepath.match("*.txt", "test.txt"); console.log(isMatch); // true const isAbsPath = $filepath.isAbs("C:\Users\user\Downloads\file.mkv"); console.log(isAbsPath); // true // Test toSlash and fromSlash const slashPath = $filepath.toSlash("C:\Users\user\Downloads\file.mkv"); console.log(slashPath); // C:/Users/user/Downloads/file.mkv const fromSlashPath = $filepath.fromSlash(slashPath); console.log(fromSlashPath); // C:\Users\user\Downloads\file.mkv ### Walk directories[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#walk-directories) [PreviousOS](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/os) [NextCommands](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands) Last updated 3 months ago * [$filepath](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#usdfilepath) * [Helpers](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#helpers) * [Walk directories](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/filepath#walk-directories) Copy // walk calls 'lstat' on each file path it encounters, which can be slower $filepath.walk("C:\Users\user\Downloads", (path, info, err) => { if (err) { console.log("Walk error:", path, err); return; // Continue walking } // We can skip directories if (info.isDir() && info.name() === "ignoredDir") { console.log("Skipping directory:", path); return $filepath.skipDir; } console.log("Walk path:", path, "isDir:", info.isDir()); return; // Continue walking }); // walkDir is more efficient $filepath.walkDir("C:\Users\user\Downloads", (path, d, err) => { if (err) { console.log("WalkDir error:", path, err); return; // Continue walking } // We can skip directories if (d.isDir() && d.name() === "ignoredDir") { console.log("Skipping directory:", path); return $filepath.skipDir; } console.log("WalkDir path:", path, "isDir:", d.isDir()); return; // Continue walking }); --- # Playback (External) | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external.md) . This API allows you to control the interface between Seanime and desktop media players (MPV, IINA, VLC, MPC-HC). Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#permissions) -------------------------------------------------------------------------------------------------------------------- `playback` permission is required Copy { //... "plugin": { "permissions": { "scopes": ["playback"] } } } Core methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#core-methods) ---------------------------------------------------------------------------------------------------------------------- ### playUsingMediaPlayer[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#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:** ### streamUsingMediaPlayer[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#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:** ### registerEventListener[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#registereventlistener) `registerEventListener(callback)` Registers a listener for playback events. **Parameters:** * `callback`: Function(event: PlaybackEvent) - Function called when an event occurs **Example:** ### pause[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#pause) Pauses the current playback. **Example:** ### resume[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#resume) Resumes the paused playback. **Example:** ### seekTo[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#seekto) Seeks to a specific position in the current playback. **Parameters:** * `seconds`: Number - The position to seek to in seconds **Example:** ### cancel[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#cancel) Cancels the current playback. **Example:** ### startManualTracking[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#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:** ### syncCurrentProgress[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#synccurrentprogress) `syncCurrentProgress()` Synchronizes the currently tracked progress with AniList. This is most useful after `startManualTracking()` in custom player or external-player-link workflows. **Example:** ### cancelManualTracking[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#cancelmanualtracking) `cancelManualTracking()` Stops the current manual tracking session. **Example:** ### getNextEpisode[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#getnextepisode) Gets the next episode to play after the current one. **Example:** ### playNextEpisode[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#playnextepisode) Plays the next episode for the current media. **Example:** Best Practices[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#best-practices) -------------------------------------------------------------------------------------------------------------------------- #### Media Tracking[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#media-tracking) The playback API is designed for tracked media files that are part of the Seanime library: [PreviousAnime](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/anime) [NextVideoCore](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#permissions) * [Core methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#core-methods) * [playUsingMediaPlayer](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#playusingmediaplayer) * [streamUsingMediaPlayer](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#streamusingmediaplayer) * [registerEventListener](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#registereventlistener) * [pause](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#pause) * [resume](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#resume) * [seekTo](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#seekto) * [cancel](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#cancel) * [startManualTracking](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#startmanualtracking) * [syncCurrentProgress](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#synccurrentprogress) * [cancelManualTracking](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#cancelmanualtracking) * [getNextEpisode](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#getnextepisode) * [playNextEpisode](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#playnextepisode) * [Best Practices](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external#best-practices) Copy // 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) } Copy // 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) } Copy // 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() Copy try { ctx.playback.pause() console.log("Playback paused") } catch (error) { console.error("Could not pause:", error) } Copy // Resume after pausing try { ctx.playback.resume() console.log("Playback resumed") } catch (error) { console.error("Could not resume:", error) } Copy // Skip ahead 30 seconds try { ctx.playback.seekTo(currentTimeInSeconds + 30) console.log("Skipped forward 30 seconds") } catch (error) { console.error("Could not seek:", error) } Copy try { ctx.playback.cancel() console.log("Playback canceled") } catch (error) { console.error("Could not cancel playback:", error) } Copy await ctx.playback.startManualTracking({ mediaId: 21, episodeNumber: 1, }) Copy await ctx.playback.syncCurrentProgress() Copy ctx.playback.cancelManualTracking() Copy // 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) } Copy // 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) {} } }) Copy // 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 --- # Buffers, I/O | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o.md) . Bufio[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#bufio) --------------------------------------------------------------------------------------------- `$bufio` provides functionalities to read or write binary data in chunks rather than one byte at a time. ### Reader[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#reader) Copy const file = $os.openFile("C:\Users\user\Downloads\multiline.txt", $os.O_RDONLY, 0); const reader = $bufio.newReader(file); // Read lines manually with try/catch to handle EOF const lines = []; for (let i = 0; i < 10; i++) { // Try to read more lines than exist try { const line = reader.readString($toBytes('\n')); lines.push(line.trim()); } catch (e) { console.log("Caught expected EOF:", e.message); } } file.close(); console.log(lines) // ["Line 1", "Line 2", "Line 3"] ### Writer[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#writer) ### Scanner[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#scanner) Bytes[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#bytes) --------------------------------------------------------------------------------------------- `$bytes` provides functionalities to manipulate binary data. ### Read, write[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#read-write) I/O[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#i-o) ----------------------------------------------------------------------------------------- `$io` provides generalized I/O interface functionalities. [PreviousCommands](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/commands) [NextMIME](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/mime) Last updated 3 months ago * [Bufio](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#bufio) * [Reader](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#reader) * [Writer](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#writer) * [Scanner](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#scanner) * [Bytes](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#bytes) * [Read, write](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#read-write) * [I/O](https://seanime.gitbook.io/seanime-extensions/plugins/apis/system/buffers-i-o#i-o) Copy const writeFile = $os.create("C:\Users\user\Downloads\bufio_write.txt"); const writer = $bufio.newWriter(writeFile); // Write multiple strings writer.writeString("Buffered "); writer.writeString("write "); writer.writeString("test"); // Flush to ensure data is written writer.flush(); writeFile.close(); Copy const scanFile = $os.openFile("C:\Users\user\Downloads\multiline.txt", $os.O_RDONLY, 0); const scanner = $bufio.newScanner(scanFile); // Scan lines const scannedLines = []; while (scanner.scan()) { scannedLines.push(scanner.text()); } scanFile.close(); console.log(scannedLines) // ["Line 1", "Line 2", "Line 3"] Copy // Write string to buffer const buffer = $bytes.newBuffer($toBytes("Hello")); buffer.writeString(", world!"); // Get buffer content const bufferContent = $toString(buffer.bytes()); console.log(bufferContent); // Hello, world! // Create a new buffer string const strBuffer = $bytes.newBufferString("String buffer"); strBuffer.writeString(" test"); const strBufferContent = strBuffer.string(); console.log(strBufferContent); // String buffer test // Create a byte reader const reader = $bytes.newReader($toBytes("Bytes reader test")); const readerBuffer = new Uint8Array(100); // Empty buffer const bytesRead = reader.read(readerBuffer); // Read into buffer console.log(bytesRead, "bytes read") // 17 bytes read const readerContent = $toString(readerBuffer.subarray(0, bytesRead)); console.log(readerContent); // Bytes reader test // Buffer methods const testBuffer = $bytes.newBuffer($toBytes("")); testBuffer.writeString("Test"); testBuffer.writeByte(32); // Space testBuffer.writeString("methods"); const testBufferContent = testBuffer.string(); console.log(testBufferContent); // Test methods // Read methods const readBuffer = $bytes.newBuffer($toBytes("Read test")); const readByte = readBuffer.readByte(); console.log("Read byte:", String.fromCharCode(readByte)); // Read byte: R const nextBytes = new Uint8Array(5); readBuffer.read(nextBytes); console.log("Next bytes:", $toString(nextBytes)); // Next bytes: ead t --- # Shared Modules | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared.md) . `$shared` lets a plugin define reusable factories once and load them from other runtimes. This is useful when the same helper is needed in hook callbacks and in the UI context. Quick example[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#quick-example) ------------------------------------------------------------------------------------------------- Copy 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[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#methods) ------------------------------------------------------------------------------------- ### define[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#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. `$shared.define()` should be called before registering hooks or the UI scope. Rules: * Names are trimmed and must not be empty. * The name can only be defined once. * The factory must return a value. ### use[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#use) `$shared.use(name)` Evaluates the shared factory inside the current runtime and returns its exported value. **Parameters:** * `name`: String - The shared module name. `use()` runs the shared factory in the current runtime. Treat the result as a fresh runtime-local value, not as cross-runtime shared state. Good to know[](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#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`. [PreviousAniList](https://seanime.gitbook.io/seanime-extensions/plugins/apis/anilist) [NextDebug](https://seanime.gitbook.io/seanime-extensions/plugins/apis/debug) Last updated 3 months ago * [Quick example](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#quick-example) * [Methods](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#methods) * [define](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#define) * [use](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#use) * [Good to know](https://seanime.gitbook.io/seanime-extensions/plugins/apis/shared#good-to-know) --- # VideoCore | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore.md) . VideoCore is the built-in video player used by the Denshi desktop app and the online streaming web player. Permissions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#permissions) ------------------------------------------------------------------------------------------------------------ `playback` permission is required Copy { //... "plugin": { "permissions": { "scopes": ["playback"] } } } Code methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#code-methods) -------------------------------------------------------------------------------------------------------------- ### addEventListener[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### removeEventListener[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#removeeventlistener) `removeEventListener(eventId)` Removes a previously registered event listener. Parameters: * `eventId`: String - The identifier of the event to remove. _Example:_ ### playEpisodeFromPlaylist[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### playStream[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### playLocalFile[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### pause[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#pause) `pause()` Pauses the current playback. _Example:_ ### resume[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#resume) `resume()` Resumes playback if it is currently paused. _Example:_ ### seek[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### seekTo[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#seekto) `seekTo(seconds)` Seeks to a specific timestamp in the video. Parameters: * `seconds`: Number - The absolute timestamp to seek to. _Example:_ ### terminate[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#terminate) `terminate()` Stops playback and terminates the media player instance. _Example:_ ### setFullscreen[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setfullscreen) `setFullscreen(fullscreen)` Toggles or sets the fullscreen state of the player. Parameters: * `fullscreen`: Boolean - `true` to enter fullscreen, `false` to exit. _Example:_ ### setPip[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### showMessage[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### setSkipData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setskipdata) Overrides skip data. `setSkipData(skipData)` Parameters: * `skipData` : `$videocore.SkipData` - The data _Example:_ ### clearSkipData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#clearskipdata) `clearSkipData()` Removes skip data. ### getTextTracks[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#gettexttracks) `getTextTracks()` Asynchronously retrieves the list of available subtitle/caption tracks. _Example:_ ### setSubtitleTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setsubtitletrack) `setSubtitleTrack(trackNumber)` Selects a specific subtitle track. Parameters: * `trackNumber`: Number - The ID/index of the subtitle track to select. _Example:_ ### setMediaCaptionTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setmediacaptiontrack) `setMediaCaptionTrack(trackIndex)` Selects a specific media caption track. Parameters: * `trackIndex`: Number - The index of the caption track to select. _Example:_ ### addExternalSubtitleTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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:_ ### setAudioTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setaudiotrack) `setAudioTrack(trackNumber)` Selects a specific audio track. Parameters: * `trackNumber`: Number - The ID/index of the audio track to select. _Example:_ ### getPlaybackStatus[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaybackstatus) `getPlaybackStatus()` Synchronously retrieves the current status of the playback _Example:_ ### getPlaybackState[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaybackstate) `getPlaybackState()` Synchronously retrieves the comprehensive state object of the player. _Example:_ ### getCurrentMedia[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentmedia) `getCurrentMedia()` Synchronously retrieves information about the media currently being played. _Example:_ ### getPlaylist[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaylist) `getPlaylist()` Asynchronously retrieves the current playlist. _Example:_ ### pullStatus[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#pullstatus) `pullStatus()` Asynchronously forces a status update from the player and returns the result. _Example:_ ### getCurrentPlaybackInfo[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplaybackinfo) `getCurrentPlaybackInfo()` Synchronously retrieves the current playback information _Example:_ ### getCurrentClientId[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentclientid) `getCurrentClientId()` Synchronously retrieves the unique identifier of the connected client/player. _Returns:_ `String` _Example:_ ### getCurrentPlayerType[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplayertype) `getCurrentPlayerType()` Synchronously retrieves the type of player currently active (e.g., "native", "web"). _Returns:_ `String` _Example:_ ### getCurrentPlaybackType[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplaybacktype) `getCurrentPlaybackType()` Synchronously retrieves the type of playback being performed (e.g., "torrent", "debrid", "file", "onlinestream"). _Returns:_ `String` _Example:_ ### getSkipData[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getskipdata) `getSkipData()` Asynchronously retrieve existing skip data (from AniSkip) if it exists. _Returns:_ `$videocore.SkipData` _Example:_ ### State Request Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#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[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetfullscreen) `sendGetFullscreen()` Requests the current fullscreen state. #### sendGetPip[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetpip) `sendGetPip()` Requests the current Picture-in-Picture state. #### sendGetAnime4K[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetanime4k) `sendGetAnime4K()` Requests the current Anime4K configuration/state. #### sendGetSubtitleTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetsubtitletrack) `sendGetSubtitleTrack()` Requests the currently selected subtitle track. #### sendGetAudioTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetaudiotrack) `sendGetAudioTrack()` Requests the currently selected audio track. #### sendGetMediaCaptionTrack[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetmediacaptiontrack) `sendGetMediaCaptionTrack()` Requests the currently selected media caption track. #### sendGetPlaybackState[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#sendgetplaybackstate) `sendGetPlaybackState()` Requests the full playback state. [PreviousPlayback (External)](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/playback-external) [NextMPV](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/mpv) Last updated 3 months ago * [Permissions](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#permissions) * [Code methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#code-methods) * [addEventListener](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#addeventlistener) * [removeEventListener](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#removeeventlistener) * [playEpisodeFromPlaylist](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#playepisodefromplaylist) * [playStream](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#playstream) * [playLocalFile](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#playlocalfile) * [pause](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#pause) * [resume](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#resume) * [seek](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#seek) * [seekTo](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#seekto) * [terminate](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#terminate) * [setFullscreen](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setfullscreen) * [setPip](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setpip) * [showMessage](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#showmessage) * [setSkipData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setskipdata) * [clearSkipData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#clearskipdata) * [getTextTracks](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#gettexttracks) * [setSubtitleTrack](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setsubtitletrack) * [setMediaCaptionTrack](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setmediacaptiontrack) * [addExternalSubtitleTrack](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#addexternalsubtitletrack) * [setAudioTrack](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#setaudiotrack) * [getPlaybackStatus](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaybackstatus) * [getPlaybackState](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaybackstate) * [getCurrentMedia](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentmedia) * [getPlaylist](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getplaylist) * [pullStatus](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#pullstatus) * [getCurrentPlaybackInfo](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplaybackinfo) * [getCurrentClientId](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentclientid) * [getCurrentPlayerType](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplayertype) * [getCurrentPlaybackType](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getcurrentplaybacktype) * [getSkipData](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#getskipdata) * [State Request Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library/videocore#state-request-methods) Copy 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) }) Copy ctx.videoCore.removeEventListener("video-paused") Copy ctx.videoCore.playEpisodeFromPlaylist("next") Copy const media = $anilist.getAnime(21) await ctx.videoCore.playStream( "https://example.com/streams/one-piece-1.m3u8", "1", media, ) Copy await ctx.videoCore.playLocalFile("/anime/One Piece/One Piece - 1015.mkv") Copy ctx.videoCore.pause() Copy ctx.videoCore.resume() Copy // Skip forward 10 seconds ctx.videoCore.seek(10) Copy // Go to the 1-minute mark ctx.videoCore.seekTo(60) Copy ctx.videoCore.terminate() Copy ctx.videoCore.setFullscreen(true) Copy ctx.videoCore.setPip(true) Copy ctx.videoCore.showMessage("Hello World", 2000) Copy ctx.videoCore.setSkipData({ op: { interval: { startTime: 0, endTime: 1500 } }, ed: null }) Copy const tracks = ctx.videoCore.getTextTracks() for(const track of tracks) { console.log(track.type) // "subtitles" or "captions" console.log(track.index) } Copy ctx.videoCore.setSubtitleTrack(1) Copy ctx.videoCore.setMediaCaptionTrack(0) Copy 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", }) Copy ctx.videoCore.setAudioTrack(2) Copy const status = ctx.videoCore.getPlaybackStatus() if (status.paused) { // ... } Copy const state = ctx.videoCore.getPlaybackState() console.log(state.clientId, state.playerType, state.playbackInfo) Copy const media = ctx.videoCore.getCurrentMedia() console.log(media.id) Copy const playlist = await ctx.videoCore.getPlaylist() console.log(playlist.nextEpisode) Copy const status = await ctx.videoCore.pullStatus() Copy const info = ctx.videoCore.getCurrentPlaybackInfo() Copy const clientId = ctx.videoCore.getCurrentClientId() Copy const type = ctx.videoCore.getCurrentPlayerType() Copy const type = ctx.videoCore.getCurrentPlaybackType() Copy const skipData = ctx.videoCore.getSkipData() --- # DOM | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom.md) . Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#core-methods) --------------------------------------------------------------------------------------------------------- ### onReady[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#onready) Executes a callback when the DOM is ready. **Parameters:** * `callback`: Function to execute **Example:** Copy ctx.dom.onReady(() => { console.log("DOM is ready...") }) ### onMainTabReady[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#onmaintabready) Executes a callback when the the main tab is ready or each time there is a new main tab. It will run right after `onReady` . A "main tab" is the currently focused tab that sends and receives DOM events. **Parameters:** * `callback`: Function to execute **Example:** ### query[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#query) Queries the DOM for elements matching the selector. **Parameters:** * `selector`: CSS selector string * `options`: (Optional) * `withInnerHTML`: Boolean - Include the `innerHTML` property in the matched elements * `identifyChildren`: Boolean - Assign IDs to all child elements **Returns:** Promise **Example:** ### queryOne[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#queryone) Queries the DOM for a single element matching the selector. **Parameters:** * `selector`: CSS selector string * `options`: Same as query() **Returns:** Promise **Example:** ### observe[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#observe) Observes changes to the DOM for elements matching the selector. Returns functions to stop observing and to manually refetch elements. **Parameters:** * `selector`: CSS selector string * `callback`: Function(elements: DOMElement\[\]) => void * `options`: Same as query() **Returns:** \[stopObserving: () => void, refetch: () => void\] **Example:** ### createElement[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#createelement) Creates a new DOM element. **Parameters:** * `tagName`: HTML tag name **Returns:** Promise **Example:** ### asElement[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#aselement) Returns a DOM element object from an element ID. Useful when using identifyChildren option. **Parameters:** * `elementId`: String ID of the element **Returns:** DOMElement **Example:** DOM Element Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#dom-element-methods) ----------------------------------------------------------------------------------------------------------------------- #### Content Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#content-methods) #### **getText()**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#gettext) Gets the text content of the element. **Returns:** Promise **Example:** #### **setText(text)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#settext-text) Sets the text content of the element. **Parameters:** * `text`: String to set as text content **Example:** #### **getAttribute(name)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getattribute-name) Gets the value of an attribute. **Parameters:** * `name`: Attribute name **Returns:** Promise **Example:** #### **getAttributes()**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getattributes) Gets all attributes of the element. **Returns:** Promise> **Example:** #### **setAttribute(name, value)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#setattribute-name-value) Sets the value of an attribute. **Parameters:** * `name`: Attribute name * `value`: Attribute value **Example:** #### **removeAttribute(name)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#removeattribute-name) Removes an attribute. **Parameters:** * `name`: Attribute name **Example:** #### **hasAttribute(name)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#hasattribute-name) Checks if the element has an attribute. **Parameters:** * `name`: Attribute name **Returns:** Promise **Example:** #### Style Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#style-methods) #### **setStyle(property, value)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#setstyle-property-value) Sets a style property on the element. **Parameters:** * `property`: CSS property name * `value`: Property value **Example:** #### **getStyle(property?)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getstyle-property) Gets the style of the element. **Parameters:** * `property`: (Optional) Property name **Returns:** Promise> **Example:** #### **removeStyle(property)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#removestyle-property) Removes a style property. **Parameters:** * `property`: CSS property name **Example:** #### **hasStyle(property)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#hasstyle-property) Checks if the element has a style property set. **Parameters:** * `property`: CSS property name **Returns:** Promise **Example:** #### **getComputedStyle(property)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getcomputedstyle-property) Gets the computed style of the element. **Parameters:** * `property`: CSS property name **Returns:** Promise **Example:** #### CSS Class Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#css-class-methods) #### **addClass(className)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#addclass-classname) Adds a class to the element. **Parameters:** * `className`: CSS class name **Example:** #### **hasClass(className)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#hasclass-classname) Checks if the element has a class. **Parameters:** * `className`: CSS class name **Returns:** Promise **Example:** #### DOM Traversal and Manipulation[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#dom-traversal-and-manipulation) **append(child)** Appends a child to the element. **Parameters:** * `child`: DOMElement to append **Example:** #### **before(sibling)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#before-sibling) Inserts a sibling before the element. **Parameters:** * `sibling`: DOMElement to insert **Example:** #### **after(sibling)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#after-sibling) Inserts a sibling after the element. **Parameters:** * `sibling`: DOMElement to insert **Example:** #### **remove()**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#remove) Removes the element from the DOM. **Example:** #### **getParent(opts?)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getparent-opts) Gets the parent of the element. **Parameters:** * `opts`: (Optional) Same options as query() **Returns:** Promise **Example:** #### **getChildren(opts?)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#getchildren-opts) Gets the children of the element. **Parameters:** * `opts`: (Optional) Same options as query() **Returns:** Promise **Example:** #### **query(selector)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#query-selector) Queries the DOM for elements that are descendants of this element and match the selector. **Parameters:** * `selector`: CSS selector string **Returns:** Promise **Example:** #### **queryOne(selector)**[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#queryone-selector) Queries the DOM for a single element that is a descendant of this element and matches the selector. **Parameters:** * `selector`: CSS selector string **Returns:** Promise **Example:** **addEventListener(event, callback)** Adds an event listener to the element. **Parameters:** * `event`: Event name (e.g., "click") * `callback`: Function to call when the event occurs **Returns:** Function to remove the event listener **Example:** Performance Best Practices[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#performance-best-practices) ------------------------------------------------------------------------------------------------------------------------------------- #### Minimize Roundtrips[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#minimize-roundtrips) Each async DOM method call represents a roundtrip between your plugin (on the server) and the browser. Minimize these for better performance. **Inefficient:** **Efficient:** #### Use observe() Efficiently[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#use-observe-efficiently) When using observe(), apply performance optimizations to handle elements efficiently. **Example:** [PreviousAction](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action) [NextAnime/Library](https://seanime.gitbook.io/seanime-extensions/plugins/ui/anime-library) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#core-methods) * [onReady](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#onready) * [onMainTabReady](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#onmaintabready) * [query](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#query) * [queryOne](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#queryone) * [observe](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#observe) * [createElement](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#createelement) * [asElement](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#aselement) * [DOM Element Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#dom-element-methods) * [Performance Best Practices](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom#performance-best-practices) Copy ctx.dom.onMainTabReady(() => { console.log("Main tab is ready...") }) Copy // Get all episode cards const episodeCards = await ctx.dom.query("[data-episode-card]", { withInnerHTML: true }) Copy // Get the main container const mainContainer = await ctx.dom.queryOne("#main-container") // Get user profile with inner HTML const userProfile = await ctx.dom.queryOne(".user-profile", { withInnerHTML: true }) Copy // Observe when new content cards are added to the page const [stopObserving, refetchCards] = ctx.dom.observe(".content-card", async (cards) => { console.log(`Found ${cards.length} content cards`) // Process each card for (const card of cards) { const title = await card.queryOne(".card-title") if (title) { console.log(`Card title: ${await title.getText()}`) } } }) // Later, to stop observing: stopObserving() // To manually trigger a refresh: refetchCards() Copy // Create a new div element const newDiv = await ctx.dom.createElement("div") newDiv.setAttribute("class", "custom-element") newDiv.setText("This is a dynamically created element") // Create a button const newButton = await ctx.dom.createElement("button") newButton.setText("Click me") Copy // When using with identifyChildren const container = await ctx.dom.queryOne("#container", { withInnerHTML: true, identifyChildren: true }) // Parse HTML locally const $ = LoadDoc(container.innerHTML) const buttonId = $(".action-button").attr("id") // Get a reference to the actual DOM element const button = ctx.dom.asElement(buttonId) button.setText("New Button Text") Copy const titleElement = await ctx.dom.queryOne(".title") const titleText = await titleElement.getText() console.log(`Title: ${titleText}`) Copy const statusElement = await ctx.dom.queryOne(".status") statusElement.setText("Active") Copy const link = await ctx.dom.queryOne("a.resource-link") const href = await link.getAttribute("href") console.log(`Resource URL: ${href}`) Copy const image = await ctx.dom.queryOne("img.poster") const attributes = await image.getAttributes() console.log(`Image src: ${attributes.src}`) console.log(`Image alt: ${attributes.alt}`) Copy const image = await ctx.dom.queryOne(".thumbnail") image.setAttribute("src", "https://example.com/new-image.jpg") image.setAttribute("alt", "Updated thumbnail image") Copy const button = await ctx.dom.queryOne(".disabled-button") button.removeAttribute("disabled") Copy const form = await ctx.dom.queryOne("form") const isSubmitted = await form.hasAttribute("data-submitted") if (isSubmitted) { console.log("Form was already submitted") } Copy const spoilerText = await ctx.dom.queryOne(".spoiler") spoilerText.setStyle("filter", "blur(5px)") spoilerText.setStyle("cursor", "pointer") Copy const element = await ctx.dom.queryOne(".styled-element") // Get single property const color = await element.getStyle("color") // Get all styles const allStyles = await element.getStyle() Copy const spoilerText = await ctx.dom.queryOne(".spoiler") // Remove blur effect when clicked spoilerText.removeStyle("filter") Copy const element = await ctx.dom.queryOne(".target") const hasTransition = await element.hasStyle("transition") if (!hasTransition) { element.setStyle("transition", "opacity 0.3s ease") } Copy const box = await ctx.dom.queryOne(".box") const actualWidth = await box.getComputedStyle("width") console.log(`Box actual width: ${actualWidth}`) Copy const card = await ctx.dom.queryOne(".card") card.addClass("highlighted") card.addClass("selected") Copy const row = await ctx.dom.queryOne("tr") const isActive = await row.hasClass("active") if (!isActive) { row.addClass("active") } Copy const container = await ctx.dom.queryOne(".container") const newElement = await ctx.dom.createElement("div") newElement.setText("New child element") container.append(newElement) Copy const referenceElement = await ctx.dom.queryOne(".reference") const newElement = await ctx.dom.createElement("div") newElement.setText("Inserted before reference") referenceElement.before(newElement) Copy const referenceElement = await ctx.dom.queryOne(".reference") const newElement = await ctx.dom.createElement("div") newElement.setText("Inserted after reference") referenceElement.after(newElement) Copy const outdatedNotice = await ctx.dom.queryOne(".outdated-notice") if (outdatedNotice) { outdatedNotice.remove() } Copy const listItem = await ctx.dom.queryOne("li.active") const list = await listItem.getParent() console.log(`Parent element tag: ${list.tagName}`) Copy const list = await ctx.dom.queryOne("ul.menu") const listItems = await list.getChildren() console.log(`Menu has ${listItems.length} items`) Copy const articleBody = await ctx.dom.queryOne("article.blog-post") const paragraphs = await articleBody.query("p") console.log(`Article has ${paragraphs.length} paragraphs`) Copy const card = await ctx.dom.queryOne(".card") const title = await card.queryOne(".card-title") const description = await card.queryOne(".card-description") Copy const button = await ctx.dom.queryOne(".action-button") const removeListener = button.addEventListener("click", (event) => { console.log("Button clicked!") }) // Later, to remove the listener: removeListener() Copy // ❌ Multiple sequential roundtrips const items = await ctx.dom.query(".item") for (const item of items) { const title = await item.queryOne(".title") const description = await item.queryOne(".description") const image = await item.queryOne("img") if (title) await title.setText("New Title") if (description) await description.setText("New Description") } Copy // ✅ Get all data at once, process locally const items = await ctx.dom.query(".item", { withInnerHTML: true, identifyChildren: true }) for (const item of items) { const $ = LoadDoc(item.innerHTML) // Access elements without additional roundtrips const titleId = $(".title").attr("id") const descriptionId = $(".description").attr("id") const imageId = $("img").attr("id") // Now make direct modifications if (titleId) ctx.dom.asElement(titleId).setText("New Title") if (descriptionId) ctx.dom.asElement(descriptionId).setText("New Description") } Copy const [stopObserving, refetch] = ctx.dom.observe(".dynamic-content", async (elements) => { // Use withInnerHTML and identifyChildren for efficient processing for (const element of elements) { const $ = LoadDoc(element.innerHTML) // Process locally first const buttonIds = $("button").map((i, el) => $(el).attr("id")).get() // Then make direct DOM updates for (const buttonId of buttonIds) { if (buttonId) { const button = ctx.dom.asElement(buttonId) button.addEventListener("click", handleButtonClick) } } } }, { withInnerHTML: true, identifyChildren: true }) Copy --- # Action | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action.md) . The `ctx.action` API allows your plugin to add UI elements that trigger custom actions to different parts of the Seanime interface. Core Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#core-methods) ------------------------------------------------------------------------------------------------------------ ### newAnimePageButton[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimepagebutton) Creates a button that appears on anime detail pages. **Parameters:** * `props`: Object containing: * `label`: String - Button text * `intent`: String (Optional) - Button style ("primary", "success", "warning", etc.) * `style`: Object (Optional) - Custom CSS styles **Example:** Copy // Create a play button for anime pages const playButton = ctx.action.newAnimePageButton({ label: "Play All Episodes", intent: "primary", style: { marginRight: "8px" } }) // Mount the button to make it visible playButton.mount() // Handle clicks playButton.onClick((event) => { const anime = event.media console.log(`Play all episodes for: ${anime.title.userPreferred}`) // Implement playback logic }) ### newAnimePageDropdownItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimepagedropdownitem) Creates a dropdown menu item that appears in the anime page's action menu. **Parameters:** * `props`: Object containing: * `label`: String - Menu item text * `style`: Object (Optional) - Custom CSS styles **Example:** ### newAnimeLibraryDropdownItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimelibrarydropdownitem) Creates a dropdown menu item that appears in the anime library's global action menu. **Parameters:** * `props`: Object containing: * `label`: String - Menu item text * `style`: Object (Optional) - Custom CSS styles **Example:** ### newMediaCardContextMenuItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newmediacardcontextmenuitem) Creates a context menu item that appears when right-clicking on media cards. **Parameters:** * `props`: Object containing: * `label`: String - Menu item text * `for`: String (Optional) - Which media types to show for ("anime", "manga", or "both") * `style`: Object (Optional) - Custom CSS styles **Example:** ### newMangaPageButton[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newmangapagebutton) Creates a button that appears on manga detail pages. **Parameters:** * `props`: Object containing: * `label`: String - Button text * `intent`: String (Optional) - Button style ("primary", "success", "warning", etc.) * `style`: Object (Optional) - Custom CSS styles **Example:** ### newEpisodeCardContextMenuItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newepisodecardcontextmenuitem) Creates an item that appears on episode card context menus. **Parameters:** * `props`: Object containing: * `label`: String - Button text * `style`: Object (Optional) - Custom CSS styles * `type?` : "library" | "torrentstream" | "debridstream" | string | undefined `type` can also be `episodeTab:` when using `ctx.anime.registerEntryEpisodeTab` ### newEpisodeGridItemMenuItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newepisodegriditemmenuitem) Creates an item that appears on episode grid item menus. **Parameters:** * `props`: Object containing: * `label`: String - Button text * `type` : "library" | "torrentstream" | "debridstream" | "onlinestream" | "undownloaded" | "medialinks" | "mediastream" | string * `style`: Object (Optional) - Custom CSS styles `type` can also be `episodeTab:` when using `ctx.anime.registerEntryEpisodeTab` ### Action Object Methods[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#action-object-methods) All action objects share these common methods: #### mount()[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#mount) Makes the action visible in the UI. **Example:** #### unmount()[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#unmount) Removes the action from the UI. **Example:** #### setLabel(label)[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#setlabel-label) Updates the action's label text. **Parameters:** * `label`: String - New label text **Example:** #### setStyle(style)[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#setstyle-style) Updates the action's custom CSS styles. **Parameters:** * `style`: Object - CSS style properties **Example:** #### onClick(callback)[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#onclick-callback) Sets a function to be called when the action is clicked. **Parameters:** * `callback`: Function(event) - Function to call when clicked **Example:** ### Additional Properties[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#additional-properties) #### AnimePageButton and MangaPageButton[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#animepagebutton-and-mangapagebutton) These button types have an additional method: **setIntent(intent)** Sets the button's visual style. **Parameters:** * `intent`: String - Intent style ("primary", "success", "warning", "error", etc.) **Example:** #### MediaCardContextMenuItem[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#mediacardcontextmenuitem) This action type has an additional method: **setFor(type)** Sets which media types the context menu item appears for. **Parameters:** * `type`: String - "anime", "manga", or "both" **Example:** ### Best Practices[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#best-practices) #### Limit Number of Actions[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#limit-number-of-actions) Each plugin is limited to a maximum of 3 actions per type. Choose the most important actions to display. #### Dynamic UI Updates[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#dynamic-ui-updates) Update action properties based on application state: #### Conditional Mounting[](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#conditional-mounting) Only mount actions when they're relevant: [PreviousCommand Palette](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/command-palette) [NextDOM](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/dom) Last updated 3 months ago * [Core Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#core-methods) * [newAnimePageButton](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimepagebutton) * [newAnimePageDropdownItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimepagedropdownitem) * [newAnimeLibraryDropdownItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newanimelibrarydropdownitem) * [newMediaCardContextMenuItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newmediacardcontextmenuitem) * [newMangaPageButton](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newmangapagebutton) * [newEpisodeCardContextMenuItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newepisodecardcontextmenuitem) * [newEpisodeGridItemMenuItem](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#newepisodegriditemmenuitem) * [Action Object Methods](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#action-object-methods) * [Additional Properties](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#additional-properties) * [Best Practices](https://seanime.gitbook.io/seanime-extensions/plugins/ui/user-interface/action#best-practices) Copy // Add a download option to anime page dropdown const downloadItem = ctx.action.newAnimePageDropdownItem({ label: "Download Episodes" }) downloadItem.mount() downloadItem.onClick((event) => { const anime = event.media console.log(`Preparing download for: ${anime.title.userPreferred}`) // Show download dialog }) Copy // Add a scan option to library menu const scanItem = ctx.action.newAnimeLibraryDropdownItem({ label: "Scan for Missing Files" }) scanItem.mount() scanItem.onClick(() => { console.log("Starting library scan") // Implement scan logic }) Copy // Add a quick-watch option to anime cards const watchItem = ctx.action.newMediaCardContextMenuItem({ label: "Quick Watch", for: "anime" // Only show for anime cards }) watchItem.mount() watchItem.onClick((event) => { const media = event.media console.log(`Quick watching: ${media.title.userPreferred}`) // Implement quick watch feature }) Copy // Create a read button for manga pages const readButton = ctx.action.newMangaPageButton({ label: "Continue Reading", intent: "primary" }) readButton.mount() readButton.onClick((event) => { const manga = event.media console.log(`Opening reader for: ${manga.title.userPreferred}`) // Open manga reader }) Copy const episodeDetailsButton = ctx.action.newEpisodeCardContextMenuItem({ label: "View details", }) episodeDetailsButton.mount() episodeDetailsButton.onClick((event) => { // You get the episode object const episode = event.episode }) Copy const episodeDetailsButton = ctx.action.newEpisodeGridItemMenuItem({ label: "View details", type: "library" }) episodeDetailsButton.mount() episodeDetailsButton.onClick((event) => { // You get the episode object // It is of type [Anime_Episode], unless the chosen type is 'onlinestream', // in which case it will be of type [Onlinestream_Episode] const episode = event.episode }) Copy const button = ctx.action.newAnimePageButton({ label: "My Button" }) button.mount() // Now visible Copy // Remove when no longer needed button.unmount() Copy // Change button text based on state if (isDownloading) { button.setLabel("Downloading...") } else { button.setLabel("Download") } Copy // Highlight button when active if (isActive) { button.setStyle({ backgroundColor: "#4caf50", color: "white" }) } else { button.setStyle({}) } Copy button.onClick((event) => { // For anime/manga actions, event contains the media object if (event.media) { console.log(`Clicked on ${event.media.title.userPreferred}`) } // Your custom action logic performAction() }) Copy const button = ctx.action.newAnimePageButton({ label: "Watch" }) button.setIntent("primary") // Blue button // Other options: "primary-subtle", "success", "warning", "alert", etc. Copy const menuItem = ctx.action.newMediaCardContextMenuItem({ label: "Open" }) menuItem.setFor("anime") // Only show for anime cards Copy // Good: Update button state dynamically let isProcessing = false button.onClick((event) => { if (isProcessing) return isProcessing = true button.setLabel("Processing...") button.setIntent("warning") button.mount() performLongOperation().then(() => { isProcessing = false button.setLabel("Done!") button.setIntent("success") button.mount() // Reset after a delay setTimeout(() => { button.setLabel("Process Again") button.setIntent("primary") button.mount() }, 3000) }) }) Copy // Good: Mount/unmount based on context function updateButtonVisibility(media) { if (media.format === "MOVIE") { watchButton.mount() episodesButton.unmount() // No episodes for movies } else { watchButton.mount() episodesButton.mount() } } --- # 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/store.md). # Store ## When to use \* Create a cache \* Share values or functions between hooks \* Share values or functions between hooks and UI context {% hint style="info" %} The values aren't persisted when your plugin is reloaded. {% endhint %} ## How to use \`$store\` makes state sharing between runtimes possible.
\`\`\`typescript $store.set("foo", "bar") $store.get("foo") $store.watch("foo", (value) => {}) $store.getAll() // { "foo": "bar" } $store.remove("foo") $store.removeAll() $store.has("foo") // false $store.getOrSet("foo", () => { return "bar" }) $store.values() // \["bar"\] \`\`\` ### Unsafe access {% hint style="warning" %} Available from Seanime \`v3.7.2\`. {% endhint %} These helpers skip the defensive clone performed by the safe methods: \* \`$store.getUnsafe(key)\` \* \`$store.getAllUnsafe()\` \* \`$store.valuesUnsafe()\` \`\`\`typescript const rawValue = $store.getUnsafe("foo") const rawEntries = $store.getAllUnsafe() const rawValues = $store.valuesUnsafe() \`\`\` Use them only for read-only access in performance-sensitive paths. {% hint style="warning" %} Do not mutate data returned by the unsafe methods. Mutating shared references can bypass watcher notifications and may lead to concurrent map write panics. {% endhint %} ### Example
// 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: {% hint style="warning" %} As of Seanime \`v3.8.0\`, if the user enables Extension Secure Mode, sensitive filesystem actions can prompt for confirmation. If the user rejects the prompt, reads, writes, directory listing, creation, rename, delete, move, and extraction calls can throw. If prompts cannot be shown, such as during app startup, these calls fail immediately. {% endhint %} ## $os ### Info \`\`\`typescript console.log("Platform:", $os.platform); // darwin console.log("Arch:", $os.arch); // arm64 \`\`\` ### Directories \`\`\`typescript $os.tempDir() // $TEMP must be in the allow list $os.cacheDir() // $CACHE must be in the allow list $os.configDir() // $CONFIG must be in the allow list $os.homeDir() // $HOME must be in the allow list \`\`\` ### File operations {% hint style="warning" %} Always call \`close()\` once you're done manipulating a file. {% endhint %} {% code title="Example" %} \`\`\`typescript // C:\\Users\\user\\Downloads\\test.txt Hello World! // my-plugin.ts // "C:/Users/user/Downloads/\*\*/\*" and "$TEMP/\*\*/\*" have been added to 'allowReadPaths' and 'allowWritePaths' // Access the temp directory const tempDirPath = $os.tempDir(); console.log("Temp dir:", tempDirPath); // Read files const content = $os.readFile("C:\\Users\\user\\Downloads\\test.txt"); console.log("File content:", $toString(content)); // Hello World! // Write/create files $os.writeFile("C:\\Users\\user\\Downloads\\test.txt.new", $toBytes("New content"), 0644); const newContent = $os.readFile("C:\\Users\\user\\Downloads\\test.txt.new"); console.log("New file content:", $toString(newContent)); // New content // Read directories const entries = $os.readDir("C:\\Users\\user\\Downloads"); for (const entry of entries) { console.log(entry.name()); // test.txt, test.txt.new } // Create directories $os.mkdir("C:\\Users\\user\\Downloads\\newdir", 0755); const newEntries = $os.readDir("C:\\Users\\user\\Downloads"); for (const entry of newEntries) { console.log(entry.name()); // test.txt, test.txt.new, newdir } // Rename files $os.rename("C:\\Users\\user\\Downloads\\test.txt.new", "C:\\Users\\user\\Downloads\\test.txt.renamed"); let renameSuccess = true try { // File exists, no error thrown $os.stat("C:\\Users\\user\\Downloads\\test.txt.renamed"); } catch(e) { renameSuccess = false } console.log(renameSuccess); // true // Remove files $os.remove("C:\\Users\\user\\Downloads\\test.txt.renamed"); let removeSuccess = true; try { $os.stat("C:\\Users\\user\\Downloads\\test.txt.renamed"); removeSuccess = false; } catch (e) { // Error thrown becuase file should not exist removeSuccess = true; } console.log(removeSuccess); // true \`\`\` {% endcode %} ## $osExtra This API gives you additional functionalities ### Directories \`\`\`typescript $osExtra.desktopDir() // $DESKTOP must be in the allow list $osExtra.documentDir() // $DOCUMENT must be in the allow list $osExtra.downloadDir() // $DOWNLOAD must be in the allow list \`\`\` ### Unarchive files \* unzipFile, unrarFile \`\`\`typescript // If "file.zip" contains \`folder > file.text\` $osExtra.unzipFile("/path/to/downloaded/file.zip", "/path/to/dest") // -> "/path/to/dest/folder/file.txt" // If "file.rar" contains \`file.txt\` $osExtra.unrarFile("/path/to/downloaded/file.zip", "/path/to/dest") // -> "/path/to/dest/file.txt" \`\`\` --- # 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/dom.md). # DOM ## Core Methods ### onReady Executes a callback when the DOM is ready. \*\*Parameters:\*\* \* \`callback\`: Function to execute \*\*Example:\*\* \`\`\`typescript ctx.dom.onReady(() => { console.log("DOM is ready...") }) \`\`\` ### onMainTabReady Executes a callback when the the main tab is ready or each time there is a new main tab. It will run right after \`onReady\` . {% hint style="info" %} A "main tab" is the currently focused tab that sends and receives DOM events. {% endhint %} \*\*Parameters:\*\* \* \`callback\`: Function to execute \*\*Example:\*\* \`\`\`typescript ctx.dom.onMainTabReady(() => { console.log("Main tab is ready...") }) \`\`\` ### query Queries the DOM for elements matching the selector. \*\*Parameters:\*\* \* \`selector\`: CSS selector string \* \`options\`: (Optional) \* \`withInnerHTML\`: Boolean - Include the \`innerHTML\` property in the matched elements \* \`identifyChildren\`: Boolean - Assign IDs to all child elements \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript // Get all episode cards const episodeCards = await ctx.dom.query("\[data-episode-card\]", { withInnerHTML: true }) \`\`\` ### queryOne Queries the DOM for a single element matching the selector. \*\*Parameters:\*\* \* \`selector\`: CSS selector string \* \`options\`: Same as query() \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript // Get the main container const mainContainer = await ctx.dom.queryOne("#main-container") // Get user profile with inner HTML const userProfile = await ctx.dom.queryOne(".user-profile", { withInnerHTML: true }) \`\`\` ### observe Observes changes to the DOM for elements matching the selector. Returns functions to stop observing and to manually refetch elements. \*\*Parameters:\*\* \* \`selector\`: CSS selector string \* \`callback\`: Function(elements: DOMElement\\\[\]) => void \* \`options\`: Same as query() \*\*Returns:\*\* \\\[stopObserving: () => void, refetch: () => void\] \*\*Example:\*\* \`\`\`typescript // Observe when new content cards are added to the page const \[stopObserving, refetchCards\] = ctx.dom.observe(".content-card", async (cards) => { console.log(\`Found ${cards.length} content cards\`) // Process each card for (const card of cards) { const title = await card.queryOne(".card-title") if (title) { console.log(\`Card title: ${await title.getText()}\`) } } }) // Later, to stop observing: stopObserving() // To manually trigger a refresh: refetchCards() \`\`\` ### createElement Creates a new DOM element. \*\*Parameters:\*\* \* \`tagName\`: HTML tag name \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript // Create a new div element const newDiv = await ctx.dom.createElement("div") newDiv.setAttribute("class", "custom-element") newDiv.setText("This is a dynamically created element") // Create a button const newButton = await ctx.dom.createElement("button") newButton.setText("Click me") \`\`\` ### asElement Returns a DOM element object from an element ID. Useful when using identifyChildren option. \*\*Parameters:\*\* \* \`elementId\`: String ID of the element \*\*Returns:\*\* DOMElement \*\*Example:\*\* \`\`\`typescript // When using with identifyChildren const container = await ctx.dom.queryOne("#container", { withInnerHTML: true, identifyChildren: true }) // Parse HTML locally const $ = LoadDoc(container.innerHTML) const buttonId = $(".action-button").attr("id") // Get a reference to the actual DOM element const button = ctx.dom.asElement(buttonId) button.setText("New Button Text") \`\`\` ## DOM Element Methods #### Content Methods #### \*\*getText()\*\* Gets the text content of the element. \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript const titleElement = await ctx.dom.queryOne(".title") const titleText = await titleElement.getText() console.log(\`Title: ${titleText}\`) \`\`\` #### \*\*setText(text)\*\* Sets the text content of the element. \*\*Parameters:\*\* \* \`text\`: String to set as text content \*\*Example:\*\* \`\`\`typescript const statusElement = await ctx.dom.queryOne(".status") statusElement.setText("Active") \`\`\` #### \*\*getAttribute(name)\*\* Gets the value of an attribute. \*\*Parameters:\*\* \* \`name\`: Attribute name \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript const link = await ctx.dom.queryOne("a.resource-link") const href = await link.getAttribute("href") console.log(\`Resource URL: ${href}\`) \`\`\` #### \*\*getAttributes()\*\* Gets all attributes of the element. \*\*Returns:\*\* Promise\\> \*\*Example:\*\* \`\`\`typescript const image = await ctx.dom.queryOne("img.poster") const attributes = await image.getAttributes() console.log(\`Image src: ${attributes.src}\`) console.log(\`Image alt: ${attributes.alt}\`) \`\`\` #### \*\*setAttribute(name, value)\*\* Sets the value of an attribute. \*\*Parameters:\*\* \* \`name\`: Attribute name \* \`value\`: Attribute value \*\*Example:\*\* \`\`\`typescript const image = await ctx.dom.queryOne(".thumbnail") image.setAttribute("src", "https://example.com/new-image.jpg") image.setAttribute("alt", "Updated thumbnail image") \`\`\` #### \*\*removeAttribute(name)\*\* Removes an attribute. \*\*Parameters:\*\* \* \`name\`: Attribute name \*\*Example:\*\* \`\`\`typescript const button = await ctx.dom.queryOne(".disabled-button") button.removeAttribute("disabled") \`\`\` #### \*\*hasAttribute(name)\*\* Checks if the element has an attribute. \*\*Parameters:\*\* \* \`name\`: Attribute name \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript const form = await ctx.dom.queryOne("form") const isSubmitted = await form.hasAttribute("data-submitted") if (isSubmitted) { console.log("Form was already submitted") } \`\`\` #### Style Methods #### \*\*setStyle(property, value)\*\* Sets a style property on the element. \*\*Parameters:\*\* \* \`property\`: CSS property name \* \`value\`: Property value \*\*Example:\*\* \`\`\`typescript const spoilerText = await ctx.dom.queryOne(".spoiler") spoilerText.setStyle("filter", "blur(5px)") spoilerText.setStyle("cursor", "pointer") \`\`\` #### \*\*getStyle(property?)\*\* Gets the style of the element. \*\*Parameters:\*\* \* \`property\`: (Optional) Property name \*\*Returns:\*\* Promise\\> \*\*Example:\*\* \`\`\`typescript const element = await ctx.dom.queryOne(".styled-element") // Get single property const color = await element.getStyle("color") // Get all styles const allStyles = await element.getStyle() \`\`\` #### \*\*removeStyle(property)\*\* Removes a style property. \*\*Parameters:\*\* \* \`property\`: CSS property name \*\*Example:\*\* \`\`\`typescript const spoilerText = await ctx.dom.queryOne(".spoiler") // Remove blur effect when clicked spoilerText.removeStyle("filter") \`\`\` #### \*\*hasStyle(property)\*\* Checks if the element has a style property set. \*\*Parameters:\*\* \* \`property\`: CSS property name \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript const element = await ctx.dom.queryOne(".target") const hasTransition = await element.hasStyle("transition") if (!hasTransition) { element.setStyle("transition", "opacity 0.3s ease") } \`\`\` #### \*\*getComputedStyle(property)\*\* Gets the computed style of the element. \*\*Parameters:\*\* \* \`property\`: CSS property name \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript const box = await ctx.dom.queryOne(".box") const actualWidth = await box.getComputedStyle("width") console.log(\`Box actual width: ${actualWidth}\`) \`\`\` #### CSS Class Methods #### \*\*addClass(className)\*\* Adds a class to the element. \*\*Parameters:\*\* \* \`className\`: CSS class name \*\*Example:\*\* \`\`\`typescript const card = await ctx.dom.queryOne(".card") card.addClass("highlighted") card.addClass("selected") \`\`\` #### \*\*hasClass(className)\*\* Checks if the element has a class. \*\*Parameters:\*\* \* \`className\`: CSS class name \*\*Returns:\*\* Promise \*\*Example:\*\* \`\`\`typescript const row = await ctx.dom.queryOne("tr") const isActive = await row.hasClass("active") if (!isActive) { row.addClass("active") } \`\`\` #### DOM Traversal and Manipulation \*\*append(child)\*\* Appends a child to the element. \*\*Parameters:\*\* \* \`child\`: DOMElement to append \*\*Example:\*\* \`\`\`typescript const container = await ctx.dom.queryOne(".container") const newElement = await ctx.dom.createElement("div") newElement.setText("New child element") container.append(newElement) \`\`\` #### \*\*before(sibling)\*\* Inserts a sibling before the element. \*\*Parameters:\*\* \* \`sibling\`: DOMElement to insert \*\*Example:\*\* \`\`\`typescript const referenceElement = await ctx.dom.queryOne(".reference") const newElement = await ctx.dom.createElement("div") newElement.setText("Inserted before reference") referenceElement.before(newElement) \`\`\` #### \*\*after(sibling)\*\* Inserts a sibling after the element. \*\*Parameters:\*\* \* \`sibling\`: DOMElement to insert \*\*Example:\*\* \`\`\`typescript const referenceElement = await ctx.dom.queryOne(".reference") const newElement = await ctx.dom.createElement("div") newElement.setText("Inserted after reference") referenceElement.after(newElement) \`\`\` #### \*\*remove()\*\* Removes the element from the DOM. \*\*Example:\*\* \`\`\`typescript const outdatedNotice = await ctx.dom.queryOne(".outdated-notice") if (outdatedNotice) { outdatedNotice.remove() } \`\`\` #### \*\*getParent(opts?)\*\* Gets the parent of the element. \*\*Parameters:\*\* \* \`opts\`: (Optional) Same options as query() \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript const listItem = await ctx.dom.queryOne("li.active") const list = await listItem.getParent() console.log(\`Parent element tag: ${list.tagName}\`) \`\`\` #### \*\*getChildren(opts?)\*\* Gets the children of the element. \*\*Parameters:\*\* \* \`opts\`: (Optional) Same options as query() \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript const list = await ctx.dom.queryOne("ul.menu") const listItems = await list.getChildren() console.log(\`Menu has ${listItems.length} items\`) \`\`\` #### \*\*query(selector)\*\* Queries the DOM for elements that are descendants of this element and match the selector. \*\*Parameters:\*\* \* \`selector\`: CSS selector string \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript const articleBody = await ctx.dom.queryOne("article.blog-post") const paragraphs = await articleBody.query("p") console.log(\`Article has ${paragraphs.length} paragraphs\`) \`\`\` #### \*\*queryOne(selector)\*\* Queries the DOM for a single element that is a descendant of this element and matches the selector. \*\*Parameters:\*\* \* \`selector\`: CSS selector string \*\*Returns:\*\* Promise\\ \*\*Example:\*\* \`\`\`typescript const card = await ctx.dom.queryOne(".card") const title = await card.queryOne(".card-title") const description = await card.queryOne(".card-description") \`\`\` \*\*addEventListener(event, callback)\*\* Adds an event listener to the element. \*\*Parameters:\*\* \* \`event\`: Event name (e.g., "click") \* \`callback\`: Function to call when the event occurs \*\*Returns:\*\* Function to remove the event listener \*\*Example:\*\* \`\`\`typescript const button = await ctx.dom.queryOne(".action-button") const removeListener = button.addEventListener("click", (event) => { console.log("Button clicked!") }) // Later, to remove the listener: removeListener() \`\`\` ## Performance Best Practices #### Minimize Roundtrips Each async DOM method call represents a roundtrip between your plugin (on the server) and the browser. Minimize these for better performance. \*\*Inefficient:\*\* \`\`\`typescript // ❌ Multiple sequential roundtrips const items = await ctx.dom.query(".item") for (const item of items) { const title = await item.queryOne(".title") const description = await item.queryOne(".description") const image = await item.queryOne("img") if (title) await title.setText("New Title") if (description) await description.setText("New Description") } \`\`\` \*\*Efficient:\*\* \`\`\`typescript // ✅ Get all data at once, process locally const items = await ctx.dom.query(".item", { withInnerHTML: true, identifyChildren: true }) for (const item of items) { const $ = LoadDoc(item.innerHTML) // Access elements without additional roundtrips const titleId = $(".title").attr("id") const descriptionId = $(".description").attr("id") const imageId = $("img").attr("id") // Now make direct modifications if (titleId) ctx.dom.asElement(titleId).setText("New Title") if (descriptionId) ctx.dom.asElement(descriptionId).setText("New Description") } \`\`\` #### Use observe() Efficiently When using observe(), apply performance optimizations to handle elements efficiently. \*\*Example:\*\* \`\`\`typescript const \[stopObserving, refetch\] = ctx.dom.observe(".dynamic-content", async (elements) => { // Use withInnerHTML and identifyChildren for efficient processing for (const element of elements) { const $ = LoadDoc(element.innerHTML) // Process locally first const buttonIds = $("button").map((i, el) => $(el).attr("id")).get() // Then make direct DOM updates for (const buttonId of buttonIds) { if (buttonId) { const button = ctx.dom.asElement(buttonId) button.addEventListener("click", handleButtonClick) } } } }, { withInnerHTML: true, identifyChildren: 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/user-interface/action.md). # Action The \`ctx.action\` API allows your plugin to add UI elements that trigger custom actions to different parts of the Seanime interface. ## Core Methods ### newAnimePageButton Creates a button that appears on anime detail pages. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Button text \* \`intent\`: String (Optional) - Button style ("primary", "success", "warning", etc.) \* \`style\`: Object (Optional) - Custom CSS styles \*\*Example:\*\* \`\`\`typescript // Create a play button for anime pages const playButton = ctx.action.newAnimePageButton({ label: "Play All Episodes", intent: "primary", style: { marginRight: "8px" } }) // Mount the button to make it visible playButton.mount() // Handle clicks playButton.onClick((event) => { const anime = event.media console.log(\`Play all episodes for: ${anime.title.userPreferred}\`) // Implement playback logic }) \`\`\` ### newAnimePageDropdownItem Creates a dropdown menu item that appears in the anime page's action menu. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Menu item text \* \`style\`: Object (Optional) - Custom CSS styles \*\*Example:\*\* \`\`\`typescript // Add a download option to anime page dropdown const downloadItem = ctx.action.newAnimePageDropdownItem({ label: "Download Episodes" }) downloadItem.mount() downloadItem.onClick((event) => { const anime = event.media console.log(\`Preparing download for: ${anime.title.userPreferred}\`) // Show download dialog }) \`\`\` ### newAnimeLibraryDropdownItem Creates a dropdown menu item that appears in the anime library's global action menu. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Menu item text \* \`style\`: Object (Optional) - Custom CSS styles \*\*Example:\*\* \`\`\`typescript // Add a scan option to library menu const scanItem = ctx.action.newAnimeLibraryDropdownItem({ label: "Scan for Missing Files" }) scanItem.mount() scanItem.onClick(() => { console.log("Starting library scan") // Implement scan logic }) \`\`\` ### newMediaCardContextMenuItem Creates a context menu item that appears when right-clicking on media cards. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Menu item text \* \`for\`: String (Optional) - Which media types to show for ("anime", "manga", or "both") \* \`style\`: Object (Optional) - Custom CSS styles \*\*Example:\*\* \`\`\`typescript // Add a quick-watch option to anime cards const watchItem = ctx.action.newMediaCardContextMenuItem({ label: "Quick Watch", for: "anime" // Only show for anime cards }) watchItem.mount() watchItem.onClick((event) => { const media = event.media console.log(\`Quick watching: ${media.title.userPreferred}\`) // Implement quick watch feature }) \`\`\` ### newMangaPageButton Creates a button that appears on manga detail pages. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Button text \* \`intent\`: String (Optional) - Button style ("primary", "success", "warning", etc.) \* \`style\`: Object (Optional) - Custom CSS styles \*\*Example:\*\* \`\`\`typescript // Create a read button for manga pages const readButton = ctx.action.newMangaPageButton({ label: "Continue Reading", intent: "primary" }) readButton.mount() readButton.onClick((event) => { const manga = event.media console.log(\`Opening reader for: ${manga.title.userPreferred}\`) // Open manga reader }) \`\`\` ### newEpisodeCardContextMenuItem Creates an item that appears on episode card context menus. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Button text \* \`style\`: Object (Optional) - Custom CSS styles \* \`type?\` : "library" | "torrentstream" | "debridstream" | string | undefined {% hint style="info" %} \`type\` can also be \`episodeTab:\` when using \`ctx.anime.registerEntryEpisodeTab\` {% endhint %} \`\`\`typescript const episodeDetailsButton = ctx.action.newEpisodeCardContextMenuItem({ label: "View details", }) episodeDetailsButton.mount() episodeDetailsButton.onClick((event) => { // You get the episode object const episode = event.episode }) \`\`\` ### newEpisodeGridItemMenuItem Creates an item that appears on episode grid item menus. \*\*Parameters:\*\* \* \`props\`: Object containing: \* \`label\`: String - Button text \* \`type\` : "library" | "torrentstream" | "debridstream" | "onlinestream" | "undownloaded" | "medialinks" | "mediastream" | string \* \`style\`: Object (Optional) - Custom CSS styles {% hint style="info" %} \`type\` can also be \`episodeTab:\` when using \`ctx.anime.registerEntryEpisodeTab\` {% endhint %} \`\`\`typescript const episodeDetailsButton = ctx.action.newEpisodeGridItemMenuItem({ label: "View details", type: "library" }) episodeDetailsButton.mount() episodeDetailsButton.onClick((event) => { // You get the episode object // It is of type \[Anime\_Episode\], unless the chosen type is 'onlinestream', // in which case it will be of type \[Onlinestream\_Episode\] const episode = event.episode }) \`\`\` ### Action Object Methods All action objects share these common methods: #### mount() Makes the action visible in the UI. \*\*Example:\*\* \`\`\`typescript const button = ctx.action.newAnimePageButton({ label: "My Button" }) button.mount() // Now visible \`\`\` #### unmount() Removes the action from the UI. \*\*Example:\*\* \`\`\`typescript // Remove when no longer needed button.unmount() \`\`\` #### setLabel(label) Updates the action's label text. \*\*Parameters:\*\* \* \`label\`: String - New label text \*\*Example:\*\* \`\`\`typescript // Change button text based on state if (isDownloading) { button.setLabel("Downloading...") } else { button.setLabel("Download") } \`\`\` #### setStyle(style) Updates the action's custom CSS styles. \*\*Parameters:\*\* \* \`style\`: Object - CSS style properties \*\*Example:\*\* \`\`\`typescript // Highlight button when active if (isActive) { button.setStyle({ backgroundColor: "#4caf50", color: "white" }) } else { button.setStyle({}) } \`\`\` #### onClick(callback) Sets a function to be called when the action is clicked. \*\*Parameters:\*\* \* \`callback\`: Function(event) - Function to call when clicked \*\*Example:\*\* \`\`\`typescript button.onClick((event) => { // For anime/manga actions, event contains the media object if (event.media) { console.log(\`Clicked on ${event.media.title.userPreferred}\`) } // Your custom action logic performAction() }) \`\`\` ### Additional Properties #### AnimePageButton and MangaPageButton These button types have an additional method: \*\*setIntent(intent)\*\* Sets the button's visual style. \*\*Parameters:\*\* \* \`intent\`: String - Intent style ("primary", "success", "warning", "error", etc.) \*\*Example:\*\* \`\`\`typescript const button = ctx.action.newAnimePageButton({ label: "Watch" }) button.setIntent("primary") // Blue button // Other options: "primary-subtle", "success", "warning", "alert", etc. \`\`\` #### MediaCardContextMenuItem This action type has an additional method: \*\*setFor(type)\*\* Sets which media types the context menu item appears for. \*\*Parameters:\*\* \* \`type\`: String - "anime", "manga", or "both" \*\*Example:\*\* \`\`\`typescript const menuItem = ctx.action.newMediaCardContextMenuItem({ label: "Open" }) menuItem.setFor("anime") // Only show for anime cards \`\`\` ### Best Practices #### Limit Number of Actions Each plugin is limited to a maximum of 3 actions per type. Choose the most important actions to display. #### Dynamic UI Updates Update action properties based on application state: \`\`\`typescript // Good: Update button state dynamically let isProcessing = false button.onClick((event) => { if (isProcessing) return isProcessing = true button.setLabel("Processing...") button.setIntent("warning") button.mount() performLongOperation().then(() => { isProcessing = false button.setLabel("Done!") button.setIntent("success") button.mount() // Reset after a delay setTimeout(() => { button.setLabel("Process Again") button.setIntent("primary") button.mount() }, 3000) }) }) \`\`\` #### Conditional Mounting Only mount actions when they're relevant: \`\`\`typescript // Good: Mount/unmount based on context function updateButtonVisibility(media) { if (media.format === "MOVIE") { watchButton.mount() episodesButton.unmount() // No episodes for movies } else { watchButton.mount() episodesButton.mount() } } \`\`\` --- # 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/permissions.md). # Permissions {% hint style="warning" %} Difficulty: Moderate \* Some knowledge of the filesystem, platform differences is required {% endhint %} You can check out the type definition file to see the exhaustive list of methods available and use Go's documentation to learn how to use them. {% hint style="info" %} The examples may use hardcoded paths but this is not recommended. Seanime is a cross-platform app, keep that in mind. {% endhint %} {% hint style="warning" %} As of Seanime \`v3.8.0\`, if the user enables Extension Secure Mode, sensitive system actions such as file reads, writes, directory inspection, and command execution can prompt for confirmation. If the user rejects a prompt, the call throws. If the prompt cannot be shown, such as during app startup before a UI client is available, the call fails immediately. {% endhint %} ## Permissions {% hint style="warning" %} \`system\` permission is required. {% endhint %}
{
    //...
    "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 ## Usage Hooks should be used carefully as they can introduce undefined behavior and even slow down the app. Some hooks, like \`onGetAnime\` , are triggered very often, so it's a good habit to start by logging the event in order to figure out its frequency. You should also avoid expensive calculations or fetch calls in hook handlers unless you can guarantee that the hook is not triggered often. {% hint style="info" %} Any error/exception that happens in a hook handler will result in a server and client error. Test your code carefully. {% endhint %} {% code title="Example" overflow="wrap" %} \`\`\`typescript function init() { // This hook is triggered before Seanime formats the library data of an anime // The event contains the variables that Seanime will use, and you can modify them $app.onAnimeEntryLibraryDataRequested((e) => { // Setting this to an empty array will cause Seanime to think that the anime // has not been downloaded. e.entryLocalFiles = \[\] e.next() // Continue hook chain }) } \`\`\` {% endcode %} {% hint style="warning" %} Each hook handler must call \`e.next()\` in order for the hook chain listening to that event to proceed. Not calling it will impact other plugins listening to that event. {% endhint %} ## Best Practices ### Editing events Let's say we want your plugin to change anime banner images based on what custom banner image has been set for that anime. However you want to do it without manipulating the DOM and before the page is even loaded. We can use \`onGetAnimeCollection\` and \`onGetRawAnimeCollection\` since these are triggered when Seanime fetches the user's anime collection from AniList. Note that this will not change banner images for the same anime if it's fetched using another query (e.g. discover, search). \`\`\`typescript // Triggers the app loads the user's AniList anime collection $app.onGetAnimeCollection((e) => { // 1. Get all the custom banner images const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } // 2. Go through all anime in the collection for (let i = 0; i < e.animeCollection!.MediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.MediaListCollection!.lists!\[i\]!.entries!.length; j++) { const mediaId = e.animeCollection!.MediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.id // 3. If this anime has a custom image, change it const bannerImage = bannerImages\[mediaId.toString()\] if (!!bannerImage) { e.animeCollection!.MediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.bannerImage = bannerImage } } } // 4. Continue e.next() }) // Do the same with $app.onGetRawAnimeCollection \`\`\` ### Listening to events Let's say we want to make a plugin that stores the history of scanning durations. \`\`\`typescript // ⚠️ Not recommended: Doing unnecessary work in the hook callback function init() { $app.onScanCompleted((e) => { const now = new Date().toISOString().replaceall(".", "\_") // Add the value to the history // NOTE: In reality this operation is very fast $storage.set("scan-duration-history."+now, e.duration) e.next() }) $ui.register((ctx) => { }) } // ✅ Good practice: Defer business logic to the UI context function init() { $app.onScanCompleted((e) => { // Send a copy of the event $store.set("scan-completed", $clone(e)) e.next() }) // Let the UI context "listen" to the event and execute business logic $ui.register((ctx) => { // Callback is triggered anytime 'set' is called on that key $store.watch("scan-completed", (e) => { const now = new Date().toISOString().replaceall(".", "\_") // Add the value to the history $storage.set("scan-duration-history."+now, e.duration) ctx.toast.info(\`Scanning took ${e.duration/1000} seconds!\`) }) }) } \`\`\`
--- # Feature requests | Seanime Extensions For the complete documentation index, see [llms.txt](https://seanime.gitbook.io/seanime-extensions/llms.txt) . This page is also available as [Markdown](https://seanime.gitbook.io/seanime-extensions/frequently-asked/feature-requests.md) . Feature requests on GitHub pertain to features that will be integrated in the source code **only**. ### Why was this feature request closed?[](https://seanime.gitbook.io/seanime-extensions/frequently-asked#why-was-this-feature-request-closed) As of `v2.8.0` , Seanime supports [Plugins](https://seanime.gitbook.io/seanime-extensions/plugins/introduction) , which can be developed entirely in JavaScript. A feature request will be closed as not planned with the label `status: plugin-suitable` if: * The feature can be reasonably added via plugin * The feature will not benefit a majority of users * The feature is mostly subjective or cosmetic (e.g. removing elements, changing layout, etc.) This is done to: * Reduce development time and update cycles * Avoid bloat by offloading noncritical features * Improve contribution ### What if I can't develop a plugin?[](https://seanime.gitbook.io/seanime-extensions/frequently-asked#what-if-i-cant-develop-a-plugin) Join the Discord server and make a request in the `#extension-proposals` channel, someone might make it for you. [PreviousExample](https://seanime.gitbook.io/seanime-extensions/plugins/example) Last updated 3 months ago --- # 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/anime.md). # Anime The \`ctx.anime\` API provides methods to interact with the anime system in Seanime. ## Core Methods ### getAnimeEntry Gets a an anime entry, using the cache if available. \*\*Parameters:\*\* \* \`mediaId\`: Number - The AniList media ID \*\*Example:\*\* \`\`\`typescript const animeEntry = await ctx.anime.getAnimeEntry(21) \`\`\` ### getAnimeMetadata Gets raw anime metadata from the metadata provider \*\*Parameters:\*\* \* \`from\` : "anilist" | "mal" | "kitsu" | "anidb" \* \`mediaId\`: Number - The media ID Example: \`\`\`typescript const metadata = await ctx.anime.getAnimeMetadata("anilist", 21) \`\`\` ### getEntryDownloadInfo \`getEntryDownloadInfo(mediaId)\` Builds download information for an anime entry using Seanime's local files, AniList collection progress/status, and metadata provider data. \*\*Parameters:\*\* \* \`mediaId\`: Number - The AniList media ID \*\*Returns:\*\* \`Promise<$app.Anime\_EntryDownloadInfo>\` \*\*Example:\*\* \`\`\`typescript const info = await ctx.anime.getEntryDownloadInfo(21) console.log(info) \`\`\` ### getEpisodeCollection \`getEpisodeCollection(mediaId)\` Returns the normalized episode collection for an anime. This is the same shape used by entry episode tabs and other Seanime library views. \*\*Parameters:\*\* \* \`mediaId\`: Number - The AniList media ID \*\*Returns:\*\* \`Promise<$app.Anime\_EpisodeCollection>\` \*\*Example:\*\* \`\`\`typescript const episodeCollection = await ctx.anime.getEpisodeCollection(21) console.log(episodeCollection.episodes) \`\`\` ### clearEpisodeMetadataCache Empties the episode metadata cache \`\`\`typescript ctx.anime.clearEpisodeMetadataCache() \`\`\` ### registerEntryEpisodeTab Adds a custom episode tab.
\*\*Example:\*\* \`\`\`typescript const tab = ctx.anime.registerEntryEpisodeTab({ shouldShow: ({ mediaId }) => true, icon: \`\`, name: "Test", onEpisodeCollection: ({ episodeCollection }) => { // You can edit the default episode collection return episodeCollection }, onSelectEpisode: ({ episodeNumber }) => { ctx.toast.info(\`Episode ${episodeNumber} selected\`) }, }) const isOpen = tab.getIsOpen() ctx.effect(() => { console.log(isOpen.get()) }, \[isOpen\]) \`\`\` --- # 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/buffers-i-o.md). # Buffers, I/O ## Bufio \`$bufio\` provides functionalities to read or write binary data in chunks rather than one byte at a time. ### Reader \`\`\`typescript const file = $os.openFile("C:\\Users\\user\\Downloads\\multiline.txt", $os.O\_RDONLY, 0); const reader = $bufio.newReader(file); // Read lines manually with try/catch to handle EOF const lines = \[\]; for (let i = 0; i < 10; i++) { // Try to read more lines than exist try { const line = reader.readString($toBytes('\\n')); lines.push(line.trim()); } catch (e) { console.log("Caught expected EOF:", e.message); } } file.close(); console.log(lines) // \["Line 1", "Line 2", "Line 3"\] \`\`\` ### Writer \`\`\`typescript const writeFile = $os.create("C:\\Users\\user\\Downloads\\bufio\_write.txt"); const writer = $bufio.newWriter(writeFile); // Write multiple strings writer.writeString("Buffered "); writer.writeString("write "); writer.writeString("test"); // Flush to ensure data is written writer.flush(); writeFile.close(); \`\`\` ### Scanner \`\`\`typescript const scanFile = $os.openFile("C:\\Users\\user\\Downloads\\multiline.txt", $os.O\_RDONLY, 0); const scanner = $bufio.newScanner(scanFile); // Scan lines const scannedLines = \[\]; while (scanner.scan()) { scannedLines.push(scanner.text()); } scanFile.close(); console.log(scannedLines) // \["Line 1", "Line 2", "Line 3"\] \`\`\` ## Bytes \`$bytes\` provides functionalities to manipulate binary data. ### Read, write \`\`\`typescript // Write string to buffer const buffer = $bytes.newBuffer($toBytes("Hello")); buffer.writeString(", world!"); // Get buffer content const bufferContent = $toString(buffer.bytes()); console.log(bufferContent); // Hello, world! // Create a new buffer string const strBuffer = $bytes.newBufferString("String buffer"); strBuffer.writeString(" test"); const strBufferContent = strBuffer.string(); console.log(strBufferContent); // String buffer test // Create a byte reader const reader = $bytes.newReader($toBytes("Bytes reader test")); const readerBuffer = new Uint8Array(100); // Empty buffer const bytesRead = reader.read(readerBuffer); // Read into buffer console.log(bytesRead, "bytes read") // 17 bytes read const readerContent = $toString(readerBuffer.subarray(0, bytesRead)); console.log(readerContent); // Bytes reader test // Buffer methods const testBuffer = $bytes.newBuffer($toBytes("")); testBuffer.writeString("Test"); testBuffer.writeByte(32); // Space testBuffer.writeString("methods"); const testBufferContent = testBuffer.string(); console.log(testBufferContent); // Test methods // Read methods const readBuffer = $bytes.newBuffer($toBytes("Read test")); const readByte = readBuffer.readByte(); console.log("Read byte:", String.fromCharCode(readByte)); // Read byte: R const nextBytes = new Uint8Array(5); readBuffer.read(nextBytes); console.log("Next bytes:", $toString(nextBytes)); // Next bytes: ead t \`\`\` ## I/O \`$io\` provides generalized I/O interface functionalities. --- # 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/shared.md). # Shared Modules \`$shared\` lets a plugin define reusable factories once and load them from other runtimes. This is useful when the same helper is needed in hook callbacks and in the UI context. ## Quick example
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
\`\`\`typescript const webview = ctx.newWebview({ slot: "fixed", width: "100%", maxWidth: "1200px", height: "500px", window: { draggable: true, defaultPosition: "bottom-right", }, hidden: true, }) button.onClick(() => { webview.show() }) \`\`\` ## Events \`\`\`typescript webview.onMount(() => { // Webview has been mounted on the screen }) webview.onUnmount(() => { // Webview has been unmounted }) webview.onLoad(() => { // Webview content has been loaded (after mount) }) // Force the webview content to update webview.update() // Returns the path to the webview screen webview.getScreenPath() // /webview?id=my-plugin webview.hide() webview.show() \`\`\` ## Rendering ### HTML
// 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>(\[\ { id: "2", text: "Check discussion for latest episode", checked: true },\ { id: "1", text: "Watch the next season", checked: false },\ \]) // Create the Webview const panel = ctx.newWebview({ slot: "screen", fullWidth: true, autoHeight: true, sidebar: { label: "Notepad", icon: \`\`, }, }) // Setup Communication // Automatically keep 'notes' and 'currentMedia' variables in sync with the webview panel.channel.sync("notes", notes) panel.channel.sync("media", currentMedia) // Handle events sent from the webview panel.channel.on("add-note", (text) => { console.log("Received note:", text) const newNote = { id: Date.now().toString(), text, checked: false } // Updating this state automatically sends the new value to the webview thanks to .sync() notes.set(\[...notes.get(), newNote\]) ctx.toast.success("Note added") }) panel.channel.on("toggle-note", (id) => { notes.set(prev => prev.map(n => n.id === id ? { ...n, checked: !n.checked } : n)) }) panel.channel.on("delete-note", (id) => { notes.set(prev => prev.filter(n => n.id !== id)) }) // Render the UI panel.setContent(() => \`
\`) }) } \`\`\` --- # 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/storage.md). # Storage ## Permission {% hint style="warning" %} \`storage\` permission is required. {% endhint %}
{
    //...
    "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("foo.bar") // 1 $storage.get>("foo") // { "bar": 1, "baz": "2" } $storage.set("foo", "bar") $storage.get("foo") // bar $storage.watch("foo", (value) => {}) \`\`\` ### Unsafe access {% hint style="warning" %} Available from Seanime \`v3.7.2\`. {% endhint %} \`$storage.getUnsafe(key)\` returns the raw stored reference without cloning it first. Use it only when you need to avoid the cloning cost for large values and you can treat the result as read-only. \`\`\`typescript const rawHistory = $storage.getUnsafe>("scan-duration-history") \`\`\` {% hint style="warning" %} Do not mutate objects returned by \`getUnsafe()\`. The safe \`get()\` method exists to avoid accidental shared-reference writes and concurrent map write panics. {% endhint %} ## Example {% code title="my-plugin.ts" %} \`\`\`typescript // 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("scan-completed", (value) => { const date = new Date() const now = date.toISOString().replaceall(".", "\_") // Add the value to the history $storage.set("scan-duration-history."+now, { duration: value, durationInSeconds: value/1000, addedAt: date, }) ctx.toast.info(\`Scanning took ${value/1000} seconds!\`) }) function deleteHistory() { $storage.remove("scan-duration-history") } }) } \`\`\` {% endcode %} {% hint style="warning" %} Make sure your storage doesn't grow too big by doing some cleanup. {% endhint %} ## Good to know The plugin storage is deleted when the plugin is uninstalled. --- # 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/anilist.md). # AniList {% hint style="success" %} Difficulty: Easy {% endhint %} ## Permission {% hint style="warning" %} \`anilist\` permission is required. {% endhint %}
{
    //...
    "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(body: Record, token: string): T \`\`\` --- # 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/filler-manager.md). # Filler Manager ## Core Methods ### getFillerEpisodes Retrieves filler episode data for an anime. \*\*Parameters\*\*: \* \`mediaId\`: number - AniList media ID Returns: string\\\[\] | undefined - List of filler episode numbers or undefined if not found Example: \`\`\`javascript // Get filler episodes for One Piece const fillerEpisodes = ctx.fillerManager.getFillerEpisodes(21) if (fillerEpisodes) { console.log("Filler episodes:", fillerEpisodes) } else { console.log("No filler data found") } \`\`\` ### removeFillerData Removes filler episode data for an anime. \*\*Parameters\*\*: \* \`mediaId\`: number - AniList media ID Example: \`\`\`javascript // Remove filler data for One Piece ctx.fillerManager.removeFillerData(21) console.log("Filler data removed") \`\`\` ### setFillerEpisodes Sets custom filler episode data for an anime. \*\*Parameters\*\*: \* \`mediaId\`: number - AniList media ID \* \`fillerEpisodes\`: string\\\[\] - List of episode numbers that are filler Example: \`\`\`javascript // Set custom filler episodes for One Piece ctx.fillerManager.setFillerEpisodes(21, \["50", "51", "52", "99", "100"\]) console.log("Custom filler data set") \`\`\` ### isEpisodeFiller \`\`\` isEpisodeFiller(mediaId, episodeNumber) \`\`\` Checks if a specific episode is marked as filler. \*\*Parameters\*\*: \* \`mediaId\`: number - AniList media ID \* \`episodeNumber\`: number - Episode number to check Returns: boolean - True if the episode is filler, false otherwise Example: \`\`\`javascript // Check if episode 99 of One Piece is filler const isFiller = ctx.fillerManager.isEpisodeFiller(21, 99) console.log("Episode 99 is filler:", isFiller) \`\`\` ### hydrateFillerData \`\`\` hydrateFillerData(entry) \`\`\` Updates a library entry with filler episode data. \*\*Parameters\*\*: \* \`entry\`: Entry - Anime library entry object Example: \`\`\`javascript // Hydrate filler data for a library entry const entry = getAnimeEntry(21) // One Piece ctx.fillerManager.hydrateFillerData(entry) console.log("Filler data added to entry") \`\`\` ### hydrateOnlinestreamFillerData \`\`\` hydrateOnlinestreamFillerData(mediaId, episodes) \`\`\` Updates online stream episodes with filler episode data. \*\*Parameters\*\*: \* \`mediaId\`: number - AniList media ID \* \`episodes\`: Episode\\\[\] - Array of online stream episodes Example: \`\`\`javascript // Hydrate filler data for online stream episodes const episodes = getOnlineStreamEpisodes(21) // One Piece episodes ctx.fillerManager.hydrateOnlinestreamFillerData(21, episodes) console.log("Filler data added to online stream episodes") \`\`\` --- # 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/commands.md). # Commands {% hint style="warning" %} As of Seanime \`v3.8.0\`, if the user enables Extension Secure Mode, command execution can prompt for confirmation. If the user rejects the prompt, the command call throws. If prompts cannot be shown, such as during app startup before a UI client is connected, the command call fails immediately. {% endhint %} ## Permissions By default, Seanime disallows running commands, you must manually defines the command and arguments your plugin will want to run using \`commandScopes\` . ### Example
{
  // ...
  "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: ### Helpers \`\`\`typescript const baseName = $filepath.base("C:\\Users\\user\\Downloads\\file.mkv"); console.log(baseName); // file.mkv const dirName = $filepath.dir("C:\\Users\\user\\Downloads\\file.mkv"); console.log(dirName); // C:\\Users\\user\\Donwloads const extName = $filepath.ext("C:\\Users\\user\\Downloads\\file.mkv"); console.log(extName); // .mkv const joinedPath = $filepath.join("C:", "Users", "user", "subdir", "file.txt"); console.log(joinedPath); // C:\\Users\\user\\subdir\\file.txt const \[dir, file\] = $filepath.split("C:\\Users\\user\\Downloads\\file.mkv"); console.log(dir, file); // C:\\Users\\user\\Downloads, file.mkv const globResults = $filepath.glob("C:\\Users\\user\\Downloads", "\*.txt"); console.log(globResults); // test.txt, test2.txt const isMatch = $filepath.match("\*.txt", "test.txt"); console.log(isMatch); // true const isAbsPath = $filepath.isAbs("C:\\Users\\user\\Downloads\\file.mkv"); console.log(isAbsPath); // true // Test toSlash and fromSlash const slashPath = $filepath.toSlash("C:\\Users\\user\\Downloads\\file.mkv"); console.log(slashPath); // C:/Users/user/Downloads/file.mkv const fromSlashPath = $filepath.fromSlash(slashPath); console.log(fromSlashPath); // C:\\Users\\user\\Downloads\\file.mkv \`\`\` ### Walk directories \`\`\`typescript // walk calls 'lstat' on each file path it encounters, which can be slower $filepath.walk("C:\\Users\\user\\Downloads", (path, info, err) => { if (err) { console.log("Walk error:", path, err); return; // Continue walking } // We can skip directories if (info.isDir() && info.name() === "ignoredDir") { console.log("Skipping directory:", path); return $filepath.skipDir; } console.log("Walk path:", path, "isDir:", info.isDir()); return; // Continue walking }); // walkDir is more efficient $filepath.walkDir("C:\\Users\\user\\Downloads", (path, d, err) => { if (err) { console.log("WalkDir error:", path, err); return; // Continue walking } // We can skip directories if (d.isDir() && d.name() === "ignoredDir") { console.log("Skipping directory:", path); return $filepath.skipDir; } console.log("WalkDir path:", path, "isDir:", d.isDir()); return; // Continue walking }); \`\`\` --- # 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/torrentstream.md). # Torrentstream The \`ctx.torrentstream\` API lets plugins stream torrents through Seanime. ## Permissions {% hint style="warning" %} \`playback\` permission is required {% endhint %}
{
  //...
  "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

## Create a tray icon {% hint style="info" %} You can only create one tray icon in your plugin {% endhint %} \`\`\`typescript const tray = ctx.newTray({ tooltipText: "My plugin", iconUrl: "https://seanime.rahim.app/logo\_2.png", withContent: true, isDrawer: false, // Choose whether the tray contents are displayed in a drawer }) \`\`\` ## Add a badge \`\`\`typescript tray.updateBadge({ number: 1, intent: "alert" }) // Remove the badge tray.updateBadge({ number: 0 }) \`\`\` \* \`intent\`: "alert" | "info" | "warning" | "success" ## Rendering content {% hint style="info" %} \`withContent\` should be set to 'true'. {% endhint %} The \`state\` is a mechanism to keep track of variable data over time, enabling components to re-render automatically when the data changes. The \`render()\` function is used to define how content should be presented in the tray popover. It takes a callback function that returns a tree of components. Components, like \`tray.text\` and \`tray.button\`, are reusable building blocks of the UI, each responsible for rendering a piece of the interface according to the current state. The tray will be re-rendered anytime there is a state change even if the state isn't in the render function.
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 radioGroupRef = ctx.fieldRef() tray.render(() => tray.stack(\[\ tray.select("Label", { \ placeholder: "Select...",\ options: \[\ { label: "One Piece", value: "21" },\ { label: "Sakamoto Days", value: "177709" },\ \],\ fieldRef: selectRef,\ }),\ tray.radioGroup("Label", { \ options: \[\ { label: "One Piece", value: "21" },\ { label: "Sakamoto Days", value: "177709" },\ \],\ fieldRef: radioGroupRef,\ }),\ \])) \`\`\` ### Checkbox, Switch \`\`\`typescript const checkboxRef = ctx.fieldRef() const switchRef = ctx.fieldRef() tray.render(() => tray.stack(\[\ tray.checkbox("Do something", { \ fieldRef: checkboxRef\ }),\ tray.switch("Do something else", { \ fieldRef: switchRef\ }),\ \])) \`\`\` ## Complex components ### CSS \`\`\`javascript tray.div(\[\ tray.stack(\[\ tray.css(\`\ .red { background-color: red; }\ \`),\ // Red square\ tray.tooltip(tray.div(\[\], { className: "square red relative" }), { text: "Test tooltip" }),\ \]),\ tray.stack(\[\ // No red\ tray.tooltip(tray.div(\[\], { className: "square red relative" }), { text: "Test tooltip" }),\ \]),\ \]) \`\`\` ### Tabs, Dropdown, Modal \`\`\`javascript tray.tabs(\[\ tray.tabsList(\[\ tray.tabsTrigger(tray.span("Item 1"), { value: "1" }),\ tray.tabsTrigger(tray.span("Item 2"), { value: "2" }),\ \]),\ tray.tabsContent(\[\ tray.text("Hello, World!"),\ tray.a(\[\ tray.span("A "),\ tray.span("link", { className: "font-bold" }),\ \], { href: "#" }),\ tray.p(\[\ tray.span("This is a paragraph."),\ \]),\ tray.modal({\ trigger: tray.button("Open modal"),\ open: modalOpen.get(),\ onOpenChange: ctx.eventHandler("modal-open-change", ({ open }) => {\ console.log(open)\ modalOpen.set(open)\ }),\ items: \[\ tray.text("Hello, World!"),\ \],\ }),\ tray.dropdownMenu({\ trigger: tray.button("Open dropdown"),\ items: \[\ tray.dropdownMenuItem(tray.span("Item 1")),\ tray.dropdownMenuItem(tray.span("Item 2")),\ tray.dropdownMenuItem(tray.span("Item 3")),\ \],\ }),\ \], { value: "1" }),\ tray.tabsContent(\[\ tray.text("Item 2 content"),\ \], { value: "2" }),\ \], { defaultValue: "1" }), \`\`\` --- # 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.md). # User Interface - \[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. --- # 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.md). # Downloading - \[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) --- # 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/basics.md). # Basics {% hint style="warning" %} Difficulty: Moderate \* Some basic knowledge of reactive UIs and state management is recommended. {% endhint %} ## What is $ui.register? You can interact with the user interface and execute business logic via APIs provided to your plugin when you register a UI context. \`\`\`typescript function init() { // This function registers the UI context for your plugin, allowing it to // have access to UI APIs $ui.register((ctx) => { // The 'ctx' objects contains all the APIs }) } \`\`\` Unlike hooks which are called every time a specific event is triggered, the function inside \`$ui.register\` is called only once during the lifetime of your plugin, right after \`init(),\` in other words, each time your plugin is loaded. {% hint style="info" %} You cannot register hook handlers inside the UI callback. {% endhint %} ## Fetch {% hint style="info" %} As of v3.3.0, network requests require you to whitelist domains \[Permissions\](/seanime-extensions/plugins/permissions.md#network-requests) {% endhint %} {% hint style="warning" %} In the UI context, \`ctx.fetch\` should be used instead of simply \`fetch\` . {% endhint %} \`\`\`typescript $ui.register(async (ctx) => { const res = await ctx.fetch("https://jsonplaceholder.typicode.com/todos/1") const data = res.json() }) \`\`\` ## States State management allows you to keep track of dynamic data within your plugin. This approach not only helps maintain a clear separation of concerns but also enables reactive programming, where UI components like the \`Tray\` automatically update in response to changes in states.
//...

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(null) const fetchedData = ctx.state(\[\]) // When the user navigates to an anime, get the media ID ctx.screen.onNavigate((e) => { if (e.pathname === "/entry" && !!e.searchParams.id) { const id = parseInt(e.searchParams.id); currentMediaId.set(id); } else { currentMediaId.set(null); } }); // Trigger 'ctx.screen.onNavigate' when the plugin loads ctx.screen.loadCurrent() // Fetch data each time the media ID changes. ctx.effect(async () => { if (!currentMediaId.get()) return const res = ctx.fetch(\`https://example.com/anilistId?=${currentMediaId.get()}\`) // Store the results fetchedData.set(res.json()) }, \[currentMediaId\]) \`\`\` {% endcode %} --- # 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/scanner.md). # Scanner The \`ctx.scanner\` API runs a library scan immediately and persists the result to Seanime's database. ## Core Methods ### scan \`scan(options?)\` Runs a library scan immediately and resolves to the scanned local files. If no local files are found, Seanime resolves this call with an empty array. \*\*Parameters:\*\* \* \`options\`: \`ScannerScanOptions\` \* \`options.enhanced\`: Boolean - Optional \* \`options.enhanceWithOfflineDatabase\`: Boolean - Optional \* \`options.skipLockedFiles\`: Boolean - Optional \* \`options.skipIgnoredFiles\`: Boolean - Optional \*\*Returns:\*\* \`Promise<$app.Anime\_LocalFile\[\]>\` \*\*Example:\*\* \`\`\`typescript const localFiles = await ctx.scanner.scan({ enhanced: true, enhanceWithOfflineDatabase: true, skipLockedFiles: true, skipIgnoredFiles: true, }) console.log(\`Scanned ${localFiles.length} files\`) \`\`\` --- # 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/mpv.md). # MPV ## Permissions {% hint style="warning" %} \`playback\` permission is required {% endhint %}
{
  //...
  "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\` ### logout \`ctx.auth.logout()\` Logs the user out of AniList after the user approves the prompt. \*\*Returns:\*\* \`Promise\` ### Example \`\`\`typescript $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "AniList auth", withContent: true, }) const tokenRef = ctx.fieldRef("") ctx.registerEventHandler("login-anilist", async () => { try { await ctx.auth.login(tokenRef.current ?? "") tokenRef.setValue("") ctx.toast.success("AniList login updated") } catch (error) { ctx.toast.alert(\`Login failed: ${error.message}\`) } }) ctx.registerEventHandler("logout-anilist", async () => { try { await ctx.auth.logout() ctx.toast.info("AniList logged out") } catch (error) { ctx.toast.alert(\`Logout failed: ${error.message}\`) } }) tray.render(() => tray.stack(\[\ tray.input("AniList token", { fieldRef: tokenRef }),\ tray.button("Log in", { onClick: "login-anilist" }),\ tray.button("Log out", { onClick: "logout-anilist" }),\ \])) }) \`\`\` --- # 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/manga.md). # Manga The \`ctx.manga\` API provides methods to interact with the manga system in Seanime. ## Core Methods ### getProviders Gets all provider extensions \`\`\`typescript const providers = ctx.manga.getProviders() for (const providerId in providers) { console.log("ID:", providerId, "Name:", providers\[providerId\]) } \`\`\` ### getChapterContainer Gets a chapter container for a specific manga, using the cache if available. \*\*Parameters:\*\* \* \`opts\`: Object containing: \* \`mediaId\`: Number - The AniList media ID \* \`provider\`: String - The manga provider identifier \* \`titles\`: String\\\[\] (Optional) - Alternative titles to help find the manga \* \`year\`: Number (Optional) - Release year to help with identification \*\*Example:\*\* \`\`\`typescript // Get chapter container for a manga from MangaDex const mangaContainer = await ctx.manga.getChapterContainer({ mediaId: 000000, provider: "mangadex", titles: \["Kimetsu no Yaiba", "Demon Slayer"\], year: 2016 }) if (mangaContainer) { console.log(\`Found ${mangaContainer.chapters.length} chapters from ${mangaContainer.provider}\`) // Process chapters for (const chapter of mangaContainer.chapters) { console.log(\`Chapter ${chapter.chapter}: ${chapter.title}\`) } } \`\`\` ### getDownloadedChapters Retrieves all downloaded manga chapters grouped by provider and manga ID. \*\*Example:\*\* \`\`\`typescript // Get all downloaded chapters const downloadedChapters = await ctx.manga.getDownloadedChapters() // Count chapters per manga const chaptersByManga = {} for (const container of downloadedChapters) { if (!chaptersByManga\[container.mediaId\]) { chaptersByManga\[container.mediaId\] = 0 } chaptersByManga\[container.mediaId\] += container.chapters.length } console.log("Downloaded chapters by manga:", chaptersByManga) \`\`\` ### getCollection Retrieves the user's manga collection with all media list data. \*\*Example:\*\* \`\`\`typescript // Get the user's manga collection const mangaCollection = await ctx.manga.getCollection() // Process each list in the collection for (const list of mangaCollection.lists) { console.log(\`List ${list.status}: ${list.entries.length} entries\`) // Process each manga in the list for (const entry of list.entries) { const manga = entry.media const progress = entry.listData?.progress || 0 console.log(\`${manga.title.userPreferred}: ${progress}/${manga.chapters || '?'} chapters read\`) } } \`\`\` ### refreshChapters Deletes all cached chapters and refetches them based on the selected provider for each manga. \*\*Parameters:\*\* \* \`selectedProviderMap\`: Record\\ - A map of manga IDs to provider IDs \*\*Example:\*\* \`\`\`typescript // Refresh chapters for specific manga using selected providers const providerSelections = { 30013: "mangadex", 21: "mangasee", 31706: "manganato" } // Refresh all chapters based on these provider preferences await ctx.manga.refreshChapters(providerSelections) console.log("Chapter data refreshed for selected manga") \`\`\` ### emptyCache Empties cached chapters for a specific manga. \*\*Parameters:\*\* \* \`mediaId\`: Number - The AniList media ID \*\*Example:\*\* \`\`\`typescript // Clear cached chapters for a manga (e.g., after a major update) await ctx.manga.emptyCache(30013) console.log("Cache cleared for Demon Slayer") // Refetch immediately to get fresh data const freshData = await ctx.manga.getChapterContainer({ mediaId: 30013, provider: "mangadex" }) \`\`\` --- # 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/helpers.md). # Helpers The UI context includes some helper groups for common plugin workflows: \* \`ctx.cache\` \* \`ctx.settings\` \* \`ctx.jobs\` ## cache \`ctx.cache\` stores runtime-local values in memory. ### get \`ctx.cache.get(key, fallback?)\` Returns the cached value, or the fallback if the key does not exist. ### set \`ctx.cache.set(key, value, ttl?)\` Stores a value and optionally expires it after a number of milliseconds. You can pass either a raw number or an object like \`{ ttl: 5000 }\`. ### has \`ctx.cache.has(key)\` Returns whether the key exists and has not expired. ### remove \`ctx.cache.remove(key)\` Deletes a cached entry and returns whether it existed. \`ctx.cache.delete(key)\` is an alias. ### clear \`ctx.cache.clear()\` Removes every cached entry. ### size \`ctx.cache.size()\` Returns the current number of cached entries. ### getOrSet \`ctx.cache.getOrSet(key, loader, ttl?)\` Returns the cached value when present. Otherwise it runs \`loader\`, stores the result, and returns it. If \`loader\` returns a promise, concurrent calls with the same key share the same in-flight promise. \`ctx.cache.getOrLoad(...)\` and \`ctx.cache.remember(...)\` are aliases. Example: \`\`\`typescript $ui.register((ctx) => { const currentMediaId = ctx.state(null) const currentTitle = ctx.state("Open an anime entry") ctx.screen.onNavigate((event) => { if (event.pathname === "/entry" && event.searchParams.id) { currentMediaId.set(parseInt(event.searchParams.id)) return } currentMediaId.set(null) currentTitle.set("Open an anime entry") ctx.cache.remove("current-entry") }) ctx.effect(async () => { const mediaId = currentMediaId.get() if (!mediaId) { return } const animeEntry = await ctx.cache.getOrSet( \`anime-entry:${mediaId}\`, () => ctx.anime.getAnimeEntry(mediaId), { ttl: 60\_000 }, ) currentTitle.set(animeEntry?.media?.title?.userPreferred ?? "Unknown title") ctx.cache.set("current-entry", { mediaId, loadedAt: Date.now(), }, 5\_000) console.log("cache size", ctx.cache.size()) }, \[currentMediaId\]) ctx.screen.loadCurrent() }) \`\`\` ## settings \`ctx.settings.define(name, defaults)\` creates a settings helper scoped to one namespace. {% hint style="info" %} \`ctx.settings\` stores plugin-local UI settings. To read or edit Seanime's app settings, use \[App Settings\](/seanime-extensions/plugins/ui/other/app-settings.md). {% endhint %} \`\`\`typescript const settings = ctx.settings.define("banner-images", { enabled: true, opacity: 0.8, }) \`\`\` {% hint style="info" %} Settings are always mirrored in \`$store\`. If the plugin also has the \`storage\` permission, they are persisted in \`$storage\` as well. {% endhint %} The returned object exposes these fields and methods. ### key \`settings.key\` The internal storage key, prefixed with \`settings:\`. ### defaults \`settings.defaults\` The default object passed to \`define()\`. ### get \`settings.get(path?, fallback?)\` Returns the whole settings object, or one dot-path value. ### set \`settings.set(path, value)\` Updates a single dot-path value. \`settings.set(object)\` merges the provided object into the current settings. ### save \`settings.save(value?)\` Saves a full value after merging it with defaults. If you omit the value, Seanime re-saves the current settings. ### reset \`settings.reset()\` Restores the settings to their defaults. ### fieldRef \`settings.fieldRef(path?)\` Returns a field reference for the whole settings object or for a nested path. ### watch \`settings.watch(callback)\` Registers a callback that receives the full settings object every time it changes. Returns an unsubscribe function. Example: \`\`\`typescript $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "DX settings example", withContent: true, }) const settings = ctx.settings.define("entry-filters", { query: "", onlyUnwatched: false, sort: "title", }) const queryRef = settings.fieldRef("query") queryRef.onValueChange((value) => { settings.set("query", value) }) const stopWatching = settings.watch((nextSettings) => { console.log("filters updated", nextSettings) }) ctx.registerEventHandler("toggle-unwatched", () => { settings.set("onlyUnwatched", !settings.get("onlyUnwatched", false)) }) ctx.registerEventHandler("reset-filters", () => { settings.reset() queryRef.setValue(settings.get("query", "")) ctx.toast.success("Filters reset") }) ctx.registerEventHandler("stop-filter-watch", () => { stopWatching() ctx.toast.info("Stopped watching settings changes") }) tray.render(() => tray.stack(\[\ tray.text(\`Sort: ${settings.get("sort", "title")}\`),\ tray.text(\`Only unwatched: ${settings.get("onlyUnwatched", false) ? "yes" : "no"}\`),\ tray.input("Search", { fieldRef: queryRef }),\ tray.button("Toggle unwatched", { onClick: "toggle-unwatched" }),\ tray.button("Reset filters", { onClick: "reset-filters" }),\ tray.button("Stop watch", { onClick: "stop-filter-watch" }),\ \])) }) \`\`\` ## jobs \`ctx.jobs\` helps coordinate repeated or overlapping UI work. ### singleflight \`ctx.jobs.singleflight(key, callback)\` Runs only one job for the same key at a time. If another call starts while the first one is still running, Seanime returns the existing promise. This is useful for deduplicating button clicks or repeated fetches. ### debounce \`ctx.jobs.debounce(key, callback, delayMs)\` Schedules a callback after \`delayMs\`. Calling it again with the same key resets the timer. Returns a cancel function. ### poll \`ctx.jobs.poll(key, callback, intervalMs, options?)\` Runs a callback on an interval until canceled. \*\*Options:\*\* \* \`options.immediate\`: Boolean - Optional. When \`true\`, runs once immediately before the interval starts. Returns a cancel function. ### cancel \`ctx.jobs.cancel(key)\` Cancels a debounced or polling job for the key. Returns \`true\` when a cancelable job existed. ### cancelAll \`ctx.jobs.cancelAll()\` Cancels every debounced or polling job. ### isRunning \`ctx.jobs.isRunning(key)\` Returns whether a \`singleflight()\` job with this key is currently in progress. Example: \`\`\`typescript $ui.register((ctx) => { const tray = ctx.newTray({ tooltipText: "Jobs example", withContent: true, }) const currentMediaId = ctx.state(null) const status = ctx.state("idle") const lastRefreshAt = ctx.state(null) async function refreshEntryInfo() { const mediaId = currentMediaId.get() if (!mediaId) { return } status.set("refreshing") await ctx.jobs.singleflight("entry-info-refresh", () => { return ctx.anime.getEntryDownloadInfo(mediaId) }) lastRefreshAt.set(Date.now()) status.set("ready") } ctx.screen.onNavigate((event) => { if (event.pathname === "/entry" && event.searchParams.id) { currentMediaId.set(parseInt(event.searchParams.id)) ctx.jobs.debounce("entry-info-debounce", () => { return refreshEntryInfo() }, 400) return } currentMediaId.set(null) status.set("idle") ctx.jobs.cancel("entry-info-debounce") }) const cancelPolling = ctx.jobs.poll("entry-info-poll", () => { if (!currentMediaId.get()) { return } return refreshEntryInfo() }, 30\_000, { immediate: true }) ctx.registerEventHandler("refresh-now", () => { return refreshEntryInfo() }) ctx.registerEventHandler("stop-polling", () => { cancelPolling() ctx.toast.info("Stopped background refresh") }) tray.render(() => tray.stack(\[\ tray.text(\`Status: ${status.get()}\`),\ tray.text(\`Running: ${ctx.jobs.isRunning("entry-info-refresh") ? "yes" : "no"}\`),\ tray.text(\`Last refresh: ${lastRefreshAt.get() ?? "never"}\`),\ tray.button("Refresh now", { onClick: "refresh-now" }),\ tray.button("Stop polling", { onClick: "stop-polling" }),\ \])) 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/other/extensions.md). # Extensions The \`ctx.extensions\` API lets a plugin enable or disable other extensions after the user approves the prompt. ## Permissions {% hint style="warning" %} \`extensions\` permission is required. {% endhint %}
{
  //...
  "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\` - The debrid torrent item ID \*\*Example:\*\* \`\`\`typescript const torrentItemId = await ctx.debrid.addTorrent({ magnetLink: "magnet:?xt=urn:btih:...", }) console.log(torrentItemId) \`\`\` ### addAndQueueTorrent \`addAndQueueTorrent(options)\` Adds a torrent to the configured debrid provider and queues it for local download. The destination must be an absolute path and must be authorized by the plugin's \`allow.writePaths\`. \*\*Returns:\*\* \`Promise\` - The debrid torrent item ID \*\*Example:\*\* \`\`\`typescript const torrentItemId = await ctx.debrid.addAndQueueTorrent({ magnetLink: "magnet:?xt=urn:btih:...", destination: "/Users/rahim/Downloads/Anime", mediaId: 21, }) console.log(torrentItemId) \`\`\` ### getTorrentInfo \`getTorrentInfo(options)\` Gets torrent info from the configured debrid provider. \*\*Parameters:\*\* \* \`options\`: \`DebridGetTorrentInfoOptions\` \* \`options.magnetLink\`: String - Optional \* \`options.infoHash\`: String - Optional ### getTorrents \`getTorrents()\` Gets torrents from the configured debrid provider. \*\*Returns:\*\* \`Promise\` ### deleteTorrent \`deleteTorrent(torrentId)\` Deletes a torrent from the configured debrid provider. \*\*Parameters:\*\* \* \`torrentId\`: String - Debrid torrent item ID ### cancelDownload \`cancelDownload(itemId)\` Cancels an active local debrid download. \*\*Parameters:\*\* \* \`itemId\`: String - Queued download item ID ### downloadTorrent \`downloadTorrent(options)\` Downloads a debrid torrent locally. The destination must be an absolute path and must be authorized by the plugin's \`allow.writePaths\`. \*\*Parameters:\*\* \* \`options.torrentItem\`: \`DebridTorrentItem\` \* \`options.destination\`: String - Absolute local path \*\*Example:\*\* \`\`\`typescript const torrents = await ctx.debrid.getTorrents() if (torrents\[0\]) { await ctx.debrid.downloadTorrent({ torrentItem: torrents\[0\], destination: "/Users/rahim/Downloads/Anime", }) } \`\`\` ### getTorrentFilePreviews \`getTorrentFilePreviews(options)\` Returns parsed file previews for a torrent before manual selection. \*\*Parameters:\*\* \* \`options.torrent\`: \`$app.HibikeTorrent\_AnimeTorrent\` \* \`options.episodeNumber\`: Number \* \`options.media\`: \`$app.AL\_BaseAnime\` \*\*Returns:\*\* \`Promise\` \*\*Example:\*\* \`\`\`typescript const previews = await ctx.debrid.getTorrentFilePreviews({ torrent, episodeNumber: 1, media: $anilist.getAnime(21), }) console.log(previews) \`\`\` --- # 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.md). # Anime/Library - \[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) --- # 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/command-palette.md). # Command Palette ## Create a command palette {% hint style="info" %} You can only create one command palette in your plugin. {% endhint %} \`\`\`typescript // ... const cmd = ctx.newCommandPalette({ placeholder: "Search for something", // The command palette will open when the user presses 't' // You can choose to not have a keyboard shortcut keyboardShortcut: "t", }) // Open the command palette when the tray icon is clicked tray.onClick(() => { cmd.open() }) \`\`\` ### Keyboard shortcut You can set a keyboard shortcut for your command palette. Read this documentation to learn how to format it: . ## Items \`\`\`typescript async function fetchTodos() { // Fetch the todos const res = await ctx.fetch("https://jsonplaceholder.typicode.com/todos") const todos = res.json<{ title: string }\[\]>() // Set the items // Calling \`setItems\` will automatically re-render the command palette cmd.setItems(todos.map((todo) => ({ label: todo.title, value: todo.title, // This is used for filtering, should be unique! // Optional filtering for when the user writes something in the input filterType: "includes", // or "contains" onSelect: () => { ctx.toast.info(\`Todo ${todo.title} selected\`) }, }))) } // Default item cmd.setItems(\[\ {\ label: "Fetch Todos",\ value: "fetch todos",\ onSelect: async () => {\ await fetchTodos()\ },\ },\ \]) \`\`\` --- # 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/frequently-asked/feature-requests.md). # Feature requests {% hint style="info" %} Feature requests on GitHub pertain to features that will be integrated in the source code \*\*only\*\*. {% endhint %} ### Why was this feature request closed? As of \`v2.8.0\` , Seanime supports \[Plugins\](/seanime-extensions/plugins/introduction.md), which can be developed entirely in JavaScript. A feature request will be closed as not planned with the label \`status: plugin-suitable\` if: \* The feature can be reasonably added via plugin \* The feature will not benefit a majority of users \* The feature is mostly subjective or cosmetic (e.g. removing elements, changing layout, etc.) This is done to: \* Reduce development time and update cycles \* Avoid bloat by offloading noncritical features \* Improve contribution ### What if I can't develop a plugin? Join the Discord server and make a request in the \`#extension-proposals\` channel, someone might make it for you. --- # 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/debug.md). # Debug \`$debug\` is available in all plugin runtimes. {% hint style="info" %} These methods are only active for development plugins. Outside development mode, \`$debug.enabled\` is \`false\` and every method becomes a no-op. {% endhint %} ## Quick example \`\`\`typescript $app.onScanCompleted((event) => { $debug.info("scan completed", { duration: event.duration, mediaCount: event.stats?.animeCount, }) event.next() }) $ui.register((ctx) => { $debug.mark("ui thread started") }) \`\`\` ## Properties ### enabled \`$debug.enabled\` Returns \`true\` when the plugin runs in development mode. ## Methods ### log \`$debug.log(...values)\` Logs values with the default \`log\` level. ### info \`$debug.info(...values)\` Logs values with the \`info\` level. ### warn \`$debug.warn(...values)\` Logs values with the \`warn\` level. ### error \`$debug.error(...values)\` Logs values with the \`error\` level. ### debug \`$debug.debug(...values)\` Logs values with the \`debug\` level. ### clear \`$debug.clear()\` Clears the current debug output. ### time \`$debug.time(label?)\` Starts a named timer. If you omit the label, Seanime uses \`"default"\`. ### timeEnd \`$debug.timeEnd(label?)\` Stops a named timer and logs the elapsed duration in milliseconds. \`\`\`typescript $debug.time("sync") await doWork() $debug.timeEnd("sync") \`\`\` ## Notes \* Errors are serialized with \`name\`, \`message\`, and \`stack\` when available. \* Plain objects and arrays are serialized as structured values, so you can inspect them in your plugin's debug panel. --- # 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.md). # Other {% content-ref url="/pages/2uenUBFMegQ2BYNPk9jy" %} \[Auth\](/seanime-extensions/plugins/ui/other/auth.md) {% endcontent-ref %} {% content-ref url="/pages/LHelVB5KSH61sjsUHnAf" %} \[App Settings\](/seanime-extensions/plugins/ui/other/app-settings.md) {% endcontent-ref %} {% content-ref url="/pages/PHJPMqWJ2QNuxbiyGie0" %} \[Extensions\](/seanime-extensions/plugins/ui/other/extensions.md) {% endcontent-ref %} {% content-ref url="/pages/ZsX7ePDUumKpNFYsULuk" %} \[Manga\](/seanime-extensions/plugins/ui/other/manga.md) {% endcontent-ref %} {% content-ref url="/pages/xREUZDljofcjRmzIbwge" %} \[Discord\](/seanime-extensions/plugins/ui/other/discord.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/example.md). # Example Let's create a plugin that allows users to change banner images of anime that are in the AniList collection. We'll use both the UI and hooks APIs. {% code title="my-plugin.ts" %} \`\`\`typescript /// /// function init() { $ui.register((ctx) => { // Create the tray icon const tray = ctx.newTray({ tooltipText: "Anime banner image", iconUrl: "https://seanime.rahim.app/logo\_2.png", withContent: true, }) // Keep track of the current media ID const currentMediaId = ctx.state(0) // Create a field ref for the URL input const inputRef = ctx.fieldRef() // When the plugin loads, fetch the current screen and set the badge to 0 ctx.screen.loadCurrent() // Triggers onNavigate tray.updateBadge({ number: 0 }) // Also fetch current screen when tray is open tray.onOpen(() => { ctx.screen.loadCurrent() }) // Updates the field's value and badge based on the current anime page function updateState() { // Reset the badge and input if the user currently isn't on an anime page if (!currentMediaId.get()) { inputRef.setValue("") tray.updateBadge({ number: 0 }) } // Get the stored banner image URL for this anime const url = $storage.get("bannerImages." + currentMediaId.get()) if (url) { // If there's a URL, set the value of the input inputRef.setValue(url) // Add a badge tray.updateBadge({ number: 1, intent: "info" }) } else { inputRef.setValue("") tray.updateBadge({ number: 0 }) } } // Run the function when the plugin loads updateState() // Update currentMediaId when the user navigates ctx.screen.onNavigate((e) => { // If the user navigates to an anime page if (e.pathname === "/entry" && !!e.searchParams.id) { // Get the ID from the URL const id = parseInt(e.searchParams.id) currentMediaId.set(id) } else { currentMediaId.set(0) } }) // This effect will update the state each time currentMediaId changes ctx.effect(() => { updateState() }, \[currentMediaId\]) // Create a handler to store the custom banner image URL ctx.registerEventHandler("save", () => { if (!!inputRef.current) { $storage.set(\`bannerImages.${currentMediaId.get()}\`, inputRef.current) } else { $storage.remove(\`bannerImages.${currentMediaId.get()}\`) } ctx.toast.success("Banner image saved") updateState() // Update the state // Updates the data on the client // This is better than calling ctx.screen.reload() $anilist.refreshAnimeCollection() }); // Tray content tray.render(() => { return tray.stack(\[\ currentMediaId.get() === 0 \ ? tray.text("Open an anime") \ : tray.stack(\[\ tray.text(\`Current media ID: ${currentMediaId.get()}\`),\ tray.input({ fieldRef: inputRef }),\ tray.button({ label: "Save", onClick: "save" }),\ \])\ \]) }) }) // Register hook handlers to listen and modify the anime collection. // Triggers the app fetches the user's AniList anime collection $app.onGetAnimeCollection((e) => { const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } for (let i = 0; i < e.animeCollection!.mediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!.length; j++) { const mediaId = e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.id const bannerImage = bannerImages\[mediaId.toString()\] if (!!bannerImage) { e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.bannerImage = bannerImage } } } e.next() }) // Same as onGetAnimeCollection but also includes custom lists. $app.onGetRawAnimeCollection((e) => { const bannerImages = $storage.get>('bannerImages'); if (!bannerImages) { e.next() return } if (!e.animeCollection?.mediaListCollection?.lists?.length) { e.next() return } for (let i = 0; i < e.animeCollection!.mediaListCollection!.lists!.length; i++) { for (let j = 0; j < e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!.length; j++) { const mediaId = e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.id const bannerImage = bannerImages\[mediaId.toString()\] if (!!bannerImage) { e.animeCollection!.mediaListCollection!.lists!\[i\]!.entries!\[j\]!.media!.bannerImage = bannerImage } } } e.next() }) } \`\`\` {% endcode %} ---