diff --git a/.aiignore b/.aiignore new file mode 100644 index 0000000..c880c25 --- /dev/null +++ b/.aiignore @@ -0,0 +1,15 @@ +node_modules +coverage +coverage.lcov + +package-lock.json +pnpm-lock.yaml +bun.lock + +.env + +dist +storage + +# AI Session Files (Private Context) +.sessions diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 60c2913..78c1a12 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -12,27 +12,24 @@ jobs: strategy: matrix: - node_version: [20.x, 22.x, 24.x] + node_version: [22.x, 24.x, 25.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: setup Node.js v${{ matrix.node_version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node_version }} - name: run npm scripts - env: - PROXY_SERVER: ${{ secrets.PROXY_SERVER }} run: | npm install npm run lint - npm run build --if-present npm run test - name: cache node modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a77d776..5547051 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.gitignore b/.gitignore index b83527e..f43a3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ yarn.lock pnpm-lock.yaml package-lock.json deno.lock +bun.lock diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1f1566f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,114 @@ +# AI Agent Instructions + +Coding guidelines for AI agents working in this project. + +## Philosophy + +- Minimalism. Simple is better. KISS (Keep It Simple, Stupid). +- Clean code, easy to read, easy to delete. +- Functional Programming — pure functions, immutability, no side effects. +- MVP mindset — deliver the smallest thing that works, then iterate. + +## Security Rules (CRITICAL — no exceptions) + +- NEVER output or request .env and example.env file contents +- NEVER hardcode API credentials, secret tokens, private keys or passwords in source code +- NEVER send sensitive data to external AI services +- Follow `.aiignore` and `.gitignore` for excluded files — do not read or reference them +- When asking for help, sanitize data (replace real IDs, emails, tokens with placeholders) +- Do not log sensitive information + +## Coding Standards (Strict) +- Language: JavaScript (ESM syntax). No TypeScript. +- Style: No semicolons, single quotes, 2-space indentation. +- Respect `eslint.config.js` — do not suggest rule changes +- Patterns: + - Functional Programming only. No Classes or OOP. + - Arrow functions are preferred. + - Maximum 3 parameters per function. Use objects for more. +- Naming: camelCase for variables/functions, SNAKE_CASE for constants. +- Documentation: + - Add JSDocs before all functions and exported variables. + - Language: Use American English for all comments and JSDocs. + - Constraint: NEVER use Vietnamese or other languages in the source code. + +### Error Handling + +- Handle errors explicitly — never swallow silently +- Use try/catch with proper logging +- Return null or throw meaningful errors + +```javascript +export const send = async (params) => { + try { + const response = await ai.ask(params) + logger.info(`send() -> success: ${response.id}`) + return response + } catch (err) { + logger.error(`send() -> failed: ${err.message}`) + console.error(err) + return null + } +} +``` + +## Testing Standards + +- Write tests for critical business logic, all error cases +- Use simple test runners (node:test, bun:test, vitest) +- No complex mocking frameworks unless necessary +- Tests live alongside source: `[module].test.js` next to `[module].js` + +## Dependency Rules + +- Prefer built-in APIs over external packages +- Before adding dependency, explain: + - Why it is needed + - Alternatives considered + - Bundle size impact +- Never add dependency for trivial utilities +- Avoid packages with large dependency trees + +## Architecture Rules + +- Do NOT change existing project architecture without explicit approval +- Do NOT move or rename core modules unless requested +- Respect module boundaries +- Avoid cross-module coupling +- New modules must follow existing folder structure + +## When Making Changes + +1. Read existing patterns first +2. Follow current coding style strictly +3. Keep dependencies minimal +4. Handle errors explicitly +5. Add JSDoc comments for new functions +6. Run `npm run lint` before committing +7. Do NOT refactor unrelated code +8. Do NOT modify working code outside task scope +9. Prefer minimal diff changes +10. Preserve existing behavior unless explicitly requested + +## When in Doubt + +- Ask for clarification before generating code +- State your assumption explicitly if proceeding without confirmation +- Prefer doing less and asking over doing more and guessing + +## Git Workflow + +- Work only inside the current branch +- Do NOT create or delete branches +- Do NOT rewrite git history +- Do NOT modify commit messages +- Changes must correspond to the current issue + +## Agent References + +Reference these URLs when working on related topics: + +- Bun: https://bun.sh/llms-full.txt +- oEmbed: https://oembed.com + +--- diff --git a/README.md b/README.md index d92d3b8..79718b9 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,21 @@ Extract oEmbed content from given URL. ## Demo -- [Give it a try!](https://extractus-demo.vercel.app/oembed) +- [Give it a try!](https://extractus.pwshub.com/oembed) ## Install & Usage -### Node.js +### Bun & Node.js ```bash +# bun +bun add @extractus/oembed-extractor + +# npm npm i @extractus/oembed-extractor # pnpm -pnpm i @extractus/oembed-extractor +pnpm install @extractus/oembed-extractor # yarn yarn add @extractus/oembed-extractor @@ -38,12 +42,6 @@ console.log(result) import { extract } from 'npm:@extractus/oembed-extractor' ``` -### Browser - -```ts -import { extract } from "https://esm.sh/@extractus/oembed-extractor@latest" -``` - ## APIs ### `.extract()` diff --git a/eval.js b/eval.js index 907e210..ce6af88 100644 --- a/eval.js +++ b/eval.js @@ -3,6 +3,11 @@ import { extract } from './src/main.js' +/** + * Extract and log oEmbed data from a URL. + * + * @param {string} url - URL to extract oEmbed from + */ const run = async (url) => { try { console.time('extract-oembed') @@ -14,6 +19,12 @@ const run = async (url) => { } } +/** + * Parse CLI arguments and run extraction. + * + * @param {Array} argv - Process argument array + * @returns {Promise|string} Extraction promise or info message + */ const init = (argv) => { if (argv.length === 3) { const url = argv[2] diff --git a/index.d.ts b/index.d.ts index b12c20a..26a4879 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,162 +5,189 @@ // Marc McIntosh // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +/** + * An oEmbed provider endpoint definition as used in the provider registry. + */ export interface Endpoint { - schemes?: string[]; - url: string; - formats?: string[]; // "json" "xml" - discovery?: boolean; + /** URL scheme patterns that match resources for this provider */ + schemes?: string[] + /** The oEmbed API endpoint URL */ + url: string + /** Supported response formats, e.g. "json", "xml" */ + formats?: string[] + /** Whether auto-discovery is supported */ + discovery?: boolean } +/** + * A provider entry from the oEmbed registry. + */ export interface Provider { - "provider_name": string; - "provider_url": string; - "endpoints": Endpoint[]; + /** Human-readable provider name */ + provider_name: string + /** Provider's homepage URL */ + provider_url: string + /** List of API endpoints for this provider */ + endpoints: Endpoint[] } +/** + * Result returned by findProvider(). + */ export interface FindProviderResult { - "fetchEndpoint": string; - "provider_name": string; - "provider_url": string; + /** Matched oEmbed API endpoint URL */ + endpoint: string + /** Regex patterns used to match this URL */ + schemes: RegExp[] + /** The original queried URL */ + url: string } /** - * Basic data structure of every oembed response see https://oembed.com/ + * Basic fields present in every oEmbed response. + * See https://oembed.com/ */ export interface OembedData { - type: 'rich' | 'video' | 'photo' | 'link'; - version: string; - /** A text title, describing the resource. */ - title?: string; - /** The name of the author/owner of the resource. */ - author_name?: string; - /** A URL for the author/owner of the resource. */ - author_url?: string; - /** The name of the resource provider. */ - provider_name?: string; - /** The url of the resource provider. */ - provider_url?: string; - /** The suggested cache lifetime for this resource, in seconds. Consumers may choose to use this value or not. */ - cache_age?: string | number; - /** - * A URL to a thumbnail image representing the resource. - * The thumbnail must respect any maxwidth and maxheight parameters. - * If this parameter is present, thumbnail_width and thumbnail_height must also be present. - */ - thumbnail_url?: string; - /** - * The width of the optional thumbnail. - * If this parameter is present, thumbnail_url and thumbnail_height must also be present. - */ - thumbnail_width?: number; - /** - * The height of the optional thumbnail. - * If this parameter is present, thumbnail_url and thumbnail_width must also be present. - */ - thumbnail_height?: number; + /** Resource type: rich, video, photo, or link */ + type: 'rich' | 'video' | 'photo' | 'link' + /** oEmbed version number */ + version: string + /** A text title describing the resource */ + title?: string + /** Name of the author/owner of the resource */ + author_name?: string + /** URL for the author/owner of the resource */ + author_url?: string + /** Name of the resource provider */ + provider_name?: string + /** URL of the resource provider */ + provider_url?: string + /** Suggested cache lifetime in seconds */ + cache_age?: string | number + /** URL to a thumbnail image representing the resource */ + thumbnail_url?: string + /** Width of the optional thumbnail in pixels */ + thumbnail_width?: number + /** Height of the optional thumbnail in pixels */ + thumbnail_height?: number + /** How the oEmbed data was retrieved: "provider-api" or "auto-discovery" */ + method?: string } +/** + * oEmbed response for type "link". + */ export interface LinkTypeData extends OembedData { - readonly type: 'link'; + readonly type: 'link' } +/** + * oEmbed response for type "photo". + */ export interface PhotoTypeData extends OembedData { - readonly type: 'photo'; - /** - * The source URL of the image. Consumers should be able to insert this URL into an element. - * Only HTTP and HTTPS URLs are valid. - */ - url: string; - /** The width in pixels of the image specified in the url parameter. */ - width: number; - /** The height in pixels of the image specified in the url parameter. */ - height: number; + readonly type: 'photo' + /** Source URL of the image */ + url: string + /** Width of the image in pixels */ + width: number + /** Height of the image in pixels */ + height: number } +/** + * oEmbed response for type "video". + */ export interface VideoTypeData extends OembedData { - readonly type: 'video'; - /** - * The HTML required to embed a video player. - * The HTML should have no padding or margins. - * Consumers may wish to load the HTML in an off-domain iframe to avoid XSS vulnerabilities. - */ - html: string; - /** The width in pixels required to display the HTML. */ - width: number; - /** The height in pixels required to display the HTML. */ - height: number; + readonly type: 'video' + /** HTML required to embed a video player */ + html: string + /** Width required to display the HTML in pixels */ + width: number + /** Height required to display the HTML in pixels */ + height: number } +/** + * oEmbed response for type "rich". + */ export interface RichTypeData extends OembedData { - readonly type: 'rich'; - /** - * The HTML required to display the resource. - * The HTML should have no padding or margins. - * Consumers may wish to load the HTML in an off-domain iframe to avoid XSS vulnerabilities. - * The markup should be valid XHTML 1.0 Basic. - */ - html: string; - /** The width in pixels required to display the HTML. */ - width: number; - /** The height in pixels required to display the HTML. */ - height: number; + readonly type: 'rich' + /** HTML required to display the resource */ + html: string + /** Width required to display the HTML in pixels */ + width: number + /** Height required to display the HTML in pixels */ + height: number } +/** + * Optional parameters passed to extract(). + */ export interface Params { - /** - * max width of embed size - * Default: null - */ + /** Max width of embed size */ maxwidth?: number - /** - * max height of embed size - * Default: null - */ + /** Max height of embed size */ maxheight?: number - /** - * theme for the embed, such as "dark" or "light" - * Default: null - */ + /** Theme for the embed, e.g. "dark" or "light" */ theme?: string - /** - * language for the embed, e.g. "en", "fr", "vi", etc - * Default: null - */ + /** Language for the embed, e.g. "en", "fr", "vi" */ lang?: string } +/** + * Configuration for proxy-based requests. + */ export interface ProxyConfig { - target?: string; - headers?: Record; + /** Base URL of the proxy server */ + target?: string + /** Headers to send to the proxy (e.g. Proxy-Authorization) */ + headers?: Record } +/** + * Advanced fetch options for extract(). + */ export interface FetchOptions { - /** - * list of request headers - * default: null - */ - headers?: Record; - /** - * the values to configure proxy - * default: null - */ - proxy?: ProxyConfig; - /** - * http proxy agent - * default: null - */ - agent?: object; - /** - * signal to terminate request - * default: null - */ - signal?: object; + /** Custom request headers */ + headers?: Record + /** Proxy configuration */ + proxy?: ProxyConfig + /** HTTP proxy agent (e.g. HttpsProxyAgent) */ + agent?: object + /** AbortSignal to cancel the request */ + signal?: AbortSignal } -export function extract(url: string, params?: Params, fetchOptions?: FetchOptions): Promise; +/** + * Extract oEmbed data from a given URL. + * + * @param url - URL of a valid oEmbed resource + * @param params - Optional parameters (maxwidth, maxheight, etc.) + * @param fetchOptions - Advanced fetch options (headers, proxy, agent, signal) + * @returns Promise resolving to oEmbed data + */ +export function extract(url: string, params?: Params, fetchOptions?: FetchOptions): Promise +/** + * Check if a URL is supported by any registered provider. + * + * @param url - URL to check + * @returns True if a matching provider exists + */ export function hasProvider(url: string): boolean -export function findProvider(url: string): FindProviderResult +/** + * Find the provider that matches a given URL. + * + * @param url - URL to look up + * @returns Provider info or undefined if not found + */ +export function findProvider(url: string): FindProviderResult | null -export function setProviderList(providers: Provider[]): void +/** + * Replace the provider list with a custom set of providers. + * + * @param providers - List of providers in oEmbed registry format + * @returns Number of providers in the new list + */ +export function setProviderList(providers: Provider[]): number diff --git a/package.json b/package.json index fe9a9ca..418c74e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "4.0.9", + "version": "4.1.0", "name": "@extractus/oembed-extractor", "description": "Get oEmbed data from given URL.", "homepage": "https://github.com/extractus/oembed-extractor", @@ -10,16 +10,9 @@ "author": "@extractus", "main": "./src/main.js", "type": "module", - "imports": { - "cross-fetch": "./src/deno/cross-fetch.js" - }, - "browser": { - "cross-fetch": "./src/deno/cross-fetch.js", - "linkedom": "./src/browser/linkedom.js" - }, "types": "./index.d.ts", "engines": { - "node": ">= 18" + "node": ">= 20" }, "scripts": { "lint": "eslint .", @@ -31,15 +24,14 @@ "reset": "node reset" }, "dependencies": { - "cross-fetch": "^4.1.0", "linkedom": "^0.18.12" }, "devDependencies": { - "@eslint/js": "^9.34.0", - "eslint": "^9.34.0", - "globals": "^16.3.0", - "https-proxy-agent": "^7.0.6", - "nock": "^14.0.10" + "@eslint/js": "^10.0.1", + "eslint": "^10.3.0", + "globals": "^17.6.0", + "https-proxy-agent": "^9.0.0", + "nock": "^14.0.14" }, "keywords": [ "oembed", diff --git a/src/browser/linkedom.js b/src/browser/linkedom.js deleted file mode 100644 index 6d5be04..0000000 --- a/src/browser/linkedom.js +++ /dev/null @@ -1 +0,0 @@ -export const DOMParser = window.DOMParser diff --git a/src/deno/cross-fetch.js b/src/deno/cross-fetch.js deleted file mode 100644 index d084f98..0000000 --- a/src/deno/cross-fetch.js +++ /dev/null @@ -1,2 +0,0 @@ -// cross-fetch.js -export default fetch diff --git a/src/main.js b/src/main.js index a9dbe78..0a0ce9b 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,15 @@ import fetchEmbed from './utils/fetchEmbed.js' import { getEndpoint } from './utils/provider.js' +/** + * Extract oEmbed data from a given URL. + * + * @param {string} url - URL of a valid oEmbed resource + * @param {object} [params] - Optional parameters (maxwidth, maxheight, theme, lang, etc.) + * @param {object} [options] - Fetch options (headers, proxy, agent, signal) + * @returns {Promise} oEmbed data object + * @throws {Error} If URL is invalid + */ export const extract = async (url, params = {}, options = {}) => { if (!isValidURL(url)) { throw new Error('Invalid input URL') diff --git a/src/utils/autoDiscovery.js b/src/utils/autoDiscovery.js index a6b4cfb..3790917 100644 --- a/src/utils/autoDiscovery.js +++ b/src/utils/autoDiscovery.js @@ -4,6 +4,16 @@ import { DOMParser } from 'linkedom' import { getHtml, getJson } from './retrieve.js' +/** + * Extract oEmbed data via auto-discovery by parsing the HTML page for a + * oEmbed link tag. + * + * @param {string} url - Resource URL to discover oEmbed for + * @param {object} [params={}] - Additional oEmbed query parameters + * @param {object} [options={}] - Fetch options (headers, proxy, agent, signal) + * @returns {Promise} oEmbed response data + * @throws {Error} If no oEmbed link tag is found in the HTML + */ export default async (url, params = {}, options = {}) => { const html = await getHtml(url, options) const doc = new DOMParser().parseFromString(html, 'text/html') @@ -12,7 +22,7 @@ export default async (url, params = {}, options = {}) => { throw new Error('No oEmbed link found') } const href = elm.getAttribute('href') - const q = new URL(href) + const q = new URL(href, url) const { origin, pathname, searchParams } = q Object.keys(params).forEach((key) => { if (!searchParams.has(key)) { diff --git a/src/utils/fetchEmbed.js b/src/utils/fetchEmbed.js index d915db7..cb6e56a 100644 --- a/src/utils/fetchEmbed.js +++ b/src/utils/fetchEmbed.js @@ -3,17 +3,37 @@ import { getJson } from './retrieve.js' import { getDomain } from './linker.js' +/** + * Check if an endpoint belongs to Facebook Graph API. + * + * @param {string} url - Endpoint URL + * @returns {boolean} True if domain is graph.facebook.com + */ const isFacebookGraphDependent = (url) => { return getDomain(url) === 'graph.facebook.com' } +/** + * Build the Facebook access token from environment variables. + * + * @returns {string} Token in format "appId|clientToken" + */ const getFacebookGraphToken = () => { const env = process.env || {} - const appId = env.FACEBOOK_APP_ID - const clientToken = env.FACEBOOK_CLIENT_TOKEN + const appId = env.FACEBOOK_APP_ID || '' + const clientToken = env.FACEBOOK_CLIENT_TOKEN || '' return `${appId}|${clientToken}` } +/** + * Fetch oEmbed data from a known provider endpoint. + * + * @param {string} url - Original resource URL + * @param {object} [params={}] - oEmbed parameters (maxwidth, maxheight, etc.) + * @param {string} [endpoint=''] - Provider oEmbed API endpoint + * @param {object} [options={}] - Fetch options (headers, proxy, agent, signal) + * @returns {Promise} oEmbed response data + */ export default async (url, params = {}, endpoint = '', options = {}) => { // eslint-disable-line const query = { url, diff --git a/src/utils/linker.js b/src/utils/linker.js index 496cce0..4387e72 100644 --- a/src/utils/linker.js +++ b/src/utils/linker.js @@ -1,5 +1,11 @@ // utils -> linker +/** + * Check if a string is a valid HTTP or HTTPS URL. + * + * @param {string} [url=''] - URL to validate + * @returns {boolean} True if URL is valid and uses http/https protocol + */ export const isValid = (url = '') => { try { const ourl = new URL(url) @@ -9,6 +15,12 @@ export const isValid = (url = '') => { } } +/** + * Extract the domain from a URL, stripping the www. prefix. + * + * @param {string} url - Full URL + * @returns {string} Domain without www. + */ export const getDomain = (url) => { const host = (new URL(url)).host return host.replace('www.', '') diff --git a/src/utils/provider.js b/src/utils/provider.js index c211649..9fca019 100644 --- a/src/utils/provider.js +++ b/src/utils/provider.js @@ -4,22 +4,52 @@ import { isValid as isValidURL, getDomain } from './linker.js' import { providers as defaultProviderList } from './providers.latest.js' +/** + * Convert a scheme string into a RegExp for URL matching. + * + * @param {string} [scheme=''] - Scheme pattern with wildcards + * @returns {RegExp} Compiled regular expression + */ const toRegExp = (scheme = '') => { return new RegExp(scheme.replace(/\\./g, '.').replace(/\*/g, '(.*)').replace(/\?/g, '\\?').replace(/,$/g, ''), 'i') } +/** + * Remove duplicate entries from an array. + * + * @param {Array} [arr=[]] - Input array + * @returns {Array} Array with unique values + */ const uniquify = (arr = []) => { return [...(new Set(arr))] } +/** + * Escape dots in a scheme string for regex use. + * + * @param {string} [scheme=''] - Scheme string + * @returns {string} Scheme with escaped dots + */ const undotted = (scheme = '') => { return scheme.replace(/\./g, '\\.') } +/** + * Strip protocol prefix (https:// or http://) from a URL. + * + * @param {string} url - URL with protocol + * @returns {string} URL without protocol (protocol-relative) + */ const removeProtocol = (url) => { return url.replace('https://', '//').replace('http://', '//') } +/** + * Simplify raw provider data into a compact format for internal use. + * + * @param {Array} [providers=[]] - Raw provider list from oEmbed registry + * @returns {Array} Simplified provider entries with { s: patterns, e: endpoint } + */ export const simplify = (providers = []) => { return providers.map((item) => { const { @@ -39,6 +69,12 @@ export const simplify = (providers = []) => { }, []) } +/** + * Build internal provider entries from simplified format with compiled regex patterns. + * + * @param {Array} [providers=[]] - Simplified provider entries + * @returns {Array} Provider entries with endpoint URL and RegExp schemes + */ const providersFromList = (providers = []) => { return providers.map((provider) => { const { e: endpoint, s: schemes } = provider @@ -49,19 +85,41 @@ const providersFromList = (providers = []) => { }) } +/** + * Internal mutable store holding the current provider list. + */ const store = { providers: providersFromList(defaultProviderList), } +/** + * Get a copy of the current provider list. + * + * @returns {Array} List of provider entries with endpoint and schemes + */ export const get = () => { return [...store.providers] } +/** + * Replace the provider list with a custom set of providers. + * + * @param {Array} [providers=[]] - Raw provider list in oEmbed registry format + * @returns {number} Length of the new provider list + */ export const set = (providers = []) => { store.providers = providersFromList(simplify(providers)) return store.providers.length } +/** + * Match a URL against a provider's schemes; fall back to domain comparison when no schemes exist. + * + * @param {string} [url=''] - URL to match + * @param {string} [endpoint=''] - Provider endpoint URL + * @param {Array} [schemes=[]] - RegExp schemes to test against + * @returns {boolean} True if URL matches the provider + */ const compare = (url = '', endpoint = '', schemes = []) => { if (!schemes.length) { const domain = getDomain(url) @@ -73,6 +131,12 @@ const compare = (url = '', endpoint = '', schemes = []) => { }) } +/** + * Find a provider that matches the given URL. + * + * @param {string} [url=''] - URL to look up + * @returns {object|null} Provider info with schemes, endpoint, and url, or null if not found + */ export const find = (url = '') => { if (!isValidURL(url)) { return null @@ -95,10 +159,22 @@ export const find = (url = '') => { return null } +/** + * Check if any registered provider supports the given URL. + * + * @param {string} [url=''] - URL to check + * @returns {boolean} True if a matching provider exists + */ export const has = (url = '') => { return find(url) !== null } +/** + * Get the oEmbed API endpoint for a given URL. + * + * @param {string} url - URL to resolve + * @returns {string|null} Endpoint URL or null if no provider matches + */ export const getEndpoint = (url) => { const p = find(url) return p ? p.endpoint : null diff --git a/src/utils/providers.latest.js b/src/utils/providers.latest.js index c9474bb..513d042 100644 --- a/src/utils/providers.latest.js +++ b/src/utils/providers.latest.js @@ -1,4 +1,4 @@ -// provider data, synchronized at 2025-09-04T09:00:29.575Z +// provider data, synchronized at 2026-05-03T06:03:25.358Z /* eslint-disable */ @@ -97,6 +97,18 @@ export const providers = [ ], "e": "//api.anniemusic.app/api/v1/oembed" }, + { + "s": [ + "//appforcestudio\\.com/playground/*" + ], + "e": "//us-central1-themerax-cc903.cloudfunctions.net/playgroundApi/oembed" + }, + { + "s": [ + "//podcasts\\.apple\\.com/*" + ], + "e": "//podcasts.apple.com/api/oembed" + }, { "s": [ "//storymaps\\.arcgis\\.com/stories/*" @@ -162,6 +174,12 @@ export const providers = [ ], "e": "//podcasts.audiomeans.fr/services/oembed" }, + { + "s": [ + "//audius\\.co/*" + ], + "e": "//audius.co/oembed" + }, { "s": [ "//backtracks\\.fm/*/*/e/*", @@ -244,8 +262,18 @@ export const providers = [ "e": "//www.bornetube.dk/media/lasync/oembed/" }, { - "s": [], - "e": "//boxofficebuz.com/oembed" + "s": [ + "//boxofficebuz\\.com/embed/video/*" + ], + "e": "//api.boxofficebuz.com/v2/oembed" + }, + { + "s": [ + "//players\\.brightcove\\.net/*/*/index\\.html?videoId=*", + "//players\\.brightcove\\.net/*/*/index\\.html?playlistId=*", + "//bcove\\.video/*" + ], + "e": "//oembed.brightcove.com/" }, { "s": [ @@ -262,6 +290,7 @@ export const providers = [ }, { "s": [ + "//player\\.mediadelivery\\.net/*", "//iframe\\.mediadelivery\\.net/*", "//video\\.bunnycdn\\.com/*" ], @@ -356,6 +385,16 @@ export const providers = [ ], "e": "//chroco.ooo/embed" }, + { + "s": [ + "//www\\.circlezeroeight\\.com/features/*", + "//www\\.circlezeroeight\\.com/news/*", + "//www\\.circlezeroeight\\.com/sport/*", + "//www\\.circlezeroeight\\.com/style/*", + "//www\\.circlezeroeight\\.com/culture/*" + ], + "e": "//www.circlezeroeight.com/api/oembed" + }, { "s": [ "//www\\.circuitlab\\.com/circuit/*" @@ -368,6 +407,12 @@ export const providers = [ ], "e": "//www.clipland.com/api/oembed" }, + { + "s": [ + "//clueso\\.site/*" + ], + "e": "//clueso.site/api/oembed" + }, { "s": [ "//clyp\\.it/*", @@ -482,6 +527,13 @@ export const providers = [ ], "e": "//api.datawrapper.de/v3/oembed/" }, + { + "s": [ + "//app\\.demofly\\.ai/s/*", + "//demofly\\.ai/s/*" + ], + "e": "//app.demofly.ai/api/oembed" + }, { "s": [ "//*\\.deseret\\.com/*" @@ -521,6 +573,12 @@ export const providers = [ ], "e": "//www.docswell.com/service/oembed" }, + { + "s": [ + "//www\\.documentcloud\\.org/documents/*" + ], + "e": "//api.www.documentcloud.org/api/oembed" + }, { "s": [ "//dotsub\\.com/view/*" @@ -559,6 +617,18 @@ export const providers = [ ], "e": "//egliseinfo.catholique.fr/api/oembed" }, + { + "s": [ + "//elevenlabs\\.io/*" + ], + "e": "//elevenlabs.io/next/oembed" + }, + { + "s": [ + "//www\\.embases\\.com/e/*" + ], + "e": "//www.embases.com/api/oembed" + }, { "s": [ "//embedery\\.com/widget/*" @@ -652,7 +722,13 @@ export const providers = [ }, { "s": [ - "//www\\.figma\\.com/file/*" + "//www\\.figma\\.com/file/*", + "//www\\.figma\\.com/design/*", + "//www\\.figma\\.com/board/*", + "//www\\.figma\\.com/slides/*", + "//www\\.figma\\.com/buzz/*", + "//www\\.figma\\.com/site/*", + "//www\\.figma\\.com/make/*" ], "e": "//www.figma.com/api/oembed" }, @@ -770,6 +846,13 @@ export const providers = [ ], "e": "//geo.hlipp.de/restapi.php/api/oembed" }, + { + "s": [ + "//geometryviewer\\.com/v/*", + "//www\\.geometryviewer\\.com/v/*" + ], + "e": "//geometryviewer.com/oembed" + }, { "s": [ "//gty\\.im/*" @@ -808,6 +891,12 @@ export const providers = [ ], "e": "//app.gong.io/oembed" }, + { + "s": [ + "//www\\.good-for-job\\.jp/slides/*" + ], + "e": "//www.good-for-job.jp/api/oembed" + }, { "s": [ "//grain\\.co/highlight/*", @@ -880,6 +969,19 @@ export const providers = [ ], "e": "//www.hippovideo.io/services/oembed" }, + { + "s": [ + "//cdn\\.hivo\\.com\\.au/*", + "//cdn\\.digital-assets\\.uq\\.edu\\.au/*", + "//cdn\\.southerndesigngroup\\.com/*", + "//freedom\\.hivocdn\\.com/*", + "//cdn\\.cockburn\\.wa\\.gov\\.au/*", + "//cdn\\.somnomed\\.com/*", + "//cdn\\.snowtunnel\\.com/*", + "//cdn\\.brenclosures\\.com\\.au/*" + ], + "e": "//app.hivo.com.au/api/drupal/oembed" + }, { "s": [ "//homey\\.app/f/*", @@ -1059,6 +1161,13 @@ export const providers = [ ], "e": "//api.jovian.com/oembed.json" }, + { + "s": [ + "//play\\.juntos\\.live/solo/*", + "//play\\.juntos\\.live/host/quiz/*" + ], + "e": "//play.juntos.live/api/oembed" + }, { "s": [ "//tv\\.kakao\\.com/channel/*/cliplink/*", @@ -1141,6 +1250,22 @@ export const providers = [ ], "e": "//kurozora.app/oembed" }, + { + "s": [ + "//landofassets\\.com/*", + "//landofassets\\.com/*/assets", + "//landofassets\\.com/*/assets/*", + "//landofassets\\.com/*/assets/*/embed" + ], + "e": "//api.landofassets.com/oembed" + }, + { + "s": [ + "//laude\\.org/impact/*", + "//laude\\.org/im/*" + ], + "e": "//laude.org/api/oembed" + }, { "s": [ "//learningapps\\.org/*" @@ -1174,14 +1299,9 @@ export const providers = [ }, { "s": [ - "//livestream\\.com/accounts/*/events/*", - "//livestream\\.com/accounts/*/events/*/videos/*", - "//livestream\\.com/*/events/*", - "//livestream\\.com/*/events/*/videos/*", - "//livestream\\.com/*/*", - "//livestream\\.com/*/*/videos/*" + "//livid\\.com/watch/*" ], - "e": "//livestream.com/oembed" + "e": "//livid.com/oembed" }, { "s": [ @@ -1278,6 +1398,12 @@ export const providers = [ ], "e": "//miro.com/api/v1/oembed" }, + { + "s": [ + "//miro\\.com/video-player/*" + ], + "e": "//miro.com/video-player/oembed" + }, { "s": [ "//www\\.mixcloud\\.com/*/*/" @@ -1513,6 +1639,13 @@ export const providers = [ ], "e": "//api-v2.pandavideo.com.br/oembed" }, + { + "s": [ + "//app\\.parta\\.io/core/embed/*", + "//*\\.parta\\.io/core/embed/*" + ], + "e": "//app.parta.io/core/oembed" + }, { "s": [ "//pastery\\.net/*", @@ -1574,6 +1707,12 @@ export const providers = [ ], "e": "//store.pixdor.com/oembed" }, + { + "s": [ + "//plinth\\.it/model-viewer/*/*" + ], + "e": "//plinth.it/model-viewer/oembed" + }, { "s": [ "//app\\.plusdocs\\.com/*/snapshots/*", @@ -1615,6 +1754,12 @@ export const providers = [ ], "e": "//prezi.com/v/oembed" }, + { + "s": [ + "//programmingly\\.dev/snippets/*" + ], + "e": "//programmingly.dev/api/oembed" + }, { "s": [ "//qtpi\\.gg/fashion/*" @@ -1728,7 +1873,10 @@ export const providers = [ "e": "//roosterteeth.com/oembed" }, { - "s": [], + "s": [ + "//rumble\\.com/*", + "//rumble\\.com/shorts/*" + ], "e": "//rumble.com/api/Media/oembed.json" }, { @@ -1755,6 +1903,12 @@ export const providers = [ ], "e": "//www.satcat.com/api/sats/oembed" }, + { + "s": [ + "//api\\.satoplayer\\.com/players/embed/*" + ], + "e": "//api.satoplayer.com/players/oembed" + }, { "s": [ "//sbedit\\.net/*" @@ -1857,6 +2011,13 @@ export const providers = [ ], "e": "//onsizzle.com/oembed" }, + { + "s": [ + "//www\\.sketch\\.com/s/*", + "//www\\.sketch\\.com/preview/*" + ], + "e": "//graphql.sketch.cloud/embed/oembed" + }, { "s": [ "//sketchfab\\.com/*models/*", @@ -1919,13 +2080,13 @@ export const providers = [ }, { "s": [ - "//vod\\.sooplive\\.co\\.kr/player/", + "//vod\\.sooplive\\.com/player/", "//v\\.afree\\.ca/ST/", - "//vod\\.sooplive\\.co\\.kr/ST/", - "//vod\\.sooplive\\.co\\.kr/PLAYER/STATION/", - "//play\\.sooplive\\.co\\.kr/" + "//vod\\.sooplive\\.com/ST/", + "//vod\\.sooplive\\.com/PLAYER/STATION/", + "//play\\.sooplive\\.com/" ], - "e": "//openapi.sooplive.co.kr/oembed/embedinfo" + "e": "//openapi.sooplive.com/oembed/embedinfo" }, { "s": [ @@ -2012,6 +2173,7 @@ export const providers = [ { "s": [ "//www\\.sudomemo\\.net/watch/*", + "//archive\\.sudomemo\\.net/watch/*", "//flipnot\\.es/*" ], "e": "//www.sudomemo.net/oembed" @@ -2060,6 +2222,13 @@ export const providers = [ ], "e": "//www.ted.com/services/v1/oembed.json" }, + { + "s": [ + "//www\\.tella\\.tv/video/*", + "//tella\\.video/*" + ], + "e": "//www.tella.tv/api/oembed" + }, { "s": [ "//hubspot-media-bridge\\.thedamconsultants\\.com/*" @@ -2087,7 +2256,9 @@ export const providers = [ "//www\\.tickcounter\\.com/countup/*", "//www\\.tickcounter\\.com/ticker/*", "//www\\.tickcounter\\.com/clock/*", - "//www\\.tickcounter\\.com/worldclock/*" + "//www\\.tickcounter\\.com/worldclock/*", + "//www\\.tickcounter\\.com/embed/*", + "//www\\.tickcounter\\.com/full/*" ], "e": "//www.tickcounter.com/oembed" }, @@ -2149,6 +2320,13 @@ export const providers = [ ], "e": "//trinitymedia.ai/services/oembed" }, + { + "s": [ + "//trycli\\.com/e/*", + "//trycli\\.com/*/*" + ], + "e": "//trycli.com/api/oembed" + }, { "s": [ "//*\\.tumblr\\.com/post/*" @@ -2365,7 +2543,8 @@ export const providers = [ }, { "s": [ - "//whimsical\\.com/*" + "//whimsical\\.com/*", + "//whimsical\\.com/*/*" ], "e": "//whimsical.com/api/oembed" }, diff --git a/src/utils/providers.orginal.json b/src/utils/providers.original.json similarity index 92% rename from src/utils/providers.orginal.json rename to src/utils/providers.original.json index 2c3a4d7..bb790af 100644 --- a/src/utils/providers.orginal.json +++ b/src/utils/providers.original.json @@ -197,6 +197,39 @@ } ] }, + { + "provider_name": "AppForceStudio", + "provider_url": "https://appforcestudio.com", + "endpoints": [ + { + "schemes": [ + "https://appforcestudio.com/playground/*" + ], + "url": "https://us-central1-themerax-cc903.cloudfunctions.net/playgroundApi/oembed", + "discovery": true, + "formats": [ + "json", + "xml" + ] + } + ] + }, + { + "provider_name": "Apple Podcasts", + "provider_url": "https://podcasts.apple.com", + "endpoints": [ + { + "schemes": [ + "https://podcasts.apple.com/*" + ], + "url": "https://podcasts.apple.com/api/oembed", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, { "provider_name": "ArcGIS StoryMaps", "provider_url": "https://storymaps.arcgis.com", @@ -333,6 +366,22 @@ } ] }, + { + "provider_name": "Audius", + "provider_url": "https://audius.co", + "endpoints": [ + { + "schemes": [ + "https://audius.co/*" + ], + "url": "https://audius.co/oembed", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, { "provider_name": "Backtracks", "provider_url": "https://backtracks.fm", @@ -514,11 +563,36 @@ }, { "provider_name": "Box Office Buz", - "provider_url": "http://boxofficebuz.com", + "provider_url": "https://boxofficebuz.com", "endpoints": [ { - "url": "http://boxofficebuz.com/oembed", - "discovery": true + "schemes": [ + "https://boxofficebuz.com/embed/video/*" + ], + "url": "https://api.boxofficebuz.com/v2/oembed", + "discovery": true, + "formats": [ + "json", + "xml" + ] + } + ] + }, + { + "provider_name": "Brightcove", + "provider_url": "https://www.brightcove.com/", + "endpoints": [ + { + "schemes": [ + "https://players.brightcove.net/*/*/index.html?videoId=*", + "https://players.brightcove.net/*/*/index.html?playlistId=*", + "https://bcove.video/*" + ], + "url": "https://oembed.brightcove.com/", + "discovery": false, + "formats": [ + "json" + ] } ] }, @@ -549,11 +623,13 @@ ] }, { - "provider_name": "Bunny", + "provider_name": "bunny.net", "provider_url": "https://bunny.net/", "endpoints": [ { "schemes": [ + "https://player.mediadelivery.net/*", + "http://player.mediadelivery.net/*", "https://iframe.mediadelivery.net/*", "http://iframe.mediadelivery.net/*", "https://video.bunnycdn.com/*", @@ -765,6 +841,23 @@ } ] }, + { + "provider_name": "CircleZeroEight", + "provider_url": "https://www.circlezeroeight.com", + "endpoints": [ + { + "schemes": [ + "https://www.circlezeroeight.com/features/*", + "https://www.circlezeroeight.com/news/*", + "https://www.circlezeroeight.com/sport/*", + "https://www.circlezeroeight.com/style/*", + "https://www.circlezeroeight.com/culture/*" + ], + "url": "https://www.circlezeroeight.com/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "CircuitLab", "provider_url": "https://www.circuitlab.com/", @@ -792,6 +885,19 @@ } ] }, + { + "provider_name": "Clueso", + "provider_url": "https://clueso.io", + "endpoints": [ + { + "schemes": [ + "https://clueso.site/*" + ], + "url": "https://clueso.site/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "Clyp", "provider_url": "http://clyp.it/", @@ -1034,6 +1140,23 @@ } ] }, + { + "provider_name": "Demofly", + "provider_url": "https://demofly.ai/", + "endpoints": [ + { + "schemes": [ + "https://app.demofly.ai/s/*", + "https://demofly.ai/s/*" + ], + "url": "https://app.demofly.ai/api/oembed", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, { "provider_name": "Deseret News", "provider_url": "https://www.deseret.com", @@ -1120,6 +1243,19 @@ } ] }, + { + "provider_name": "DocumentCloud", + "provider_url": "https://www.documentcloud.org", + "endpoints": [ + { + "schemes": [ + "https://www.documentcloud.org/documents/*" + ], + "url": "https://api.www.documentcloud.org/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "Dotsub", "provider_url": "http://dotsub.com/", @@ -1202,6 +1338,32 @@ } ] }, + { + "provider_name": "ElevenLabs", + "provider_url": "https://elevenlabs.io/", + "endpoints": [ + { + "schemes": [ + "https://elevenlabs.io/*" + ], + "url": "https://elevenlabs.io/next/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Embases", + "provider_url": "https://embases.com/", + "endpoints": [ + { + "schemes": [ + "https://www.embases.com/e/*" + ], + "url": "https://www.embases.com/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "Embedery", "provider_url": "https://embedery.com/", @@ -1374,7 +1536,13 @@ "endpoints": [ { "schemes": [ - "https://www.figma.com/file/*" + "https://www.figma.com/file/*", + "https://www.figma.com/design/*", + "https://www.figma.com/board/*", + "https://www.figma.com/slides/*", + "https://www.figma.com/buzz/*", + "https://www.figma.com/site/*", + "https://www.figma.com/make/*" ], "url": "https://www.figma.com/api/oembed", "discovery": true @@ -1615,6 +1783,20 @@ } ] }, + { + "provider_name": "GeometryViewer", + "provider_url": "https://geometryviewer.com", + "endpoints": [ + { + "schemes": [ + "https://geometryviewer.com/v/*", + "https://www.geometryviewer.com/v/*" + ], + "url": "https://geometryviewer.com/oembed", + "discovery": true + } + ] + }, { "provider_name": "Getty Images", "provider_url": "http://www.gettyimages.com/", @@ -1698,6 +1880,22 @@ } ] }, + { + "provider_name": "GOOD FOR JOB", + "provider_url": "https://www.good-for-job.jp", + "endpoints": [ + { + "schemes": [ + "https://www.good-for-job.jp/slides/*" + ], + "url": "https://www.good-for-job.jp/api/oembed", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, { "provider_name": "Grain", "provider_url": "https://grain.com", @@ -1852,6 +2050,30 @@ } ] }, + { + "provider_name": "Hivo", + "provider_url": "https://app.hivo.com.au/", + "endpoints": [ + { + "schemes": [ + "https://cdn.hivo.com.au/*", + "https://cdn.digital-assets.uq.edu.au/*", + "https://cdn.southerndesigngroup.com/*", + "https://freedom.hivocdn.com/*", + "https://cdn.cockburn.wa.gov.au/*", + "https://cdn.somnomed.com/*", + "https://cdn.snowtunnel.com/*", + "https://cdn.brenclosures.com.au/*" + ], + "url": "https://app.hivo.com.au/api/drupal/oembed", + "discovery": true, + "formats": [ + "json", + "xml" + ] + } + ] + }, { "provider_name": "Homey", "provider_url": "https://homey.app", @@ -2236,6 +2458,23 @@ } ] }, + { + "provider_name": "Juntos", + "provider_url": "https://juntos.live", + "endpoints": [ + { + "schemes": [ + "https://play.juntos.live/solo/*", + "https://play.juntos.live/host/quiz/*" + ], + "url": "https://play.juntos.live/api/oembed", + "formats": [ + "json" + ], + "discovery": true + } + ] + }, { "provider_name": "KakaoTv", "provider_url": "https://tv.kakao.com/", @@ -2403,6 +2642,42 @@ } ] }, + { + "provider_name": "Land of Assets", + "provider_url": "https://landofassets.com", + "endpoints": [ + { + "schemes": [ + "https://landofassets.com/*", + "https://landofassets.com/*/assets", + "https://landofassets.com/*/assets/*", + "https://landofassets.com/*/assets/*/embed" + ], + "url": "https://api.landofassets.com/oembed", + "formats": [ + "json" + ], + "discovery": true + } + ] + }, + { + "provider_name": "Laude", + "provider_url": "https://laude.org", + "endpoints": [ + { + "schemes": [ + "https://laude.org/impact/*", + "https://laude.org/im/*" + ], + "url": "http://laude.org/api/oembed", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, { "provider_name": "LearningApps.org", "provider_url": "http://learningapps.org/", @@ -2469,19 +2744,18 @@ ] }, { - "provider_name": "Livestream", - "provider_url": "https://livestream.com/", + "provider_name": "Livid", + "provider_url": "https://livid.com", "endpoints": [ { "schemes": [ - "https://livestream.com/accounts/*/events/*", - "https://livestream.com/accounts/*/events/*/videos/*", - "https://livestream.com/*/events/*", - "https://livestream.com/*/events/*/videos/*", - "https://livestream.com/*/*", - "https://livestream.com/*/*/videos/*" + "https://livid.com/watch/*" + ], + "url": "https://livid.com/oembed", + "formats": [ + "json", + "xml" ], - "url": "https://livestream.com/oembed", "discovery": true } ] @@ -2687,6 +2961,13 @@ ], "url": "https://miro.com/api/v1/oembed", "discovery": true + }, + { + "schemes": [ + "https://miro.com/video-player/*" + ], + "url": "https://miro.com/video-player/oembed", + "discovery": true } ] }, @@ -3186,6 +3467,20 @@ } ] }, + { + "provider_name": "Parta.io", + "provider_url": "https://parta.io/", + "endpoints": [ + { + "schemes": [ + "https://app.parta.io/core/embed/*", + "https://*.parta.io/core/embed/*" + ], + "url": "https://app.parta.io/core/oembed", + "discovery": true + } + ] + }, { "provider_name": "Pastery", "provider_url": "https://www.pastery.net", @@ -3328,6 +3623,23 @@ } ] }, + { + "provider_name": "Plinth", + "provider_url": "https://plinth.it", + "endpoints": [ + { + "schemes": [ + "https://plinth.it/model-viewer/*/*" + ], + "url": "https://plinth.it/model-viewer/oembed", + "discovery": true, + "formats": [ + "json", + "xml" + ] + } + ] + }, { "provider_name": "Plusdocs", "provider_url": "http://plusdocs.com", @@ -3415,6 +3727,19 @@ } ] }, + { + "provider_name": "Programmingly.dev", + "provider_url": "https://programmingly.dev", + "endpoints": [ + { + "schemes": [ + "https://programmingly.dev/snippets/*" + ], + "url": "https://programmingly.dev/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "QTpi", "provider_url": "https://qtpi.gg/", @@ -3648,6 +3973,10 @@ "provider_url": "https://rumble.com/", "endpoints": [ { + "schemes": [ + "https://rumble.com/*", + "https://rumble.com/shorts/*" + ], "url": "https://rumble.com/api/Media/oembed.{format}", "discovery": true } @@ -3706,6 +4035,19 @@ } ] }, + { + "provider_name": "Sato Video Player", + "provider_url": "http://satoplayer.com", + "endpoints": [ + { + "schemes": [ + "https://api.satoplayer.com/players/embed/*" + ], + "url": "https://api.satoplayer.com/players/oembed", + "discovery": true + } + ] + }, { "provider_name": "sbedit", "provider_url": "https://sbedit.net", @@ -3924,6 +4266,20 @@ } ] }, + { + "provider_name": "Sketch", + "provider_url": "https://www.sketch.com", + "endpoints": [ + { + "schemes": [ + "https://www.sketch.com/s/*", + "https://www.sketch.com/preview/*" + ], + "url": "https://graphql.sketch.cloud/embed/oembed", + "discovery": true + } + ] + }, { "provider_name": "Sketchfab", "provider_url": "http://sketchfab.com", @@ -4053,17 +4409,17 @@ }, { "provider_name": "SOOP", - "provider_url": "https://www.sooplive.co.kr", + "provider_url": "https://www.sooplive.com", "endpoints": [ { "schemes": [ - "https://vod.sooplive.co.kr/player/", + "https://vod.sooplive.com/player/", "https://v.afree.ca/ST/", - "https://vod.sooplive.co.kr/ST/", - "https://vod.sooplive.co.kr/PLAYER/STATION/", - "https://play.sooplive.co.kr/" + "https://vod.sooplive.com/ST/", + "https://vod.sooplive.com/PLAYER/STATION/", + "https://play.sooplive.com/" ], - "url": "https://openapi.sooplive.co.kr/oembed/embedinfo", + "url": "https://openapi.sooplive.com/oembed/embedinfo", "discovery": true } ] @@ -4256,6 +4612,8 @@ "schemes": [ "https://www.sudomemo.net/watch/*", "http://www.sudomemo.net/watch/*", + "https://archive.sudomemo.net/watch/*", + "http://archive.sudomemo.net/watch/*", "https://flipnot.es/*", "http://flipnot.es/*" ], @@ -4364,6 +4722,20 @@ } ] }, + { + "provider_name": "Tella", + "provider_url": "https://www.tella.tv/", + "endpoints": [ + { + "schemes": [ + "https://www.tella.tv/video/*", + "https://tella.video/*" + ], + "url": "https://www.tella.tv/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "The DAM consultants", "provider_url": "https://hubspot-media-bridge.thedamconsultants.com/", @@ -4417,12 +4789,16 @@ "http://www.tickcounter.com/ticker/*", "http://www.tickcounter.com/clock/*", "http://www.tickcounter.com/worldclock/*", + "http://www.tickcounter.com/embed/*", + "http://www.tickcounter.com/full/*", "https://www.tickcounter.com/widget/*", "https://www.tickcounter.com/countdown/*", "https://www.tickcounter.com/countup/*", "https://www.tickcounter.com/ticker/*", "https://www.tickcounter.com/clock/*", - "https://www.tickcounter.com/worldclock/*" + "https://www.tickcounter.com/worldclock/*", + "https://www.tickcounter.com/embed/*", + "https://www.tickcounter.com/full/*" ], "url": "https://www.tickcounter.com/oembed", "discovery": true @@ -4548,6 +4924,20 @@ } ] }, + { + "provider_name": "TryCLI Studio", + "provider_url": "https://trycli.com", + "endpoints": [ + { + "schemes": [ + "https://trycli.com/e/*", + "https://trycli.com/*/*" + ], + "url": "https://trycli.com/api/oembed", + "discovery": true + } + ] + }, { "provider_name": "Tumblr", "provider_url": "https://www.tumblr.com", @@ -5017,7 +5407,8 @@ "endpoints": [ { "schemes": [ - "https://whimsical.com/*" + "https://whimsical.com/*", + "https://whimsical.com/*/*" ], "url": "https://whimsical.com/api/oembed", "discovery": true diff --git a/src/utils/providers.prev.js b/src/utils/providers.prev.js index c62652d..d584b17 100644 --- a/src/utils/providers.prev.js +++ b/src/utils/providers.prev.js @@ -1,4 +1,4 @@ -// provider data, synchronized at 2025-05-04T13:26:33.140Z +// provider data, synchronized at 2026-05-03T05:39:11.041Z /* eslint-disable */ @@ -49,16 +49,6 @@ export const providers = [ ], "e": "//openapi.afreecatv.com/oembed/embedinfo" }, - { - "s": [ - "//vod\\.sooplive\\.co\\.kr/player/", - "//v\\.afree\\.ca/ST/", - "//vod\\.sooplive\\.co\\.kr/ST/", - "//vod\\.sooplive\\.co\\.kr/PLAYER/STATION/", - "//play\\.sooplive\\.co\\.kr/" - ], - "e": "//openapi.sooplive.co.kr/oembed/embedinfo" - }, { "s": [ "//altium\\.com/viewer/*" @@ -107,6 +97,18 @@ export const providers = [ ], "e": "//api.anniemusic.app/api/v1/oembed" }, + { + "s": [ + "//appforcestudio\\.com/playground/*" + ], + "e": "//us-central1-themerax-cc903.cloudfunctions.net/playgroundApi/oembed" + }, + { + "s": [ + "//podcasts\\.apple\\.com/*" + ], + "e": "//podcasts.apple.com/api/oembed" + }, { "s": [ "//storymaps\\.arcgis\\.com/stories/*" @@ -172,6 +174,12 @@ export const providers = [ ], "e": "//podcasts.audiomeans.fr/services/oembed" }, + { + "s": [ + "//audius\\.co/*" + ], + "e": "//audius.co/oembed" + }, { "s": [ "//backtracks\\.fm/*/*/e/*", @@ -204,6 +212,12 @@ export const providers = [ ], "e": "//www.behance.net/services/oembed" }, + { + "s": [ + "//beta\\.quellensuche\\.de/*" + ], + "e": "//beta.quellensuche.de/api/oembed" + }, { "s": [ "//cloud\\.biqapp\\.com/*" @@ -248,8 +262,18 @@ export const providers = [ "e": "//www.bornetube.dk/media/lasync/oembed/" }, { - "s": [], - "e": "//boxofficebuz.com/oembed" + "s": [ + "//boxofficebuz\\.com/embed/video/*" + ], + "e": "//api.boxofficebuz.com/v2/oembed" + }, + { + "s": [ + "//players\\.brightcove\\.net/*/*/index\\.html?videoId=*", + "//players\\.brightcove\\.net/*/*/index\\.html?playlistId=*", + "//bcove\\.video/*" + ], + "e": "//oembed.brightcove.com/" }, { "s": [ @@ -266,6 +290,7 @@ export const providers = [ }, { "s": [ + "//player\\.mediadelivery\\.net/*", "//iframe\\.mediadelivery\\.net/*", "//video\\.bunnycdn\\.com/*" ], @@ -295,6 +320,13 @@ export const providers = [ ], "e": "//www.canva.com/_oembed" }, + { + "s": [ + "//carbon\\.music/*", + "//www\\.carbon\\.music/*" + ], + "e": "//carbon.music/oembed" + }, { "s": [ "//minesweeper\\.today/*" @@ -353,6 +385,16 @@ export const providers = [ ], "e": "//chroco.ooo/embed" }, + { + "s": [ + "//www\\.circlezeroeight\\.com/features/*", + "//www\\.circlezeroeight\\.com/news/*", + "//www\\.circlezeroeight\\.com/sport/*", + "//www\\.circlezeroeight\\.com/style/*", + "//www\\.circlezeroeight\\.com/culture/*" + ], + "e": "//www.circlezeroeight.com/api/oembed" + }, { "s": [ "//www\\.circuitlab\\.com/circuit/*" @@ -365,6 +407,12 @@ export const providers = [ ], "e": "//www.clipland.com/api/oembed" }, + { + "s": [ + "//clueso\\.site/*" + ], + "e": "//clueso.site/api/oembed" + }, { "s": [ "//clyp\\.it/*", @@ -479,6 +527,13 @@ export const providers = [ ], "e": "//api.datawrapper.de/v3/oembed/" }, + { + "s": [ + "//app\\.demofly\\.ai/s/*", + "//demofly\\.ai/s/*" + ], + "e": "//app.demofly.ai/api/oembed" + }, { "s": [ "//*\\.deseret\\.com/*" @@ -518,6 +573,12 @@ export const providers = [ ], "e": "//www.docswell.com/service/oembed" }, + { + "s": [ + "//www\\.documentcloud\\.org/documents/*" + ], + "e": "//api.www.documentcloud.org/api/oembed" + }, { "s": [ "//dotsub\\.com/view/*" @@ -556,6 +617,18 @@ export const providers = [ ], "e": "//egliseinfo.catholique.fr/api/oembed" }, + { + "s": [ + "//elevenlabs\\.io/*" + ], + "e": "//elevenlabs.io/next/oembed" + }, + { + "s": [ + "//www\\.embases\\.com/e/*" + ], + "e": "//www.embases.com/api/oembed" + }, { "s": [ "//embedery\\.com/widget/*" @@ -583,6 +656,12 @@ export const providers = [ ], "e": "//api.everviz.com/oembed" }, + { + "s": [ + "//cdn\\.everwall\\.com/hubs/iframe/*" + ], + "e": "//cdn.everwall.com/hubs/oembed" + }, { "s": [ "//app\\.ex\\.co/stories/*", @@ -643,10 +722,22 @@ export const providers = [ }, { "s": [ - "//www\\.figma\\.com/file/*" + "//www\\.figma\\.com/file/*", + "//www\\.figma\\.com/design/*", + "//www\\.figma\\.com/board/*", + "//www\\.figma\\.com/slides/*", + "//www\\.figma\\.com/buzz/*", + "//www\\.figma\\.com/site/*", + "//www\\.figma\\.com/make/*" ], "e": "//www.figma.com/api/oembed" }, + { + "s": [ + "//app\\.filestage\\.io/step/**" + ], + "e": "//app.filestage.io/oembed" + }, { "s": [ "//*\\.fireworktv\\.com/*", @@ -697,6 +788,12 @@ export const providers = [ ], "e": "//fooday.app/oembed" }, + { + "s": [ + "//forms\\.form-data\\.com/*" + ], + "e": "//forms.form-data.com/api/oembed" + }, { "s": [ "//fiso\\.foxsports\\.com\\.au/isomorphic-widget/*" @@ -749,6 +846,13 @@ export const providers = [ ], "e": "//geo.hlipp.de/restapi.php/api/oembed" }, + { + "s": [ + "//geometryviewer\\.com/v/*", + "//www\\.geometryviewer\\.com/v/*" + ], + "e": "//geometryviewer.com/oembed" + }, { "s": [ "//gty\\.im/*" @@ -787,6 +891,12 @@ export const providers = [ ], "e": "//app.gong.io/oembed" }, + { + "s": [ + "//www\\.good-for-job\\.jp/slides/*" + ], + "e": "//www.good-for-job.jp/api/oembed" + }, { "s": [ "//grain\\.co/highlight/*", @@ -808,6 +918,12 @@ export const providers = [ ], "e": "//api.gumlet.com/v1/oembed" }, + { + "s": [ + "//gw2fashions\\.com/fashion/*" + ], + "e": "//gw2fashions.com/fashion/oembed" + }, { "s": [ "//gyazo\\.com/*" @@ -853,6 +969,19 @@ export const providers = [ ], "e": "//www.hippovideo.io/services/oembed" }, + { + "s": [ + "//cdn\\.hivo\\.com\\.au/*", + "//cdn\\.digital-assets\\.uq\\.edu\\.au/*", + "//cdn\\.southerndesigngroup\\.com/*", + "//freedom\\.hivocdn\\.com/*", + "//cdn\\.cockburn\\.wa\\.gov\\.au/*", + "//cdn\\.somnomed\\.com/*", + "//cdn\\.snowtunnel\\.com/*", + "//cdn\\.brenclosures\\.com\\.au/*" + ], + "e": "//app.hivo.com.au/api/drupal/oembed" + }, { "s": [ "//homey\\.app/f/*", @@ -1032,6 +1161,13 @@ export const providers = [ ], "e": "//api.jovian.com/oembed.json" }, + { + "s": [ + "//play\\.juntos\\.live/solo/*", + "//play\\.juntos\\.live/host/quiz/*" + ], + "e": "//play.juntos.live/api/oembed" + }, { "s": [ "//tv\\.kakao\\.com/channel/*/cliplink/*", @@ -1100,6 +1236,13 @@ export const providers = [ ], "e": "//embed.kooapp.com/services/oembed" }, + { + "s": [ + "//kubit\\.ai/*", + "//*\\.kubit\\.ai/*" + ], + "e": "//kubit.ai/services/oembed" + }, { "s": [ "//kurozora\\.app/episodes/*", @@ -1107,6 +1250,22 @@ export const providers = [ ], "e": "//kurozora.app/oembed" }, + { + "s": [ + "//landofassets\\.com/*", + "//landofassets\\.com/*/assets", + "//landofassets\\.com/*/assets/*", + "//landofassets\\.com/*/assets/*/embed" + ], + "e": "//api.landofassets.com/oembed" + }, + { + "s": [ + "//laude\\.org/impact/*", + "//laude\\.org/im/*" + ], + "e": "//laude.org/api/oembed" + }, { "s": [ "//learningapps\\.org/*" @@ -1140,14 +1299,9 @@ export const providers = [ }, { "s": [ - "//livestream\\.com/accounts/*/events/*", - "//livestream\\.com/accounts/*/events/*/videos/*", - "//livestream\\.com/*/events/*", - "//livestream\\.com/*/events/*/videos/*", - "//livestream\\.com/*/*", - "//livestream\\.com/*/*/videos/*" + "//livid\\.com/watch/*" ], - "e": "//livestream.com/oembed" + "e": "//livid.com/oembed" }, { "s": [ @@ -1209,7 +1363,8 @@ export const providers = [ }, { "s": [ - "//medienarchiv\\.zhdk\\.ch/entries/*" + "//medienarchiv\\.zhdk\\.ch/entries/*", + "//zhdk\\.medienarchiv\\.ch/entries/*" ], "e": "//medienarchiv.zhdk.ch/oembed.json" }, @@ -1243,6 +1398,12 @@ export const providers = [ ], "e": "//miro.com/api/v1/oembed" }, + { + "s": [ + "//miro\\.com/video-player/*" + ], + "e": "//miro.com/video-player/oembed" + }, { "s": [ "//www\\.mixcloud\\.com/*/*/" @@ -1314,6 +1475,13 @@ export const providers = [ ], "e": "//naturalatlas.com/oembed.json" }, + { + "s": [ + "//naver\\.me/*", + "//m\\.naver\\.com/shorts/*" + ], + "e": "//m.naver.com/shorts/oEmbed" + }, { "s": [ "//ndla\\.no/*", @@ -1471,6 +1639,13 @@ export const providers = [ ], "e": "//api-v2.pandavideo.com.br/oembed" }, + { + "s": [ + "//app\\.parta\\.io/core/embed/*", + "//*\\.parta\\.io/core/embed/*" + ], + "e": "//app.parta.io/core/oembed" + }, { "s": [ "//pastery\\.net/*", @@ -1532,6 +1707,12 @@ export const providers = [ ], "e": "//store.pixdor.com/oembed" }, + { + "s": [ + "//plinth\\.it/model-viewer/*/*" + ], + "e": "//plinth.it/model-viewer/oembed" + }, { "s": [ "//app\\.plusdocs\\.com/*/snapshots/*", @@ -1573,6 +1754,12 @@ export const providers = [ ], "e": "//prezi.com/v/oembed" }, + { + "s": [ + "//programmingly\\.dev/snippets/*" + ], + "e": "//programmingly.dev/api/oembed" + }, { "s": [ "//qtpi\\.gg/fashion/*" @@ -1586,6 +1773,12 @@ export const providers = [ ], "e": "//web.quartr.com/api/oembed" }, + { + "s": [ + "//quellensuche\\.de/*" + ], + "e": "//quellensuche.de/api/oembed" + }, { "s": [ "//www\\.quiz\\.biz/quizz-*\\.html" @@ -1680,7 +1873,10 @@ export const providers = [ "e": "//roosterteeth.com/oembed" }, { - "s": [], + "s": [ + "//rumble\\.com/*", + "//rumble\\.com/shorts/*" + ], "e": "//rumble.com/api/Media/oembed.json" }, { @@ -1707,6 +1903,12 @@ export const providers = [ ], "e": "//www.satcat.com/api/sats/oembed" }, + { + "s": [ + "//api\\.satoplayer\\.com/players/embed/*" + ], + "e": "//api.satoplayer.com/players/oembed" + }, { "s": [ "//sbedit\\.net/*" @@ -1809,6 +2011,13 @@ export const providers = [ ], "e": "//onsizzle.com/oembed" }, + { + "s": [ + "//www\\.sketch\\.com/s/*", + "//www\\.sketch\\.com/preview/*" + ], + "e": "//graphql.sketch.cloud/embed/oembed" + }, { "s": [ "//sketchfab\\.com/*models/*", @@ -1869,6 +2078,16 @@ export const providers = [ ], "e": "//www.socialexplorer.com/services/oembed/" }, + { + "s": [ + "//vod\\.sooplive\\.com/player/", + "//v\\.afree\\.ca/ST/", + "//vod\\.sooplive\\.com/ST/", + "//vod\\.sooplive\\.com/PLAYER/STATION/", + "//play\\.sooplive\\.com/" + ], + "e": "//openapi.sooplive.com/oembed/embedinfo" + }, { "s": [ "//soundcloud\\.com/*", @@ -1954,15 +2173,16 @@ export const providers = [ { "s": [ "//www\\.sudomemo\\.net/watch/*", + "//archive\\.sudomemo\\.net/watch/*", "//flipnot\\.es/*" ], "e": "//www.sudomemo.net/oembed" }, { "s": [ - "//supercut\\.video/share/*" + "//supercut\\.ai/share/*" ], - "e": "//supercut.video/oembed" + "e": "//supercut.ai/oembed" }, { "s": [ @@ -2002,6 +2222,13 @@ export const providers = [ ], "e": "//www.ted.com/services/v1/oembed.json" }, + { + "s": [ + "//www\\.tella\\.tv/video/*", + "//tella\\.video/*" + ], + "e": "//www.tella.tv/api/oembed" + }, { "s": [ "//hubspot-media-bridge\\.thedamconsultants\\.com/*" @@ -2029,7 +2256,9 @@ export const providers = [ "//www\\.tickcounter\\.com/countup/*", "//www\\.tickcounter\\.com/ticker/*", "//www\\.tickcounter\\.com/clock/*", - "//www\\.tickcounter\\.com/worldclock/*" + "//www\\.tickcounter\\.com/worldclock/*", + "//www\\.tickcounter\\.com/embed/*", + "//www\\.tickcounter\\.com/full/*" ], "e": "//www.tickcounter.com/oembed" }, @@ -2091,6 +2320,13 @@ export const providers = [ ], "e": "//trinitymedia.ai/services/oembed" }, + { + "s": [ + "//trycli\\.com/e/*", + "//trycli\\.com/*/*" + ], + "e": "//trycli.com/api/oembed" + }, { "s": [ "//*\\.tumblr\\.com/post/*" @@ -2295,11 +2531,9 @@ export const providers = [ }, { "s": [ - "//share\\.webcrumbs\\.org/*", - "//tools\\.webcrumbs\\.org/*", - "//www\\.webcrumbs\\.org/*" + "//plugins\\.webcrumbs\\.dev/*" ], - "e": "//share.webcrumbs.org/" + "e": "//webcrumbs.dev/oembed" }, { "s": [ @@ -2309,7 +2543,8 @@ export const providers = [ }, { "s": [ - "//whimsical\\.com/*" + "//whimsical\\.com/*", + "//whimsical\\.com/*/*" ], "e": "//whimsical.com/api/oembed" }, diff --git a/src/utils/retrieve.js b/src/utils/retrieve.js index e60f662..d4b9e78 100644 --- a/src/utils/retrieve.js +++ b/src/utils/retrieve.js @@ -1,7 +1,16 @@ // utils -> retrieve -import fetch from 'cross-fetch' - +/** + * Fetch a resource through a proxy endpoint. + * + * @param {string} url - Target URL to fetch + * @param {object} [options={}] - Options containing proxy config and signal + * @param {object} [options.proxy] - Proxy configuration + * @param {string} options.proxy.target - Proxy base URL + * @param {object} [options.proxy.headers] - Headers to send to the proxy + * @param {object} [options.signal] - AbortSignal for request cancellation + * @returns {Promise} Fetch response object + */ const profetch = async (url, options = {}) => { const { proxy = {}, signal = null } = options const { @@ -15,6 +24,14 @@ const profetch = async (url, options = {}) => { return res } +/** + * Fetch a URL and return the response body as text. + * + * @param {string} url - URL to fetch + * @param {object} [options={}] - Fetch options (headers, proxy, agent, signal) + * @returns {Promise} Response body as text + * @throws {Error} If HTTP status is 400 or higher + */ export const getHtml = async (url, options = {}) => { const { headers = { @@ -36,6 +53,14 @@ export const getHtml = async (url, options = {}) => { return text } +/** + * Fetch a URL and parse the response body as JSON. + * + * @param {string} url - URL to fetch + * @param {object} [options={}] - Fetch options (headers, proxy, agent, signal) + * @returns {Promise} Parsed JSON response + * @throws {Error} If HTTP status is 400+ or response is not valid JSON + */ export const getJson = async (url, options = {}) => { const { headers = { diff --git a/sync.js b/sync.js index ddc2f51..7df9a87 100644 --- a/sync.js +++ b/sync.js @@ -8,11 +8,21 @@ import { import { getJson } from './src/utils/retrieve.js' import { simplify } from './src/utils/provider.js' +/** URL of the oEmbed providers registry */ const source = 'https://oembed.com/providers.json' +/** Path to the latest providers module */ const latest = './src/utils/providers.latest.js' +/** Path to the previous providers backup */ const prev = './src/utils/providers.prev.js' -const orginal = './src/utils/providers.orginal.json' +/** Path to the raw original JSON dump */ +const original = './src/utils/providers.original.json' +/** + * Write raw provider data to a JSON file. + * + * @param {Array} data - Provider data from the registry + * @param {string} file - Output file path + */ const saveOriginal = (data, file) => { writeFileSync( file, @@ -21,10 +31,15 @@ const saveOriginal = (data, file) => { ) } +/** + * Synchronize the local provider list with the remote oEmbed registry. + * Backs up the current list, downloads the latest, and writes both + * raw JSON and the compact JS module. + */ const sync = async () => { try { const result = await getJson(source) - saveOriginal(result, orginal) + saveOriginal(result, original) const arr = simplify(result) const data = JSON.stringify(arr, undefined, 2)