From 359edaad1f36b3c2bb413698de9539db28e9a88f Mon Sep 17 00:00:00 2001
From: Stefan Cruz <17972991+stefancruz@users.noreply.github.com>
Date: Sun, 14 Jul 2024 17:15:44 +0100
Subject: [PATCH] fix: improve extracting the api credentias from the page from
 the page

---
 DailymotionConfig.json       |   3 +-
 build/DailymotionConfig.json |   3 +-
 build/DailymotionScript.js   | 200 ++++++++++++++++++++++++-----------
 src/DailymotionScript.ts     | 125 ++++++----------------
 src/Mappers.ts               |  37 ++++---
 src/Pagers.ts                |   5 +-
 src/constants.ts             |  24 +++--
 src/extraction.ts            | 182 +++++++++++++++++++++++++++++++
 types/plugin.d.ts            |   8 +-
 types/types.d.ts             |   6 ++
 10 files changed, 410 insertions(+), 183 deletions(-)
 create mode 100644 src/extraction.ts

diff --git a/DailymotionConfig.json b/DailymotionConfig.json
index 9a06c09..cc34c3c 100644
--- a/DailymotionConfig.json
+++ b/DailymotionConfig.json
@@ -13,7 +13,8 @@
   "scriptSignature": "",
   "scriptPublicKey": "",
   "packages": [
-    "Http"
+    "Http",
+    "DOMParser"
   ],
   "allowEval": false,
   "allowAllHttpHeaderAccess": false,
diff --git a/build/DailymotionConfig.json b/build/DailymotionConfig.json
index 9a06c09..cc34c3c 100644
--- a/build/DailymotionConfig.json
+++ b/build/DailymotionConfig.json
@@ -13,7 +13,8 @@
   "scriptSignature": "",
   "scriptPublicKey": "",
   "packages": [
-    "Http"
+    "Http",
+    "DOMParser"
   ],
   "allowEval": false,
   "allowAllHttpHeaderAccess": false,
diff --git a/build/DailymotionScript.js b/build/DailymotionScript.js
index ba84748..8a7a571 100644
--- a/build/DailymotionScript.js
+++ b/build/DailymotionScript.js
@@ -14,7 +14,8 @@ const REGEX_VIDEO_URL_1 = /^https:\/\/dai\.ly\/[a-zA-Z0-9]+$/i;
 const REGEX_VIDEO_URL_EMBED = /^https:\/\/(?:www\.)?dailymotion\.com\/embed\/video\/[a-zA-Z0-9]+(\?.*)?$/i;
 const REGEX_VIDEO_CHANNEL_URL = /^https:\/\/(?:www\.)?dailymotion\.com\/[a-zA-Z0-9-]+$/i;
 const REGEX_VIDEO_PLAYLIST_URL = /^https:\/\/(?:www\.)?dailymotion\.com\/playlist\/[a-zA-Z0-9]+$/i;
-const REGEX_INITIAL_DATA_API_AUTH = /(?<=window\.__LOADABLE_LOADED_CHUNKS__=.*)\b[a-f0-9]{20}\b|\b[a-f0-9]{40}\b/g;
+const REGEX_INITIAL_DATA_API_AUTH_1 = /(?<=window\.__LOADABLE_LOADED_CHUNKS__=.*)\b[a-f0-9]{20}\b|\b[a-f0-9]{40}\b/g;
+const createAuthRegexByTextLength = (length) => new RegExp(`\\b\\w+\\s*=\\s*"([a-zA-Z0-9]{${length}})"`);
 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.
 const FALLBACK_CLIENT_ID = 'f1a362d288c1b98099c7';
@@ -1094,10 +1095,10 @@ class SearchChannelPager extends ChannelPager {
         this.cb = cb;
     }
     nextPage() {
-        const page = this.context.page += 1;
+        const page = (this.context.page += 1);
         const opts = {
             q: this.context.params.query,
-            page
+            page,
         };
         return this.cb(opts);
     }
@@ -1151,7 +1152,8 @@ const SourceChannelToGrayjayChannel = (pluginId, sourceChannel) => {
         return acc;
     }, {});
     let description = '';
-    if (sourceChannel?.tagline && sourceChannel?.tagline != sourceChannel?.description) {
+    if (sourceChannel?.tagline &&
+        sourceChannel?.tagline != sourceChannel?.description) {
         description = `${sourceChannel?.tagline}\n\n`;
     }
     description += `${sourceChannel?.description ?? ''}`;
@@ -1160,7 +1162,8 @@ const SourceChannelToGrayjayChannel = (pluginId, sourceChannel) => {
         name: sourceChannel?.displayName ?? '',
         thumbnail: sourceChannel?.avatar?.url ?? '',
         banner: sourceChannel.banner?.url ?? '',
-        subscribers: sourceChannel?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ?? 0,
+        subscribers: sourceChannel?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ??
+            0,
         description,
         url: `${BASE_URL}/${sourceChannel.name}`,
         links,
@@ -1257,7 +1260,7 @@ const SourceVideoToPlatformVideoDetailsDef = (pluginId, sourceVideo, player_meta
     }
     const isLive = getIsLive(sourceVideo);
     const viewCount = getViewCount(sourceVideo);
-    const duration = isLive ? 0 : sourceVideo?.duration ?? 0;
+    const duration = isLive ? 0 : (sourceVideo?.duration ?? 0);
     const source = new HLSSource({
         name: 'HLS',
         duration,
@@ -1348,6 +1351,120 @@ const convertSRTtoVTT = (srt) => {
     return vtt.join('');
 };
 
+function oauthClientCredentialsRequest(httpClient, url, clientId, secret, throwOnInvalid = false) {
+    if (!httpClient || !url || !clientId || !secret) {
+        throw new ScriptException('Invalid parameters provided to oauthClientCredentialsRequest');
+    }
+    const body = objectToUrlEncodedString({
+        client_id: clientId,
+        client_secret: secret,
+        grant_type: 'client_credentials',
+    });
+    try {
+        return httpClient.POST(url, 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);
+    }
+    catch (error) {
+        console.error('Error making OAuth client credentials request:', error);
+        if (throwOnInvalid) {
+            throw new ScriptException('Failed to obtain OAuth client credentials');
+        }
+        return null;
+    }
+}
+function extractClientCredentials(httpClient) {
+    const detailsRequestHtml = httpClient.GET(BASE_URL, {}, false);
+    if (!detailsRequestHtml.isOk) {
+        throw new ScriptException('Failed to fetch page to extract auth details');
+    }
+    const result = [
+        {
+            clientId: FALLBACK_CLIENT_ID,
+            secret: FALLBACK_CLIENT_SECRET,
+        },
+    ];
+    const match = detailsRequestHtml.body.match(REGEX_INITIAL_DATA_API_AUTH_1);
+    if (match?.length === 2 && match[0] && match[1]) {
+        result.unshift({
+            clientId: match[0],
+            secret: match[1],
+        });
+        console.log('Successfully extracted API credentials from page:', match[1]);
+    }
+    else {
+        console.log('Failed to extract API credentials from page using regex. Using DOM parsing.');
+        const htmlElement = domParser.parseFromString(detailsRequestHtml.body, 'text/html');
+        const extractedId = getScriptVariableByTextLength(htmlElement, 20);
+        const extractedSecret = getScriptVariableByTextLength(htmlElement, 40);
+        if (extractedId && extractedSecret) {
+            result.unshift({
+                clientId: extractedId,
+                secret: extractedSecret,
+            });
+            console.log('Successfully extracted API credentials from page using DOM parsing:', extractedId);
+        }
+        else {
+            console.log('Failed to extract API credentials using DOM parsing with exact text length.');
+        }
+    }
+    return result;
+}
+function getScriptVariableByTextLength(htmlElement, length) {
+    const scriptTags = htmlElement.querySelectorAll('script[type="text/javascript"]');
+    if (!scriptTags.length) {
+        console.error('No script tags found.');
+        return null; // or throw an error, depending on your use case
+    }
+    let pageContent = '';
+    scriptTags.forEach((tag) => {
+        pageContent += tag.outerHTML;
+    });
+    let matches = createAuthRegexByTextLength(length).exec(pageContent);
+    if (matches?.length == 2) {
+        return matches[1];
+    }
+}
+function getTokenFromClientCredentials(httpClient, credentials, throwOnInvalid = false) {
+    let result = {
+        isValid: false,
+    };
+    for (const credential of credentials) {
+        const res = oauthClientCredentialsRequest(httpClient, BASE_URL_API_AUTH, credential.clientId, credential.secret);
+        if (res?.isOk) {
+            const anonymousTokenResponse = JSON.parse(res.body);
+            if (!anonymousTokenResponse.token_type ||
+                !anonymousTokenResponse.access_token) {
+                console.error('Invalid token response', res);
+                if (throwOnInvalid) {
+                    throw new ScriptException('', 'Invalid token response: ' + res.body);
+                }
+            }
+            result = {
+                anonymousUserAuthorizationToken: `${anonymousTokenResponse.token_type} ${anonymousTokenResponse.access_token}`,
+                anonymousUserAuthorizationTokenExpirationDate: Date.now() + anonymousTokenResponse.expires_in * 1000,
+                isValid: true,
+            };
+            break;
+        }
+        else {
+            console.error('Failed to get token', res);
+        }
+    }
+    return result;
+}
+
 let config;
 let _settings;
 const state = {
@@ -1425,43 +1542,19 @@ source.enable = function (conf, settings, saveStateStr) {
     }
     if (!didSaveState) {
         log('Getting a new tokens');
-        const detailsRequestHtml = http.GET(BASE_URL, {}, false);
-        if (!detailsRequestHtml.isOk) {
-            log("Failed to get page to extract auth details");
+        const clientCredentials = extractClientCredentials(http);
+        const { anonymousUserAuthorizationToken, anonymousUserAuthorizationTokenExpirationDate, isValid, } = getTokenFromClientCredentials(http, clientCredentials);
+        if (!isValid) {
+            console.error('Failed to get token');
+            throw new ScriptException('Failed to get authentication token');
         }
-        let clientId = FALLBACK_CLIENT_ID;
-        let secret = FALLBACK_CLIENT_SECRET;
-        const match = detailsRequestHtml.body.match(REGEX_INITIAL_DATA_API_AUTH);
-        if (match?.length === 2 && match[0] && match[1]) {
-            clientId = match[0];
-            secret = match[1];
-            log('Successfully extracted API credentials from page.');
-        }
-        else {
-            log('Failed to extract api credentials from page.');
-        }
-        const body = objectToUrlEncodedString({
-            client_id: clientId,
-            client_secret: secret,
-            grant_type: 'client_credentials',
-        });
-        let batchRequests = http.batch().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);
+        state.anonymousUserAuthorizationToken =
+            anonymousUserAuthorizationToken ?? '';
+        state.anonymousUserAuthorizationTokenExpirationDate =
+            anonymousUserAuthorizationTokenExpirationDate ?? 0;
         if (config.allowAllHttpHeaderAccess) {
             // get token for message service api-2-0.spot.im
-            batchRequests = batchRequests.POST(BASE_URL_COMMENTS_AUTH, '', {
+            const authenticateIm = http.POST(BASE_URL_COMMENTS_AUTH, '', {
                 'User-Agent': USER_AGENT,
                 Accept: '*/*',
                 'Accept-Language': 'en-US,en;q=0.5',
@@ -1477,23 +1570,6 @@ source.enable = function (conf, settings, saveStateStr) {
                 Priority: 'u=6',
                 'Content-Length': '0',
             }, false);
-        }
-        const responses = batchRequests.execute();
-        const res = responses[0];
-        if (res.code !== 200) {
-            console.error('Failed to get token', res);
-            throw new ScriptException('', 'Failed to get token: ' + res.code + ' - ' + res.body);
-        }
-        const anonymousTokenResponse = JSON.parse(res.body);
-        if (!anonymousTokenResponse.token_type || !anonymousTokenResponse.access_token) {
-            console.error('Invalid token response', res);
-            throw new ScriptException('', 'Invalid token response: ' + res.body);
-        }
-        state.anonymousUserAuthorizationToken = `${anonymousTokenResponse.token_type} ${anonymousTokenResponse.access_token}`;
-        state.anonymousUserAuthorizationTokenExpirationDate =
-            Date.now() + anonymousTokenResponse.expires_in * 1000;
-        if (config.allowAllHttpHeaderAccess) {
-            const authenticateIm = responses[1];
             if (!authenticateIm.isOk) {
                 log('Failed to authenticate to comments service');
             }
@@ -1565,11 +1641,7 @@ source.getChannelCapabilities = () => {
 };
 //Video
 source.isContentDetailsUrl = function (url) {
-    return [
-        REGEX_VIDEO_URL,
-        REGEX_VIDEO_URL_1,
-        REGEX_VIDEO_URL_EMBED
-    ].some(r => r.test(url));
+    return [REGEX_VIDEO_URL, REGEX_VIDEO_URL_1, REGEX_VIDEO_URL_EMBED].some((r) => r.test(url));
 };
 source.getContentDetails = function (url) {
     return getSavedVideo(url, false);
@@ -1721,7 +1793,7 @@ source.getUserSubscriptions = () => {
             operationName: 'SUBSCRIPTIONS_QUERY',
             variables: {
                 first: first,
-                page: page
+                page: page,
             },
             headers,
             query: GET_USER_SUBSCRIPTIONS,
@@ -2139,7 +2211,7 @@ function getChannelPlaylists(url, page = 1) {
     });
     const channel = gqlResponse.data.channel;
     const content = (channel?.collections?.edges ?? [])
-        .filter(e => e?.node?.metrics?.engagement?.videos?.edges?.[0]?.node?.total) //exclude empty playlists. could be empty doe to geographic restrictions
+        .filter((e) => e?.node?.metrics?.engagement?.videos?.edges?.[0]?.node?.total) //exclude empty playlists. could be empty doe to geographic restrictions
         .map((edge) => {
         return SourceCollectionToGrayjayPlaylist(config.id, edge?.node);
     });
diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts
index 3419329..48a4e6f 100644
--- a/src/DailymotionScript.ts
+++ b/src/DailymotionScript.ts
@@ -21,9 +21,6 @@ import {
   BASE_URL_METADATA,
   ERROR_TYPES,
   LikedMediaSort,
-  FALLBACK_CLIENT_ID,
-  FALLBACK_CLIENT_SECRET,
-  BASE_URL_API_AUTH,
   PLATFORM,
   BASE_URL_COMMENTS,
   BASE_URL_COMMENTS_AUTH,
@@ -36,7 +33,6 @@ import {
   REGEX_VIDEO_URL,
   REGEX_VIDEO_URL_1,
   REGEX_VIDEO_URL_EMBED,
-  REGEX_INITIAL_DATA_API_AUTH,
 } from './constants';
 
 import {
@@ -57,12 +53,7 @@ import {
   USER_WATCH_LATER_VIDEOS_QUERY,
 } from './gqlQueries';
 
-import {
-  getChannelNameFromUrl,
-  getQuery,
-  objectToUrlEncodedString,
-  generateUUIDv4,
-} from './util';
+import { getChannelNameFromUrl, getQuery, generateUUIDv4 } from './util';
 
 import {
   Channel,
@@ -95,7 +86,15 @@ import {
   SourceVideoToPlatformVideoDetailsDef,
 } from './Mappers';
 
-import { IDailymotionPluginSettings, IDictionary, IPlatformSystemPlaylist } from '../types/types';
+import {
+  IDailymotionPluginSettings,
+  IDictionary,
+  IPlatformSystemPlaylist,
+} from '../types/types';
+import {
+  extractClientCredentials,
+  getTokenFromClientCredentials,
+} from './extraction';
 
 // Will be used to store private playlists that require authentication
 const authenticatedPlaylistCollection: string[] = [];
@@ -183,54 +182,27 @@ source.enable = function (conf, settings, saveStateStr) {
   if (!didSaveState) {
     log('Getting a new tokens');
 
-    const detailsRequestHtml = http.GET(BASE_URL, {}, false);
+    const clientCredentials = extractClientCredentials(http);
 
-    if (!detailsRequestHtml.isOk) {
-      log("Failed to get page to extract auth details");
-    }
+    const {
+      anonymousUserAuthorizationToken,
+      anonymousUserAuthorizationTokenExpirationDate,
+      isValid,
+    } = getTokenFromClientCredentials(http, clientCredentials);
 
-    let clientId = FALLBACK_CLIENT_ID;
-    let secret = FALLBACK_CLIENT_SECRET;
-    
-    const match = detailsRequestHtml.body.match(REGEX_INITIAL_DATA_API_AUTH);
-
-    if(match?.length === 2 && match[0] && match[1]) {
-      clientId = match[0];
-      secret = match[1];
-      log('Successfully extracted API credentials from page.')
-    } else {
-      log('Failed to extract api credentials from page.')
+    if (!isValid) {
+      console.error('Failed to get token');
+      throw new ScriptException('Failed to get authentication token');
     }
 
-    const body = objectToUrlEncodedString({
-      client_id: clientId,
-      client_secret: secret,
-      grant_type: 'client_credentials',
-    });
-
-    let batchRequests = http.batch().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,
-    );
+    state.anonymousUserAuthorizationToken =
+      anonymousUserAuthorizationToken ?? '';
+    state.anonymousUserAuthorizationTokenExpirationDate =
+      anonymousUserAuthorizationTokenExpirationDate ?? 0;
 
     if (config.allowAllHttpHeaderAccess) {
       // get token for message service api-2-0.spot.im
-      batchRequests = batchRequests.POST(
+      const authenticateIm = http.POST(
         BASE_URL_COMMENTS_AUTH,
         '',
         {
@@ -251,33 +223,6 @@ source.enable = function (conf, settings, saveStateStr) {
         },
         false,
       );
-    }
-
-    const responses = batchRequests.execute();
-
-    const res = responses[0];
-
-    if (res.code !== 200) {
-      console.error('Failed to get token', res);
-      throw new ScriptException(
-        '',
-        'Failed to get token: ' + res.code + ' - ' + res.body,
-      );
-    }
-
-    const anonymousTokenResponse = JSON.parse(res.body);
-
-    if (!anonymousTokenResponse.token_type || !anonymousTokenResponse.access_token) {
-      console.error('Invalid token response', res);
-      throw new ScriptException('', 'Invalid token response: ' + res.body);
-    }
-
-    state.anonymousUserAuthorizationToken = `${anonymousTokenResponse.token_type} ${anonymousTokenResponse.access_token}`;
-    state.anonymousUserAuthorizationTokenExpirationDate =
-      Date.now() + anonymousTokenResponse.expires_in * 1000;
-
-    if (config.allowAllHttpHeaderAccess) {
-      const authenticateIm = responses[1];
 
       if (!authenticateIm.isOk) {
         log('Failed to authenticate to comments service');
@@ -370,11 +315,9 @@ source.getChannelCapabilities = (): ResultCapabilities => {
 
 //Video
 source.isContentDetailsUrl = function (url) {
-  return [
-    REGEX_VIDEO_URL,
-    REGEX_VIDEO_URL_1,
-    REGEX_VIDEO_URL_EMBED
-  ].some(r => r.test(url))
+  return [REGEX_VIDEO_URL, REGEX_VIDEO_URL_1, REGEX_VIDEO_URL_EMBED].some((r) =>
+    r.test(url),
+  );
 };
 
 source.getContentDetails = function (url) {
@@ -603,7 +546,7 @@ source.getUserSubscriptions = (): string[] => {
       operationName: 'SUBSCRIPTIONS_QUERY',
       variables: {
         first: first,
-        page: page
+        page: page,
       },
       headers,
       query: GET_USER_SUBSCRIPTIONS,
@@ -847,7 +790,7 @@ function getHomePager(params, page) {
   const hasMore =
     obj?.data?.home?.neon?.sections?.edges?.[0]?.node?.components?.pageInfo
       ?.hasNextPage ?? false;
-  
+
   return new SearchPagerAll(results, hasMore, params, page, getHomePager);
 }
 
@@ -1172,12 +1115,12 @@ function getChannelPlaylists(
   const channel = gqlResponse.data.channel as Channel;
 
   const content: PlatformPlaylist[] = (channel?.collections?.edges ?? [])
-  .filter(e => e?.node?.metrics?.engagement?.videos?.edges?.[0]?.node?.total)//exclude empty playlists. could be empty doe to geographic restrictions
-  .map(
-    (edge) => {
+    .filter(
+      (e) => e?.node?.metrics?.engagement?.videos?.edges?.[0]?.node?.total,
+    ) //exclude empty playlists. could be empty doe to geographic restrictions
+    .map((edge) => {
       return SourceCollectionToGrayjayPlaylist(config.id, edge?.node);
-    },
-  );
+    });
 
   if (content?.length === 0) {
     return new ChannelPlaylistPager([]);
diff --git a/src/Mappers.ts b/src/Mappers.ts
index ccc3fe9..7759fac 100644
--- a/src/Mappers.ts
+++ b/src/Mappers.ts
@@ -6,7 +6,10 @@ import {
   Video,
 } from '../types/CodeGenDailymotion';
 
-import { DailymotionStreamingContent, IDailymotionSubtitle } from '../types/types';
+import {
+  DailymotionStreamingContent,
+  IDailymotionSubtitle,
+} from '../types/types';
 
 import {
   BASE_URL,
@@ -34,13 +37,16 @@ export const SourceChannelToGrayjayChannel = (
     {} as Record<string, string>,
   );
 
-  let description = ''
+  let description = '';
 
-  if(sourceChannel?.tagline && sourceChannel?.tagline != sourceChannel?.description){
+  if (
+    sourceChannel?.tagline &&
+    sourceChannel?.tagline != sourceChannel?.description
+  ) {
     description = `${sourceChannel?.tagline}\n\n`;
   }
 
-  description += `${sourceChannel?.description ?? ''}`
+  description += `${sourceChannel?.description ?? ''}`;
 
   return new PlatformChannel({
     id: new PlatformID(
@@ -53,7 +59,8 @@ export const SourceChannelToGrayjayChannel = (
     thumbnail: sourceChannel?.avatar?.url ?? '',
     banner: sourceChannel.banner?.url ?? '',
     subscribers:
-      sourceChannel?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ?? 0,
+      sourceChannel?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ??
+      0,
     description,
     url: `${BASE_URL}/${sourceChannel.name}`,
     links,
@@ -70,8 +77,8 @@ export const SourceAuthorToGrayjayPlatformAuthorLink = (
     creator?.name ? `${BASE_URL}/${creator?.name}` : '',
     creator?.avatar?.url ?? '',
     creator?.followers?.totalCount ??
-    creator?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ??
-    0,
+      creator?.metrics?.engagement?.followers?.edges?.[0]?.node?.total ??
+      0,
   );
 };
 
@@ -171,20 +178,18 @@ const getViewCount = (sourceVideo?: DailymotionStreamingContent): number => {
   let viewCount = 0;
 
   if (getIsLive(sourceVideo)) {
-
     const live = sourceVideo as Live;
 
     //TODO: live?.audienceCount and live.stats.views.total are deprecated
     //live?.metrics?.engagement?.audience?.edges?.[0]?.node?.total is still empty
-    viewCount = 
-    live?.metrics?.engagement?.audience?.edges?.[0]?.node?.total ??
-    live?.audienceCount ??
-    live?.stats?.views?.total ??
-    0
+    viewCount =
+      live?.metrics?.engagement?.audience?.edges?.[0]?.node?.total ??
+      live?.audienceCount ??
+      live?.stats?.views?.total ??
+      0;
   } else {
-
     const video = sourceVideo as Video;
-    
+
     // TODO: both fields are deprecated.
     // video?.stats?.views?.total replaced video?.viewCount
     // now video?.viewCount is deprecated too but there replacement is not accessible yet
@@ -218,7 +223,7 @@ export const SourceVideoToPlatformVideoDetailsDef = (
 
   const isLive = getIsLive(sourceVideo);
   const viewCount = getViewCount(sourceVideo);
-  const duration = isLive ? 0 : (sourceVideo as Video)?.duration ?? 0;
+  const duration = isLive ? 0 : ((sourceVideo as Video)?.duration ?? 0);
 
   const source = new HLSSource({
     name: 'HLS',
diff --git a/src/Pagers.ts b/src/Pagers.ts
index ea0c0a5..037b4c1 100644
--- a/src/Pagers.ts
+++ b/src/Pagers.ts
@@ -34,12 +34,11 @@ export class SearchChannelPager extends ChannelPager {
   }
 
   nextPage() {
-
-    const page = this.context.page += 1;
+    const page = (this.context.page += 1);
 
     const opts = {
       q: this.context.params.query,
-      page
+      page,
     };
     return this.cb(opts);
   }
diff --git a/src/constants.ts b/src/constants.ts
index 5e48122..9506801 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -19,21 +19,33 @@ export const BASE_URL_PLAYLIST = `${BASE_URL}/playlist`;
 
 export const BASE_URL_METADATA = `${BASE_URL}/player/metadata/video`;
 
-export const REGEX_VIDEO_URL = /^https:\/\/(?:www\.)?dailymotion\.com\/video\/[a-zA-Z0-9]+$/i;
+export const REGEX_VIDEO_URL =
+  /^https:\/\/(?:www\.)?dailymotion\.com\/video\/[a-zA-Z0-9]+$/i;
+
 export const REGEX_VIDEO_URL_1 = /^https:\/\/dai\.ly\/[a-zA-Z0-9]+$/i;
-export const REGEX_VIDEO_URL_EMBED = /^https:\/\/(?:www\.)?dailymotion\.com\/embed\/video\/[a-zA-Z0-9]+(\?.*)?$/i;
 
-export const REGEX_VIDEO_CHANNEL_URL = /^https:\/\/(?:www\.)?dailymotion\.com\/[a-zA-Z0-9-]+$/i;
-export const REGEX_VIDEO_PLAYLIST_URL = /^https:\/\/(?:www\.)?dailymotion\.com\/playlist\/[a-zA-Z0-9]+$/i;
+export const REGEX_VIDEO_URL_EMBED =
+  /^https:\/\/(?:www\.)?dailymotion\.com\/embed\/video\/[a-zA-Z0-9]+(\?.*)?$/i;
+
+export const REGEX_VIDEO_CHANNEL_URL =
+  /^https:\/\/(?:www\.)?dailymotion\.com\/[a-zA-Z0-9-]+$/i;
+  
+export const REGEX_VIDEO_PLAYLIST_URL =
+  /^https:\/\/(?:www\.)?dailymotion\.com\/playlist\/[a-zA-Z0-9]+$/i;
+
+export const REGEX_INITIAL_DATA_API_AUTH_1 =
+  /(?<=window\.__LOADABLE_LOADED_CHUNKS__=.*)\b[a-f0-9]{20}\b|\b[a-f0-9]{40}\b/g;
 
-export const REGEX_INITIAL_DATA_API_AUTH = /(?<=window\.__LOADABLE_LOADED_CHUNKS__=.*)\b[a-f0-9]{20}\b|\b[a-f0-9]{40}\b/g;
+export const createAuthRegexByTextLength = (length: number) =>
+  new RegExp(`\\b\\w+\\s*=\\s*"([a-zA-Z0-9]{${length}})"`);
 
 export 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.
 export const FALLBACK_CLIENT_ID = 'f1a362d288c1b98099c7';
-export const FALLBACK_CLIENT_SECRET = 'eea605b96e01c796ff369935357eca920c5da4c5';
+export const FALLBACK_CLIENT_SECRET =
+  'eea605b96e01c796ff369935357eca920c5da4c5';
 
 export const X_DM_AppInfo_Id = 'com.dailymotion.neon';
 export const X_DM_AppInfo_Type = 'website';
diff --git a/src/extraction.ts b/src/extraction.ts
new file mode 100644
index 0000000..d459749
--- /dev/null
+++ b/src/extraction.ts
@@ -0,0 +1,182 @@
+import { AnonymousUserAuthorization } from '../types/types';
+import {
+  BASE_URL,
+  BASE_URL_API_AUTH,
+  createAuthRegexByTextLength,
+  FALLBACK_CLIENT_ID,
+  FALLBACK_CLIENT_SECRET,
+  REGEX_INITIAL_DATA_API_AUTH_1,
+  USER_AGENT,
+} from './constants';
+import { objectToUrlEncodedString } from './util';
+
+export function oauthClientCredentialsRequest(
+  httpClient: IHttp,
+  url: string,
+  clientId: string,
+  secret: string,
+  throwOnInvalid = false,
+): HttpResponse {
+  if (!httpClient || !url || !clientId || !secret) {
+    throw new ScriptException(
+      'Invalid parameters provided to oauthClientCredentialsRequest',
+    );
+  }
+
+  const body = objectToUrlEncodedString({
+    client_id: clientId,
+    client_secret: secret,
+    grant_type: 'client_credentials',
+  });
+
+  try {
+    return httpClient.POST(
+      url,
+      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,
+    );
+  } catch (error) {
+    console.error('Error making OAuth client credentials request:', error);
+    if (throwOnInvalid) {
+      throw new ScriptException('Failed to obtain OAuth client credentials');
+    }
+    return null;
+  }
+}
+
+export function extractClientCredentials(httpClient: IHttp) {
+  const detailsRequestHtml = httpClient.GET(BASE_URL, {}, false);
+
+  if (!detailsRequestHtml.isOk) {
+    throw new ScriptException('Failed to fetch page to extract auth details');
+  }
+
+  const result = [
+    {
+      clientId: FALLBACK_CLIENT_ID,
+      secret: FALLBACK_CLIENT_SECRET,
+    },
+  ];
+
+  const match = detailsRequestHtml.body.match(REGEX_INITIAL_DATA_API_AUTH_1);
+
+  if (match?.length === 2 && match[0] && match[1]) {
+    result.unshift({
+      clientId: match[0],
+      secret: match[1],
+    });
+    console.log('Successfully extracted API credentials from page:', match[1]);
+  } else {
+    console.log(
+      'Failed to extract API credentials from page using regex. Using DOM parsing.',
+    );
+
+    const htmlElement = domParser.parseFromString(
+      detailsRequestHtml.body,
+      'text/html',
+    );
+    const extractedId = getScriptVariableByTextLength(htmlElement, 20);
+    const extractedSecret = getScriptVariableByTextLength(htmlElement, 40);
+
+    if (extractedId && extractedSecret) {
+      result.unshift({
+        clientId: extractedId,
+        secret: extractedSecret,
+      });
+
+      console.log(
+        'Successfully extracted API credentials from page using DOM parsing:',
+        extractedId,
+      );
+    } else {
+      console.log(
+        'Failed to extract API credentials using DOM parsing with exact text length.',
+      );
+    }
+  }
+
+  return result;
+}
+
+export function getScriptVariableByTextLength(htmlElement, length: number) {
+  const scriptTags = htmlElement.querySelectorAll(
+    'script[type="text/javascript"]',
+  );
+
+  if (!scriptTags.length) {
+    console.error('No script tags found.');
+    return null; // or throw an error, depending on your use case
+  }
+
+  let pageContent = '';
+
+  scriptTags.forEach((tag) => {
+    pageContent += tag.outerHTML;
+  });
+
+  let matches = createAuthRegexByTextLength(length).exec(pageContent);
+
+  if (matches?.length == 2) {
+    return matches[1];
+  }
+}
+
+export function getTokenFromClientCredentials(
+  httpClient: IHttp,
+  credentials,
+  throwOnInvalid = false,
+) {
+  let result: AnonymousUserAuthorization = {
+    isValid: false,
+  };
+
+  for (const credential of credentials) {
+    const res = oauthClientCredentialsRequest(
+      httpClient,
+      BASE_URL_API_AUTH,
+      credential.clientId,
+      credential.secret,
+    );
+
+    if (res?.isOk) {
+      const anonymousTokenResponse = JSON.parse(res.body);
+
+      if (
+        !anonymousTokenResponse.token_type ||
+        !anonymousTokenResponse.access_token
+      ) {
+        console.error('Invalid token response', res);
+        if (throwOnInvalid) {
+          throw new ScriptException('', 'Invalid token response: ' + res.body);
+        }
+      }
+
+      result = {
+        anonymousUserAuthorizationToken: `${anonymousTokenResponse.token_type} ${anonymousTokenResponse.access_token}`,
+        anonymousUserAuthorizationTokenExpirationDate:
+          Date.now() + anonymousTokenResponse.expires_in * 1000,
+        isValid: true,
+      };
+
+      break;
+    } else {
+      console.error('Failed to get token', res);
+    }
+  }
+
+  return result;
+}
diff --git a/types/plugin.d.ts b/types/plugin.d.ts
index 2e6b975..74a026f 100644
--- a/types/plugin.d.ts
+++ b/types/plugin.d.ts
@@ -1361,11 +1361,17 @@ let Language = {
 };
 
 interface HttpResponse {
-  isOk(): boolean;
+  isOk: boolean;
   body: string;
   code: number;
 }
 
+domParser.parseFromString(detailsRequestHtml.body, "text/html")
+
+let domParser = {
+  parseFromString: function (elementText: string, contentType: string): Unit {},
+}
+
 //Package Bridge (variable: bridge)
 let bridge = {
   /**
diff --git a/types/types.d.ts b/types/types.d.ts
index ef6e55a..490eae8 100644
--- a/types/types.d.ts
+++ b/types/types.d.ts
@@ -30,3 +30,9 @@ interface IPlatformSystemPlaylist {
   usePlatformAuth: boolean;
   thumbnailResolutionIndex: number;
 }
+
+type AnonymousUserAuthorization = {
+  anonymousUserAuthorizationToken?: string,
+  anonymousUserAuthorizationTokenExpirationDate?: number,
+  isValid: boolean
+} 
\ No newline at end of file
-- 
GitLab