diff --git a/build/DailymotionConfig.json b/build/DailymotionConfig.json index 179f8bbddd98cf6ea72c494f1aeab40d193c4dee..c4ffc4775635573cf84fe9b68e2a87f433312488 100644 --- a/build/DailymotionConfig.json +++ b/build/DailymotionConfig.json @@ -10,7 +10,7 @@ "version": 8, "iconUrl": "./dailymotion.png", "id": "9c87e8db-e75d-48f4-afe5-2d203d4b95c5", - "scriptSignature": "OjAgpayE+8LfSvm+TLWCUyPzoUHzaGgSJ5bLgrYFi7nlX3jQapqgS9el5J4NBlN9/C4kSLiQ7UFNn2qcozCROPmA3F+yNxO5XISGopziFT9axARUXbY0p5AmlPoqKHWkw1LseHmRADPc/2J+BTOnbpl4QhBWBpm6ljP8SkSzTiZxTdafiQLU78yi89fYyti5329xsUzyuQztq2R32d/R0Oo3I+94hltpJnIz2MzZ/5FLGHByPjqMw8aB4F1M0DW+CzJtQ3wkMZn3/Y6vVr7Xpy0DW29pynA+tJnuurd88xWtO/aJ+s0FodDyP3OAEYuxe2OiFMTJRKIbzNzGR0i0CA==", + "scriptSignature": "Mkn8NphaKXrs9i0LDd0/TlNZTxZFPAErtRgGhQZiWsxCWucxmmaPuovY6dAfNc88BD+3H0R3n7mcEANFZm0AtOOV8LfezzmYOEugZgRxgYgwCTkBYlyP821WGRnPY3+SiDUA6Sv+vLwlua9jlQSixNhxOfFSWm1Ne2FwrOG2s92n1sWnQ6T6pvJxlqclq4AlsEY6HzMsGL5T0nlRlM5XDrgxxc1yI4pAObggscRPq/D7FGD3JP+D0Lls30j1W2rXhG5IYwteIFTqo2keUwWW4qxkKTqtkxBgbeEuB2W4KVJOtZkYpYDDWzDMRp/eNE/6WGN0YgxduVJTLlA9aibGfQ==", "scriptPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwrBXc5lMXrDD4y5GLT1rnxq64nmmBcE19lvIWL/o+wTK6jebmlQss4CrpH2Pkw8cwczcA4aHQKN8+ThveJFMBxW57gf30odKrAglvvPAv9Rm9D0BZZ501jBXvz+2Rt7su/ICN97JNabjeniAxWBGgr+It3gwvgGAXmWo9FcGfPTooy+g7lErDx5S9Hy/W83b/L3Q64Ytcb4ed0zi6pfZhgpXW0ZJxiZSJRPj6JusTJclPd4OHnl6EwAL979PloyKT0EjMgMX89LAsgctJzSOJkWcQnsbcWV3SE87GyFN4/4Py+GEvvZLKv/iWhY9Dhw9G7j3hy+3QPpHd5L3KSXC4wIDAQAB", "packages": [ "Http" @@ -26,9 +26,9 @@ "s3.dmcdn.net" ], "authentication": { - "loginUrl": "https://www.dailymotion.com/signin", - "completionUrl": "https://www.dailymotion.com/cookie/refresh_token", + "loginUrl": "https://www.dailymotion.com/signin?urlback=/video/x58d0gt", "cookiesToFind": [ + "damd", "refresh_token", "access_token" ], diff --git a/build/DailymotionScript.js b/build/DailymotionScript.js index 531a25df6aefdfd6b765928f5d3c7a2c7bd6c453..e4829e85946a902d82b5de9d4239e21b6e96d531 100644 --- a/build/DailymotionScript.js +++ b/build/DailymotionScript.js @@ -1,25 +1,36 @@ 'use strict'; -const errorTypes = { - "DM001": "No video has been specified, you need to specify one.", - "DM002": "Content has been deleted.", - "DM003": "Live content is not available, i.e. it may not have started yet.", - "DM004": "Copyrighted content, access forbidden.", - "DM005": "Content rejected (this video may have been removed due to a breach of the terms of use, a copyright claim or an infringement upon third party rights).", - "DM006": "Publishing in progress…", - "DM007": "Video geo-restricted by its owner.", - "DM008": "Explicit content. Explicit content can be enabled using the plugin settings", - "DM009": "Explicit content (offsite embed)", - "DM010": "Private content", - "DM011": "An encoding error occurred", - "DM012": "Encoding in progress", - "DM013": "This video has no preset (no video stream)", - "DM014": "This video has not been made available on your device by its owner", - "DM015": "Kids host error", - "DM016": "Content not available on this website, it can only be watched on Dailymotion", - "DM019": "This content has been uploaded by an inactive channel and its access is limited" -}; - +const BASE_URL = "https://www.dailymotion.com"; +const BASE_URL_API = "https://graphql.api.dailymotion.com"; +const BASE_URL_API_AUTH = `${BASE_URL_API}/oauth/token`; +const BASE_URL_VIDEO = `${BASE_URL}/video`; +const BASE_URL_PLAYLIST = `${BASE_URL}/playlist`; +const BASE_URL_METADATA = `${BASE_URL}/player/metadata/video`; +const USER_AGENT = 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36'; +// Those are used even for not logged users to make requests on the graphql api. +//TODO: check how to get them dynamically +const CLIENT_ID = 'f1a362d288c1b98099c7'; +const CLIENT_SECRET = 'eea605b96e01c796ff369935357eca920c5da4c5'; +const X_DM_AppInfo_Id = "com.dailymotion.neon"; +const X_DM_AppInfo_Type = "website"; +const X_DM_AppInfo_Version = "v2024-05-16T12:17:57.363Z"; //TODO check how to get this dynamically +const X_DM_Neon_SSR = "0"; +const X_DM_Preferred_Country = ""; //TODO check how to get this from Grayjay +const PLATFORM = "Dailymotion"; +const PLATFORM_CLAIMTYPE = 3; +const ITEMS_PER_PAGE = 5; +// search capabilities - upload date +const LESS_THAN_MINUTE = "LESS_THAN_MINUTE"; +const ONE_TO_FIVE_MINUTES = "ONE_TO_FIVE_MINUTES"; +const FIVE_TO_THIRTY_MINUTES = "FIVE_TO_THIRTY_MINUTES"; +const THIRTY_TO_ONE_HOUR = "THIRTY_TO_ONE_HOUR"; +const MORE_THAN_ONE_HOUR = "MORE_THAN_ONE_HOUR"; +const DURATION_THRESHOLDS = {}; +DURATION_THRESHOLDS[LESS_THAN_MINUTE] = { min: 0, max: 60 }; +DURATION_THRESHOLDS[ONE_TO_FIVE_MINUTES] = { min: 60, max: 300 }; +DURATION_THRESHOLDS[FIVE_TO_THIRTY_MINUTES] = { min: 300, max: 1800 }; +DURATION_THRESHOLDS[THIRTY_TO_ONE_HOUR] = { min: 1800, max: 3600 }; +DURATION_THRESHOLDS[MORE_THAN_ONE_HOUR] = { min: 3600, max: null }; const countryNamesToCode = { "": "", "Afghanistan": "AF", @@ -293,11 +304,25 @@ const thumbnailHeight = [ "PORTRAIT_720", "PORTRAIT_1080" ]; -const constants = { - creatorAvatarHeight, - thumbnailHeight, - countryNamesToCode, - countryNames: Object.keys(countryNamesToCode) +const countryNames = Object.keys(countryNamesToCode); +const errorTypes = { + "DM001": "No video has been specified, you need to specify one.", + "DM002": "Content has been deleted.", + "DM003": "Live content is not available, i.e. it may not have started yet.", + "DM004": "Copyrighted content, access forbidden.", + "DM005": "Content rejected (this video may have been removed due to a breach of the terms of use, a copyright claim or an infringement upon third party rights).", + "DM006": "Publishing in progress…", + "DM007": "Video geo-restricted by its owner.", + "DM008": "Explicit content. Explicit content can be enabled using the plugin settings", + "DM009": "Explicit content (offsite embed)", + "DM010": "Private content", + "DM011": "An encoding error occurred", + "DM012": "Encoding in progress", + "DM013": "This video has no preset (no video stream)", + "DM014": "This video has not been made available on your device by its owner", + "DM015": "Kids host error", + "DM016": "Content not available on this website, it can only be watched on Dailymotion", + "DM019": "This content has been uploaded by an inactive channel and its access is limited" }; const SEARCH_SUGGESTIONS_QUERY = ` @@ -331,8 +356,9 @@ query CHANNEL_QUERY_DESKTOP( avatar(height:$avatar_size) { url } - coverURL1024x: coverURL(size: "1024x") - coverURL1920x: coverURL(size: "1920x") + banner(width:LANDSCAPE_1920) { + url + } tagline country { id @@ -1106,6 +1132,7 @@ query PLAYLIST_VIDEO_QUERY($xid: String!, $numberOfVideos: Int = 100, $avatar_si } creator { id + name displayName xid avatar(height:$avatar_size) { @@ -1149,6 +1176,7 @@ query PLAYLIST_VIDEO_QUERY($xid: String!, $numberOfVideos: Int = 100, $avatar_si } creator { id + name displayName xid avatar(height:$avatar_size) { @@ -1203,7 +1231,60 @@ query SUBSCRIPTIONS_QUERY($first: Int, $page: Int) { } } `; +const GET_CHANNEL_PLAYLISTS = ` +query CHANNEL_PLAYLISTS_QUERY( + $channel_name: String! + $sort: String + $page: Int! + $first: Int! +) { + channel(name: $channel_name) { + id + xid + channel_playlist_collections: collections( + sort: $sort + page: $page + first: $first + ) { + pageInfo { + hasNextPage + nextPage + } + edges { + node { + id + xid + updatedAt + createdAt + name + description + thumbnailx60: thumbnailURL(size: "x60") + thumbnailx120: thumbnailURL(size: "x120") + thumbnailx240: thumbnailURL(size: "x240") + thumbnailx720: thumbnailURL(size: "x720") + stats { + id + videos { + id + total + } + } + } + } + } + } +} +`; +let AUTHORIZATION_TOKEN_ANONYMOUS_USER = ""; +let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE; +let httpClientRequestToken = http.newClient(false); +function getPreferredCountry(preferredCountryIndex) { + const countryName = countryNames[preferredCountryIndex]; + const code = countryNamesToCode[countryName]; + const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); + return preferredCountry; +} const objectToUrlEncodedString = (obj) => { const encodedParams = []; for (const key in obj) { @@ -1215,49 +1296,181 @@ const objectToUrlEncodedString = (obj) => { } return encodedParams.join('&'); }; -var util = { - objectToUrlEncodedString, -}; +function getChannelNameFromUrl(url) { + const channel_name = url.split('/').pop(); + return channel_name; +} +function isUsernameUrl(url) { + var regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); + return regex.test(url); +} +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; +} +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; +} -const BASE_URL = "https://www.dailymotion.com"; -const BASE_URL_API = "https://graphql.api.dailymotion.com"; -const BASE_URL_API_AUTH = `${BASE_URL_API}/oauth/token`; -const BASE_URL_VIDEO = `${BASE_URL}/video`; -const BASE_URL_PLAYLIST = `${BASE_URL}/playlist`; -const BASE_URL_METADATA = `${BASE_URL}/player/metadata/video`; -const USER_AGENT = 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36'; -// Those are used even for not logged users to make requests on the graphql api. -//TODO: check how to get them dynamically -const CLIENT_ID = 'f1a362d288c1b98099c7'; -const CLIENT_SECRET = 'eea605b96e01c796ff369935357eca920c5da4c5'; -var config = {}; -var _settings = {}; -const X_DM_AppInfo_Id = "com.dailymotion.neon"; -const X_DM_AppInfo_Type = "website"; -const X_DM_AppInfo_Version = "v2024-05-16T12:17:57.363Z"; //TODO check how to get this dynamically -const X_DM_Neon_SSR = "0"; -const X_DM_Preferred_Country = ""; //TODO check how to get this from Grayjay -const PLATFORM = "Dailymotion"; -const PLATFORM_CLAIMTYPE = 3; -const ITEMS_PER_PAGE = 5; -// search capabilities - upload date -const LESS_THAN_MINUTE = "LESS_THAN_MINUTE"; -const ONE_TO_FIVE_MINUTES = "ONE_TO_FIVE_MINUTES"; -const FIVE_TO_THIRTY_MINUTES = "FIVE_TO_THIRTY_MINUTES"; -const THIRTY_TO_ONE_HOUR = "THIRTY_TO_ONE_HOUR"; -const MORE_THAN_ONE_HOUR = "MORE_THAN_ONE_HOUR"; -const DURATION_THRESHOLDS = {}; -DURATION_THRESHOLDS[LESS_THAN_MINUTE] = { min: 0, max: 60 }; -DURATION_THRESHOLDS[ONE_TO_FIVE_MINUTES] = { min: 60, max: 300 }; -DURATION_THRESHOLDS[FIVE_TO_THIRTY_MINUTES] = { min: 300, max: 1800 }; -DURATION_THRESHOLDS[THIRTY_TO_ONE_HOUR] = { min: 1800, max: 3600 }; -DURATION_THRESHOLDS[MORE_THAN_ONE_HOUR] = { min: 3600, max: null }; -let AUTHORIZATION_TOKEN_ANONYMOUS_USER = null; -let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE = null; -var httpClientAnonymous = null; -var httpClientRequestToken = null; +class SearchPagerAll extends VideoPager { + /** + * @param {import("./types.d.ts").SearchContext} context the query params + * @param {(PlatformVideo | PlatformChannel)[]} results the initial results + */ + cb; + constructor(results, hasMore, params, page, cb) { + 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); + } +} +class SearchChannelPager extends ChannelPager { + cb; + 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); + } +} +class ChannelVideoPager extends VideoPager { + /** + * @param {import("./types.d.ts").URLContext} context the context + * @param {PlatformVideo[]} results the initial results + * @param {boolean} hasNextPage if there is a next page + */ + cb; + constructor(context, results, hasNextPage, cb) { + super(results, hasNextPage, context); + this.cb = cb; + } + nextPage() { + return this.cb(this.context); + } +} +class SearchPlaylistPager extends VideoPager { + /** + * @param {import("./types.d.ts").SearchContext} context the query params + * @param {(PlatformVideo | PlatformChannel)[]} results the initial results + */ + cb; + constructor(results, hasMore, params, page, cb) { + 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 searchPlaylists(opts); + return this.cb(opts); + } +} + +var config; +var _settings; +let httpClientAnonymous = http.newClient(false); +// Will be used to store playlists that require authentication +const authenticatedPlaylistCollection = []; source.setSettings = function (settings) { _settings = settings; + http.GET(BASE_URL, {}, true); }; //Source Methods source.enable = function (conf, settings, saveStateStr) { @@ -1267,9 +1480,15 @@ source.enable = function (conf, settings, saveStateStr) { _settings.hideSensitiveContent = false; _settings.avatarSize = 8; _settings.thumbnailResolution = 7; + if (!config) { + config = { + id: "9c87e8db-e75d-48f4-afe5-2d203d4b95c5" + }; + } } }; source.getHome = function () { + getAnonymousUserTokenSingleton(); return getVideoPager({}, 0); }; source.searchSuggestions = function (query) { @@ -1277,12 +1496,12 @@ source.searchSuggestions = function (query) { "query": query }; try { - const jsonResponse = executeGqlQuery({ + const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'AUTOCOMPLETE_QUERY', variables: variables, query: SEARCH_SUGGESTIONS_QUERY }); - return jsonResponse?.data?.search?.suggestedVideos?.edges?.map(edge => edge?.node?.name); + return jsonResponse?.data?.search?.suggestedVideos?.edges?.map(edge => edge?.node?.name ?? "") ?? []; } catch (error) { log('Failed to get search suggestions:' + error?.message); @@ -1331,10 +1550,6 @@ source.getSearchCapabilities = () => { source.search = function (query, type, order, filters) { return getSearchPagerAll({ q: query, page: 1, type, order, filters }); }; -// source.getSearchChannelContentsCapabilities = function () { -// }; -// source.searchChannelContents = function (channelUrl, query, type, order, filters) { -// }; source.searchChannels = function (query) { return getSearchChannelPager({ q: query, page: 1 }); }; @@ -1344,17 +1559,16 @@ source.isChannelUrl = function (url) { }; source.getChannel = function (url) { const channel_name = getChannelNameFromUrl(url); - const channelDetails = executeGqlQuery({ + const channelDetails = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'CHANNEL_QUERY_DESKTOP', variables: { channel_name: channel_name, - avatar_size: constants.creatorAvatarHeight[_settings?.avatarSize] + avatar_size: creatorAvatarHeight[_settings?.avatarSize] }, query: CHANNEL_BY_URL_QUERY }); - const user = channelDetails.data.channel; - const banner = user?.coverURL1024x ?? user?.coverURL1920x; - const externalLinks = user?.externalLinks ?? {}; + const channel = channelDetails.data.channel; + const externalLinks = channel?.externalLinks ?? {}; const links = {}; Object .keys(externalLinks) @@ -1364,12 +1578,12 @@ source.getChannel = function (url) { } }); return new PlatformChannel({ - id: new PlatformID(PLATFORM, user?.id, config?.id, PLATFORM_CLAIMTYPE), - name: user?.displayName, - thumbnail: user?.avatar?.url, - banner, - subscribers: user?.metrics?.engagement?.followers?.edges[0]?.node?.total, - description: user?.description, + id: new PlatformID(PLATFORM, channel?.id, config.id, PLATFORM_CLAIMTYPE), + name: channel?.displayName ?? "", + thumbnail: channel?.avatar?.url ?? "", + banner: channel.banner?.url ?? "", + subscribers: channel?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, + description: channel?.description ?? "", url, links, }); @@ -1386,8 +1600,7 @@ source.getContentDetails = function (url) { }; //Playlist source.isPlaylistUrl = (url) => { - var isPlaylist = url.startsWith(BASE_URL_PLAYLIST); - return isPlaylist; + return url.startsWith(BASE_URL_PLAYLIST); }; source.searchPlaylists = (query, type, order, filters) => { return searchPlaylists({ q: query, type, order, filters }); @@ -1396,27 +1609,29 @@ source.getPlaylist = (url) => { const xid = url.split('/').pop(); const variables = { xid, - avatar_size: constants.creatorAvatarHeight[_settings?.avatarSize], - thumbnail_resolution: constants.thumbnailHeight[_settings?.thumbnailResolution], + avatar_size: creatorAvatarHeight[_settings.avatarSize], + thumbnail_resolution: thumbnailHeight[_settings.thumbnailResolution], }; - const jsonResponse = executeGqlQuery({ + const usePlatformAuth = authenticatedPlaylistCollection.includes(url); + let jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth }), { operationName: 'PLAYLIST_VIDEO_QUERY', variables, - query: PLAYLIST_DETAILS_QUERY + query: PLAYLIST_DETAILS_QUERY, + usePlatformAuth }); const videos = jsonResponse?.data?.collection?.videos?.edges.map(edge => { const resource = edge.node; const opts = { id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), - name: resource.title, + name: resource.title ?? "", thumbnails: new Thumbnails([ - new Thumbnail(resource?.thumbnail?.url, 0) + new Thumbnail(resource?.thumbnail?.url ?? "", 0) ]), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, resource.creator.id, config.id, PLATFORM_CLAIMTYPE), resource.creator.displayName, `${BASE_URL}/${resource.creator.name}`, resource.creator.avatar.url ?? "", 0), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, resource?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), resource?.creator?.displayName ?? "", `${BASE_URL}/${resource?.creator?.name}`, resource?.creator?.avatar?.url ?? "", 0), uploadDate: parseInt(new Date(resource.createdAt).getTime() / 1000), datetime: parseInt(new Date(resource.createdAt).getTime() / 1000), - url: resource.url, - duration: resource.duration, + url: resource.url ?? "", + duration: resource.duration ?? 0, viewCount: resource?.viewCount ?? 0, isLive: false }; @@ -1425,8 +1640,8 @@ source.getPlaylist = (url) => { const playlist = jsonResponse?.data?.collection; return new PlatformPlaylistDetails({ url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid, config.id), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist?.creator?.id, config.id, PLATFORM_CLAIMTYPE), playlist?.creator?.displayName, `${BASE_URL}/${playlist?.creator?.name}`, playlist?.creator?.avatar?.url ?? "", 0), + id: new PlatformID(PLATFORM, playlist?.xid, config.id, PLATFORM_CLAIMTYPE), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), playlist?.creator?.displayName ?? "", `${BASE_URL}/${playlist?.creator?.name}`, playlist?.creator?.avatar?.url ?? "", 0), name: playlist.name, thumbnail: playlist?.thumbnail?.url, videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, @@ -1448,7 +1663,7 @@ source.getUserSubscriptions = () => { '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': 'it', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), Origin: BASE_URL, DNT: '1', Connection: 'keep-alive', @@ -1459,18 +1674,20 @@ source.getUserSubscriptions = () => { Pragma: 'no-cache', 'Cache-Control': 'no-cache', }; + const usePlatformAuth = true; const fetchSubscriptions = (page, first) => { - const jsonResponse = executeGqlQuery({ + const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth }), { operationName: 'SUBSCRIPTIONS_QUERY', variables: { first: first, page: page, - avatar_size: constants.creatorAvatarHeight[_settings?.avatarSize], + avatar_size: creatorAvatarHeight[_settings?.avatarSize], }, headers, - query: GET_USER_SUBSCRIPTIONS - }, true); - return jsonResponse?.data?.me?.channel?.followings?.edges?.map(edge => edge.node.creator.name); + query: GET_USER_SUBSCRIPTIONS, + usePlatformAuth + }); + return jsonResponse?.data?.me?.channel?.followings?.edges?.map(edge => edge?.node?.creator?.name ?? "") ?? []; }; const first = 100; // Number of records to fetch per page let page = 1; @@ -1487,6 +1704,73 @@ source.getUserSubscriptions = () => { } while (items.length); return subscriptions; }; +source.getUserPlaylists = () => { + 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 userInfoQuery = ` + query SUBSCRIPTIONS_QUERY { + me { + xid + channel { + name + } + } + } + `; + const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: true }), { + operationName: 'SUBSCRIPTIONS_QUERY', + headers, + query: userInfoQuery, + usePlatformAuth: true + }); + const userName = jsonResponse?.data?.me?.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 + }, + headers, + query: GET_CHANNEL_PLAYLISTS, + usePlatformAuth + }); + const playlists = jsonResponse1.data.channel.channel_playlist_collections.edges.map(edge => { + const playlistUrl = `${BASE_URL_PLAYLIST}/${edge.node.xid}`; + if (!authenticatedPlaylistCollection.includes(playlistUrl)) { + authenticatedPlaylistCollection.push(playlistUrl); + } + return playlistUrl; + }); + return playlists; +} function getQuery(context) { context.sort = parseSort(context.order); if (!context.filters) { @@ -1523,27 +1807,28 @@ function searchPlaylists(contextQuery) { "shouldIncludeLives": false, "page": context.page, "limit": ITEMS_PER_PAGE, - "thumbnail_resolution": constants.thumbnailHeight[_settings?.thumbnailResolution], - "avatar_size": constants.creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], + "avatar_size": creatorAvatarHeight[_settings?.avatarSize], }; - const jsonResponse = executeGqlQuery({ + const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'SEARCH_QUERY', variables: variables, query: MAIN_SEARCH_QUERY, headers: undefined }); - var searchResults = jsonResponse?.data?.search?.playlists?.edges?.map(edge => { - const playlist = edge.node; + const playlistConnection = jsonResponse?.data?.search?.playlists; + var searchResults = playlistConnection?.edges?.map(edge => { + const playlist = edge?.node; return new PlatformPlaylist({ url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid, config.id), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist.creator.id, config.id, PLATFORM_CLAIMTYPE), playlist.creator.displayName, `${BASE_URL}/${playlist.creator.name}`, playlist.creator.avatar.url ?? "", 0), - name: playlist.name, + id: new PlatformID(PLATFORM, playlist?.xid ?? "", config.id, PLATFORM_CLAIMTYPE), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), playlist?.creator?.displayName ?? "", `${BASE_URL}/${playlist?.creator?.name}`, playlist?.creator?.avatar?.url ?? "", 0), + name: playlist?.name, thumbnail: playlist?.thumbnail?.url, videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, }); }); - const hasMore = jsonResponse?.data?.search?.playlists?.pageInfo?.hasNextPage; + const hasMore = playlistConnection?.pageInfo?.hasNextPage; if (!searchResults || !searchResults?.length) { return new PlaylistPager([]); } @@ -1552,71 +1837,9 @@ function searchPlaylists(contextQuery) { sort: context.sort, filters: context.filters, }; - return new SearchPlaylistPager(searchResults, hasMore, params, context.page); + return new SearchPlaylistPager(searchResults, hasMore, params, context.page, searchPlaylists); } //Internals -function getChannelNameFromUrl(url) { - const channel_name = url.split('/').pop(); - return channel_name; -} -function isUsernameUrl(url) { - // Define the regex pattern to match the username URL - var regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); - // Test the URL against the regex pattern - return regex.test(url); -} -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 = util.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; -} -function getPreferredCountry() { - const countryName = constants.countryNames[_settings?.preferredCountry]; - const code = constants.countryNamesToCode[countryName]; - const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); - return preferredCountry; -} function getVideoPager(params, page) { const count = ITEMS_PER_PAGE; if (!params) { @@ -1632,7 +1855,7 @@ function getVideoPager(params, page) { "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(), + "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), "Origin": BASE_URL, "DNT": "1", "Sec-Fetch-Site": "same-site", @@ -1642,12 +1865,12 @@ function getVideoPager(params, page) { }; let obj; try { - obj = executeGqlQuery({ + obj = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'SEACH_DISCOVERY_QUERY', variables: { shouldQueryPromotedHashtag: false, - avatar_size: constants.creatorAvatarHeight[_settings?.avatarSize], - thumbnail_resolution: constants.thumbnailHeight[_settings?.thumbnailResolution], + avatar_size: creatorAvatarHeight[_settings?.avatarSize], + thumbnail_resolution: thumbnailHeight[_settings?.thumbnailResolution], }, query: HOME_QUERY, headers: headersToAdd, @@ -1661,7 +1884,7 @@ function getVideoPager(params, page) { ?.filter(edge => edge?.node?.id) ?.map(edge => { const v = edge.node; - const metadata = GetVideoExtraDEtails(v.xid); + const metadata = GetVideoExtraDetails(v.xid); return ToPlatformVideo({ id: v.id, name: v.title ?? "", @@ -1680,10 +1903,10 @@ function getVideoPager(params, page) { }); }); const hasMore = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.pageInfo?.hasNextPage ?? false; - return new SearchPagerAll(results, hasMore, params, page); + return new SearchPagerAll(results, hasMore, params, page, getVideoPager); } -function GetVideoExtraDEtails(xid) { - const json = executeGqlQuery({ +function GetVideoExtraDetails(xid) { + const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'WATCHING_VIDEO', variables: { xid }, query: GET_VIDEO_EXTRA_DETAILS @@ -1695,7 +1918,7 @@ function GetVideoExtraDEtails(xid) { function getChannelPager(context) { const url = context.url; const channel_name = getChannelNameFromUrl(url); - const json = executeGqlQuery({ + const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'CHANNEL_VIDEOS_QUERY', variables: { "channel_name": channel_name, @@ -1703,26 +1926,27 @@ function getChannelPager(context) { "page": context.page ?? 1, "allowExplicit": !_settings.hideSensitiveContent, "first": context.page_size ?? ITEMS_PER_PAGE, - "avatar_size": constants.creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": constants.thumbnailHeight[_settings?.thumbnailResolution], + "avatar_size": creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], }, query: CHANNEL_VIDEOS_BY_CHANNEL_NAME }); const edges = json?.data?.channel?.channel_videos_all_videos?.edges ?? []; let videos = edges.map((edge) => { - const metadata = GetVideoExtraDEtails(edge.node.xid); + const video = edge.node; + const metadata = GetVideoExtraDetails(video.xid); return ToPlatformVideo({ - id: edge.node.id, - name: edge.node.title, - thumbnail: edge?.node?.thumbnail.url ?? "", - createdAt: edge?.node?.createdAt, - creatorId: edge?.node?.creator?.id, - creatorDisplayName: edge?.node?.creator?.displayName, - creatorName: edge.node.creator.name, - creatorAvatar: edge?.node?.creator?.avatar?.url, - creatorUrl: `${BASE_URL}/${edge?.node?.creator?.name}`, - duration: edge.node.duration, - url: `${BASE_URL_VIDEO}/${edge?.node?.xid}`, + id: video.id, + name: video.title, + thumbnail: video?.thumbnail?.url ?? "", + createdAt: video?.createdAt, + creatorId: video?.creator?.id, + creatorDisplayName: video?.creator?.displayName, + creatorName: video?.creator?.name, + creatorAvatar: video?.creator?.avatar?.url, + creatorUrl: `${BASE_URL}/${video?.creator?.name}`, + duration: video.duration, + url: `${BASE_URL_VIDEO}/${video?.xid}`, viewCount: metadata.views ?? 0, isLive: false }); @@ -1730,10 +1954,10 @@ function getChannelPager(context) { if (edges.length > 0) { context.page++; } - return new ChannelVideoPager(context, videos, json?.data?.channel?.channel_videos_all_videos?.pageInfo?.hasNextPage); + return new ChannelVideoPager(context, videos, json?.data?.channel?.channel_videos_all_videos?.pageInfo?.hasNextPage, getChannelPager); } function ToPlatformVideo(resource) { - const opts = { + return new PlatformVideo({ id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), name: resource.name, thumbnails: new Thumbnails([new Thumbnail(resource.thumbnail, 0)]), @@ -1743,8 +1967,7 @@ function ToPlatformVideo(resource) { duration: resource.duration, viewCount: resource.viewCount, isLive: resource.isLive - }; - return new PlatformVideo(opts); + }); } function parseSort(order) { let sort; @@ -1806,37 +2029,39 @@ function getSearchPagerAll(contextQuery) { "shouldIncludeLives": true, "page": context.page ?? 1, "limit": ITEMS_PER_PAGE, - "avatar_size": constants.creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": constants.thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution] }; - const jsonResponse = executeGqlQuery({ + const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'SEARCH_QUERY', variables: variables, query: MAIN_SEARCH_QUERY, headers: undefined }); const results = []; + const videoConnection = jsonResponse?.data?.search?.videos; + const liveConnection = jsonResponse?.data?.search?.videos; const all = [ - ...(jsonResponse?.data?.search?.videos?.edges ?? []), - ...(jsonResponse?.data?.search?.lives?.edges ?? []) + ...(videoConnection?.edges ?? []), + ...(liveConnection?.edges ?? []) ]; for (const edge of all) { - const sv = edge.node; - const isLive = sv.isOnAir == true; - const viewCount = isLive ? sv.audienceCount : sv?.stats?.views?.total; + const sv = edge?.node; + const isLive = sv?.isOnAir == true; + const viewCount = isLive ? sv?.audienceCount : sv?.stats?.views?.total; var video = ToPlatformVideo({ - id: sv.id, - name: sv.title, + id: sv?.id, + name: sv?.title, thumbnail: sv?.thumbnail?.url, - createdAt: sv.createdAt, - creatorId: sv.creator?.id, - creatorName: sv.creator?.name, - creatorDisplayName: sv.creator?.displayName, + createdAt: sv?.createdAt, + creatorId: sv?.creator?.id, + creatorName: sv?.creator?.name, + creatorDisplayName: sv?.creator?.displayName, creatorUrl: `${BASE_URL}/${sv?.creator?.name}`, - creatorAvatar: sv.creator?.avatar?.url ?? "", - duration: sv.duration, + creatorAvatar: sv?.creator?.avatar?.url ?? "", + duration: sv?.duration, viewCount, - url: `${BASE_URL_VIDEO}/${sv.xid}`, + url: `${BASE_URL_VIDEO}/${sv?.xid}`, isLive, description: sv?.description ?? '', }); @@ -1848,45 +2073,11 @@ function getSearchPagerAll(contextQuery) { sort: context.sort, filters: context.filters, }; - return new SearchPagerAll(results, jsonResponse?.data?.search?.videos?.pageInfo?.hasNextPage, params, context.page); + return new SearchPagerAll(results, videoConnection?.pageInfo?.hasNextPage, params, context.page, getSearchPagerAll); } -function executeGqlQuery(requestOptions, usePlatformAuth = false) { - 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 res = getHttpContext(usePlatformAuth).POST(BASE_URL_API, gql, headersToAdd, usePlatformAuth); - if (!res.isOk) { - console.error('Failed to get token', res); - 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(', '); - log(JSON.stringify(message)); - throw new UnavailableException(message); - } - return body; -} -function checkHLS(url, headersToAdd, use_authenticated = false) { +function checkHLS(url, headersToAdd, usePlatformAuth = false) { // const resp = http.GET(url, headersToAdd, true); - var resp = getHttpContext(use_authenticated).GET(url, headersToAdd, use_authenticated, use_authenticated); + var resp = getHttpContext({ usePlatformAuth }).GET(url, headersToAdd, usePlatformAuth); if (!resp.isOk) { throw new UnavailableException('This content is not available'); } @@ -1915,7 +2106,7 @@ function getSavedVideo(url, usePlatformAuth = false) { else { headers1["Cookie"] = "ff=off"; } - var player_metadataResponse = getHttpContext(usePlatformAuth).GET(player_metadata_url, headers1, usePlatformAuth); + var player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); if (!player_metadataResponse.isOk) { throw new UnavailableException('Unable to get player metadata'); } @@ -1938,7 +2129,7 @@ function getSavedVideo(url, usePlatformAuth = false) { "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(), + "X-DM-Preferred-Country": getPreferredCountry(_settings?.preferredCountry), "Origin": BASE_URL, "DNT": "1", "Connection": "keep-alive", @@ -1955,15 +2146,15 @@ function getSavedVideo(url, usePlatformAuth = false) { const variables = { "xid": id, "isSEO": false, - "avatar_size": constants.creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": constants.thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": thumbnailHeight[_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); + 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'); } @@ -2027,9 +2218,9 @@ function getSearchChannelPager(context) { query: context.q, page: context.page ?? 1, limit: ITEMS_PER_PAGE, - avatar_size: constants.creatorAvatarHeight[_settings?.avatarSize] + avatar_size: creatorAvatarHeight[_settings?.avatarSize] }; - const json = executeGqlQuery({ + const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: "SEARCH_QUERY", variables, query: SEARCH_CHANNEL @@ -2043,114 +2234,16 @@ function getSearchChannelPager(context) { subscribers: c?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, url: `${BASE_URL}/${c.name}`, links: [], - banner: null, + banner: "", description: c.description, }); }); var params = { query: context.q, }; - return new SearchChannelPager(results, json?.data?.search?.channels?.pageInfo?.hasNextPage, params, context.page); -} -function getHttpContext(usePlatformAuth = false) { - return usePlatformAuth ? http : httpClientAnonymous; -} -function ensureDefaultSettings() { - if (!_settings) { - _settings = {}; - } - if (_settings.hideSensitiveContent == undefined) { - _settings.hideSensitiveContent = true; - } - if (_settings.avatarSize == undefined) { - _settings.avatarSize = 8; - } - if (_settings.thumbnailResolution == undefined) { - _settings.thumbnailResolution = 7; - } - if (_settings.preferredCountry == undefined) { - const settingIndex = constants?.countryNames?.indexOf(''); - _settings.preferredCountry = settingIndex; - } -} -function ensureInitHttpClients() { - try { - if (!httpClientRequestToken) { - httpClientRequestToken = http.newClient(false); - httpClientAnonymous = http.newClient(false); - const authorization = getAnonymousUserTokenSingleton(); - httpClientAnonymous.setDefaultHeaders({ 'Authorization': authorization }); - } - } - catch (e) { - // log(e.message) - } -} -function ensureDefaults() { - ensureDefaultSettings(); - ensureInitHttpClients(); -} -//Pagers -class SearchPagerAll extends VideoPager { - /** - * @param {import("./types.d.ts").SearchContext} context the query params - * @param {(PlatformVideo | PlatformChannel)[]} results the initial results - */ - constructor(results, hasMore, params, page) { - super(results, hasMore, { params, page }); - } - 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 getSearchPagerAll(opts); - } + return new SearchChannelPager(results, json?.data?.search?.channels?.pageInfo?.hasNextPage, params, context.page, getSearchChannelPager); } -class SearchChannelPager extends ChannelPager { - constructor(results, hasNextPage, params, page) { - super(results, hasNextPage, { params, page }); - } - nextPage() { - const opts = { q: this.context.params.query, page: this.context.page += 1 }; - return getSearchChannelPager(opts); - } -} -class ChannelVideoPager extends VideoPager { - /** - * @param {import("./types.d.ts").URLContext} context the context - * @param {PlatformVideo[]} results the initial results - * @param {boolean} hasNextPage if there is a next page - */ - constructor(context, results, hasNextPage) { - super(results, hasNextPage, context); - } - nextPage() { - return getChannelPager(this.context); - } -} -class SearchPlaylistPager extends VideoPager { - /** - * @param {import("./types.d.ts").SearchContext} context the query params - * @param {(PlatformVideo | PlatformChannel)[]} results the initial results - */ - constructor(results, hasMore, params, page) { - super(results, hasMore, { params, page }); - } - 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 searchPlaylists(opts); - } +function getHttpContext(opts = { usePlatformAuth: false }) { + return opts.usePlatformAuth ? http : httpClientAnonymous; } -//wip -ensureDefaults(); log("LOADED"); diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts index 5c4a10ff8db39862f3ca5c8af007e877bfcfc864..a8567e789579b23a70b3664dee46d5ec678531fa 100644 --- a/src/DailymotionScript.ts +++ b/src/DailymotionScript.ts @@ -1,14 +1,10 @@ var config: Config; var _settings: DailymotionPluginSettings; -let AUTHORIZATION_TOKEN_ANONYMOUS_USER: string = ""; -let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE: number; import { creatorAvatarHeight, thumbnailHeight, - countryNames, - countryNamesToCode, BASE_URL, LESS_THAN_MINUTE, ONE_TO_FIVE_MINUTES, @@ -26,10 +22,6 @@ import { X_DM_AppInfo_Version, X_DM_Neon_SSR, DURATION_THRESHOLDS, - CLIENT_ID, - CLIENT_SECRET, - BASE_URL_API_AUTH, - X_DM_Preferred_Country, BASE_URL_API, BASE_URL_METADATA, errorTypes, @@ -50,11 +42,11 @@ import { } from './gqlQueries'; import { - objectToUrlEncodedString, getChannelNameFromUrl, isUsernameUrl, executeGqlQuery, - getPreferredCountry + getPreferredCountry, + getAnonymousUserTokenSingleton } from './util'; import { @@ -77,9 +69,8 @@ import { } from './Pagers'; -let httpClientAnonymous: IHttp +let httpClientAnonymous: IHttp = http.newClient(false); -var httpClientRequestToken = null; // Will be used to store playlists that require authentication const authenticatedPlaylistCollection: string[] = []; @@ -101,14 +92,18 @@ source.enable = function (conf, settings, saveStateStr) { _settings.thumbnailResolution = 7; if (!config) { - config = {} + config = { + id: "9c87e8db-e75d-48f4-afe5-2d203d4b95c5" + } } - config.id = "9c87e8db-e75d-48f4-afe5-2d203d4b95c5" } } source.getHome = function () { + + getAnonymousUserTokenSingleton(); + return getVideoPager({}, 0); }; @@ -267,7 +262,7 @@ source.getPlaylist = (url: string): PlatformPlaylistDetails => { } const usePlatformAuth = authenticatedPlaylistCollection.includes(url); - + let jsonResponse = executeGqlQuery( getHttpContext({ usePlatformAuth }), { @@ -340,7 +335,7 @@ source.getUserSubscriptions = (): string[] => { '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': 'it', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), Origin: BASE_URL, DNT: '1', Connection: 'keep-alive', @@ -352,10 +347,11 @@ source.getUserSubscriptions = (): string[] => { 'Cache-Control': 'no-cache', } + const usePlatformAuth = true; const fetchSubscriptions = (page, first): string[] => { const jsonResponse = executeGqlQuery( - getHttpContext({ usePlatformAuth: true }), + getHttpContext({ usePlatformAuth }), { operationName: 'SUBSCRIPTIONS_QUERY', variables: { @@ -365,7 +361,7 @@ source.getUserSubscriptions = (): string[] => { }, headers, query: GET_USER_SUBSCRIPTIONS, - usePlatformAuth: true + usePlatformAuth }); return (jsonResponse?.data?.me?.channel as Channel)?.followings?.edges?.map(edge => edge?.node?.creator?.name ?? "") ?? []; @@ -383,7 +379,7 @@ source.getUserSubscriptions = (): string[] => { do { const response = fetchSubscriptions(page, first); - + items = response.map(creatorName => `${BASE_URL}/${creatorName}`); subscriptions.push(...items); @@ -411,7 +407,7 @@ source.getUserPlaylists = (): string[] => { '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': 'it', + 'X-DM-Preferred-Country': getPreferredCountry(_settings?.preferredCountry), Origin: BASE_URL, DNT: '1', Connection: 'keep-alive', @@ -581,63 +577,6 @@ function searchPlaylists(contextQuery) { //Internals -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; -} - - function getVideoPager(params, page) { const count = ITEMS_PER_PAGE; @@ -1162,54 +1101,4 @@ function getHttpContext(opts: { usePlatformAuth: Boolean } = { usePlatformAuth: return opts.usePlatformAuth ? http : httpClientAnonymous; } -function ensureDefaultSettings() { - - if (!_settings) { - _settings = {}; - } - - if (_settings.hideSensitiveContent == undefined) { - _settings.hideSensitiveContent = true; - } - - if (_settings.avatarSize == undefined) { - _settings.avatarSize = 8; - } - - if (_settings.thumbnailResolution == undefined) { - _settings.thumbnailResolution = 7; - } - - if (_settings.preferredCountry == undefined) { - const settingIndex = countryNames?.indexOf(''); - _settings.preferredCountry = settingIndex; - } -} - -function ensureInitHttpClients() { - - try { - if (!httpClientRequestToken) { - httpClientRequestToken = http.newClient(false); - httpClientAnonymous = http.newClient(false); - - const authorization = getAnonymousUserTokenSingleton(); - - httpClientAnonymous.setDefaultHeaders({ 'Authorization': authorization }); - - } - } catch (e) { - // log(e.message) - } -} - -function ensureDefaults() { - ensureDefaultSettings(); - ensureInitHttpClients(); -} - -//wip -ensureDefaults(); - - log("LOADED"); \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index d28338fc4173b8067a1b89d73713a27668b68995..4dea800f887dfe6723f9829f33669baec43b4bb8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,10 +1,17 @@ +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, countryNames, - countryNamesToCode + countryNamesToCode, + CLIENT_ID, + CLIENT_SECRET, + BASE_URL_API_AUTH, } from './constants' export function getPreferredCountry(preferredCountryIndex) { @@ -44,6 +51,63 @@ export function isUsernameUrl(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 || { @@ -71,6 +135,10 @@ export function executeGqlQuery(httpClient, requestOptions) { 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) {