fix(extractor): introduce format and set notWebReady accordingly

According to https://github.com/Stremio/stremio-addon-sdk/blob/master/docs/api/responses/stream.md
This commit is contained in:
WebStreamr 2025-06-20 14:13:02 +00:00
parent d641fa1c2d
commit fd1145c162
No known key found for this signature in database
24 changed files with 127 additions and 31 deletions

View file

@ -2,7 +2,7 @@ import randomstring from 'randomstring';
import * as cheerio from 'cheerio';
import { Extractor } from './Extractor';
import { Fetcher, guessFromTitle } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export class DoodStream extends Extractor {
@ -52,6 +52,7 @@ export class DoodStream extends Extractor {
return [
{
url: mp4Url,
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -2,7 +2,7 @@ import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Extractor } from './Extractor';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export class Dropload extends Extractor {
@ -41,6 +41,7 @@ export class Dropload extends Extractor {
return [
{
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -1,6 +1,6 @@
import { Extractor } from './Extractor';
import { Fetcher, showExternalUrls } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
export class ExternalUrl extends Extractor {
public readonly id = 'external';
@ -32,6 +32,7 @@ export class ExternalUrl extends Extractor {
return [
{
url: url,
format: Format.unknown,
isExternal: true,
label: `${url.host}`,
sourceId: `${this.id}_${countryCode}`,

View file

@ -1,4 +1,4 @@
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export abstract class Extractor {
@ -27,6 +27,7 @@ export abstract class Extractor {
return [
{
url,
format: Format.unknown,
isExternal: true,
error,
label: this.label,

View file

@ -1,7 +1,7 @@
import * as cheerio from 'cheerio';
import { Extractor } from './Extractor';
import { Fetcher } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
export class Fsst extends Extractor {
public readonly id = 'fsst';
@ -35,6 +35,7 @@ export class Fsst extends Extractor {
return {
url: new URL(fileHref),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}_${index}`,
meta: {

View file

@ -1,7 +1,7 @@
import crypto from 'crypto';
import { Extractor } from './Extractor';
import { Fetcher, guessFromTitle } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */
export class KinoGer extends Extractor {
@ -41,6 +41,7 @@ export class KinoGer extends Extractor {
return [
{
url: new URL(cf),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -2,7 +2,7 @@ import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Extractor } from './Extractor';
import { buildMediaFlowProxyUrl, Fetcher, hasMediaFlowProxy } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export class Mixdrop extends Extractor {
@ -40,6 +40,7 @@ export class Mixdrop extends Extractor {
return [
{
url: buildMediaFlowProxyUrl(ctx.config, 'Mixdrop', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -1,6 +1,6 @@
import { Extractor } from './Extractor';
import { Fetcher, guessFromPlaylist } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
interface SoaperInfoResponsePartial {
val: string;
@ -53,6 +53,7 @@ export class Soaper extends Extractor {
return [
{
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -2,7 +2,7 @@ import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Extractor } from './Extractor';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export class SuperVideo extends Extractor {
@ -45,6 +45,7 @@ export class SuperVideo extends Extractor {
return [
{
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,

View file

@ -2,7 +2,7 @@ import * as cheerio from 'cheerio';
import slugify from 'slugify';
import { Extractor } from './Extractor';
import { Fetcher, guessFromPlaylist } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { NotFoundError } from '../error';
export class VidSrc extends Extractor {
@ -50,6 +50,7 @@ export class VidSrc extends Extractor {
return {
url: m3u8Url,
format: Format.hls,
label: `${this.label} (${serverName})`,
sourceId: `${this.id}_${slugify(serverName)}_${countryCode}`,
ttl: this.ttl,

View file

@ -3,6 +3,7 @@
exports[`DoodStream can guess height from title 1`] = `
[
{
"format": "mp4",
"label": "DoodStream",
"meta": {
"countryCode": "fr",
@ -22,6 +23,7 @@ exports[`DoodStream can guess height from title 1`] = `
exports[`DoodStream dood.to 1`] = `
[
{
"format": "mp4",
"label": "DoodStream",
"meta": {
"bytes": 791702409,
@ -41,6 +43,7 @@ exports[`DoodStream dood.to 1`] = `
exports[`DoodStream doodster 1`] = `
[
{
"format": "mp4",
"label": "DoodStream",
"meta": {
"countryCode": "de",

View file

@ -3,6 +3,7 @@
exports[`Dropload dropload.io 1`] = `
[
{
"format": "hls",
"label": "Dropload",
"meta": {
"bytes": 1395864371,

View file

@ -5,6 +5,7 @@ exports[`ExternalUrl 404 - not found 1`] = `[]`;
exports[`ExternalUrl johnalwayssame.com with title 1`] = `
[
{
"format": "unknown",
"isExternal": true,
"label": "johnalwayssame.com",
"meta": {
@ -21,6 +22,7 @@ exports[`ExternalUrl johnalwayssame.com with title 1`] = `
exports[`ExternalUrl netu.fanstream.us 1`] = `
[
{
"format": "unknown",
"isExternal": true,
"label": "netu.fanstream.us",
"meta": {

View file

@ -5,6 +5,7 @@ exports[`ExtractorRegistry returns error result from extractor 1`] = `[]`;
exports[`ExtractorRegistry returns external URLs if enabled by config 1`] = `
[
{
"format": "unknown",
"isExternal": true,
"label": "mixdrop.ag",
"meta": {
@ -22,6 +23,7 @@ exports[`ExtractorRegistry returns external url for error 1`] = `
{
"error": [Error: Fetcher error: 403: Forbidden
],
"format": "unknown",
"isExternal": true,
"label": "Dropload",
"meta": {
@ -38,6 +40,7 @@ exports[`ExtractorRegistry returns external url for error with title 1`] = `
{
"error": [Error: Fetcher error: 403: Forbidden
],
"format": "unknown",
"isExternal": true,
"label": "Dropload",
"meta": {

View file

@ -3,6 +3,7 @@
exports[`Fsst Blood & Sinners 1`] = `
[
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -13,6 +14,7 @@ exports[`Fsst Blood & Sinners 1`] = `
"url": "https://www.secvideo1.online/get_file/16/a135ab46b1243e41c0282c16d7cdcbcdf0faf7ee79/900000/900576/900576_360p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -23,6 +25,7 @@ exports[`Fsst Blood & Sinners 1`] = `
"url": "https://www.secvideo1.online/get_file/16/801a42b7ac11f7e08101fe8c3cb537045ddf5c9b19/900000/900576/900576_720p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -38,6 +41,7 @@ exports[`Fsst Blood & Sinners 1`] = `
exports[`Fsst Dead City 1`] = `
[
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -48,6 +52,7 @@ exports[`Fsst Dead City 1`] = `
"url": "https://www.secvideo1.online/get_file/9/99510c9abde68b5e82b08cd2391724050c8e47cc86/901000/901994/901994_360p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -58,6 +63,7 @@ exports[`Fsst Dead City 1`] = `
"url": "https://www.secvideo1.online/get_file/9/868ae0229e543e7a9a6f7cf4b3df88f45a49aa11fa/901000/901994/901994_720p.mp4/",
},
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",
@ -73,6 +79,7 @@ exports[`Fsst Dead City 1`] = `
exports[`Fsst How to Train Your Dragon 1`] = `
[
{
"format": "mp4",
"label": "Fsst",
"meta": {
"countryCode": "de",

View file

@ -3,6 +3,7 @@
exports[`KinoGer Blood & Sinners 1`] = `
[
{
"format": "hls",
"label": "KinoGer",
"meta": {
"countryCode": "de",
@ -21,6 +22,7 @@ exports[`KinoGer Blood & Sinners 1`] = `
exports[`KinoGer Dead City 1`] = `
[
{
"format": "hls",
"label": "KinoGer",
"meta": {
"countryCode": "de",

View file

@ -5,6 +5,7 @@ exports[`Mixdrop deleted or expired file 1`] = `[]`;
exports[`Mixdrop mixdrop.my /e/ 1`] = `
[
{
"format": "mp4",
"label": "Mixdrop (via MediaFlow Proxy)",
"meta": {
"bytes": 1059806248,

View file

@ -3,6 +3,7 @@
exports[`Soaper Black Mirror 1`] = `
[
{
"format": "hls",
"label": "Soaper",
"meta": {
"countryCode": "en",
@ -19,6 +20,7 @@ exports[`Soaper Black Mirror 1`] = `
exports[`Soaper Full Metal Jacket 1`] = `
[
{
"format": "hls",
"label": "Soaper",
"meta": {
"countryCode": "en",
@ -35,6 +37,7 @@ exports[`Soaper Full Metal Jacket 1`] = `
exports[`Soaper last of us s2e3 1`] = `
[
{
"format": "hls",
"label": "Soaper",
"meta": {
"countryCode": "de",

View file

@ -5,6 +5,7 @@ exports[`SuperVideo deleted or expired file 1`] = `[]`;
exports[`SuperVideo embed only 1`] = `
[
{
"format": "hls",
"label": "SuperVideo",
"meta": {
"countryCode": "it",
@ -20,6 +21,7 @@ exports[`SuperVideo embed only 1`] = `
exports[`SuperVideo supervideo.cc /e/ 1`] = `
[
{
"format": "hls",
"label": "SuperVideo",
"meta": {
"bytes": 1073741824,
@ -37,6 +39,7 @@ exports[`SuperVideo supervideo.cc /e/ 1`] = `
exports[`SuperVideo supervideo.tv /embed-/ 1`] = `
[
{
"format": "hls",
"label": "SuperVideo",
"meta": {
"bytes": 219571814,

View file

@ -3,6 +3,7 @@
exports[`VidSrc Black Mirror 1`] = `
[
{
"format": "hls",
"label": "VidSrc (CloudStream Pro)",
"meta": {
"countryCode": "en",
@ -19,6 +20,7 @@ exports[`VidSrc Black Mirror 1`] = `
exports[`VidSrc Full Metal Jacket 1`] = `
[
{
"format": "hls",
"label": "VidSrc (CloudStream Pro)",
"meta": {
"countryCode": "en",

View file

@ -33,13 +33,21 @@ export interface Meta {
title?: string;
}
export enum Format {
hls = 'hls',
mp4 = 'mp4',
unknown = 'unknown',
}
export interface UrlResult {
url: URL;
format: Format;
isExternal?: boolean;
error?: unknown;
label: string;
sourceId: string;
ttl?: number;
meta: Meta;
notWebReady?: boolean;
requestHeaders?: Record<string, string>;
}

View file

@ -2,9 +2,9 @@ import { ContentType } from 'stremio-addon-sdk';
import winston from 'winston';
import { createExtractors, Extractor, ExtractorRegistry } from '../extractor';
import { StreamResolver } from './StreamResolver';
import { Source, SourceResult, MeineCloud, MostraGuarda } from '../source';
import { BlockedReason, CountryCode, UrlResult } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyTimeoutsError, TooManyRequestsError } from '../error';
import { MeineCloud, MostraGuarda, Source, SourceResult } from '../source';
import { BlockedReason, CountryCode, Format, UrlResult } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError, TimeoutError, TooManyRequestsError, TooManyTimeoutsError } from '../error';
import { ImdbId } from './id';
import { FetcherMock } from './FetcherMock';
import { createTestContext } from '../test';
@ -92,6 +92,7 @@ describe('resolve', () => {
[
{
url: new URL('https://example.com'),
format: Format.unknown,
isExternal: true,
error: new BlockedError(BlockedReason.cloudflare_challenge, {}),
label: 'hoster.com',
@ -102,6 +103,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example.com'),
format: Format.unknown,
isExternal: true,
error: new BlockedError(BlockedReason.unknown, {}),
label: 'hoster.com',
@ -112,6 +114,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example.com'),
format: Format.unknown,
isExternal: true,
error: new TooManyRequestsError(10),
label: 'hoster.com',
@ -122,6 +125,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example.com'),
format: Format.unknown,
isExternal: true,
error: new TooManyTimeoutsError(),
label: 'hoster.com',
@ -132,6 +136,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example2.com'),
format: Format.unknown,
isExternal: true,
error: new TypeError(),
label: 'hoster.com',
@ -142,6 +147,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example2.com'),
format: Format.unknown,
isExternal: true,
error: new TimeoutError(),
label: 'hoster.com',
@ -152,6 +158,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example3.com'),
format: Format.unknown,
isExternal: true,
error: new QueueIsFullError(),
label: 'hoster.com',
@ -162,6 +169,7 @@ describe('resolve', () => {
},
{
url: new URL('https://example4.com'),
format: Format.unknown,
isExternal: true,
error: new HttpError(500, 'Internal Server Error', { 'x-foo': 'bar' }),
label: 'hoster.com',

View file

@ -1,7 +1,7 @@
import { ContentType, Stream } from 'stremio-addon-sdk';
import winston from 'winston';
import bytes from 'bytes';
import { Context, UrlResult } from '../types';
import { Context, Format, UrlResult } from '../types';
import { Source } from '../source';
import {
BlockedError,
@ -9,8 +9,8 @@ import {
NotFoundError,
QueueIsFullError,
TimeoutError,
TooManyTimeoutsError,
TooManyRequestsError,
TooManyTimeoutsError,
} from '../error';
import { flagFromCountryCode } from './language';
import { envGetAppName } from './env';
@ -107,6 +107,7 @@ export class StreamResolver {
title: this.buildTitle(ctx, urlResult),
behaviorHints: {
...(urlResult.sourceId && { bingeGroup: `webstreamr-${urlResult.sourceId}` }),
...((urlResult.format !== Format.mp4 || urlResult.url.protocol !== 'https:') && { notWebReady: true }),
...(urlResult.requestHeaders !== undefined && {
notWebReady: true,
proxyHeaders: { request: urlResult.requestHeaders },

View file

@ -4,56 +4,72 @@ exports[`resolve adds error info 1`] = `
{
"streams": [
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
⚠️ Request was blocked.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
⚠️ Request was blocked.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
🚦 Request was rate-limited. Please try again later or consider self-hosting.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
🚦 Too many recent timeouts. Please try again later.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
❌ Request failed. Request-id: test.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
🐢 Request timed out.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
⏳ Request queue is full. Please try again later or consider self-hosting.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "http://localhost/",
"name": "WebStreamr 🇩🇪",
"title": "🔗 hoster.com
@ -67,56 +83,72 @@ exports[`resolve adds error info 2`] = `
{
"streams": [
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
⚠️ Request was blocked.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
⚠️ Request was blocked.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
🚦 Request was rate-limited. Please try again later or consider self-hosting.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
🚦 Too many recent timeouts. Please try again later.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example2.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
❌ Request failed. Request-id: test.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example2.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
🐢 Request timed out.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example3.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
⏳ Request queue is full. Please try again later or consider self-hosting.",
},
{
"behaviorHints": {},
"behaviorHints": {
"notWebReady": true,
},
"externalUrl": "https://example4.com/",
"name": "WebStreamr 🇩🇪 ⚠️ external",
"title": "🔗 hoster.com
@ -164,6 +196,7 @@ exports[`resolve returns sorted results 1`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-dropload_de",
"notWebReady": true,
"videoSize": 1395864371,
},
"name": "WebStreamr 🇩🇪 1080p",
@ -174,6 +207,7 @@ exports[`resolve returns sorted results 1`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-dropload_it",
"notWebReady": true,
"videoSize": 1181116006,
},
"name": "WebStreamr 🇮🇹 720p",
@ -184,6 +218,7 @@ exports[`resolve returns sorted results 1`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-supervideo_it",
"notWebReady": true,
"videoSize": 1181116006,
},
"name": "WebStreamr 🇮🇹 720p",
@ -194,6 +229,7 @@ exports[`resolve returns sorted results 1`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-supervideo_de",
"notWebReady": true,
"videoSize": 1073741824,
},
"name": "WebStreamr 🇩🇪 720p",
@ -240,6 +276,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-dropload_de",
"notWebReady": true,
"videoSize": 1395864371,
},
"name": "WebStreamr 🇩🇪 1080p",
@ -250,6 +287,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-dropload_it",
"notWebReady": true,
"videoSize": 1181116006,
},
"name": "WebStreamr 🇮🇹 720p",
@ -260,6 +298,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-supervideo_it",
"notWebReady": true,
"videoSize": 1181116006,
},
"name": "WebStreamr 🇮🇹 720p",
@ -270,6 +309,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-supervideo_de",
"notWebReady": true,
"videoSize": 1073741824,
},
"name": "WebStreamr 🇩🇪 720p",
@ -311,6 +351,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-external_de",
"notWebReady": true,
},
"externalUrl": "https://mixdrop.ag/e/3nzwveprim63or6",
"name": "WebStreamr 🇩🇪 ⚠️ external",
@ -319,6 +360,7 @@ exports[`resolve returns sorted results 2`] = `
{
"behaviorHints": {
"bingeGroup": "webstreamr-external_it",
"notWebReady": true,
},
"externalUrl": "https://mixdrop.ag/e/vk196d6xfzwwo1",
"name": "WebStreamr 🇮🇹 ⚠️ external",