From 1c0134726d26b7cf0f7544ea00a70e30bd6cf1b4 Mon Sep 17 00:00:00 2001 From: Stefan Cruz <17972991+stefancruz@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:22:35 +0100 Subject: [PATCH] chore: type checking and code improvements --- src/DailymotionScript.ts | 1709 ++++++++++++----------- src/Mappers.ts | 39 +- src/Pagers.ts | 171 +-- src/constants.ts | 3 +- src/util.ts | 547 ++++---- types/plugin.d.ts | 2851 +++++++++++++++++++------------------- types/types.d.ts | 32 +- 7 files changed, 2679 insertions(+), 2673 deletions(-) diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts index 685f023..dfacd38 100644 --- a/src/DailymotionScript.ts +++ b/src/DailymotionScript.ts @@ -1,858 +1,853 @@ -let config: Config; -let _settings: IDailymotionPluginSettings; - - -import { - CREATOR_AVATAR_HEIGHT, - THUMBNAIL_HEIGHT, - BASE_URL, - SEARCH_CAPABILITIES, - BASE_URL_VIDEO, - BASE_URL_PLAYLIST, - USER_AGENT, - X_DM_AppInfo_Id, - X_DM_AppInfo_Type, - X_DM_AppInfo_Version, - X_DM_Neon_SSR, - BASE_URL_API, - BASE_URL_METADATA, - ERROR_TYPES, - LikedMediaSort, - VIDEOS_PER_PAGE_OPTIONS, - PLAYLISTS_PER_PAGE_OPTIONS -} from './constants'; - -import { - SEARCH_SUGGESTIONS_QUERY, - CHANNEL_BY_URL_QUERY, - PLAYLIST_DETAILS_QUERY, - GET_USER_SUBSCRIPTIONS, - MAIN_SEARCH_QUERY, - HOME_QUERY, - CHANNEL_VIDEOS_BY_CHANNEL_NAME, - VIDEO_DETAILS_QUERY, - SEARCH_CHANNEL, - GET_CHANNEL_PLAYLISTS, - SUBSCRIPTIONS_QUERY, - GET_CHANNEL_PLAYLISTS_XID -} from './gqlQueries'; - -import { - getChannelNameFromUrl, - isUsernameUrl, - executeGqlQuery, - getPreferredCountry, - getAnonymousUserTokenSingleton, - getQuery -} from './util'; - -import { - Channel, - Collection, - CollectionConnection, - LiveConnection, - LiveEdge, - SuggestionConnection, - Video, - VideoConnection, - VideoEdge -} from '../types/CodeGenDailymotion'; - -import { - SearchPagerAll, - SearchChannelPager, - ChannelVideoPager, - SearchPlaylistPager, - ChannelPlaylistPager -} from './Pagers'; - - -import { - SourceChannelToGrayjayChannel, - SourceCollectionToGrayjayPlaylist, - SourceCollectionToGrayjayPlaylistDetails, - SourceVideoToGrayjayVideo, - SourceVideoToPlatformVideoDetailsDef -} from './Mappers'; - - -if (IS_TESTING) { - - - if (!_settings) { - _settings = {} - } - - _settings.hideSensitiveContent = false; - _settings.avatarSize = 8; - _settings.thumbnailResolution = 7; - _settings.preferredCountry = 0; - _settings.videosPerPageIndex = 4; - _settings.playlistsPerPageIndex = 0; - - if (!config) { - config = { - id: "9c87e8db-e75d-48f4-afe5-2d203d4b95c5" - } - } -} - -let httpClientAnonymous: IHttp = http.newClient(false); - - -// Will be used to store private playlists that require authentication -const authenticatedPlaylistCollection: string[] = []; - -source.setSettings = function (settings) { - _settings = settings; - http.GET(BASE_URL, {}, true); -} - -//Source Methods -source.enable = function (conf, settings, saveStateStr) { - - config = conf ?? {}; - _settings = settings ?? {}; - -} - - -source.getHome = function () { - - getAnonymousUserTokenSingleton(); - - return getVideoPager({}, 0); -}; - -source.searchSuggestions = function (query): string[] { - - try { - - const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), - { - operationName: 'AUTOCOMPLETE_QUERY', - variables: { - query - }, - query: SEARCH_SUGGESTIONS_QUERY - }); - - return (jsonResponse?.data?.search?.suggestedVideos as SuggestionConnection)?.edges?.map(edge => edge?.node?.name ?? "") ?? []; - } catch (error: any) { - log('Failed to get search suggestions:' + error?.message); - return []; - } -}; - - -source.getSearchCapabilities = () => SEARCH_CAPABILITIES; - - -source.search = function (query: string, type: string, order: string, filters) { - return getSearchPagerAll({ q: query, page: 1, type, order, filters }); -} - -source.searchChannels = function (query) { - return getSearchChannelPager({ q: query, page: 1 }) -} - -//Channel -source.isChannelUrl = function (url) { - return isUsernameUrl(url); -}; - -source.getChannel = function (url) { - - const channel_name = getChannelNameFromUrl(url); - - const channelDetails = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), - { - operationName: 'CHANNEL_QUERY_DESKTOP', - variables: { - channel_name, - avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] - }, - query: CHANNEL_BY_URL_QUERY - }); - - return SourceChannelToGrayjayChannel(config.id, url, channelDetails.data.channel as Channel); - -}; - -source.getChannelContents = function (url, type, order) { - - return getChannelContentsPager({ - url, - page_size: VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], - page: 1, - type, - order - }) -} - -source.getChannelPlaylists = (url): SearchPlaylistPager => { - try { - return getChannelPlaylists(url, 1); - } catch (error) { - log('Failed to get channel playlists:' + error?.message); - return new ChannelPlaylistPager([]); - } -} - -source.getChannelCapabilities = (): ResultCapabilities => { - return { - types: [Type.Feed.Mixed], - sorts: [Type.Order.Chronological, "Popular"], - filters: [] - }; -}; - -//Video -source.isContentDetailsUrl = function (url) { - return url.startsWith(BASE_URL_VIDEO); -}; - -source.getContentDetails = function (url) { - return getSavedVideo(url, false); -}; - -//Playlist -source.isPlaylistUrl = (url): boolean => { - return url.startsWith(BASE_URL_PLAYLIST); -}; - -source.searchPlaylists = (query, type, order, filters) => { - return searchPlaylists({ q: query, type, order, filters }); -}; - -source.getPlaylist = (url: string): PlatformPlaylistDetails => { - - const xid = url.split('/').pop(); - - const variables = { - xid, - avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], - thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], - } - - const usePlatformAuth = authenticatedPlaylistCollection.includes(url); - - let jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth }), - { - operationName: 'PLAYLIST_VIDEO_QUERY', - variables, - query: PLAYLIST_DETAILS_QUERY, - usePlatformAuth - }); - - const videos: PlatformVideoDef[] = jsonResponse?.data?.collection?.videos?.edges.map(edge => { - return SourceVideoToGrayjayVideo(config.id, edge.node as Video); - }); - - return SourceCollectionToGrayjayPlaylistDetails(config.id, jsonResponse?.data?.collection as Collection, videos); - -} - -source.getUserSubscriptions = (): string[] => { - - if (!bridge.isLoggedIn()) { - bridge.log("Failed to retrieve subscriptions page because not logged in."); - throw new ScriptException("Not logged in"); - } - - const headers = { - 'Content-Type': 'application/json', - 'User-Agent': USER_AGENT, - // Accept: '*/*, */*', - 'Accept-Language': 'en-GB', - Referer: `${BASE_URL}/library/subscriptions`, - 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, - 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, - 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, - 'X-DM-Neon-SSR': '0', - 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), - Origin: BASE_URL, - DNT: '1', - Connection: 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site', - Priority: 'u=4', - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - } - - const usePlatformAuth = true; - - const fetchSubscriptions = (page, first): string[] => { - const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth }), - { - operationName: 'SUBSCRIPTIONS_QUERY', - variables: { - first: first, - page: page, - avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - }, - headers, - query: GET_USER_SUBSCRIPTIONS, - usePlatformAuth - }); - - return (jsonResponse?.data?.me?.channel as Channel)?.followings?.edges?.map(edge => edge?.node?.creator?.name ?? "") ?? []; - }; - - const first = 100; // Number of records to fetch per page - let page = 1; - let subscriptions: string[] = []; - - // There is a totalCount ($.data.me.channel.followings.totalCount) property but it's not reliable. - // For example, it may return 0 even if there are subscriptions, or it may return a number that is not the actual number of subscriptions. - // For now, it's better to fetch until no more results are returned - - let items: string[] = []; - - do { - const response = fetchSubscriptions(page, first); - - items = response.map(creatorName => `${BASE_URL}/${creatorName}`); - - subscriptions.push(...items); - page++; - } while (items.length); - - return subscriptions; -}; - - -source.getUserPlaylists = (): string[] => { - - if (!bridge.isLoggedIn()) { - bridge.log("Failed to retrieve subscriptions page because not logged in."); - throw new ScriptException("Not logged in"); - } - - const headers = { - 'Content-Type': 'application/json', - 'User-Agent': USER_AGENT, - // Accept: '*/*, */*', - 'Accept-Language': 'en-GB', - Referer: `${BASE_URL}/library/subscriptions`, - 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, - 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, - 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, - 'X-DM-Neon-SSR': '0', - 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), - Origin: BASE_URL, - DNT: '1', - Connection: 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site', - Priority: 'u=4', - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - } - - const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: true }), - { - operationName: 'SUBSCRIPTIONS_QUERY', - headers, - query: SUBSCRIPTIONS_QUERY, - usePlatformAuth: true - }); - - const userName = (jsonResponse?.data?.me?.channel as Channel)?.name; - - return getPlaylistsByUsername(userName, headers, true); - -} - -function getPlaylistsByUsername(userName, headers, usePlatformAuth = false) { - - - const jsonResponse1 = executeGqlQuery( - getHttpContext({ usePlatformAuth }), - { - operationName: 'CHANNEL_PLAYLISTS_QUERY', - variables: { - channel_name: userName, - sort: "recent", - page: 1, - first: 99, - avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], - thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], - }, - headers, - query: GET_CHANNEL_PLAYLISTS_XID, - usePlatformAuth - } - ); - - const playlists = jsonResponse1.data.channel.collections.edges.map(edge => { - const playlistUrl = `${BASE_URL_PLAYLIST}/${edge.node.xid}`; - if (!authenticatedPlaylistCollection.includes(playlistUrl)) { - authenticatedPlaylistCollection.push(playlistUrl); - } - return playlistUrl; - }); - - - return playlists; - -} - - -function searchPlaylists(contextQuery) { - - const context = getQuery(contextQuery); - - const variables = { - "query": context.q, - "sortByVideos": context.sort, - "durationMaxVideos": context.filters?.durationMaxVideos, - "durationMinVideos": context.filters?.durationMinVideos, - "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 - "shouldIncludeChannels": false, - "shouldIncludePlaylists": true, - "shouldIncludeVideos": false, - "shouldIncludeLives": false, - "page": context.page, - "limit": VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], - "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], - "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - } - - - const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), - { - operationName: 'SEARCH_QUERY', - variables: variables, - query: MAIN_SEARCH_QUERY, - headers: undefined - }); - - const playlistConnection = jsonResponse?.data?.search?.playlists as CollectionConnection; - - const searchResults = playlistConnection?.edges?.map(edge => { - return SourceCollectionToGrayjayPlaylist(config.id, edge?.node); - }); - - const hasMore = playlistConnection?.pageInfo?.hasNextPage; - - if (!searchResults || searchResults.length === 0) { - return new PlaylistPager([]); - } - - const params = { - query: context.q, - sort: context.sort, - filters: context.filters, - } - - return new SearchPlaylistPager(searchResults, hasMore, params, context.page, searchPlaylists); -} - - -//Internals - - -function getVideoPager(params, page) { - - const count = VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex]; - - if (!params) { - params = {}; - } - - params = { ...params, count } - - - const headersToAdd = { - "User-Agent": USER_AGENT, - "Referer": BASE_URL, - "Content-Type": "application/json", - "X-DM-AppInfo-Id": X_DM_AppInfo_Id, - "X-DM-AppInfo-Type": X_DM_AppInfo_Type, - "X-DM-AppInfo-Version": X_DM_AppInfo_Version, - "X-DM-Neon-SSR": X_DM_Neon_SSR, - "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), - "Origin": BASE_URL, - "DNT": "1", - "Sec-Fetch-Site": "same-site", - "Priority": "u=4", - "Pragma": "no-cache", - "Cache-Control": "no-cache" - }; - - - let obj; - - const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); - - try { - obj = executeGqlQuery( - anonymousHttpClient, - { - operationName: 'SEACH_DISCOVERY_QUERY', - variables: { - avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], - }, - query: HOME_QUERY, - headers: headersToAdd, - }); - - } catch (error) { - return new VideoPager([], false, { params }); - } - - const results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges - ?.filter(edge => edge?.node?.id) - ?.map(edge => { - - return SourceVideoToGrayjayVideo(config.id, edge.node as Video); - - }) - - const hasMore = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.pageInfo?.hasNextPage ?? false; - return new SearchPagerAll(results, hasMore, params, page, getVideoPager); -} - - - -function getChannelContentsPager(context) { - - const url = context.url; - - const channel_name = getChannelNameFromUrl(url); - - let shouldLoadLives = true; - let shouldLoadVideos = true; - - if (context.shouldLoadVideos === undefined) { - shouldLoadVideos = context.type === Type.Feed.Videos || context.type === Type.Feed.Mixed; - } - - if (context.shouldLoadLives === undefined) { - shouldLoadLives = context.type === Type.Feed.Live || context.type === Type.Feed.Mixed; - } - - /** - Recent = Sort liked medias by most recent. - Visited - Sort liked medias by most viewed - */ - let sort: string; - - if (context.order == Type.Order.Chronological) { - sort = LikedMediaSort.Recent; - } else if (context.order == "Popular") { - sort = LikedMediaSort.Visited; - } else { - sort = LikedMediaSort.Recent; - } - - const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); - const jsonResponse = executeGqlQuery( - anonymousHttpClient, - { - operationName: 'CHANNEL_VIDEOS_QUERY', - variables: { - channel_name, - sort, - page: context.page ?? 1, - allowExplicit: !_settings.hideSensitiveContent, - first: context.page_size ?? VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], - avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], - shouldLoadLives, - shouldLoadVideos - }, - query: CHANNEL_VIDEOS_BY_CHANNEL_NAME - }); - - const channel = jsonResponse?.data?.channel as Channel; - - const all: (VideoEdge | LiveEdge | null)[] = [ - ...(channel?.lives?.edges ?? []), - ...(channel?.videos?.edges ?? []) - ]; - - let videos = all - .map((edge => SourceVideoToGrayjayVideo(config.id, edge.node))); - - - const videosHasNext = channel?.videos?.pageInfo?.hasNextPage; - const livesHasNext = channel?.lives?.pageInfo?.hasNextPage; - const hasNext = videosHasNext || livesHasNext || false; - - - context.shouldLoadVideos = videosHasNext; - context.shouldLoadLives = livesHasNext; - - context.page++; - - - return new ChannelVideoPager(context, videos, hasNext, getChannelContentsPager); -} - -function getSearchPagerAll(contextQuery): VideoPager { - - const context = getQuery(contextQuery); - - const variables = { - "query": context.q, - "sortByVideos": context.sort, - "durationMaxVideos": context.filters?.durationMaxVideos, - "durationMinVideos": context.filters?.durationMinVideos, - "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 - "shouldIncludeChannels": false, - "shouldIncludePlaylists": false, - "shouldIncludeVideos": true, - "shouldIncludeLives": true, - "page": context.page ?? 1, - "limit": VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], - "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] - } - - - const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), - { - operationName: 'SEARCH_QUERY', - variables: variables, - query: MAIN_SEARCH_QUERY, - headers: undefined - }); - - - const videoConnection = jsonResponse?.data?.search?.videos as VideoConnection; - const liveConnection = jsonResponse?.data?.search?.lives as LiveConnection; - - const all: (VideoEdge | LiveEdge | null)[] = [ - ...(videoConnection?.edges ?? []), - ...(liveConnection?.edges ?? []) - ] - - const results: PlatformVideo[] = all.map(edge => SourceVideoToGrayjayVideo(config.id, edge?.node)); - - const params = { - query: context.q, - sort: context.sort, - filters: context.filters, - } - return new SearchPagerAll(results, videoConnection?.pageInfo?.hasNextPage, params, context.page, getSearchPagerAll); -} - - -function getSavedVideo(url, usePlatformAuth = false) { - - const id = url.split('/').pop(); - - const player_metadata_url = `${BASE_URL_METADATA}/${id}?embedder=https%3A%2F%2Fwww.dailymotion.com%2Fvideo%2Fx8yb2e8&geo=1&player-id=xjnde&locale=en-GB&dmV1st=ce2035cd-bdca-4d7b-baa4-127a17490ca5&dmTs=747022&is_native_app=0&app=com.dailymotion.neon&client_type=webapp§ion_type=player&component_style=_`; - - const headers1 = { - "User-Agent": USER_AGENT, - "Accept": "*/*", - // "Accept-Encoding": "gzip, deflate, br, zstd", - "Referer": "https://geo.dailymotion.com/", - "Origin": "https://geo.dailymotion.com", - "DNT": "1", - "Connection": "keep-alive", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "Pragma": "no-cache", - "Cache-Control": "no-cache" - } - - if (_settings.hideSensitiveContent) { - headers1["Cookie"] = "ff=on" - } else { - headers1["Cookie"] = "ff=off" - } - - const player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); - - if (!player_metadataResponse.isOk) { - throw new UnavailableException('Unable to get player metadata'); - } - - const player_metadata = JSON.parse(player_metadataResponse.body); - - if (player_metadata.error) { - - if (player_metadata.error.code && ERROR_TYPES[player_metadata.error.code] !== undefined) { - throw new UnavailableException(ERROR_TYPES[player_metadata.error.code]); - } - - throw new UnavailableException('This content is not available'); - } - - const videoDetailsRequestHeaders = { - "Content-Type": "application/json", - "User-Agent": USER_AGENT, - "Accept": "*/*, */*", - "Referer": `${BASE_URL_VIDEO}/${id}`, - "X-DM-AppInfo-Id": X_DM_AppInfo_Id, - "X-DM-AppInfo-Type": X_DM_AppInfo_Type, - "X-DM-AppInfo-Version": X_DM_AppInfo_Version, - "X-DM-Neon-SSR": X_DM_Neon_SSR, - "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), - "Origin": BASE_URL, - "DNT": "1", - "Connection": "keep-alive", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "Priority": "u=4", - "Pragma": "no-cache", - "Cache-Control": "no-cache" - }; - - if (!usePlatformAuth) { - videoDetailsRequestHeaders.Authorization = getAnonymousUserTokenSingleton(); - } - - const variables = { - "xid": id, - "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], - "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] - }; - - const videoDetailsRequestBody = JSON.stringify( - { - operationName: "WATCHING_VIDEO", - variables, - query: VIDEO_DETAILS_QUERY - }); - - const video_details_response = getHttpContext({ usePlatformAuth }).POST(BASE_URL_API, videoDetailsRequestBody, videoDetailsRequestHeaders, usePlatformAuth) - - if (video_details_response.code != 200) { - throw new UnavailableException('Failed to get video details'); - } - - const video_details = JSON.parse(video_details_response.body); - - const sources: HLSSource[] = [ - new HLSSource( - { - name: 'source', - duration: player_metadata?.duration, - url: player_metadata?.qualities?.auto[0]?.url, - } - ) - ] - - const video = video_details?.data?.video as Video; - - const subtitles = player_metadata?.subtitles as IDailymotionSubtitle; - - const platformVideoDetails: PlatformVideoDetailsDef = SourceVideoToPlatformVideoDetailsDef(config.id, video, sources, subtitles); - - return new PlatformVideoDetails(platformVideoDetails) -} - -function getSearchChannelPager(context) { - - const searchResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), { - operationName: "SEARCH_QUERY", - variables: { - query: context.q, - page: context.page ?? 1, - limit: VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], - avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] - }, - query: SEARCH_CHANNEL - }); - - const results = searchResponse?.data?.search?.channels?.edges.map(edge => { - const channel = edge.node as Channel; - return SourceChannelToGrayjayChannel(config.id, `${BASE_URL}/${channel.name}`, channel); - }); - - const params = { - query: context.q, - } - - return new SearchChannelPager(results, searchResponse?.data?.search?.channels?.pageInfo?.hasNextPage, params, context.page, getSearchChannelPager); - -} - -function getChannelPlaylists(url: string, page: number = 1): SearchPlaylistPager { - - - const headers = { - 'Content-Type': 'application/json', - 'User-Agent': USER_AGENT, - 'Accept-Language': 'en-GB', - Referer: `${BASE_URL}/library/subscriptions`, - 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, - 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, - 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, - 'X-DM-Neon-SSR': '0', - 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), - Origin: BASE_URL, - DNT: '1', - Connection: 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site', - Priority: 'u=4', - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - }; - - const usePlatformAuth = false; - const channel_name = getChannelNameFromUrl(url); - - const jsonResponse1 = executeGqlQuery( - http, - { - operationName: 'CHANNEL_PLAYLISTS_QUERY', - variables: { - channel_name, - sort: "recent", - page, - first: PLAYLISTS_PER_PAGE_OPTIONS[_settings.playlistsPerPageIndex], - avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], - thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], - }, - headers, - query: GET_CHANNEL_PLAYLISTS, - usePlatformAuth - } - ) - - const channel = (jsonResponse1.data.channel as Channel); - - const content = channel?.collections?.edges?.map(edge => { - return SourceCollectionToGrayjayPlaylist(config.id, edge.node); - }); - - if (content?.length === 0) { - return new ChannelPlaylistPager([]); - } - - const params = { - url - } - - const hasMore = channel?.collections?.pageInfo?.hasNextPage ?? false; - - return new ChannelPlaylistPager(content, hasMore, params, page, getChannelPlaylists); -} - -function getHttpContext(opts: { usePlatformAuth: boolean } = { usePlatformAuth: false }): IHttp { - return opts.usePlatformAuth ? http : httpClientAnonymous; -} - +let config: Config; +let _settings: IDailymotionPluginSettings; + + +import { + CREATOR_AVATAR_HEIGHT, + THUMBNAIL_HEIGHT, + BASE_URL, + SEARCH_CAPABILITIES, + BASE_URL_VIDEO, + BASE_URL_PLAYLIST, + USER_AGENT, + X_DM_AppInfo_Id, + X_DM_AppInfo_Type, + X_DM_AppInfo_Version, + X_DM_Neon_SSR, + BASE_URL_API, + BASE_URL_METADATA, + ERROR_TYPES, + LikedMediaSort, + VIDEOS_PER_PAGE_OPTIONS, + PLAYLISTS_PER_PAGE_OPTIONS +} from './constants'; + +import { + SEARCH_SUGGESTIONS_QUERY, + CHANNEL_BY_URL_QUERY, + PLAYLIST_DETAILS_QUERY, + GET_USER_SUBSCRIPTIONS, + MAIN_SEARCH_QUERY, + HOME_QUERY, + CHANNEL_VIDEOS_BY_CHANNEL_NAME, + VIDEO_DETAILS_QUERY, + SEARCH_CHANNEL, + GET_CHANNEL_PLAYLISTS, + SUBSCRIPTIONS_QUERY, + GET_CHANNEL_PLAYLISTS_XID +} from './gqlQueries'; + +import { + getChannelNameFromUrl, + isUsernameUrl, + executeGqlQuery, + getPreferredCountry, + getAnonymousUserTokenSingleton, + getQuery +} from './util'; + +import { + Channel, + Collection, + CollectionConnection, + Live, + LiveConnection, + LiveEdge, + SuggestionConnection, + Video, + VideoConnection, + VideoEdge +} from '../types/CodeGenDailymotion'; + +import { + SearchPagerAll, + SearchChannelPager, + ChannelVideoPager, + SearchPlaylistPager, + ChannelPlaylistPager +} from './Pagers'; + + +import { + SourceChannelToGrayjayChannel, + SourceCollectionToGrayjayPlaylist, + SourceCollectionToGrayjayPlaylistDetails, + SourceVideoToGrayjayVideo, + SourceVideoToPlatformVideoDetailsDef +} from './Mappers'; + + +if (IS_TESTING) { + + + if (!_settings) { + _settings = {} + } + + _settings.hideSensitiveContent = false; + _settings.avatarSize = 8; + _settings.thumbnailResolution = 7; + _settings.preferredCountry = 0; + _settings.videosPerPageIndex = 4; + _settings.playlistsPerPageIndex = 0; + + if (!config) { + config = { + id: "9c87e8db-e75d-48f4-afe5-2d203d4b95c5" + } + } +} + +let httpClientAnonymous: IHttp = http.newClient(false); + + +// Will be used to store private playlists that require authentication +const authenticatedPlaylistCollection: string[] = []; + +source.setSettings = function (settings) { + _settings = settings; + http.GET(BASE_URL, {}, true); +} + +//Source Methods +source.enable = function (conf, settings, saveStateStr) { + + config = conf ?? {}; + _settings = settings ?? {}; + +} + + +source.getHome = function () { + + getAnonymousUserTokenSingleton(); + + return getVideoPager({}, 0); +}; + +source.searchSuggestions = function (query): string[] { + + try { + + const jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth: false }), + { + operationName: 'AUTOCOMPLETE_QUERY', + variables: { + query + }, + query: SEARCH_SUGGESTIONS_QUERY + }); + + return (jsonResponse?.data?.search?.suggestedVideos as SuggestionConnection)?.edges?.map(edge => edge?.node?.name ?? "") ?? []; + } catch (error: any) { + log('Failed to get search suggestions:' + error?.message); + return []; + } +}; + + +source.getSearchCapabilities = (): ResultCapabilities => SEARCH_CAPABILITIES; + + +source.search = function (query: string, type: string, order: string, filters) { + return getSearchPagerAll({ q: query, page: 1, type, order, filters }); +} + +source.searchChannels = function (query) { + return getSearchChannelPager({ q: query, page: 1 }) +} + +//Channel +source.isChannelUrl = function (url) { + return isUsernameUrl(url); +}; + +source.getChannel = function (url) { + + const channel_name = getChannelNameFromUrl(url); + + const channelDetails = executeGqlQuery( + getHttpContext({ usePlatformAuth: false }), + { + operationName: 'CHANNEL_QUERY_DESKTOP', + variables: { + channel_name, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] + }, + query: CHANNEL_BY_URL_QUERY + }); + + return SourceChannelToGrayjayChannel(config.id, url, channelDetails.data.channel as Channel); + +}; + +source.getChannelContents = function (url, type, order, filters) { + + const page = 1; + return getChannelContentsPager( + url, + page, + type, + order, + filters + ) +} + +source.getChannelPlaylists = (url): SearchPlaylistPager => { + try { + return getChannelPlaylists(url, 1); + } catch (error) { + log('Failed to get channel playlists:' + error?.message); + return new ChannelPlaylistPager([]); + } +} + +source.getChannelCapabilities = (): ResultCapabilities => { + return { + types: [Type.Feed.Mixed], + sorts: [Type.Order.Chronological, "Popular"], + filters: [] + }; +}; + +//Video +source.isContentDetailsUrl = function (url) { + return url.startsWith(BASE_URL_VIDEO); +}; + +source.getContentDetails = function (url) { + return getSavedVideo(url, false); +}; + +//Playlist +source.isPlaylistUrl = (url): boolean => { + return url.startsWith(BASE_URL_PLAYLIST); +}; + +source.searchPlaylists = (query, type, order, filters) => { + return searchPlaylists({ q: query, type, order, filters }); +}; + +source.getPlaylist = (url: string): PlatformPlaylistDetails => { + + const xid = url.split('/').pop(); + + const variables = { + xid, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], + } + + const usePlatformAuth = authenticatedPlaylistCollection.includes(url); + + let jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth }), + { + operationName: 'PLAYLIST_VIDEO_QUERY', + variables, + query: PLAYLIST_DETAILS_QUERY, + usePlatformAuth + }); + + const videos: PlatformVideo[] = jsonResponse?.data?.collection?.videos?.edges.map(edge => { + return SourceVideoToGrayjayVideo(config.id, edge.node as Video); + }); + + return SourceCollectionToGrayjayPlaylistDetails(config.id, jsonResponse?.data?.collection as Collection, videos); + +} + +source.getUserSubscriptions = (): string[] => { + + if (!bridge.isLoggedIn()) { + bridge.log("Failed to retrieve subscriptions page because not logged in."); + throw new ScriptException("Not logged in"); + } + + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT, + // Accept: '*/*, */*', + 'Accept-Language': 'en-GB', + Referer: `${BASE_URL}/library/subscriptions`, + 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, + 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, + 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, + 'X-DM-Neon-SSR': '0', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), + Origin: BASE_URL, + DNT: '1', + Connection: 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + Priority: 'u=4', + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + } + + const usePlatformAuth = true; + + const fetchSubscriptions = (page, first): string[] => { + const jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth }), + { + operationName: 'SUBSCRIPTIONS_QUERY', + variables: { + first: first, + page: page, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + }, + headers, + query: GET_USER_SUBSCRIPTIONS, + usePlatformAuth + }); + + return (jsonResponse?.data?.me?.channel as Channel)?.followings?.edges?.map(edge => edge?.node?.creator?.name ?? "") ?? []; + }; + + const first = 100; // Number of records to fetch per page + let page = 1; + let subscriptions: string[] = []; + + // There is a totalCount ($.data.me.channel.followings.totalCount) property but it's not reliable. + // For example, it may return 0 even if there are subscriptions, or it may return a number that is not the actual number of subscriptions. + // For now, it's better to fetch until no more results are returned + + let items: string[] = []; + + do { + const response = fetchSubscriptions(page, first); + + items = response.map(creatorName => `${BASE_URL}/${creatorName}`); + + subscriptions.push(...items); + page++; + } while (items.length); + + return subscriptions; +}; + + +source.getUserPlaylists = (): string[] => { + + if (!bridge.isLoggedIn()) { + bridge.log("Failed to retrieve subscriptions page because not logged in."); + throw new ScriptException("Not logged in"); + } + + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT, + // Accept: '*/*, */*', + 'Accept-Language': 'en-GB', + Referer: `${BASE_URL}/library/subscriptions`, + 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, + 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, + 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, + 'X-DM-Neon-SSR': '0', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), + Origin: BASE_URL, + DNT: '1', + Connection: 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + Priority: 'u=4', + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + } + + const jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth: true }), + { + operationName: 'SUBSCRIPTIONS_QUERY', + headers, + query: SUBSCRIPTIONS_QUERY, + usePlatformAuth: true + }); + + const userName = (jsonResponse?.data?.me?.channel as Channel)?.name; + + return getPlaylistsByUsername(userName, headers, true); + +} + +function getPlaylistsByUsername(userName, headers, usePlatformAuth = false) { + + + const jsonResponse1 = executeGqlQuery( + getHttpContext({ usePlatformAuth }), + { + operationName: 'CHANNEL_PLAYLISTS_QUERY', + variables: { + channel_name: userName, + sort: "recent", + page: 1, + first: 99, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], + }, + headers, + query: GET_CHANNEL_PLAYLISTS_XID, + usePlatformAuth + } + ); + + const playlists = jsonResponse1.data.channel.collections.edges.map(edge => { + const playlistUrl = `${BASE_URL_PLAYLIST}/${edge.node.xid}`; + if (!authenticatedPlaylistCollection.includes(playlistUrl)) { + authenticatedPlaylistCollection.push(playlistUrl); + } + return playlistUrl; + }); + + + return playlists; + +} + + +function searchPlaylists(contextQuery) { + + const context = getQuery(contextQuery); + + const variables = { + "query": context.q, + "sortByVideos": context.sort, + "durationMaxVideos": context.filters?.durationMaxVideos, + "durationMinVideos": context.filters?.durationMinVideos, + "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 + "shouldIncludeChannels": false, + "shouldIncludePlaylists": true, + "shouldIncludeVideos": false, + "shouldIncludeLives": false, + "page": context.page, + "limit": VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + } + + + const jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth: false }), + { + operationName: 'SEARCH_QUERY', + variables: variables, + query: MAIN_SEARCH_QUERY, + headers: undefined + }); + + const playlistConnection = jsonResponse?.data?.search?.playlists as CollectionConnection; + + const searchResults = playlistConnection?.edges?.map(edge => { + return SourceCollectionToGrayjayPlaylist(config.id, edge?.node); + }); + + const hasMore = playlistConnection?.pageInfo?.hasNextPage; + + if (!searchResults || searchResults.length === 0) { + return new PlaylistPager([]); + } + + const params = { + query: context.q, + sort: context.sort, + filters: context.filters, + } + + return new SearchPlaylistPager(searchResults, hasMore, params, context.page, searchPlaylists); +} + + +//Internals + + +function getVideoPager(params, page) { + + const count = VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex]; + + if (!params) { + params = {}; + } + + params = { ...params, count } + + + const headersToAdd = { + "User-Agent": USER_AGENT, + "Referer": BASE_URL, + "Content-Type": "application/json", + "X-DM-AppInfo-Id": X_DM_AppInfo_Id, + "X-DM-AppInfo-Type": X_DM_AppInfo_Type, + "X-DM-AppInfo-Version": X_DM_AppInfo_Version, + "X-DM-Neon-SSR": X_DM_Neon_SSR, + "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), + "Origin": BASE_URL, + "DNT": "1", + "Sec-Fetch-Site": "same-site", + "Priority": "u=4", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + }; + + + let obj; + + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); + + try { + obj = executeGqlQuery( + anonymousHttpClient, + { + operationName: 'SEACH_DISCOVERY_QUERY', + variables: { + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], + }, + query: HOME_QUERY, + headers: headersToAdd, + }); + + } catch (error) { + return new VideoPager([], false, { params }); + } + + const results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges + ?.filter(edge => edge?.node?.id) + ?.map(edge => { + + return SourceVideoToGrayjayVideo(config.id, edge.node as Video); + + }) + + const hasMore = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.pageInfo?.hasNextPage ?? false; + return new SearchPagerAll(results, hasMore, params, page, getVideoPager); +} + +function getChannelContentsPager(url, page, type, order, filters) { + + const channel_name = getChannelNameFromUrl(url); + + const shouldLoadVideos = type === Type.Feed.Mixed || type === Type.Feed.Videos; + const shouldLoadLives = type === Type.Feed.Mixed || type === Type.Feed.Streams || type === Type.Feed.Live; + + if (IS_TESTING) { + bridge.log(`Getting channel contents for ${url}, page: ${page}, type: ${type}, order: ${order}, shouldLoadVideos: ${shouldLoadVideos}, shouldLoadLives: ${shouldLoadLives}, filters: ${JSON.stringify(filters)}`); + } + + /** + Recent = Sort liked medias by most recent. + Visited - Sort liked medias by most viewed + */ + let sort: string; + + if (order == Type.Order.Chronological) { + sort = LikedMediaSort.Recent; + } else if (order == "Popular") { + sort = LikedMediaSort.Visited; + } else { + sort = LikedMediaSort.Recent; + } + + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); + const jsonResponse = executeGqlQuery( + anonymousHttpClient, + { + operationName: 'CHANNEL_VIDEOS_QUERY', + variables: { + channel_name, + sort, + page: page ?? 1, + allowExplicit: !_settings.hideSensitiveContent, + first: VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], + shouldLoadLives, + shouldLoadVideos + }, + query: CHANNEL_VIDEOS_BY_CHANNEL_NAME + }); + + const channel = jsonResponse?.data?.channel as Channel; + + const all: (Live | Video)[] = [ + ...(channel?.lives?.edges?.map(e => e?.node as Live) ?? []), + ...(channel?.videos?.edges?.map(e => e?.node as Video) ?? []) + ]; + + let videos = all + .map((node => SourceVideoToGrayjayVideo(config.id, node))); + + + const videosHasNext = channel?.videos?.pageInfo?.hasNextPage; + const livesHasNext = channel?.lives?.pageInfo?.hasNextPage; + const hasNext = videosHasNext || livesHasNext || false; + + const params = { + url, + type, + order, + page, + filters + } + + return new ChannelVideoPager(videos, hasNext, params, getChannelContentsPager); +} + +function getSearchPagerAll(contextQuery): VideoPager { + + const context = getQuery(contextQuery); + + const variables = { + "query": context.q, + "sortByVideos": context.sort, + "durationMaxVideos": context.filters?.durationMaxVideos, + "durationMinVideos": context.filters?.durationMinVideos, + "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 + "shouldIncludeChannels": false, + "shouldIncludePlaylists": false, + "shouldIncludeVideos": true, + "shouldIncludeLives": true, + "page": context.page ?? 1, + "limit": VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] + } + + + const jsonResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth: false }), + { + operationName: 'SEARCH_QUERY', + variables: variables, + query: MAIN_SEARCH_QUERY, + headers: undefined + }); + + + const videoConnection = jsonResponse?.data?.search?.videos as VideoConnection; + const liveConnection = jsonResponse?.data?.search?.lives as LiveConnection; + + const all: (VideoEdge | LiveEdge | null)[] = [ + ...(videoConnection?.edges ?? []), + ...(liveConnection?.edges ?? []) + ] + + const results: PlatformVideo[] = all.map(edge => SourceVideoToGrayjayVideo(config.id, edge?.node)); + + const params = { + query: context.q, + sort: context.sort, + filters: context.filters, + } + return new SearchPagerAll(results, videoConnection?.pageInfo?.hasNextPage, params, context.page, getSearchPagerAll); +} + + +function getSavedVideo(url, usePlatformAuth = false) { + + const id = url.split('/').pop(); + + const player_metadata_url = `${BASE_URL_METADATA}/${id}?embedder=https%3A%2F%2Fwww.dailymotion.com%2Fvideo%2Fx8yb2e8&geo=1&player-id=xjnde&locale=en-GB&dmV1st=ce2035cd-bdca-4d7b-baa4-127a17490ca5&dmTs=747022&is_native_app=0&app=com.dailymotion.neon&client_type=webapp§ion_type=player&component_style=_`; + + const headers1 = { + "User-Agent": USER_AGENT, + "Accept": "*/*", + // "Accept-Encoding": "gzip, deflate, br, zstd", + "Referer": "https://geo.dailymotion.com/", + "Origin": "https://geo.dailymotion.com", + "DNT": "1", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + } + + if (_settings.hideSensitiveContent) { + headers1["Cookie"] = "ff=on" + } else { + headers1["Cookie"] = "ff=off" + } + + const player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); + + if (!player_metadataResponse.isOk) { + throw new UnavailableException('Unable to get player metadata'); + } + + const player_metadata = JSON.parse(player_metadataResponse.body); + + if (player_metadata.error) { + + if (player_metadata.error.code && ERROR_TYPES[player_metadata.error.code] !== undefined) { + throw new UnavailableException(ERROR_TYPES[player_metadata.error.code]); + } + + throw new UnavailableException('This content is not available'); + } + + const videoDetailsRequestHeaders: IDictionary<string> = { + "Content-Type": "application/json", + "User-Agent": USER_AGENT, + "Accept": "*/*, */*", + "Referer": `${BASE_URL_VIDEO}/${id}`, + "X-DM-AppInfo-Id": X_DM_AppInfo_Id, + "X-DM-AppInfo-Type": X_DM_AppInfo_Type, + "X-DM-AppInfo-Version": X_DM_AppInfo_Version, + "X-DM-Neon-SSR": X_DM_Neon_SSR, + "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), + "Origin": BASE_URL, + "DNT": "1", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Priority": "u=4", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + }; + + if (!usePlatformAuth) { + videoDetailsRequestHeaders.Authorization = getAnonymousUserTokenSingleton(); + } + + const variables = { + "xid": id, + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] + }; + + const videoDetailsRequestBody = JSON.stringify( + { + operationName: "WATCHING_VIDEO", + variables, + query: VIDEO_DETAILS_QUERY + }); + + const video_details_response = getHttpContext({ usePlatformAuth }).POST(BASE_URL_API, videoDetailsRequestBody, videoDetailsRequestHeaders, usePlatformAuth) + + if (video_details_response.code != 200) { + throw new UnavailableException('Failed to get video details'); + } + + const video_details = JSON.parse(video_details_response.body); + + const sources: HLSSource[] = [ + new HLSSource( + { + name: 'source', + duration: player_metadata?.duration, + url: player_metadata?.qualities?.auto[0]?.url, + } + ) + ] + + const video = video_details?.data?.video as Video; + + const subtitles = player_metadata?.subtitles as IDailymotionSubtitle; + + const platformVideoDetails: PlatformVideoDetailsDef = SourceVideoToPlatformVideoDetailsDef(config.id, video, sources, subtitles); + + return new PlatformVideoDetails(platformVideoDetails) +} + +function getSearchChannelPager(context) { + + const searchResponse = executeGqlQuery( + getHttpContext({ usePlatformAuth: false }), { + operationName: "SEARCH_QUERY", + variables: { + query: context.q, + page: context.page ?? 1, + limit: VIDEOS_PER_PAGE_OPTIONS[_settings.videosPerPageIndex], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] + }, + query: SEARCH_CHANNEL + }); + + const results = searchResponse?.data?.search?.channels?.edges.map(edge => { + const channel = edge.node as Channel; + return SourceChannelToGrayjayChannel(config.id, `${BASE_URL}/${channel.name}`, channel); + }); + + const params = { + query: context.q, + } + + return new SearchChannelPager(results, searchResponse?.data?.search?.channels?.pageInfo?.hasNextPage, params, context.page, getSearchChannelPager); + +} + +function getChannelPlaylists(url: string, page: number = 1): SearchPlaylistPager { + + + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT, + 'Accept-Language': 'en-GB', + Referer: `${BASE_URL}/library/subscriptions`, + 'X-DM-AppInfo-Id': X_DM_AppInfo_Id, + 'X-DM-AppInfo-Type': X_DM_AppInfo_Type, + 'X-DM-AppInfo-Version': X_DM_AppInfo_Version, + 'X-DM-Neon-SSR': '0', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), + Origin: BASE_URL, + DNT: '1', + Connection: 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + Priority: 'u=4', + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + }; + + const usePlatformAuth = false; + const channel_name = getChannelNameFromUrl(url); + + const jsonResponse1 = executeGqlQuery( + http, + { + operationName: 'CHANNEL_PLAYLISTS_QUERY', + variables: { + channel_name, + sort: "recent", + page, + first: PLAYLISTS_PER_PAGE_OPTIONS[_settings.playlistsPerPageIndex], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], + }, + headers, + query: GET_CHANNEL_PLAYLISTS, + usePlatformAuth + } + ) + + const channel = (jsonResponse1.data.channel as Channel); + + const content: PlatformPlaylist[] = (channel?.collections?.edges ?? []).map(edge => { + return SourceCollectionToGrayjayPlaylist(config.id, edge?.node); + }); + + if (content?.length === 0) { + return new ChannelPlaylistPager([]); + } + + const params = { + url + } + + const hasMore = channel?.collections?.pageInfo?.hasNextPage ?? false; + + return new ChannelPlaylistPager(content, hasMore, params, page, getChannelPlaylists); +} + +function getHttpContext(opts: { usePlatformAuth: boolean } = { usePlatformAuth: false }): IHttp { + return opts.usePlatformAuth ? http : httpClientAnonymous; +} + log("LOADED"); \ No newline at end of file diff --git a/src/Mappers.ts b/src/Mappers.ts index 92c4fa1..a13369b 100644 --- a/src/Mappers.ts +++ b/src/Mappers.ts @@ -34,26 +34,24 @@ export const SourceAuthorToGrayjayPlatformAuthorLink = (pluginId: string, creato ); } -export const SourceVideoToGrayjayVideo = (pluginId: string, sourceVideo: Video | Live): PlatformVideo => { +export const SourceVideoToGrayjayVideo = (pluginId: string, sourceVideo?: Video | Live): PlatformVideo => { - // const metadata = GetVideoExtraDetails(anonymousHttpClient, sv.xid); - // const viewCount = metadata.views ?? 0; const isLive = getIsLive(sourceVideo); - let viewCount = getViewCount(sourceVideo); + const viewCount = getViewCount(sourceVideo); const video: PlatformVideoDef = { - id: new PlatformID(PLATFORM, sourceVideo.id, pluginId, PLATFORM_CLAIMTYPE), + id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), description: sourceVideo?.description ?? '', name: sourceVideo?.title ?? "", thumbnails: new Thumbnails([ new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0) ]), - author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo.creator), - uploadDate: Math.floor(new Date(sourceVideo.createdAt).getTime() / 1000), - datetime: Math.floor(new Date(sourceVideo.createdAt).getTime() / 1000), + author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator), + uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000), + datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000), url: `${BASE_URL_VIDEO}/${sourceVideo?.xid}`, - duration: sourceVideo?.duration ?? 0, + duration: (sourceVideo as Video)?.duration ?? 0, viewCount, isLive }; @@ -73,20 +71,19 @@ export const SourceCollectionToGrayjayPlaylistDetails = (pluginId: string, sourc }); } -export const SourceCollectionToGrayjayPlaylist = (pluginId: string, sourceCollection: Collection): PlatformPlaylist => { +export const SourceCollectionToGrayjayPlaylist = (pluginId: string, sourceCollection?: Maybe<Collection>): PlatformPlaylist => { return new PlatformPlaylist({ url: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`, id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE), - author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection.creator), + author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection?.creator), name: sourceCollection?.name, thumbnail: sourceCollection?.thumbnail?.url, videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total, }); } -const getIsLive = (sourceVideo: Video | Live): boolean => { - return sourceVideo?.duration == undefined; - // return sourceVideo?.isOnAir === true; +const getIsLive = (sourceVideo?: Video | Live): boolean => { + return (sourceVideo as Live)?.isOnAir === true || (sourceVideo as Video)?.duration == undefined; } const getViewCount = (sourceVideo: Video | Live): number => { @@ -104,7 +101,7 @@ const getViewCount = (sourceVideo: Video | Live): number => { export const SourceVideoToPlatformVideoDetailsDef = ( pluginId: string, - sourceVideo: Video, + sourceVideo: Video | Live, sources: HLSSource[], sourceSubtitle: IDailymotionSubtitle ): PlatformVideoDetailsDef => { @@ -127,20 +124,20 @@ export const SourceVideoToPlatformVideoDetailsDef = ( const isLive = getIsLive(sourceVideo); const viewCount = getViewCount(sourceVideo); + const duration = isLive ? 0 : (sourceVideo as Video)?.duration ?? 0; const platformVideoDetails: PlatformVideoDetailsDef = { - id: new PlatformID(PLATFORM, sourceVideo.id, pluginId, PLATFORM_CLAIMTYPE), + id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), name: sourceVideo?.title ?? "", thumbnails: new Thumbnails([new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0)]), author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator), uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000), datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000), - duration: sourceVideo?.duration ?? 0, + duration, // viewCount, - viewCount: sourceVideo?.stats?.views?.total ?? 0, - url: `${BASE_URL_VIDEO}/${sourceVideo.xid}`, - // isLive, - isLive: sourceVideo?.duration == undefined, + viewCount, + url: sourceVideo?.xid ? `${BASE_URL_VIDEO}/${sourceVideo.xid}` : "", + isLive, description: sourceVideo?.description ?? "", video: new VideoSourceDescriptor(sources), rating: new RatingLikesDislikes(positiveRatingCount, negativeRatingCount), diff --git a/src/Pagers.ts b/src/Pagers.ts index 8dc9c1d..4ee15c7 100644 --- a/src/Pagers.ts +++ b/src/Pagers.ts @@ -1,86 +1,87 @@ -export class SearchPagerAll extends VideoPager { - cb: Function; - - constructor(results: PlatformVideo[], hasMore: boolean, params: any, page: number, cb: Function) { - super(results, hasMore, { params, page }); - this.cb = cb; - } - - nextPage() { - this.context.page += 1; - - const opts = { - q: this.context.params.query, - sort: this.context.params.sort, - page: this.context.page, - filters: this.context.params.filters - }; - - return this.cb(opts); - } -} - -export class SearchChannelPager extends ChannelPager { - cb: any; - constructor(results, hasNextPage, params, page, cb) { - super(results, hasNextPage, { params, page }) - this.cb = cb; - } - - nextPage() { - const opts = { q: this.context.params.query, page: this.context.page += 1 }; - return this.cb(opts); - } -} - - - -export class ChannelVideoPager extends VideoPager { - cb: Function; - constructor(context: any, results: PlatformVideo[], hasNextPage: boolean, cb: Function) { - super(results, hasNextPage, context); - this.cb = cb; - } - - nextPage() { - return this.cb(this.context) - } -} - - -export class ChannelPlaylistPager extends PlaylistPager { - cb: Function; - constructor(results: [], hasMore: boolean, params: any, page: number, cb: Function) { - super(results, hasMore, { params, page }); - this.cb = cb; - } - - nextPage() { - - this.context.page += 1; - - return this.cb(this.context.params.url, this.context.page) - } -} - -export class SearchPlaylistPager extends PlaylistPager { - cb: Function; - constructor(results: PlatformPlaylist[], hasMore: boolean, params: any, page: number, cb: Function) { - super(results, hasMore, { params, page }); - this.cb = cb; - } - - nextPage() { - - this.context.page = this.context.page + 1 - - const opts = { - q: this.context.params.query, - sort: this.context.params.sort, - page: this.context.page, - filters: this.context.params.filters - }; - - return this.cb(opts) - } +export class SearchPagerAll extends VideoPager { + cb: Function; + + constructor(results: PlatformVideo[], hasMore: boolean, params: any, page: number, cb: Function) { + super(results, hasMore, { params, page }); + this.cb = cb; + } + + nextPage() { + this.context.page += 1; + + const opts = { + q: this.context.params.query, + sort: this.context.params.sort, + page: this.context.page, + filters: this.context.params.filters + }; + + return this.cb(opts); + } +} + +export class SearchChannelPager extends ChannelPager { + cb: any; + constructor(results, hasNextPage, params, page, cb) { + super(results, hasNextPage, { params, page }) + this.cb = cb; + } + + nextPage() { + const opts = { q: this.context.params.query, page: this.context.page += 1 }; + return this.cb(opts); + } +} + + + +export class ChannelVideoPager extends VideoPager { + cb: Function; + constructor(results: PlatformVideo[], hasNextPage: boolean, params, cb: Function) { + super(results, hasNextPage, { ...params }); + this.cb = cb; + } + + nextPage() { + this.context.page += 1; + return this.cb(this.context.url, this.context.page, this.context.type, this.context.order); + } +} + + +export class ChannelPlaylistPager extends PlaylistPager { + cb: Function; + constructor(results: PlatformPlaylist[], hasMore: boolean, params: any, page: number, cb: Function) { + super(results, hasMore, { params, page }); + this.cb = cb; + } + + nextPage() { + + this.context.page += 1; + + return this.cb(this.context.params.url, this.context.page) + } +} + +export class SearchPlaylistPager extends PlaylistPager { + cb: Function; + constructor(results: PlatformPlaylist[], hasMore: boolean, params: any, page: number, cb: Function) { + super(results, hasMore, { params, page }); + this.cb = cb; + } + + nextPage() { + + this.context.page = this.context.page + 1 + + const opts = { + q: this.context.params.query, + sort: this.context.params.sort, + page: this.context.page, + filters: this.context.params.filters + }; + + return this.cb(opts) + } } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 25aaa28..89a6382 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -377,8 +377,7 @@ export const ERROR_TYPES = { export const SEARCH_CAPABILITIES = { types: [ - Type.Feed.Videos, - Type.Feed.Live + Type.Feed.Mixed ], sorts: [ "Most Recent", diff --git a/src/util.ts b/src/util.ts index 05875dc..91c83f4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,274 +1,275 @@ -let AUTHORIZATION_TOKEN_ANONYMOUS_USER: string = ""; -let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE: number; -let httpClientRequestToken: IHttp = http.newClient(false); - -import { - BASE_URL, - USER_AGENT, - BASE_URL_API, - X_DM_Preferred_Country, - COUNTRY_NAMES, - COUNTRY_NAMES_TO_CODE, - CLIENT_ID, - CLIENT_SECRET, - BASE_URL_API_AUTH, - DURATION_THRESHOLDS, -} from './constants' - -export function getPreferredCountry(preferredCountryIndex) { - const countryName = COUNTRY_NAMES[preferredCountryIndex]; - const code = COUNTRY_NAMES_TO_CODE[countryName]; - const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); - return preferredCountry; -} - -export const objectToUrlEncodedString = (obj) => { - - const encodedParams: string[] = []; - - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - - const encodedKey = encodeURIComponent(key); - const encodedValue = encodeURIComponent(obj[key]); - encodedParams.push(`${encodedKey}=${encodedValue}`); - } - } - - return encodedParams.join('&'); -} - - -export function getChannelNameFromUrl(url) { - const channel_name = url.split('/').pop(); - return channel_name; -} - -export function isUsernameUrl(url) { - - const regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); - - return regex.test(url); -} - - -export function getAnonymousUserTokenSingleton() { - // Check if the anonymous user token is available and not expired - if (AUTHORIZATION_TOKEN_ANONYMOUS_USER) { - - const isTokenValid = AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE && new Date().getTime() < AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE; - - if (isTokenValid) { - return AUTHORIZATION_TOKEN_ANONYMOUS_USER; - } - } - - // Prepare the request body for obtaining a new token - const body = objectToUrlEncodedString({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - grant_type: 'client_credentials' - }); - - // Make the HTTP POST request to the authorization API - const res = httpClientRequestToken.POST(`${BASE_URL_API_AUTH}`, body, { - 'User-Agent': USER_AGENT, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': BASE_URL, - 'DNT': '1', - 'Sec-GPC': '1', - 'Connection': 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site', - 'Priority': 'u=4', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache' - }, false); - - // Check if the response code indicates success - if (res.code !== 200) { - console.error('Failed to get token', res); - throw new ScriptException("", "Failed to get token: " + res.code + " - " + res.body); - } - - // Parse the response JSON to extract the token information - const json = JSON.parse(res.body); - - // Ensure the response contains the necessary token information - if (!json.token_type || !json.access_token) { - console.error('Invalid token response', res); - throw new ScriptException("", 'Invalid token response: ' + res.body); - } - - // Store the token and its expiration date - AUTHORIZATION_TOKEN_ANONYMOUS_USER = `${json.token_type} ${json.access_token}`; - AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE = new Date().getTime() + (json.expires_in * 1000); - - return AUTHORIZATION_TOKEN_ANONYMOUS_USER; -} - - -export function executeGqlQuery(httpClient, requestOptions) { - - const headersToAdd = requestOptions.headers || { - "User-Agent": USER_AGENT, - "Accept": "*/*", - // "Accept-Language": Accept_Language, - "Referer": BASE_URL, - "Origin": BASE_URL, - "DNT": "1", - "Connection": "keep-alive", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "Pragma": "no-cache", - "Cache-Control": "no-cache" - } - - - const gql = JSON.stringify({ - operationName: requestOptions.operationName, - variables: requestOptions.variables, - query: requestOptions.query, - }); - - const usePlatformAuth = requestOptions.usePlatformAuth == undefined ? false : requestOptions.usePlatformAuth; - const throwOnError = requestOptions.throwOnError == undefined ? true : requestOptions.throwOnError; - - if (!usePlatformAuth) { - headersToAdd.Authorization = getAnonymousUserTokenSingleton(); - } - - const res = httpClient.POST(BASE_URL_API, gql, headersToAdd, usePlatformAuth); - - if (!res.isOk) { - console.error('Failed to get token', res); - if (throwOnError) { - throw new ScriptException("Failed to get token", res); - } - } - - const body = JSON.parse(res.body); - - // some errors may be returned in the body with a status code 200 - if (body.errors) { - const message = body.errors.map(e => e.message).join(', '); - - if (throwOnError) { - throw new UnavailableException(message); - } - } - - return body; -} - - - -/** - * Converts SRT subtitle format to VTT format. - * - * @param {string} srt - The SRT subtitle string. - * @returns {string} - The converted VTT subtitle string. - */ -export const convertSRTtoVTT = (srt) => { - // Initialize the VTT output with the required header - const vtt = ['WEBVTT\n\n']; - // Split the SRT input into blocks based on double newlines - const srtBlocks = srt.split('\n\n'); - - // Process each block individually - srtBlocks.forEach((block) => { - // Split each block into lines - const lines = block.split('\n'); - if (lines.length >= 3) { - // Extract and convert the timestamp line - const timestamp = lines[1].replace(/,/g, '.'); - // Extract the subtitle text lines - const subtitleText = lines.slice(2).join('\n'); - // Add the converted block to the VTT output - vtt.push(`${timestamp}\n${subtitleText}\n\n`); - } - }); - - // Join the VTT array into a single string and return it - return vtt.join(''); -} - - - -export const parseUploadDateFilter = (filter) => { - let createdAfterVideos; - - const now = new Date(); - - switch (filter) { - case "today": - // Last 24 hours from now - const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); - createdAfterVideos = yesterday.toISOString(); - break; - case "thisweek": - // Adjusts to the start of the current week (assuming week starts on Sunday) - const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); - createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); - break; - case "thismonth": - // Adjusts to the start of the month - createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); - break; - case "thisyear": - // Adjusts to the start of the year - createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); - break; - default: - createdAfterVideos = null; - } - return createdAfterVideos; -} - - -export const parseSort = (order) => { - let sort; - switch (order) { - //TODO: refact this to use constants - case "Most Recent": - sort = "RECENT"; - break; - case "Most Viewed": - sort = "VIEW_COUNT"; - break; - case "Most Relevant": - sort = "RELEVANCE"; - break; - default: - sort = order; // Default to the original order if no match - } - return sort -} - -export const getQuery = (context) => { - context.sort = parseSort(context.order); - - if (!context.filters) { - context.filters = {}; - } - - if (!context.page) { - context.page = 1; - } - - if (context?.filters.duration) { - context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; - context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; - } else { - context.filters.durationMinVideos = null; - context.filters.durationMaxVideos = null; - } - - if (context.filters.uploaddate) { - context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); - } - - return context; +let AUTHORIZATION_TOKEN_ANONYMOUS_USER: string = ""; +let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE: number; +let httpClientRequestToken: IHttp = http.newClient(false); + +import { + BASE_URL, + USER_AGENT, + BASE_URL_API, + X_DM_Preferred_Country, + COUNTRY_NAMES, + COUNTRY_NAMES_TO_CODE, + CLIENT_ID, + CLIENT_SECRET, + BASE_URL_API_AUTH, + DURATION_THRESHOLDS, +} from './constants' + +export function getPreferredCountry(preferredCountryIndex) { + const countryName = COUNTRY_NAMES[preferredCountryIndex]; + const code = COUNTRY_NAMES_TO_CODE[countryName]; + const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); + return preferredCountry; +} + +export const objectToUrlEncodedString = (obj) => { + + const encodedParams: string[] = []; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(obj[key]); + encodedParams.push(`${encodedKey}=${encodedValue}`); + } + } + + return encodedParams.join('&'); +} + + +export function getChannelNameFromUrl(url) { + const channel_name = url.split('/').pop(); + return channel_name; +} + +export function isUsernameUrl(url) { + + const regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); + + return regex.test(url); +} + + +// TODO: save to state +export function getAnonymousUserTokenSingleton() { + // Check if the anonymous user token is available and not expired + if (AUTHORIZATION_TOKEN_ANONYMOUS_USER) { + + const isTokenValid = AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE && new Date().getTime() < AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE; + + if (isTokenValid) { + return AUTHORIZATION_TOKEN_ANONYMOUS_USER; + } + } + + // Prepare the request body for obtaining a new token + const body = objectToUrlEncodedString({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + grant_type: 'client_credentials' + }); + + // Make the HTTP POST request to the authorization API + const res = httpClientRequestToken.POST(`${BASE_URL_API_AUTH}`, body, { + 'User-Agent': USER_AGENT, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Origin': BASE_URL, + 'DNT': '1', + 'Sec-GPC': '1', + 'Connection': 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + 'Priority': 'u=4', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache' + }, false); + + // Check if the response code indicates success + if (res.code !== 200) { + console.error('Failed to get token', res); + throw new ScriptException("", "Failed to get token: " + res.code + " - " + res.body); + } + + // Parse the response JSON to extract the token information + const json = JSON.parse(res.body); + + // Ensure the response contains the necessary token information + if (!json.token_type || !json.access_token) { + console.error('Invalid token response', res); + throw new ScriptException("", 'Invalid token response: ' + res.body); + } + + // Store the token and its expiration date + AUTHORIZATION_TOKEN_ANONYMOUS_USER = `${json.token_type} ${json.access_token}`; + AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE = new Date().getTime() + (json.expires_in * 1000); + + return AUTHORIZATION_TOKEN_ANONYMOUS_USER; +} + + +export function executeGqlQuery(httpClient, requestOptions) { + + const headersToAdd = requestOptions.headers || { + "User-Agent": USER_AGENT, + "Accept": "*/*", + // "Accept-Language": Accept_Language, + "Referer": BASE_URL, + "Origin": BASE_URL, + "DNT": "1", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + } + + + const gql = JSON.stringify({ + operationName: requestOptions.operationName, + variables: requestOptions.variables, + query: requestOptions.query, + }); + + const usePlatformAuth = requestOptions.usePlatformAuth == undefined ? false : requestOptions.usePlatformAuth; + const throwOnError = requestOptions.throwOnError == undefined ? true : requestOptions.throwOnError; + + if (!usePlatformAuth) { + headersToAdd.Authorization = getAnonymousUserTokenSingleton(); + } + + const res = httpClient.POST(BASE_URL_API, gql, headersToAdd, usePlatformAuth); + + if (!res.isOk) { + console.error('Failed to get token', res); + if (throwOnError) { + throw new ScriptException("Failed to get token", res); + } + } + + const body = JSON.parse(res.body); + + // some errors may be returned in the body with a status code 200 + if (body.errors) { + const message = body.errors.map(e => e.message).join(', '); + + if (throwOnError) { + throw new UnavailableException(message); + } + } + + return body; +} + + + +/** + * Converts SRT subtitle format to VTT format. + * + * @param {string} srt - The SRT subtitle string. + * @returns {string} - The converted VTT subtitle string. + */ +export const convertSRTtoVTT = (srt) => { + // Initialize the VTT output with the required header + const vtt = ['WEBVTT\n\n']; + // Split the SRT input into blocks based on double newlines + const srtBlocks = srt.split('\n\n'); + + // Process each block individually + srtBlocks.forEach((block) => { + // Split each block into lines + const lines = block.split('\n'); + if (lines.length >= 3) { + // Extract and convert the timestamp line + const timestamp = lines[1].replace(/,/g, '.'); + // Extract the subtitle text lines + const subtitleText = lines.slice(2).join('\n'); + // Add the converted block to the VTT output + vtt.push(`${timestamp}\n${subtitleText}\n\n`); + } + }); + + // Join the VTT array into a single string and return it + return vtt.join(''); +} + + + +export const parseUploadDateFilter = (filter) => { + let createdAfterVideos; + + const now = new Date(); + + switch (filter) { + case "today": + // Last 24 hours from now + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + createdAfterVideos = yesterday.toISOString(); + break; + case "thisweek": + // Adjusts to the start of the current week (assuming week starts on Sunday) + const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); + createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); + break; + case "thismonth": + // Adjusts to the start of the month + createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); + break; + case "thisyear": + // Adjusts to the start of the year + createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); + break; + default: + createdAfterVideos = null; + } + return createdAfterVideos; +} + + +export const parseSort = (order) => { + let sort; + switch (order) { + //TODO: refact this to use constants + case "Most Recent": + sort = "RECENT"; + break; + case "Most Viewed": + sort = "VIEW_COUNT"; + break; + case "Most Relevant": + sort = "RELEVANCE"; + break; + default: + sort = order; // Default to the original order if no match + } + return sort +} + +export const getQuery = (context) => { + context.sort = parseSort(context.order); + + if (!context.filters) { + context.filters = {}; + } + + if (!context.page) { + context.page = 1; + } + + if (context?.filters.duration) { + context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; + context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; + } else { + context.filters.durationMinVideos = null; + context.filters.durationMaxVideos = null; + } + + if (context.filters.uploaddate) { + context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); + } + + return context; } \ No newline at end of file diff --git a/types/plugin.d.ts b/types/plugin.d.ts index ef407ce..2b99504 100644 --- a/types/plugin.d.ts +++ b/types/plugin.d.ts @@ -1,1422 +1,1431 @@ -//Reference Scriptfile -//Intended exclusively for auto-complete in your IDE, not for execution - -declare class ScriptException extends Error { - - plugin_type: string; - msg: string; - message: string; - - //If only one parameter is provided, acts as msg - constructor(type: string, msg: string) { - if (arguments.length == 1) { - super(arguments[0]); - this.plugin_type = "ScriptException"; - this.message = arguments[0]; - } - else { - super(msg); - this.plugin_type = type ?? ""; //string - this.msg = msg ?? ""; //string - } - } -} - -declare class LoginRequiredException extends ScriptException { - constructor(msg: string) { - super("ScriptLoginRequiredException", msg); - } -} - -//Alias -declare class ScriptLoginRequiredException extends ScriptException { - constructor(msg: string) { - super("ScriptLoginRequiredException", msg); - } -} - -declare class CaptchaRequiredException extends ScriptException { - - plugin_type: string; - url: string; - body: any; - - constructor(url: string, body: string) { - super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body })); - this.plugin_type = "CaptchaRequiredException"; - this.url = url; - this.body = body; - } -} - -declare class CriticalException extends ScriptException { - constructor(msg: string) { - super("CriticalException", msg); - } -} - -declare class UnavailableException extends ScriptException { - constructor(msg: string) { - super("UnavailableException", msg); - } -} - -declare class AgeException extends ScriptException { - constructor(msg: string) { - super("AgeException", msg); - } -} - -declare class TimeoutException extends ScriptException { - plugin_type: string; - - constructor(msg: string) { - super(msg); - this.plugin_type = "ScriptTimeoutException"; - } -} - -declare class ScriptImplementationException extends ScriptException { - plugin_type: string; - - constructor(msg: string) { - super(msg); - this.plugin_type = "ScriptImplementationException"; - } -} - -declare class Thumbnails { - constructor(thumbnails: Thumbnail[]) { - this.sources = thumbnails ?? []; // Thumbnail[] - } -} -declare class Thumbnail { - constructor(url: string, quality: number) { - this.url = url ?? ""; //string - this.quality = quality ?? 0; //integer - } -} - -declare class PlatformID { - constructor(platform: string, id: string, pluginId: string, claimType?: number = 0, claimFieldType?: number = -1) { - this.platform = platform ?? ""; //string - this.pluginId = pluginId; //string - this.value = id; //string - this.claimType = claimType ?? 0; //int - this.claimFieldType = claimFieldType ?? -1; //int - } -} - -declare class PlatformContent { - - contentType: number; - id: PlatformID; - name: string; - thumbnails: Thumbnail[]; - author: PlatformAuthorLink; - datetime: number; - url: string; - - constructor(obj: any, type: number) { - this.contentType = type; - obj = obj ?? {}; - this.id = obj.id ?? PlatformID(); //PlatformID - this.name = obj.name ?? ""; //string - this.thumbnails = obj.thumbnails; //Thumbnail[] - this.author = obj.author; //PlatformAuthorLink - this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long) - this.url = obj.url ?? ""; //String - } -} - -declare class PlatformContentDetails { - contentType: number; - constructor(type) { - this.contentType = type; - } -} - -declare class PlatformNestedMediaContent extends PlatformContent { - - contentUrl: string; - contentName: any; - contentDescription: any; - contentProvider: any; - contentThumbnails: Thumbnails; - - constructor(obj) { - super(obj, 11); - obj = obj ?? {}; - this.contentUrl = obj.contentUrl ?? ""; - this.contentName = obj.contentName; - this.contentDescription = obj.contentDescription; - this.contentProvider = obj.contentProvider; - this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails(); - } -} -declare class PlatformLockedContent extends PlatformContent { - - contentName: any; - contentThumbnails: Thumbnails; - unlockUrl: string; - lockDescription: any; - - constructor(obj) { - super(obj, 70); - obj = obj ?? {}; - this.contentName = obj.contentName; - this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails(); - this.unlockUrl = obj.unlockUrl ?? ""; - this.lockDescription = obj.lockDescription; - } -} - -//Playlist -declare class PlatformPlaylist extends PlatformContent { - plugin_type: string; - videoCount: number; - thumbnail: any; - constructor(obj) { - super(obj, 4); - this.plugin_type = "PlatformPlaylist"; - this.videoCount = obj.videoCount ?? 0; - this.thumbnail = obj.thumbnail; - } -} - -declare class PlatformPlaylistDetails extends PlatformPlaylist { - plugin_type: string; - contents: any; - constructor(obj) { - super(obj); - this.plugin_type = "PlatformPlaylistDetails"; - this.contents = obj.contents; - } -} - -//Ratings -declare class RatingLikes { - - type: number; - likes: number; - - constructor(likes) { - this.type = 1; - this.likes = likes; - } -} - -declare class RatingLikesDislikes { - type: number; - likes: number; - dislikes: number; - constructor(likes: number, dislikes: number) { - this.type = 2; - this.likes = likes; - this.dislikes = dislikes; - } -} - -declare class RatingScaler { - - type: number; - value: any; - constructor(value) { - this.type = 3; - this.value = value; - } -} - -declare class PlatformComment { - - plugin_type: string; - contextUrl: string; - author: PlatformAuthorLink; - message: string; - rating: any; - date: number; - replyCount: number; - context: any; - - constructor(obj) { - this.plugin_type = "Comment"; - this.contextUrl = obj.contextUrl ?? ""; - this.author = obj.author ?? new PlatformAuthorLink(null, "", "", null); - this.message = obj.message ?? ""; - this.rating = obj.rating ?? new RatingLikes(0); - this.date = obj.date ?? 0; - this.replyCount = obj.replyCount ?? 0; - this.context = obj.context ?? {}; - } -} - - -//Sources -declare class VideoSourceDescriptor { - - plugin_type: string; - isUnMuxed: boolean; - videoSources: any[]; - - constructor(obj) { - obj = obj ?? {}; - this.plugin_type = "MuxVideoSourceDescriptor"; - this.isUnMuxed = false; - - if (obj.constructor === Array) - this.videoSources = obj; - else - this.videoSources = obj.videoSources ?? []; - } -} - -declare class UnMuxVideoSourceDescriptor { - - plugin_type: string; - isUnMuxed: boolean; - videoSources: any[]; - audioSources: any[]; - - constructor(videoSourcesOrObj, audioSources) { - videoSourcesOrObj = videoSourcesOrObj ?? {}; - this.plugin_type = "UnMuxVideoSourceDescriptor"; - this.isUnMuxed = true; - - if (videoSourcesOrObj.constructor === Array) { - this.videoSources = videoSourcesOrObj; - this.audioSources = audioSources; - } - else { - this.videoSources = videoSourcesOrObj.videoSources ?? []; - this.audioSources = videoSourcesOrObj.audioSources ?? []; - } - } -} - -declare class VideoUrlSource { - - plugin_type: string; - width: number; - height: number; - container: string; - codec: string; - name: string; - bitrate: number; - duration: number; - url: string; - requestModifier?: any; - - constructor(obj) { - obj = obj ?? {}; - this.plugin_type = "VideoUrlSource"; - this.width = obj.width ?? 0; - this.height = obj.height ?? 0; - this.container = obj.container ?? ""; - this.codec = obj.codec ?? ""; - this.name = obj.name ?? ""; - this.bitrate = obj.bitrate ?? 0; - this.duration = obj.duration ?? 0; - this.url = obj.url; - if (obj.requestModifier) - this.requestModifier = obj.requestModifier; - } -} - -declare class VideoUrlRangeSource extends VideoUrlSource { - - plugin_type: string; - itagId: any; - initStart: any; - initEnd: any; - indexStart: any; - indexEnd: any; - - constructor(obj) { - super(obj); - this.plugin_type = "VideoUrlRangeSource"; - - this.itagId = obj.itagId ?? null; - this.initStart = obj.initStart ?? null; - this.initEnd = obj.initEnd ?? null; - this.indexStart = obj.indexStart ?? null; - this.indexEnd = obj.indexEnd ?? null; - } -} - -declare class AudioUrlSource { - - plugin_type: string; - name: string; - bitrate: number; - container: string; - codec: string; - duration: number; - url: string; - language: Language; - requestModifier?: any; - - constructor(obj) { - obj = obj ?? {}; - this.plugin_type = "AudioUrlSource"; - this.name = obj.name ?? ""; - this.bitrate = obj.bitrate ?? 0; - this.container = obj.container ?? ""; - this.codec = obj.codec ?? ""; - this.duration = obj.duration ?? 0; - this.url = obj.url; - this.language = obj.language ?? Language.UNKNOWN; - if (obj.requestModifier) - this.requestModifier = obj.requestModifier; - } -} - -declare class AudioUrlWidevineSource extends AudioUrlSource { - - plugin_type: string; - bearerToken: any; - licenseUri: any; - - constructor(obj) { - super(obj); - this.plugin_type = "AudioUrlWidevineSource"; - - this.bearerToken = obj.bearerToken; - this.licenseUri = obj.licenseUri; - } -} - -declare class AudioUrlRangeSource extends AudioUrlSource { - - plugin_type: string; - itagId: any; - initStart: any; - initEnd: any; - indexStart: any; - indexEnd: any; - audioChannels: number; - - constructor(obj) { - super(obj); - this.plugin_type = "AudioUrlRangeSource"; - - this.itagId = obj.itagId ?? null; - this.initStart = obj.initStart ?? null; - this.initEnd = obj.initEnd ?? null; - this.indexStart = obj.indexStart ?? null; - this.indexEnd = obj.indexEnd ?? null; - this.audioChannels = obj.audioChannels ?? 2; - } -} - -declare class HLSSource { - - plugin_type: string; - name: string; - duration: number; - url: string; - priority: boolean; - language?: any; - requestModifier?: any; - - constructor(obj) { - obj = obj ?? {}; - this.plugin_type = "HLSSource"; - this.name = obj.name ?? "HLS"; - this.duration = obj.duration ?? 0; - this.url = obj.url; - this.priority = obj.priority ?? false; - if (obj.language) - this.language = obj.language; - if (obj.requestModifier) - this.requestModifier = obj.requestModifier; - } -} - -declare class DashSource { - - plugin_type: string; - name: string; - duration: number; - url: string; - language?: any; - requestModifier?: any; - - constructor(obj) { - obj = obj ?? {}; - this.plugin_type = "DashSource"; - this.name = obj.name ?? "Dash"; - this.duration = obj.duration ?? 0; - this.url = obj.url; - if (obj.language) - this.language = obj.language; - if (obj.requestModifier) - this.requestModifier = obj.requestModifier; - } -} - -declare class RequestModifier { - - allowByteSkip: any; - - constructor(obj) { - obj = obj ?? {}; - this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip - } -} - -interface PluginSetting { - variable?: string; - name?: string; - description?: string; - type?: string; - default?: string; - options?: string[]; -} - -declare class Config { - name?: string; - platformUrl?: string; - description?: string; - author?: string; - authorUrl?: string; - sourceUrl?: string; - scriptUrl?: string; - repositoryUrl?: string; - version?: number; - iconUrl?: string; - id: string; - scriptSignature?: string; - scriptPublicKey?: string; - packages?: string[]; - allowEval?: boolean; - allowUrls?: string[]; - settings?: PluginSetting[]; -} - -declare class ResultCapabilities { - - types: string[]; - sorts: string[]; - filters?: FilterGroup[]; - - constructor(types: string[], sorts: string[], filters: FilterGroup[]) { - this.types = types ?? []; - this.sorts = sorts ?? []; - this.filters = filters ?? []; - } -} - -declare class FilterGroup { - - name: string; - filters: any[]; - isMultiSelect: boolean; - id: any; - - constructor(name: string, filters: string[], isMultiSelect: boolean, id: string) { - if (!name) throw new ScriptException("No name for filter group"); - if (!filters) throw new ScriptException("No filter provided"); - - this.name = name - this.filters = filters - this.isMultiSelect = isMultiSelect; - this.id = id; - } -} - -declare class FilterCapability { - - name: string; - value: any; - id: any; - - constructor(name: string, value: string, id: string) { - if (!name) throw new ScriptException("No name for filter"); - if (!value) throw new ScriptException("No filter value"); - - this.name = name; - this.value = value; - this.id = id; - } -} - -declare class PlatformAuthorLink { - - id: PlatformID; - name: string; - url: string; - thumbnail: string; - subscribers?: any; - membershipUrl?: string | null; - - constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers?: any, membershipUrl?: string | null) { - this.id = id ?? PlatformID(); //PlatformID - this.name = name ?? ""; //string - this.url = url ?? ""; //string - this.thumbnail = thumbnail; //string - if (subscribers) - this.subscribers = subscribers; - if (membershipUrl) - this.membershipUrl = membershipUrl ?? null; //string (for backcompat) - } -} - -declare class PlatformAuthorMembershipLink { - - id: PlatformID; - name: string; - url: string; - thumbnail: string; - subscribers?: any; - membershipUrl?: string | null; - - constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers?: any, membershipUrl?: string | null) { - this.id = id ?? PlatformID(); //PlatformID - this.name = name ?? ""; //string - this.url = url ?? ""; //string - this.thumbnail = thumbnail; //string - if (subscribers) - this.subscribers = subscribers; - if (membershipUrl) - this.membershipUrl = membershipUrl ?? null; //string - } -} - -declare interface PlatformVideoDef { - id: PlatformID, - name: string, - description: string, - thumbnails: Thumbnails, - author: PlatformAuthorLink, - uploadDate: number, - datetime: number, - url: string, - duration?: number, - viewCount: number, - isLive: boolean, - shareUrl?: any -} - -declare class PlatformVideo extends PlatformContent { - - plugin_type: string; - shareUrl: any; - duration: number; - viewCount: number; - isLive: boolean; - - constructor(obj: PlatformVideoDef) { - super(obj, 1); - obj = obj ?? {}; - this.plugin_type = "PlatformVideo"; - this.shareUrl = obj.shareUrl; - - this.duration = obj.duration ?? -1; //Long - this.viewCount = obj.viewCount ?? -1; //Long - - this.isLive = obj.isLive ?? false; //Boolean - } -} - -declare interface PlatformVideoDetailsDef extends PlatformVideoDef { - description: string, - video: VideoSourceDescriptor, - dash: DashSource | null, - hls: HLSSource | null, - live: IVideoSource | null, - rating: RatingLikesDislikes, - subtitles: ISubtitleSource[] -} - - -interface ISubtitleSource { - name: String; - url: String?; - format: String?; - getSubtitles?: Function; -} - -declare class PlatformVideoDetails extends PlatformVideo { - - plugin_type: string; - description: string; - video: VideoSourceDescriptor; - dash: any; - hls: any; - live: any; - rating: any; - subtitles: any[]; - - constructor(obj: PlatformVideoDetailsDef) { - super(obj); - obj = obj ?? {}; - this.plugin_type = "PlatformVideoDetails"; - - this.description = obj.description ?? "";//String - this.video = obj.video ?? {}; //VideoSourceDescriptor - this.dash = obj.dash ?? null; //DashSource, deprecated - this.hls = obj.hls ?? null; //HLSSource, deprecated - this.live = obj.live ?? null; //VideoSource - - this.rating = obj.rating ?? null; //IRating - this.subtitles = obj.subtitles ?? []; - } -} - -declare interface PlatformContentDef { - id: PlatformID, - name: string, - thumbnails: Thumbnails, - author: PlatformAuthorLink, - datetime: integer, - url: string -} - -declare interface PlatformPostDef extends PlatformContentDef { - thumbnails: string[], - thumbnails: Thumbnails[], - images: string[], - description: string -} - -class PlatformPost extends PlatformContent { - plugin_type: string; - thumbnails: Thumbnails[]; - images: any[]; - description: string; - - constructor(obj) { - super(obj, 2); - obj = obj ?? {}; - this.plugin_type = "PlatformPost"; - this.thumbnails = obj.thumbnails ?? []; - this.images = obj.images ?? []; - this.description = obj.description ?? ""; - } -} - -class PlatformPostDetails extends PlatformPost { - - plugin_type: string; - rating: any; - textType: number; - content: string; - - constructor(obj) { - super(obj); - obj = obj ?? {}; - this.plugin_type = "PlatformPostDetails"; - this.rating = obj.rating ?? RatingLikes(-1); - this.textType = obj.textType ?? 0; - this.content = obj.content ?? ""; - } -} - -// Sources -declare interface IVideoSourceDescriptor { } - -declare interface MuxVideoSourceDescriptorDef { - isUnMuxed: boolean, - videoSources: VideoSource[] -} -declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor { - constructor(obj: MuxVideoSourceDescriptorDef); -} - -declare interface UnMuxVideoSourceDescriptorDef { - isUnMuxed: boolean, - videoSources: VideoSource[] -} -declare class UnMuxVideoSourceDescriptor implements IVideoSourceDescriptor { - constructor(videoSourcesOrObj: VideoSource[] | UnMuxVideoSourceDescriptorDef, audioSources?: AudioSource[]); -} - -declare interface IVideoSource { } - -declare interface IAudioSource { } - -declare interface VideoUrlSourceDef extends IVideoSource { - width: number, - height: number, - container: string, - codec: string, - name: string, - bitrate: number, - duration: number, - url: string -} -declare class VideoUrlSource { - constructor(obj: VideoUrlSourceDef); -} - -declare interface YTVideoSourceDef extends VideoUrlSourceDef { - itagId: number, - initStart: number, - initEnd: number, - indexStart: number, - indexEnd: number, -} -declare class YTVideoSource extends VideoUrlSource { - constructor(obj: YTVideoSourceDef); -} - -declare interface AudioUrlSourceDef extends IAudioSource { - name: string, - bitrate: number, - container: string, - codecs: string, - duration: number, - url: string, - language: string -} -declare class AudioUrlSource { - constructor(obj: AudioUrlSourceDef); -} - -declare interface YTAudioSourceDef extends AudioUrlSourceDef { - itagId: number, - initStart: number, - initEnd: number, - indexStart: number, - indexEnd: number, - audioChannels: number -} -declare class YTAudioSource extends AudioUrlSource { - constructor(obj: YTAudioSourceDef); -} - -declare interface HLSSourceDef { - name: string, - duration: number, - url: string -} -declare class HLSSource implements IVideoSource { - constructor(obj: HLSSourceDef); -} - -declare interface DashSourceDef { - name: string, - duration: number, - url: string -} -declare class DashSource implements IVideoSource { - constructor(obj: DashSourceDef); -} - -// Channel -declare interface PlatformChannelDef { - id: PlatformID, - name: string, - thumbnail: string, - banner: string, - subscribers: number, - description: string, - url: string, - links?: Map<string> -} - -declare class PlatformChannel { - - plugin_type: string; - id: string; - name: string; - thumbnail: string; - banner: string; - subscribers: number; - description: string; - url: string; - urlAlternatives: string[]; - links: Map<string> - - constructor(obj: PlatformChannelDef) { - obj = obj ?? {}; - this.plugin_type = "PlatformChannel"; - this.id = obj.id ?? ""; //string - this.name = obj.name ?? ""; //string - this.thumbnail = obj.thumbnail; //string - this.banner = obj.banner; //string - this.subscribers = obj.subscribers ?? 0; //integer - this.description = obj.description; //string - this.url = obj.url ?? ""; //string - this.urlAlternatives = obj.urlAlternatives ?? []; - this.links = obj.links ?? {} //Map<string,string> - } -} - -// Ratings -declare interface IRating { - type: number -} -declare class RatingLikes implements IRating { - constructor(likes: number); -} -declare class RatingLikesDislikes implements IRating { - constructor(likes: number, dislikes: number); -} -declare class RatingScaler implements IRating { - constructor(value: number); -} - -declare interface CommentDef { - contextUrl: string, - author: PlatformAuthorLink, - message: string, - rating: IRating, - date: number, - replyCount: number, - context: any -} - -//Temporary backwards compat -declare class Comment extends PlatformComment { - constructor(obj: CommentDef) { - super(obj); - } -} - -declare class PlaybackTracker { - - nextRequest: number; - - constructor(interval) { - this.nextRequest = interval ?? 10 * 1000; - } - - setProgress(seconds: number): void { - throw new ScriptImplementationException("Missing required setProgress(seconds) on PlaybackTracker"); - } -} - -declare class LiveEventPager { - - plugin_type: string; - _entries: { [key: string]: any }; - - constructor(results: LiveEvent[], hasMore: boolean, context: any) { - this.plugin_type = "LiveEventPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - this.nextRequest = 4000; - } - - hasMorePagers(): boolean { return this.hasMore; } - nextPage(): LiveEventPager { return new Pager([], false, this.context) }; //Could be self - - delete(name: string): void; - get(name: string): any; - getAll(name: string): any[]; - has(name: string): boolean; - set(name: string, value: any): void; - forEach(callback: (value: any, name: string, pager: LiveEventPager) => void): void; - keys(): IterableIterator<string>; - values(): IterableIterator<any>; - entries(): IterableIterator<[string, any]>; - clear(): void; -} - - -declare class LiveEvent { - - plugin_type: string; - id: string; - name: string; - description: string; - startDate: number; - endDate: number; - thumbnail: string; - state: number; - upcomingText: string; - viewCount: number; - tracker: PlaybackTracker; - rating: any; - - constructor(type: string) { - this.type = type; - } -} -declare class LiveEventComment extends LiveEvent { - constructor(name: string, message: string, thumbnail?: string, colorName, badges) { - super(1); - this.name = name; - this.message = message; - this.thumbnail = thumbnail; - this.colorName = colorName; - this.badges = badges; - } -} - -declare class LiveEventEmojis extends LiveEvent { - constructor(emojis) { - super(4); - this.emojis = emojis; - } -} - -declare class LiveEventDonation extends LiveEvent { - constructor(amount: number, name: string, message: string, thumbnail?: string, expire?: any, colorDonation?: string) { - super(5); - this.amount = amount; - this.name = name; - this.message = message ?? ""; - this.thumbnail = thumbnail; - this.expire = expire; - this.colorDonation = colorDonation; - } -} - -declare class LiveEventViewCount extends LiveEvent { - constructor(viewCount: number) { - super(10); - this.viewCount = viewCount; - } -} - -declare class LiveEventRaid extends LiveEvent { - constructor(targetUrl: string, targetName: string, targetThumbnail: string) { - super(100); - this.targetUrl = targetUrl; - this.targetName = targetName; - this.targetThumbnail = targetThumbnail; - } -} - -//Pagers - -declare class ContentPager { - constructor(results: [], hasMore: boolean, context: any) { - this.plugin_type = "ContentPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - } - - hasMorePagers() { return this.hasMore; } - nextPage() { return new ContentPager([], false, this.context) } -} - -declare class VideoPager { - - hasMore: boolean; - context: any - - constructor(results: PlatformVideo[], hasMore?: boolean, context?: any) { - this.plugin_type = "VideoPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - } - hasMorePagers(): boolean { return this.hasMore; } - nextPage(): VideoPager { return new VideoPager([], false, this.context) } -} - -declare class ChannelPager { - - hasMore: boolean; - context: any - - constructor(results: PlatformVideo[], hasMore: boolean, context: any) { - this.plugin_type = "ChannelPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - } - - hasMorePagers(): boolean { return this.hasMore; } - nextPage(): ChannelPager { return new Pager([], false, this.context) } -} - - -declare class PlaylistPager { - - hasMore: boolean; - context: any - - constructor(results: PlatformPlaylist[], hasMore?: boolean, context?: any) { - this.plugin_type = "PlaylistPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - } - - hasMorePagers() { return this.hasMore; } - nextPage() { return new Pager([], false, this.context) } -} - - -declare class CommentPager { - context: any - - constructor(results: PlatformVideo[], hasMore: boolean, context: any) { - this.plugin_type = "CommentPager"; - this.results = results ?? []; - this.hasMore = hasMore ?? false; - this.context = context ?? {}; - } - hasMorePagers(): boolean { return this.hasMore; } - nextPage(): CommentPager { return new Pager([], false, this.context) } -} - -declare interface Map<T> { - [Key: string]: T; -} - -function throwException(ttype: string, message: string): void { - throw new Error("V8EXCEPTION:" + type + "-" + message); -} - -let plugin = { - config: {}, - settings: {} -}; - -// Plugin configuration -// To override by plugin -interface Source { - - getHome(): VideoPager; - - enable(conf: Config, settings: Map<string>, saveStateStr: string): void; - - setSettings(settings: any): void; - - disable(): void; - - searchSuggestions(query: string): string[]; - search(query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; - getSearchCapabilities(): ResultCapabilities; - - // Optional - searchChannelVideos?(channelUrl: string, query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; - getSearchChannelVideoCapabilities?(): ResultCapabilities; - - isChannelUrl(url: string): boolean; - getChannel(url: string): PlatformChannel | null; - - getChannelVideos(url: string, type: string, order: string, filters: FilterGroup[]): VideoPager; - getChannelCapabilities(): ResultCapabilities; - - isVideoDetailsUrl(url: string): boolean; - getVideoDetails(url: string): PlatformVideoDetails; - - // Optional - getComments?(url: string): CommentPager; - getSubComments?(comment: Comment): CommentPager; - - // Optional - getUserSubscriptions?(): string[]; - getUserPlaylists?(): string[]; - - // Optional - isPlaylistUrl?(url: string): boolean; - - searchPlaylists(query, type, order, filters); - - getPlaylist?(url: string): PlatformPlaylistDetails; - - isContentDetailsUrl(url: string): boolean; - - getChannelContents(url: string, type?: string, order?: string, filters?: Map<String, List<String>>): VideoPager; - - searchChannels(query: string): ChannelPager; - - getContentDetails(url: string): PlatformVideoDetails; - - getChannelPlaylists(url: string): PlaylistPager; -} - - -function parseSettings(settings) { - if (!settings) - return {}; - let newSettings = {}; - for (let key in settings) { - if (typeof settings[key] == "string") - newSettings[key] = JSON.parse(settings[key]); - else - newSettings[key] = settings[key]; - } - return newSettings; -} - -function log(str: string) { - if (str) { - console.log(str); - if (typeof str == "string") - bridge.log(str); - else - bridge.log(JSON.stringify(str, null, 4)); - } -} - -function encodePathSegment(segment) { - return encodeURIComponent(segment).replace(/[!'()*]/g, function (c) { - return '%' + c.charCodeAt(0).toString(16); - }); -} - -class URLSearchParams { - constructor(init) { - this._entries = {}; - if (typeof init === 'string') { - if (init !== '') { - init = init.replace(/^\?/, ''); - const attributes = init.split('&'); - let attribute; - for (let i = 0; i < attributes.length; i++) { - attribute = attributes[i].split('='); - this.append(decodeURIComponent(attribute[0]), (attribute.length > 1) ? decodeURIComponent(attribute[1]) : ''); - } - } - } - else if (init instanceof URLSearchParams) { - init.forEach((value, name) => { - this.append(value, name); - }); - } - } - append(name, value) { - value = value.toString(); - if (name in this._entries) { - this._entries[name].push(value); - } - else { - this._entries[name] = [value]; - } - } - delete(name) { - delete this._entries[name]; - } - get(name) { - return (name in this._entries) ? this._entries[name][0] : null; - } - getAll(name) { - return (name in this._entries) ? this._entries[name].slice(0) : []; - } - has(name) { - return (name in this._entries); - } - set(name, value) { - this._entries[name] = [value.toString()]; - } - forEach(callback) { - let entries; - for (let name in this._entries) { - if (this._entries.hasOwnProperty(name)) { - entries = this._entries[name]; - for (let i = 0; i < entries.length; i++) { - callback.call(this, entries[i], name, this); - } - } - } - } - keys() { - const items = []; - this.forEach((value, name) => { items.push(name); }); - return createIterator(items); - } - values() { - const items = []; - this.forEach((value) => { items.push(value); }); - return createIterator(items); - } - entries() { - const items = []; - this.forEach((value, name) => { items.push([value, name]); }); - return createIterator(items); - } - toString() { - let searchString = ''; - this.forEach((value, name) => { - if (searchString.length > 0) - searchString += '&'; - searchString += encodeURIComponent(name) + '=' + encodeURIComponent(value); - }); - return searchString; - } -} - -const source: Source; - -declare var IS_TESTING: boolean; - -let Type = { - Source: { - Dash: "DASH", - HLS: "HLS", - STATIC: "Static" - }, - Feed: { - Videos: "VIDEOS", - Streams: "STREAMS", - Mixed: "MIXED", - Live: "LIVE", - Subscriptions: "SUBSCRIPTIONS" - }, - Order: { - Chronological: "CHRONOLOGICAL" - }, - Date: { - LastHour: "LAST_HOUR", - Today: "TODAY", - LastWeek: "LAST_WEEK", - LastMonth: "LAST_MONTH", - LastYear: "LAST_YEAR" - }, - Duration: { - Short: "SHORT", - Medium: "MEDIUM", - Long: "LONG" - }, - Text: { - RAW: 0, - HTML: 1, - MARKUP: 2 - }, - Chapter: { - NORMAL: 0, - - SKIPPABLE: 5, - SKIP: 6, - SKIPONCE: 7 - } -}; - -let Language = { - UNKNOWN: "Unknown", - ARABIC: "ar", - SPANISH: "es", - FRENCH: "fr", - HINDI: "hi", - INDONESIAN: "id", - KOREAN: "ko", - PORTUGUESE: "pt", - PORTBRAZIL: "pt", - RUSSIAN: "ru", - THAI: "th", - TURKISH: "tr", - VIETNAMESE: "vi", - ENGLISH: "en" -} - - -interface HttpResponse { - isOk(): boolean, - body: string, - code: number -} - -//Package Bridge (variable: bridge) -let bridge = { - /** - * @param {String} label - * @param {String} data - * @return {Unit} - **/ - devSubmit: function (label: string, data: string): Unit { }, - - /** - * @return {Boolean} - **/ - isLoggedIn: function (): boolean { }, - - /** - * @param {String} str - * @return {Unit} - **/ - log: function (str: string): Unit { }, - - /** - * @param {String} str - * @return {Unit} - **/ - throwTest: function (str: string): Unit { }, - - /** - * @param {String} str - * @return {Unit} - **/ - toast: function (str: string): Unit { }, - -} - -//Package Http (variable: http) - -interface IHttp { - /** - * @param {String} url - * @param {Map} headers - * @param {Boolean} useAuth - * @return {BridgeHttpResponse} - **/ - GET(url: string, headers: Map<string, string>, useAuth?: boolean): BridgeHttpResponse; - - /** - * @param {String} url - * @param {String} body - * @param {Map} headers - * @param {Boolean} useAuth - * @return {BridgeHttpResponse} - **/ - POST(url: string, body: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; - - /** - * @return {BatchBuilder} - **/ - batch(): BatchBuilder; - - /** - * @param {Boolean} withAuth - * @return {PackageHttpClient} - **/ - getDefaultClient(withAuth: boolean): PackageHttpClient; - - /** - * @param {Boolean} withAuth - * @return {PackageHttpClient} - **/ - newClient(withAuth: boolean): PackageHttpClient; - - /** - * @param {String} method - * @param {String} url - * @param {Map} headers - * @param {Boolean} useAuth - * @return {BridgeHttpResponse} - **/ - request(method: string, url: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; - - /** - * @param {String} method - * @param {String} url - * @param {String} body - * @param {Map} headers - * @param {Boolean} useAuth - * @return {BridgeHttpResponse} - **/ - requestWithBody(method: string, url: string, body: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; - - /** - * @param {String} url - * @param {Map} headers - * @param {Boolean} useAuth - * @return {SocketResult} - **/ - socket(url: string, headers: Map<string, string>, useAuth: boolean): SocketResult; - - /** - * @param {Map} headers - * @return {void} - **/ - setDefaultHeaders(headers: Map<string, string>): void -} - - -let http: IHttp - - -interface IPager<T> { - hasMorePages() : Boolean; - nextPage(); - getResults() : List<T>; +//Reference Scriptfile +//Intended exclusively for auto-complete in your IDE, not for execution + +declare class ScriptException extends Error { + + plugin_type: string; + msg: string; + message: string; + + //If only one parameter is provided, acts as msg + constructor(type: string, msg: string) { + if (arguments.length == 1) { + super(arguments[0]); + this.plugin_type = "ScriptException"; + this.message = arguments[0]; + } + else { + super(msg); + this.plugin_type = type ?? ""; //string + this.msg = msg ?? ""; //string + } + } +} + +declare class LoginRequiredException extends ScriptException { + constructor(msg: string) { + super("ScriptLoginRequiredException", msg); + } +} + +//Alias +declare class ScriptLoginRequiredException extends ScriptException { + constructor(msg: string) { + super("ScriptLoginRequiredException", msg); + } +} + +declare class CaptchaRequiredException extends ScriptException { + + plugin_type: string; + url: string; + body: any; + + constructor(url: string, body: string) { + super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body })); + this.plugin_type = "CaptchaRequiredException"; + this.url = url; + this.body = body; + } +} + +declare class CriticalException extends ScriptException { + constructor(msg: string) { + super("CriticalException", msg); + } +} + +declare class UnavailableException extends ScriptException { + constructor(msg: string) { + super("UnavailableException", msg); + } +} + +declare class AgeException extends ScriptException { + constructor(msg: string) { + super("AgeException", msg); + } +} + +declare class TimeoutException extends ScriptException { + plugin_type: string; + + constructor(msg: string) { + super(msg); + this.plugin_type = "ScriptTimeoutException"; + } +} + +declare class ScriptImplementationException extends ScriptException { + plugin_type: string; + + constructor(msg: string) { + super(msg); + this.plugin_type = "ScriptImplementationException"; + } +} + +declare class Thumbnails { + constructor(thumbnails: Thumbnail[]) { + this.sources = thumbnails ?? []; // Thumbnail[] + } +} +declare class Thumbnail { + constructor(url: string, quality: number) { + this.url = url ?? ""; //string + this.quality = quality ?? 0; //integer + } +} + +declare class PlatformID { + constructor(platform: string, id: string, pluginId: string, claimType?: number = 0, claimFieldType?: number = -1) { + this.platform = platform ?? ""; //string + this.pluginId = pluginId; //string + this.value = id; //string + this.claimType = claimType ?? 0; //int + this.claimFieldType = claimFieldType ?? -1; //int + } +} + +declare class PlatformContent { + + contentType: number; + id: PlatformID; + name: string; + thumbnails: Thumbnail[]; + author: PlatformAuthorLink; + datetime: number; + url: string; + + constructor(obj: any, type: number) { + this.contentType = type; + obj = obj ?? {}; + this.id = obj.id ?? PlatformID(); //PlatformID + this.name = obj.name ?? ""; //string + this.thumbnails = obj.thumbnails; //Thumbnail[] + this.author = obj.author; //PlatformAuthorLink + this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long) + this.url = obj.url ?? ""; //String + } +} + +declare class PlatformContentDetails { + contentType: number; + constructor(type) { + this.contentType = type; + } +} + +declare class PlatformNestedMediaContent extends PlatformContent { + + contentUrl: string; + contentName: any; + contentDescription: any; + contentProvider: any; + contentThumbnails: Thumbnails; + + constructor(obj) { + super(obj, 11); + obj = obj ?? {}; + this.contentUrl = obj.contentUrl ?? ""; + this.contentName = obj.contentName; + this.contentDescription = obj.contentDescription; + this.contentProvider = obj.contentProvider; + this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails(); + } +} +declare class PlatformLockedContent extends PlatformContent { + + contentName: any; + contentThumbnails: Thumbnails; + unlockUrl: string; + lockDescription: any; + + constructor(obj) { + super(obj, 70); + obj = obj ?? {}; + this.contentName = obj.contentName; + this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails(); + this.unlockUrl = obj.unlockUrl ?? ""; + this.lockDescription = obj.lockDescription; + } +} + +//Playlist +declare class PlatformPlaylist extends PlatformContent { + plugin_type: string; + videoCount: number; + thumbnail: any; + constructor(obj) { + super(obj, 4); + this.plugin_type = "PlatformPlaylist"; + this.videoCount = obj.videoCount ?? 0; + this.thumbnail = obj.thumbnail; + } +} + +declare class PlatformPlaylistDetails extends PlatformPlaylist { + plugin_type: string; + contents: any; + constructor(obj) { + super(obj); + this.plugin_type = "PlatformPlaylistDetails"; + this.contents = obj.contents; + } +} + +//Ratings +declare class RatingLikes { + + type: number; + likes: number; + + constructor(likes) { + this.type = 1; + this.likes = likes; + } +} + +declare class RatingLikesDislikes { + type: number; + likes: number; + dislikes: number; + constructor(likes: number, dislikes: number) { + this.type = 2; + this.likes = likes; + this.dislikes = dislikes; + } +} + +declare class RatingScaler { + + type: number; + value: any; + constructor(value) { + this.type = 3; + this.value = value; + } +} + +declare class PlatformComment { + + plugin_type: string; + contextUrl: string; + author: PlatformAuthorLink; + message: string; + rating: any; + date: number; + replyCount: number; + context: any; + + constructor(obj) { + this.plugin_type = "Comment"; + this.contextUrl = obj.contextUrl ?? ""; + this.author = obj.author ?? new PlatformAuthorLink(null, "", "", null); + this.message = obj.message ?? ""; + this.rating = obj.rating ?? new RatingLikes(0); + this.date = obj.date ?? 0; + this.replyCount = obj.replyCount ?? 0; + this.context = obj.context ?? {}; + } +} + + +//Sources +declare class VideoSourceDescriptor { + + plugin_type: string; + isUnMuxed: boolean; + videoSources: any[]; + + constructor(obj) { + obj = obj ?? {}; + this.plugin_type = "MuxVideoSourceDescriptor"; + this.isUnMuxed = false; + + if (obj.constructor === Array) + this.videoSources = obj; + else + this.videoSources = obj.videoSources ?? []; + } +} + +declare class UnMuxVideoSourceDescriptor { + + plugin_type: string; + isUnMuxed: boolean; + videoSources: any[]; + audioSources: any[]; + + constructor(videoSourcesOrObj, audioSources) { + videoSourcesOrObj = videoSourcesOrObj ?? {}; + this.plugin_type = "UnMuxVideoSourceDescriptor"; + this.isUnMuxed = true; + + if (videoSourcesOrObj.constructor === Array) { + this.videoSources = videoSourcesOrObj; + this.audioSources = audioSources; + } + else { + this.videoSources = videoSourcesOrObj.videoSources ?? []; + this.audioSources = videoSourcesOrObj.audioSources ?? []; + } + } +} + +declare class VideoUrlSource { + + plugin_type: string; + width: number; + height: number; + container: string; + codec: string; + name: string; + bitrate: number; + duration: number; + url: string; + requestModifier?: any; + + constructor(obj) { + obj = obj ?? {}; + this.plugin_type = "VideoUrlSource"; + this.width = obj.width ?? 0; + this.height = obj.height ?? 0; + this.container = obj.container ?? ""; + this.codec = obj.codec ?? ""; + this.name = obj.name ?? ""; + this.bitrate = obj.bitrate ?? 0; + this.duration = obj.duration ?? 0; + this.url = obj.url; + if (obj.requestModifier) + this.requestModifier = obj.requestModifier; + } +} + +declare class VideoUrlRangeSource extends VideoUrlSource { + + plugin_type: string; + itagId: any; + initStart: any; + initEnd: any; + indexStart: any; + indexEnd: any; + + constructor(obj) { + super(obj); + this.plugin_type = "VideoUrlRangeSource"; + + this.itagId = obj.itagId ?? null; + this.initStart = obj.initStart ?? null; + this.initEnd = obj.initEnd ?? null; + this.indexStart = obj.indexStart ?? null; + this.indexEnd = obj.indexEnd ?? null; + } +} + +declare class AudioUrlSource { + + plugin_type: string; + name: string; + bitrate: number; + container: string; + codec: string; + duration: number; + url: string; + language: Language; + requestModifier?: any; + + constructor(obj) { + obj = obj ?? {}; + this.plugin_type = "AudioUrlSource"; + this.name = obj.name ?? ""; + this.bitrate = obj.bitrate ?? 0; + this.container = obj.container ?? ""; + this.codec = obj.codec ?? ""; + this.duration = obj.duration ?? 0; + this.url = obj.url; + this.language = obj.language ?? Language.UNKNOWN; + if (obj.requestModifier) + this.requestModifier = obj.requestModifier; + } +} + +declare class AudioUrlWidevineSource extends AudioUrlSource { + + plugin_type: string; + bearerToken: any; + licenseUri: any; + + constructor(obj) { + super(obj); + this.plugin_type = "AudioUrlWidevineSource"; + + this.bearerToken = obj.bearerToken; + this.licenseUri = obj.licenseUri; + } +} + +declare class AudioUrlRangeSource extends AudioUrlSource { + + plugin_type: string; + itagId: any; + initStart: any; + initEnd: any; + indexStart: any; + indexEnd: any; + audioChannels: number; + + constructor(obj) { + super(obj); + this.plugin_type = "AudioUrlRangeSource"; + + this.itagId = obj.itagId ?? null; + this.initStart = obj.initStart ?? null; + this.initEnd = obj.initEnd ?? null; + this.indexStart = obj.indexStart ?? null; + this.indexEnd = obj.indexEnd ?? null; + this.audioChannels = obj.audioChannels ?? 2; + } +} + +declare class HLSSource { + + plugin_type: string; + name: string; + duration: number; + url: string; + priority: boolean; + language?: any; + requestModifier?: any; + + constructor(obj) { + obj = obj ?? {}; + this.plugin_type = "HLSSource"; + this.name = obj.name ?? "HLS"; + this.duration = obj.duration ?? 0; + this.url = obj.url; + this.priority = obj.priority ?? false; + if (obj.language) + this.language = obj.language; + if (obj.requestModifier) + this.requestModifier = obj.requestModifier; + } +} + +declare class DashSource { + + plugin_type: string; + name: string; + duration: number; + url: string; + language?: any; + requestModifier?: any; + + constructor(obj) { + obj = obj ?? {}; + this.plugin_type = "DashSource"; + this.name = obj.name ?? "Dash"; + this.duration = obj.duration ?? 0; + this.url = obj.url; + if (obj.language) + this.language = obj.language; + if (obj.requestModifier) + this.requestModifier = obj.requestModifier; + } +} + +declare class RequestModifier { + + allowByteSkip: any; + + constructor(obj) { + obj = obj ?? {}; + this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip + } +} + +interface PluginSetting { + variable?: string; + name?: string; + description?: string; + type?: string; + default?: string; + options?: string[]; +} + +declare class Config { + name?: string; + platformUrl?: string; + description?: string; + author?: string; + authorUrl?: string; + sourceUrl?: string; + scriptUrl?: string; + repositoryUrl?: string; + version?: number; + iconUrl?: string; + id: string; + scriptSignature?: string; + scriptPublicKey?: string; + packages?: string[]; + allowEval?: boolean; + allowUrls?: string[]; + settings?: PluginSetting[]; +} + +declare class ResultCapabilities { + + types: string[]; + sorts: string[]; + filters?: FilterGroup[]; + + constructor(types: string[], sorts: string[], filters: FilterGroup[]) { + this.types = types ?? []; + this.sorts = sorts ?? []; + this.filters = filters ?? []; + } +} + +declare class FilterGroup { + + name: string; + filters: any[]; + isMultiSelect: boolean; + id: any; + + constructor(name: string, filters: string[], isMultiSelect: boolean, id: string) { + if (!name) throw new ScriptException("No name for filter group"); + if (!filters) throw new ScriptException("No filter provided"); + + this.name = name + this.filters = filters + this.isMultiSelect = isMultiSelect; + this.id = id; + } +} + +declare class FilterCapability { + + name: string; + value: any; + id: any; + + constructor(name: string, value: string, id: string) { + if (!name) throw new ScriptException("No name for filter"); + if (!value) throw new ScriptException("No filter value"); + + this.name = name; + this.value = value; + this.id = id; + } +} + +declare class PlatformAuthorLink { + + id: PlatformID; + name: string; + url: string; + thumbnail: string; + subscribers?: any; + membershipUrl?: string | null; + + constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers?: any, membershipUrl?: string | null) { + this.id = id ?? PlatformID(); //PlatformID + this.name = name ?? ""; //string + this.url = url ?? ""; //string + this.thumbnail = thumbnail; //string + if (subscribers) + this.subscribers = subscribers; + if (membershipUrl) + this.membershipUrl = membershipUrl ?? null; //string (for backcompat) + } +} + +declare class PlatformAuthorMembershipLink { + + id: PlatformID; + name: string; + url: string; + thumbnail: string; + subscribers?: any; + membershipUrl?: string | null; + + constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers?: any, membershipUrl?: string | null) { + this.id = id ?? PlatformID(); //PlatformID + this.name = name ?? ""; //string + this.url = url ?? ""; //string + this.thumbnail = thumbnail; //string + if (subscribers) + this.subscribers = subscribers; + if (membershipUrl) + this.membershipUrl = membershipUrl ?? null; //string + } +} + +declare interface PlatformVideoDef { + id: PlatformID, + name: string, + description: string, + thumbnails: Thumbnails, + author: PlatformAuthorLink, + uploadDate?: number, + datetime: number, + url: string, + duration?: number, + viewCount: number, + isLive: boolean, + shareUrl?: any +} + +declare class PlatformVideo extends PlatformContent { + + plugin_type: string; + shareUrl: any; + duration: number; + viewCount: number; + isLive: boolean; + + constructor(obj: PlatformVideoDef) { + super(obj, 1); + obj = obj ?? {}; + this.plugin_type = "PlatformVideo"; + this.shareUrl = obj.shareUrl; + + this.duration = obj.duration ?? -1; //Long + this.viewCount = obj.viewCount ?? -1; //Long + + this.isLive = obj.isLive ?? false; //Boolean + } +} + +declare interface PlatformVideoDetailsDef extends PlatformVideoDef { + description: string, + video: VideoSourceDescriptor, + dash: DashSource | null, + hls: HLSSource | null, + live: IVideoSource | null, + rating: RatingLikesDislikes, + subtitles: ISubtitleSource[] +} + + +interface ISubtitleSource { + name: String; + url: String?; + format: String?; + getSubtitles?: Function; +} + +declare class PlatformVideoDetails extends PlatformVideo { + + plugin_type: string; + description: string; + video: VideoSourceDescriptor; + dash: any; + hls: any; + live: any; + rating: any; + subtitles: any[]; + + constructor(obj: PlatformVideoDetailsDef) { + super(obj); + obj = obj ?? {}; + this.plugin_type = "PlatformVideoDetails"; + + this.description = obj.description ?? "";//String + this.video = obj.video ?? {}; //VideoSourceDescriptor + this.dash = obj.dash ?? null; //DashSource, deprecated + this.hls = obj.hls ?? null; //HLSSource, deprecated + this.live = obj.live ?? null; //VideoSource + + this.rating = obj.rating ?? null; //IRating + this.subtitles = obj.subtitles ?? []; + } +} + +declare interface PlatformContentDef { + id: PlatformID, + name: string, + thumbnails: Thumbnails, + author: PlatformAuthorLink, + datetime: integer, + url: string +} + +declare interface PlatformPostDef extends PlatformContentDef { + thumbnails: string[], + thumbnails: Thumbnails[], + images: string[], + description: string +} + +class PlatformPost extends PlatformContent { + plugin_type: string; + thumbnails: Thumbnails[]; + images: any[]; + description: string; + + constructor(obj) { + super(obj, 2); + obj = obj ?? {}; + this.plugin_type = "PlatformPost"; + this.thumbnails = obj.thumbnails ?? []; + this.images = obj.images ?? []; + this.description = obj.description ?? ""; + } +} + +class PlatformPostDetails extends PlatformPost { + + plugin_type: string; + rating: any; + textType: number; + content: string; + + constructor(obj) { + super(obj); + obj = obj ?? {}; + this.plugin_type = "PlatformPostDetails"; + this.rating = obj.rating ?? RatingLikes(-1); + this.textType = obj.textType ?? 0; + this.content = obj.content ?? ""; + } +} + +// Sources +declare interface IVideoSourceDescriptor { } + +declare interface MuxVideoSourceDescriptorDef { + isUnMuxed: boolean, + videoSources: VideoSource[] +} +declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor { + constructor(obj: MuxVideoSourceDescriptorDef); +} + +declare interface UnMuxVideoSourceDescriptorDef { + isUnMuxed: boolean, + videoSources: VideoSource[] +} +declare class UnMuxVideoSourceDescriptor implements IVideoSourceDescriptor { + constructor(videoSourcesOrObj: VideoSource[] | UnMuxVideoSourceDescriptorDef, audioSources?: AudioSource[]); +} + +declare interface IVideoSource { } + +declare interface IAudioSource { } + +declare interface VideoUrlSourceDef extends IVideoSource { + width: number, + height: number, + container: string, + codec: string, + name: string, + bitrate: number, + duration: number, + url: string +} +declare class VideoUrlSource { + constructor(obj: VideoUrlSourceDef); +} + +declare interface YTVideoSourceDef extends VideoUrlSourceDef { + itagId: number, + initStart: number, + initEnd: number, + indexStart: number, + indexEnd: number, +} +declare class YTVideoSource extends VideoUrlSource { + constructor(obj: YTVideoSourceDef); +} + +declare interface AudioUrlSourceDef extends IAudioSource { + name: string, + bitrate: number, + container: string, + codecs: string, + duration: number, + url: string, + language: string +} +declare class AudioUrlSource { + constructor(obj: AudioUrlSourceDef); +} + +declare interface YTAudioSourceDef extends AudioUrlSourceDef { + itagId: number, + initStart: number, + initEnd: number, + indexStart: number, + indexEnd: number, + audioChannels: number +} +declare class YTAudioSource extends AudioUrlSource { + constructor(obj: YTAudioSourceDef); +} + +declare interface HLSSourceDef { + name: string, + duration: number, + url: string +} +declare class HLSSource implements IVideoSource { + constructor(obj: HLSSourceDef); +} + +declare interface DashSourceDef { + name: string, + duration: number, + url: string +} +declare class DashSource implements IVideoSource { + constructor(obj: DashSourceDef); +} + +// Channel +declare interface PlatformChannelDef { + id: PlatformID, + name: string, + thumbnail: string, + banner: string, + subscribers: number, + description: string, + url: string, + links?: Map<string> +} + +declare class PlatformChannel { + + plugin_type: string; + id: string; + name: string; + thumbnail: string; + banner: string; + subscribers: number; + description: string; + url: string; + urlAlternatives: string[]; + links: Map<string> + + constructor(obj: PlatformChannelDef) { + obj = obj ?? {}; + this.plugin_type = "PlatformChannel"; + this.id = obj.id ?? ""; //string + this.name = obj.name ?? ""; //string + this.thumbnail = obj.thumbnail; //string + this.banner = obj.banner; //string + this.subscribers = obj.subscribers ?? 0; //integer + this.description = obj.description; //string + this.url = obj.url ?? ""; //string + this.urlAlternatives = obj.urlAlternatives ?? []; + this.links = obj.links ?? {} //Map<string,string> + } +} + +// Ratings +declare interface IRating { + type: number +} +declare class RatingLikes implements IRating { + constructor(likes: number); +} +declare class RatingLikesDislikes implements IRating { + constructor(likes: number, dislikes: number); +} +declare class RatingScaler implements IRating { + constructor(value: number); +} + +declare interface CommentDef { + contextUrl: string, + author: PlatformAuthorLink, + message: string, + rating: IRating, + date: number, + replyCount: number, + context: any +} + +//Temporary backwards compat +declare class Comment extends PlatformComment { + constructor(obj: CommentDef) { + super(obj); + } +} + +declare class PlaybackTracker { + + nextRequest: number; + + constructor(interval) { + this.nextRequest = interval ?? 10 * 1000; + } + + setProgress(seconds: number): void { + throw new ScriptImplementationException("Missing required setProgress(seconds) on PlaybackTracker"); + } +} + +declare class LiveEventPager { + + plugin_type: string; + _entries: { [key: string]: any }; + + constructor(results: LiveEvent[], hasMore: boolean, context: any) { + this.plugin_type = "LiveEventPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + this.nextRequest = 4000; + } + + hasMorePagers(): boolean { return this.hasMore; } + nextPage(): LiveEventPager { return new Pager([], false, this.context) }; //Could be self + + delete(name: string): void; + get(name: string): any; + getAll(name: string): any[]; + has(name: string): boolean; + set(name: string, value: any): void; + forEach(callback: (value: any, name: string, pager: LiveEventPager) => void): void; + keys(): IterableIterator<string>; + values(): IterableIterator<any>; + entries(): IterableIterator<[string, any]>; + clear(): void; +} + + +declare class LiveEvent { + + plugin_type: string; + id: string; + name: string; + description: string; + startDate: number; + endDate: number; + thumbnail: string; + state: number; + upcomingText: string; + viewCount: number; + tracker: PlaybackTracker; + rating: any; + + constructor(type: string) { + this.type = type; + } +} +declare class LiveEventComment extends LiveEvent { + constructor(name: string, message: string, thumbnail?: string, colorName, badges) { + super(1); + this.name = name; + this.message = message; + this.thumbnail = thumbnail; + this.colorName = colorName; + this.badges = badges; + } +} + +declare class LiveEventEmojis extends LiveEvent { + constructor(emojis) { + super(4); + this.emojis = emojis; + } +} + +declare class LiveEventDonation extends LiveEvent { + constructor(amount: number, name: string, message: string, thumbnail?: string, expire?: any, colorDonation?: string) { + super(5); + this.amount = amount; + this.name = name; + this.message = message ?? ""; + this.thumbnail = thumbnail; + this.expire = expire; + this.colorDonation = colorDonation; + } +} + +declare class LiveEventViewCount extends LiveEvent { + constructor(viewCount: number) { + super(10); + this.viewCount = viewCount; + } +} + +declare class LiveEventRaid extends LiveEvent { + constructor(targetUrl: string, targetName: string, targetThumbnail: string) { + super(100); + this.targetUrl = targetUrl; + this.targetName = targetName; + this.targetThumbnail = targetThumbnail; + } +} + +//Pagers + +declare class ContentPager { + constructor(results: [], hasMore: boolean, context: any) { + this.plugin_type = "ContentPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + } + + hasMorePagers() { return this.hasMore; } + nextPage() { return new ContentPager([], false, this.context) } +} + +declare class VideoPager { + + hasMore: boolean; + context: any + + constructor(results: PlatformVideo[], hasMore?: boolean, context?: any) { + this.plugin_type = "VideoPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + } + hasMorePagers(): boolean { return this.hasMore; } + nextPage(): VideoPager { return new VideoPager([], false, this.context) } +} + +declare class ChannelPager { + + hasMore: boolean; + context: any + + constructor(results: PlatformVideo[], hasMore: boolean, context: any) { + this.plugin_type = "ChannelPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + } + + hasMorePagers(): boolean { return this.hasMore; } + nextPage(): ChannelPager { return new Pager([], false, this.context) } +} + + +declare class PlaylistPager { + + hasMore: boolean; + context: any + + constructor(results: PlatformPlaylist[], hasMore?: boolean, context?: any) { + this.plugin_type = "PlaylistPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + } + + hasMorePagers() { return this.hasMore; } + nextPage() { return new Pager([], false, this.context) } +} + + +declare class CommentPager { + context: any + + constructor(results: PlatformVideo[], hasMore: boolean, context: any) { + this.plugin_type = "CommentPager"; + this.results = results ?? []; + this.hasMore = hasMore ?? false; + this.context = context ?? {}; + } + hasMorePagers(): boolean { return this.hasMore; } + nextPage(): CommentPager { return new Pager([], false, this.context) } +} + +declare interface Map<T> { + [Key: string]: T; +} + +function throwException(ttype: string, message: string): void { + throw new Error("V8EXCEPTION:" + type + "-" + message); +} + +let plugin = { + config: {}, + settings: {} +}; + +// Plugin configuration +// To override by plugin +interface Source { + + getHome(): VideoPager; + + enable(conf: Config, settings: Map<string>, saveStateStr: string): void; + + setSettings(settings: any): void; + + disable(): void; + + searchSuggestions(query: string): string[]; + search(query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; + getSearchCapabilities(): ResultCapabilities; + + // Optional + searchChannelVideos?(channelUrl: string, query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; + getSearchChannelVideoCapabilities?(): ResultCapabilities; + + isChannelUrl(url: string): boolean; + getChannel(url: string): PlatformChannel | null; + + getChannelVideos(url: string, type: string, order: string, filters: FilterGroup[]): VideoPager; + getChannelCapabilities(): ResultCapabilities; + getSearchChannelContentsCapabilities(): ResultCapabilities; + getPeekChannelTypes(): string[]; + peekChannelContents (url, type): PlatformVideo[] + + isVideoDetailsUrl(url: string): boolean; + getVideoDetails(url: string): PlatformVideoDetails; + + // Optional + getComments?(url: string): CommentPager; + getSubComments?(comment: Comment): CommentPager; + + // Optional + getUserSubscriptions?(): string[]; + getUserPlaylists?(): string[]; + + // Optional + isPlaylistUrl?(url: string): boolean; + + searchPlaylists(query, type, order, filters); + + getPlaylist?(url: string): PlatformPlaylistDetails; + + isContentDetailsUrl(url: string): boolean; + + getChannelContents(url: string, type?: string, order?: string, filters?: Map<String, List<String>>): VideoPager; + + searchChannels(query: string): ChannelPager; + + getContentDetails(url: string): PlatformVideoDetails; + + getChannelPlaylists(url: string): PlaylistPager; + + searchChannelContents(channelUrl: string, query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; + + saveState(): void; + + getChannelTemplateByClaimMap(): any; +} + + +function parseSettings(settings) { + if (!settings) + return {}; + let newSettings = {}; + for (let key in settings) { + if (typeof settings[key] == "string") + newSettings[key] = JSON.parse(settings[key]); + else + newSettings[key] = settings[key]; + } + return newSettings; +} + +function log(str: string) { + if (str) { + console.log(str); + if (typeof str == "string") + bridge.log(str); + else + bridge.log(JSON.stringify(str, null, 4)); + } +} + +function encodePathSegment(segment) { + return encodeURIComponent(segment).replace(/[!'()*]/g, function (c) { + return '%' + c.charCodeAt(0).toString(16); + }); +} + +class URLSearchParams { + constructor(init) { + this._entries = {}; + if (typeof init === 'string') { + if (init !== '') { + init = init.replace(/^\?/, ''); + const attributes = init.split('&'); + let attribute; + for (let i = 0; i < attributes.length; i++) { + attribute = attributes[i].split('='); + this.append(decodeURIComponent(attribute[0]), (attribute.length > 1) ? decodeURIComponent(attribute[1]) : ''); + } + } + } + else if (init instanceof URLSearchParams) { + init.forEach((value, name) => { + this.append(value, name); + }); + } + } + append(name, value) { + value = value.toString(); + if (name in this._entries) { + this._entries[name].push(value); + } + else { + this._entries[name] = [value]; + } + } + delete(name) { + delete this._entries[name]; + } + get(name) { + return (name in this._entries) ? this._entries[name][0] : null; + } + getAll(name) { + return (name in this._entries) ? this._entries[name].slice(0) : []; + } + has(name) { + return (name in this._entries); + } + set(name, value) { + this._entries[name] = [value.toString()]; + } + forEach(callback) { + let entries; + for (let name in this._entries) { + if (this._entries.hasOwnProperty(name)) { + entries = this._entries[name]; + for (let i = 0; i < entries.length; i++) { + callback.call(this, entries[i], name, this); + } + } + } + } + keys() { + const items = []; + this.forEach((value, name) => { items.push(name); }); + return createIterator(items); + } + values() { + const items = []; + this.forEach((value) => { items.push(value); }); + return createIterator(items); + } + entries() { + const items = []; + this.forEach((value, name) => { items.push([value, name]); }); + return createIterator(items); + } + toString() { + let searchString = ''; + this.forEach((value, name) => { + if (searchString.length > 0) + searchString += '&'; + searchString += encodeURIComponent(name) + '=' + encodeURIComponent(value); + }); + return searchString; + } +} + +const source: Source; + +declare var IS_TESTING: boolean; + +let Type = { + Source: { + Dash: "DASH", + HLS: "HLS", + STATIC: "Static" + }, + Feed: { + Videos: "VIDEOS", + Streams: "STREAMS", + Mixed: "MIXED", + Live: "LIVE", + Subscriptions: "SUBSCRIPTIONS" + }, + Order: { + Chronological: "CHRONOLOGICAL" + }, + Date: { + LastHour: "LAST_HOUR", + Today: "TODAY", + LastWeek: "LAST_WEEK", + LastMonth: "LAST_MONTH", + LastYear: "LAST_YEAR" + }, + Duration: { + Short: "SHORT", + Medium: "MEDIUM", + Long: "LONG" + }, + Text: { + RAW: 0, + HTML: 1, + MARKUP: 2 + }, + Chapter: { + NORMAL: 0, + + SKIPPABLE: 5, + SKIP: 6, + SKIPONCE: 7 + } +}; + +let Language = { + UNKNOWN: "Unknown", + ARABIC: "ar", + SPANISH: "es", + FRENCH: "fr", + HINDI: "hi", + INDONESIAN: "id", + KOREAN: "ko", + PORTUGUESE: "pt", + PORTBRAZIL: "pt", + RUSSIAN: "ru", + THAI: "th", + TURKISH: "tr", + VIETNAMESE: "vi", + ENGLISH: "en" +} + + +interface HttpResponse { + isOk(): boolean, + body: string, + code: number +} + +//Package Bridge (variable: bridge) +let bridge = { + /** + * @param {String} label + * @param {String} data + * @return {Unit} + **/ + devSubmit: function (label: string, data: string): Unit { }, + + /** + * @return {Boolean} + **/ + isLoggedIn: function (): boolean { }, + + /** + * @param {String} str + * @return {Unit} + **/ + log: function (str: string): Unit { }, + + /** + * @param {String} str + * @return {Unit} + **/ + throwTest: function (str: string): Unit { }, + + /** + * @param {String} str + * @return {Unit} + **/ + toast: function (str: string): Unit { }, + +} + +//Package Http (variable: http) + +interface IHttp { + /** + * @param {String} url + * @param {Map} headers + * @param {Boolean} useAuth + * @return {BridgeHttpResponse} + **/ + GET(url: string, headers: Map<string, string>, useAuth?: boolean): BridgeHttpResponse; + + /** + * @param {String} url + * @param {String} body + * @param {Map} headers + * @param {Boolean} useAuth + * @return {BridgeHttpResponse} + **/ + POST(url: string, body: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; + + /** + * @return {BatchBuilder} + **/ + batch(): BatchBuilder; + + /** + * @param {Boolean} withAuth + * @return {PackageHttpClient} + **/ + getDefaultClient(withAuth: boolean): PackageHttpClient; + + /** + * @param {Boolean} withAuth + * @return {PackageHttpClient} + **/ + newClient(withAuth: boolean): PackageHttpClient; + + /** + * @param {String} method + * @param {String} url + * @param {Map} headers + * @param {Boolean} useAuth + * @return {BridgeHttpResponse} + **/ + request(method: string, url: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; + + /** + * @param {String} method + * @param {String} url + * @param {String} body + * @param {Map} headers + * @param {Boolean} useAuth + * @return {BridgeHttpResponse} + **/ + requestWithBody(method: string, url: string, body: string, headers: Map<string, string>, useAuth: boolean): BridgeHttpResponse; + + /** + * @param {String} url + * @param {Map} headers + * @param {Boolean} useAuth + * @return {SocketResult} + **/ + socket(url: string, headers: Map<string, string>, useAuth: boolean): SocketResult; + + /** + * @param {Map} headers + * @return {void} + **/ + setDefaultHeaders(headers: Map<string, string>): void +} + + +let http: IHttp + + +interface IPager<T> { + hasMorePages() : Boolean; + nextPage(); + getResults() : List<T>; } \ No newline at end of file diff --git a/types/types.d.ts b/types/types.d.ts index 61a5997..b54dcd1 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1,15 +1,19 @@ - -interface IDailymotionPluginSettings { - hideSensitiveContent: boolean; - preferredCountry: number; - avatarSize: number; - thumbnailResolution: number; - videosPerPageIndex: number; - playlistsPerPageIndex: number; -} - - -interface IDailymotionSubtitle { - data: Map<string, string, { urls: string[], label: string }>, - enable: boolean + +interface IDailymotionPluginSettings { + hideSensitiveContent: boolean; + preferredCountry: number; + avatarSize: number; + thumbnailResolution: number; + videosPerPageIndex: number; + playlistsPerPageIndex: number; +} + + +interface IDailymotionSubtitle { + data: Map<string, string, { urls: string[], label: string }>, + enable: boolean +} + +interface IDictionary<T> { + [key: string]: T; } \ No newline at end of file -- GitLab