diff --git a/DailymotionConfig.json b/DailymotionConfig.json index e5db6b60a26ddbd511499b77a2cb748ccb55954c..c10cf6eec8ce479e8667a0f8afd1b657aa8d1672 100644 --- a/DailymotionConfig.json +++ b/DailymotionConfig.json @@ -16,6 +16,7 @@ "Http" ], "allowEval": false, + "allowAllHttpHeaderAccess": true, "allowUrls": [ "dailymotion.com", "www.dailymotion.com", @@ -24,7 +25,8 @@ "s1.dmcdn.net", "s2.dmcdn.net", "static1.dmcdn.net", - "static2.dmcdn.net" + "static2.dmcdn.net", + "api-2-0.spot.im" ], "authentication": { "loginUrl": "https://www.dailymotion.com/signin?urlback=/library", diff --git a/build/DailymotionConfig.json b/build/DailymotionConfig.json index e5db6b60a26ddbd511499b77a2cb748ccb55954c..c10cf6eec8ce479e8667a0f8afd1b657aa8d1672 100644 --- a/build/DailymotionConfig.json +++ b/build/DailymotionConfig.json @@ -16,6 +16,7 @@ "Http" ], "allowEval": false, + "allowAllHttpHeaderAccess": true, "allowUrls": [ "dailymotion.com", "www.dailymotion.com", @@ -24,7 +25,8 @@ "s1.dmcdn.net", "s2.dmcdn.net", "static1.dmcdn.net", - "static2.dmcdn.net" + "static2.dmcdn.net", + "api-2-0.spot.im" ], "authentication": { "loginUrl": "https://www.dailymotion.com/signin?urlback=/library", diff --git a/build/DailymotionScript.js b/build/DailymotionScript.js index e93155d4f0367807d7f0007061cabe2296700bf4..3b55d7d2aa9b598413783ab8651e607881ecf93d 100644 --- a/build/DailymotionScript.js +++ b/build/DailymotionScript.js @@ -2,6 +2,9 @@ const BASE_URL = "https://www.dailymotion.com"; const BASE_URL_API = "https://graphql.api.dailymotion.com"; +const BASE_URL_COMMENTS = "https://api-2-0.spot.im/v1.0.0/conversation/read"; +const BASE_URL_COMMENTS_AUTH = "https://api-2-0.spot.im/v1.0.0/authenticate"; +const BASE_URL_COMMENTS_THUMBNAILS = "https://images.spot.im/image/upload"; const BASE_URL_API_AUTH = `${BASE_URL_API}/oauth/token`; const BASE_URL_VIDEO = `${BASE_URL}/video`; const BASE_URL_PLAYLIST = `${BASE_URL}/playlist`; @@ -1765,7 +1768,8 @@ let config; let _settings; const state = { anonymousUserAuthorizationToken: "", - anonymousUserAuthorizationTokenExpirationDate: 0 + anonymousUserAuthorizationTokenExpirationDate: 0, + messageServiceToken: "" }; const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST"; const FAVORITES_PLAYLIST_ID = "FAVORITES_PLAYLIST"; @@ -1798,6 +1802,7 @@ source.enable = function (conf, settings, saveStateStr) { if (saveState) { state.anonymousUserAuthorizationToken = saveState.anonymousUserAuthorizationToken; state.anonymousUserAuthorizationTokenExpirationDate = saveState.anonymousUserAuthorizationTokenExpirationDate; + state.messageServiceToken = saveState.messageServiceToken; if (!isTokenValid()) { log("Token expired. Fetching a new one."); } @@ -1813,7 +1818,7 @@ source.enable = function (conf, settings, saveStateStr) { didSaveState = false; } if (!didSaveState) { - log("Getting a new token"); + log("Getting a new tokens"); const body = objectToUrlEncodedString({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, @@ -1844,8 +1849,28 @@ source.enable = function (conf, settings, saveStateStr) { } state.anonymousUserAuthorizationToken = `${json.token_type} ${json.access_token}`; state.anonymousUserAuthorizationTokenExpirationDate = Date.now() + (json.expires_in * 1000); - log(`json.expires_in: ${json.expires_in}`); - log(`state.anonymousUserAuthorizationTokenExpirationDate: ${state.anonymousUserAuthorizationTokenExpirationDate}`); + // get token for message service api-2-0.spot.im + const authenticateIm = http.POST(BASE_URL_COMMENTS_AUTH, "", { + 'User-Agent': USER_AGENT, + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'x-spot-id': 'sp_vWPN1lBu', + 'x-post-id': 'no$post', + 'Content-Type': 'application/json', + 'Origin': BASE_URL, + Connection: 'keep-alive', + Referer: BASE_URL, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + Priority: 'u=6', + 'Content-Length': '0' + }, false); + if (!authenticateIm.isOk) { + // throw new UnavailableException('Failed to authenticate to comments service'); + log('Failed to authenticate to comments service'); + } + state.messageServiceToken = authenticateIm.headers["x-access-token"][0]; } }; source.getHome = function () { @@ -1920,6 +1945,67 @@ source.getContentDetails = function (url) { source.saveState = () => { return JSON.stringify(state); }; +source.getSubComments = (comment) => { + const params = { "count": 5, "offset": 0, "parent_id": comment.context.id, "sort_by": "best", "child_count": comment.replyCount }; + return getCommentPager(comment.contextUrl, params, 0); +}; +source.getComments = (url) => { + const params = { "sort_by": "best", "offset": 0, "count": 10, "message_id": null, "depth": 2, "child_count": 2 }; + return getCommentPager(url, params, 0); +}; +function getCommentPager(url, params, page) { + try { + const xid = url.split('/').pop(); + const commentsHeaders = { + 'User-Agent': USER_AGENT, + Accept: 'application/json', + 'Accept-Language': 'en-US,en;q=0.5', + 'x-access-token': state.messageServiceToken, + 'Content-Type': 'application/json', + 'x-spot-id': 'sp_vWPN1lBu', + 'x-post-id': xid, + 'Origin': BASE_URL, + Connection: 'keep-alive', + Referer: BASE_URL, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + Priority: 'u=6', + TE: 'trailers' + }; + const commentRequest = http.POST(BASE_URL_COMMENTS, JSON.stringify(params), commentsHeaders, false); + if (!commentRequest.isOk) { + throw new UnavailableException('Failed to authenticate to comments service'); + } + const comments = JSON.parse(commentRequest.body); + const users = comments.conversation.users; + const results = comments.conversation.comments.map(v => { + const user = users[v.user_id]; + return new Comment({ + contextUrl: url, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, user.id ?? "", config.id), user.display_name ?? "", "", `${BASE_URL_COMMENTS_THUMBNAILS}/${user.image_id}`), + message: v.content[0].text, + rating: new RatingLikes(v.stars), + date: v.written_at, + replyCount: v.total_replies_count ?? 0, + context: { id: v.id } + }); + }); + return new PlatformCommentPager(results, comments.conversation.has_next, url, params, ++page); + } + catch (error) { + bridge.log('Failed to get comments:' + error?.message); + return new PlatformCommentPager([], false, url, params, 0); + } +} +class PlatformCommentPager extends CommentPager { + constructor(results, hasMore, path, params, page) { + super(results, hasMore, { path, params, page }); + } + nextPage() { + return getCommentPager(this.context.path, this.context.params, (this.context.page ?? 0) + 1); + } +} //Playlist source.isPlaylistUrl = (url) => { return url.startsWith(BASE_URL_PLAYLIST) || diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts index 828edef7c9789b206b299b66242d9d6070cff233..bc571ad0366d2f938851f4fa862decd0bfadd491 100644 --- a/src/DailymotionScript.ts +++ b/src/DailymotionScript.ts @@ -3,7 +3,8 @@ let _settings: IDailymotionPluginSettings; const state = { anonymousUserAuthorizationToken: "", - anonymousUserAuthorizationTokenExpirationDate: 0 + anonymousUserAuthorizationTokenExpirationDate: 0, + messageServiceToken: "" }; const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST"; @@ -31,7 +32,11 @@ import { PLAYLISTS_PER_PAGE_OPTIONS, CLIENT_ID, CLIENT_SECRET, - BASE_URL_API_AUTH + BASE_URL_API_AUTH, + PLATFORM, + BASE_URL_COMMENTS, + BASE_URL_COMMENTS_AUTH, + BASE_URL_COMMENTS_THUMBNAILS } from './constants'; import { @@ -122,16 +127,17 @@ source.enable = function (conf, settings, saveStateStr) { config.id = "9c87e8db-e75d-48f4-afe5-2d203d4b95c5"; } - + let didSaveState = false; - + try { if (saveStateStr) { const saveState = JSON.parse(saveStateStr); if (saveState) { state.anonymousUserAuthorizationToken = saveState.anonymousUserAuthorizationToken; state.anonymousUserAuthorizationTokenExpirationDate = saveState.anonymousUserAuthorizationTokenExpirationDate; - + state.messageServiceToken = saveState.messageServiceToken; + if (!isTokenValid()) { log("Token expired. Fetching a new one."); } else { @@ -147,7 +153,7 @@ source.enable = function (conf, settings, saveStateStr) { if (!didSaveState) { - log("Getting a new token"); + log("Getting a new tokens"); const body = objectToUrlEncodedString({ client_id: CLIENT_ID, @@ -185,8 +191,31 @@ source.enable = function (conf, settings, saveStateStr) { state.anonymousUserAuthorizationToken = `${json.token_type} ${json.access_token}`; state.anonymousUserAuthorizationTokenExpirationDate = Date.now() + (json.expires_in * 1000); - log(`json.expires_in: ${json.expires_in}`); - log(`state.anonymousUserAuthorizationTokenExpirationDate: ${state.anonymousUserAuthorizationTokenExpirationDate}`); + + // get token for message service api-2-0.spot.im + const authenticateIm = http.POST(BASE_URL_COMMENTS_AUTH, "", { + 'User-Agent': USER_AGENT, + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'x-spot-id': 'sp_vWPN1lBu', + 'x-post-id': 'no$post', + 'Content-Type': 'application/json', + 'Origin': BASE_URL, + Connection: 'keep-alive', + Referer: BASE_URL, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + Priority: 'u=6', + 'Content-Length': '0' + }, false); + + if (!authenticateIm.isOk) { + // throw new UnavailableException('Failed to authenticate to comments service'); + log('Failed to authenticate to comments service'); + } + + state.messageServiceToken = authenticateIm.headers["x-access-token"][0]; } } @@ -295,6 +324,89 @@ source.saveState = () => { return JSON.stringify(state); }; +source.getSubComments = (comment) => { + const params = { "count": 5, "offset": 0, "parent_id": comment.context.id, "sort_by": "best", "child_count": comment.replyCount }; + return getCommentPager(comment.contextUrl, params, 0); +} + + +source.getComments = (url) => { + const params = { "sort_by": "best", "offset": 0, "count": 10, "message_id": null, "depth": 2, "child_count": 2 }; + return getCommentPager(url, params, 0); +} + +function getCommentPager(url, params, page) { + + try { + const xid = url.split('/').pop(); + + const commentsHeaders = { + 'User-Agent': USER_AGENT, + Accept: 'application/json', + 'Accept-Language': 'en-US,en;q=0.5', + 'x-access-token': state.messageServiceToken, + 'Content-Type': 'application/json', + 'x-spot-id': 'sp_vWPN1lBu', + 'x-post-id': xid, + 'Origin': BASE_URL, + Connection: 'keep-alive', + Referer: BASE_URL, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + Priority: 'u=6', + TE: 'trailers' + } + + const commentRequest = http.POST(BASE_URL_COMMENTS, JSON.stringify(params), commentsHeaders, false); + + if (!commentRequest.isOk) { + throw new UnavailableException('Failed to authenticate to comments service'); + } + + const comments = JSON.parse(commentRequest.body); + + const users = comments.conversation.users; + + const results = comments.conversation.comments.map(v => { + + const user = users[v.user_id]; + + return new Comment({ + contextUrl: url, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, user.id ?? "", config.id), + user.display_name ?? "", + "", + `${BASE_URL_COMMENTS_THUMBNAILS}/${user.image_id}` + ), + message: v.content[0].text, + rating: new RatingLikes(v.stars), + date: v.written_at, + replyCount: v.total_replies_count ?? 0, + context: { id: v.id } + }); + + }); + + return new PlatformCommentPager(results, comments.conversation.has_next, url, params, ++page); + } catch (error) { + bridge.log('Failed to get comments:' + error?.message); + return new PlatformCommentPager([], false, url, params, 0); + } + +} + +class PlatformCommentPager extends CommentPager { + constructor(results, hasMore, path, params, page) { + super(results, hasMore, { path, params, page }); + } + + nextPage() { + return getCommentPager(this.context.path, this.context.params, (this.context.page ?? 0) + 1); + } +} + //Playlist source.isPlaylistUrl = (url): boolean => { return url.startsWith(BASE_URL_PLAYLIST) || diff --git a/src/constants.ts b/src/constants.ts index 6d5f09708b0e5552970be03dcde5441cfc5636cc..44a65d15122d69e1783e29111f34b67cbf4cc19e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,8 @@ export const BASE_URL = "https://www.dailymotion.com"; export const BASE_URL_API = "https://graphql.api.dailymotion.com"; +export const BASE_URL_COMMENTS = "https://api-2-0.spot.im/v1.0.0/conversation/read"; +export const BASE_URL_COMMENTS_AUTH = "https://api-2-0.spot.im/v1.0.0/authenticate"; +export const BASE_URL_COMMENTS_THUMBNAILS = "https://images.spot.im/image/upload"; export const BASE_URL_API_AUTH = `${BASE_URL_API}/oauth/token`; export const BASE_URL_VIDEO = `${BASE_URL}/video`; export const BASE_URL_PLAYLIST = `${BASE_URL}/playlist`; diff --git a/types/plugin.d.ts b/types/plugin.d.ts index 483d0693668b95756d309a927060d9e51a3eef73..48f40c3cc5e1943c58ddccebeb25e9be0159fe45 100644 --- a/types/plugin.d.ts +++ b/types/plugin.d.ts @@ -233,7 +233,7 @@ declare class PlatformComment { contextUrl: string; author: PlatformAuthorLink; message: string; - rating: any; + rating: IRating; date: number; replyCount: number; context: any; @@ -1047,7 +1047,7 @@ declare class PlaylistPager { declare class CommentPager { context: any - constructor(results: PlatformVideo[], hasMore: boolean, context: any) { + constructor(results: PlatformComment[], hasMore: boolean, context: any) { this.plugin_type = "CommentPager"; this.results = results ?? []; this.hasMore = hasMore ?? false; @@ -1085,20 +1085,20 @@ interface Source { 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[] - + peekChannelContents(url, type): PlatformVideo[] + isVideoDetailsUrl(url: string): boolean; getVideoDetails(url: string): PlatformVideoDetails; @@ -1125,6 +1125,10 @@ interface Source { getContentDetails(url: string): PlatformVideoDetails; + getComments(url: string): CommentPager; + + getSubComments(comment: PlatformComment): CommentPager; + getChannelPlaylists(url: string): PlaylistPager; searchChannelContents(channelUrl: string, query: string, type: string, order: string, filters: FilterGroup[]): VideoPager; @@ -1431,7 +1435,7 @@ let http: IHttp interface IPager<T> { - hasMorePages() : Boolean; + hasMorePages(): Boolean; nextPage(); - getResults() : List<T>; + getResults(): List<T>; } \ No newline at end of file