diff --git a/DailymotionConfig.json b/DailymotionConfig.json
index d801d80839ee9392758c4880016061f1215e3521..9c7f3f653bbb1591bddc6e6e2200c383d3f1ebd5 100644
--- a/DailymotionConfig.json
+++ b/DailymotionConfig.json
@@ -7,7 +7,7 @@
   "sourceUrl": "https://stefancruz.github.io/GrayjayDailymotion/DailymotionConfig.json",
   "scriptUrl": "./DailymotionScript.js",
   "repositoryUrl": "https://github.com/stefancruz/GrayjayDailymotion",
-  "version": 15,
+  "version": 16,
   "iconUrl": "./DailymotionIcon.png",
   "id": "9c87e8db-e75d-48f4-afe5-2d203d4b95c5",
   "scriptSignature": "",
diff --git a/Readme.md b/Readme.md
index 640b29f97a4e10fc96153b5c333b8e1c7eed7ff8..73b6526c07537430ce0d67f6ad5190c0d7d9efd7 100644
--- a/Readme.md
+++ b/Readme.md
@@ -21,23 +21,21 @@ Click [here](https://stefancruz.github.io/GrayjayDailymotion/index.html) to inst
 - [x] - Sign in (import subscriptions and playlists (and Likes, Favorites, Recently Watched))
 - [x] - Policentric Comments
 - [x] - Subtitles
+- [x] - State management
 
 ## Todo 
-- [ ] - State management
-- [ ] - Platform comments (not generally available)
+- [ ] - Unit tests - (wip)
+- [ ] - Platform comments (not generally available) - (wip)
 
 ## Known bugs
 - [ ] - Live filter in Subscriptions tab
 
 
-
 ## Install
 npm install
 
 ## Update graphql types (optional)
 
-npm run get-token
-
 npm run codegen
 
 ## Build
diff --git a/build/DailymotionConfig.json b/build/DailymotionConfig.json
index d801d80839ee9392758c4880016061f1215e3521..9c7f3f653bbb1591bddc6e6e2200c383d3f1ebd5 100644
--- a/build/DailymotionConfig.json
+++ b/build/DailymotionConfig.json
@@ -7,7 +7,7 @@
   "sourceUrl": "https://stefancruz.github.io/GrayjayDailymotion/DailymotionConfig.json",
   "scriptUrl": "./DailymotionScript.js",
   "repositoryUrl": "https://github.com/stefancruz/GrayjayDailymotion",
-  "version": 15,
+  "version": 16,
   "iconUrl": "./DailymotionIcon.png",
   "id": "9c87e8db-e75d-48f4-afe5-2d203d4b95c5",
   "scriptSignature": "",
diff --git a/build/DailymotionScript.js b/build/DailymotionScript.js
index 593025f18ce5d76755d5a95d3b1a08d1702e5a37..766f6bf116574e2cb9a15f89c1517f28ed6817f0 100644
--- a/build/DailymotionScript.js
+++ b/build/DailymotionScript.js
@@ -1413,183 +1413,6 @@ const USER_WATCHED_VIDEOS_QUERY = `
 	}
 }`;
 
-const SourceChannelToGrayjayChannel = (pluginId, url, sourceChannel) => {
-    const externalLinks = sourceChannel?.externalLinks ?? {};
-    const links = Object.keys(externalLinks).reduce((acc, key) => {
-        if (externalLinks[key]) {
-            acc[key.replace('URL', '')] = externalLinks[key];
-        }
-        return acc;
-    }, {});
-    return new PlatformChannel({
-        id: new PlatformID(PLATFORM, sourceChannel?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
-        name: sourceChannel?.displayName ?? "",
-        thumbnail: sourceChannel?.avatar?.url ?? "",
-        banner: sourceChannel.banner?.url ?? "",
-        subscribers: sourceChannel?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0,
-        description: sourceChannel?.description ?? "",
-        url,
-        links
-    });
-};
-const SourceAuthorToGrayjayPlatformAuthorLink = (pluginId, creator) => {
-    return new PlatformAuthorLink(new PlatformID(PLATFORM, creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), creator?.displayName ?? "", creator?.name ? `${BASE_URL}/${creator?.name}` : "", creator?.avatar?.url ?? "", creator?.followers?.totalCount ?? creator?.stats?.followers?.total ?? creator?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0);
-};
-const SourceVideoToGrayjayVideo = (pluginId, sourceVideo) => {
-    const isLive = getIsLive(sourceVideo);
-    const viewCount = getViewCount(sourceVideo);
-    const video = {
-        id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
-        description: sourceVideo?.description ?? '',
-        name: sourceVideo?.title ?? "",
-        thumbnails: new Thumbnails([
-            new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0)
-        ]),
-        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator),
-        uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
-        datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
-        url: `${BASE_URL_VIDEO}/${sourceVideo?.xid}`,
-        duration: sourceVideo?.duration ?? 0,
-        viewCount,
-        isLive
-    };
-    return new PlatformVideo(video);
-};
-const SourceCollectionToGrayjayPlaylistDetails = (pluginId, sourceCollection, videos = []) => {
-    return new PlatformPlaylistDetails({
-        url: sourceCollection?.xid ? `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}` : "",
-        id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE),
-        author: sourceCollection?.creator ? SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection?.creator) : {},
-        name: sourceCollection.name,
-        thumbnail: sourceCollection?.thumbnail?.url,
-        videoCount: videos.length ?? 0,
-        contents: new VideoPager(videos)
-    });
-};
-const SourceCollectionToGrayjayPlaylist = (pluginId, sourceCollection) => {
-    return new PlatformPlaylist({
-        url: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`,
-        id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE),
-        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection?.creator),
-        name: sourceCollection?.name,
-        thumbnail: sourceCollection?.thumbnail?.url,
-        videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total,
-    });
-};
-const getIsLive = (sourceVideo) => {
-    return sourceVideo?.isOnAir === true || sourceVideo?.duration == undefined;
-};
-const getViewCount = (sourceVideo) => {
-    let viewCount = 0;
-    if (getIsLive(sourceVideo)) {
-        viewCount = sourceVideo?.audienceCount ?? sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0;
-    }
-    else {
-        viewCount = sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0;
-    }
-    return viewCount;
-};
-const SourceVideoToPlatformVideoDetailsDef = (pluginId, sourceVideo, sources, sourceSubtitle) => {
-    let positiveRatingCount = 0;
-    let negativeRatingCount = 0;
-    const ratings = sourceVideo?.metrics?.engagement?.likes?.edges ?? [];
-    for (const edge of ratings) {
-        const ratingName = edge?.node?.rating;
-        const ratingTotal = edge?.node?.total;
-        if (POSITIVE_RATINGS_LABELS.includes(ratingName)) {
-            positiveRatingCount += ratingTotal;
-        }
-        else if (NEGATIVE_RATINGS_LABELS.includes(ratingName)) {
-            negativeRatingCount += ratingTotal;
-        }
-    }
-    const isLive = getIsLive(sourceVideo);
-    const viewCount = getViewCount(sourceVideo);
-    const duration = isLive ? 0 : sourceVideo?.duration ?? 0;
-    const platformVideoDetails = {
-        id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
-        name: sourceVideo?.title ?? "",
-        thumbnails: new Thumbnails([new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0)]),
-        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator),
-        uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
-        datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
-        duration,
-        // viewCount,
-        viewCount,
-        url: sourceVideo?.xid ? `${BASE_URL_VIDEO}/${sourceVideo.xid}` : "",
-        isLive,
-        description: sourceVideo?.description ?? "",
-        video: new VideoSourceDescriptor(sources),
-        rating: new RatingLikesDislikes(positiveRatingCount, negativeRatingCount),
-        dash: null,
-        live: null,
-        hls: null,
-        subtitles: []
-    };
-    if (sourceSubtitle?.enable && sourceSubtitle?.data) {
-        Object.keys(sourceSubtitle.data).forEach(key => {
-            const subtitleData = sourceSubtitle.data[key];
-            if (subtitleData) {
-                const subtitleUrl = subtitleData.urls[0];
-                platformVideoDetails.subtitles.push({
-                    name: subtitleData.label,
-                    url: subtitleUrl,
-                    format: "text/vtt",
-                    getSubtitles() {
-                        try {
-                            const subResp = http.GET(subtitleUrl, {});
-                            if (!subResp.isOk) {
-                                if (IS_TESTING) {
-                                    bridge.log(`Failed to fetch subtitles from ${subtitleUrl}`);
-                                }
-                                return "";
-                            }
-                            return convertSRTtoVTT(subResp.body);
-                        }
-                        catch (error) {
-                            if (IS_TESTING) {
-                                bridge.log(`Error fetching subtitles: ${error?.message}`);
-                            }
-                            return "";
-                        }
-                    }
-                });
-            }
-        });
-    }
-    return platformVideoDetails;
-};
-/**
- * Converts SRT subtitle format to VTT format.
- *
- * @param {string} srt - The SRT subtitle string.
- * @returns {string} - The converted VTT subtitle string.
- */
-const convertSRTtoVTT = (srt) => {
-    // Initialize the VTT output with the required header
-    const vtt = ['WEBVTT\n\n'];
-    // Split the SRT input into blocks based on double newlines
-    const srtBlocks = srt.split('\n\n');
-    // Process each block individually
-    srtBlocks.forEach((block) => {
-        // Split each block into lines
-        const lines = block.split('\n');
-        if (lines.length >= 3) {
-            // Extract and convert the timestamp line
-            const timestamp = lines[1].replace(/,/g, '.');
-            // Extract the subtitle text lines
-            const subtitleText = lines.slice(2).join('\n');
-            // Add the converted block to the VTT output
-            vtt.push(`${timestamp}\n${subtitleText}\n\n`);
-        }
-    });
-    // Join the VTT array into a single string and return it
-    return vtt.join('');
-};
-
-let AUTHORIZATION_TOKEN_ANONYMOUS_USER = "";
-let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE;
-let httpClientRequestToken = http.newClient(false);
 function getPreferredCountry(preferredCountryIndex) {
     const countryName = COUNTRY_NAMES[preferredCountryIndex];
     const code = COUNTRY_NAMES_TO_CODE[countryName];
@@ -1614,95 +1437,6 @@ function isUsernameUrl(url) {
     const regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$');
     return regex.test(url);
 }
-// TODO: save to state
-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 parseUploadDateFilter = (filter) => {
     let createdAfterVideos;
     const now = new Date();
@@ -1776,82 +1510,6 @@ function generateUUIDv4() {
         return v.toString(16);
     });
 }
-function getPages(httpClient, query, operationName, variables, usePlatformAuth, setRoot, hasNextCallback, getNextPage, map) {
-    let all = [];
-    if (!hasNextCallback) {
-        hasNextCallback = () => false;
-    }
-    let hasNext = true;
-    let nextPage = 1;
-    do {
-        variables = { ...variables, page: nextPage };
-        const jsonResponse = executeGqlQuery(httpClient, {
-            operationName,
-            variables,
-            query,
-            usePlatformAuth
-        });
-        const root = setRoot(jsonResponse);
-        nextPage = getNextPage(root, nextPage);
-        const items = map(root);
-        hasNext = hasNextCallback(root);
-        all = all.concat(items);
-    } while (hasNext);
-    return all;
-}
-function getLikePlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_LIKED_VIDEOS_QUERY,
-        operationName: 'USER_LIKED_VIDEOS_QUERY',
-        rootObject: 'likedMedias',
-        playlistName: 'Liked Videos',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    });
-}
-function getFavoritesPlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_WATCH_LATER_VIDEOS_QUERY,
-        operationName: 'USER_WATCH_LATER_VIDEOS_QUERY',
-        rootObject: 'watchLaterMedias',
-        playlistName: 'Favorites',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    });
-}
-function getRecentlyWatchedPlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_WATCHED_VIDEOS_QUERY,
-        operationName: 'USER_WATCHED_VIDEOS_QUERY',
-        rootObject: 'watchedVideos',
-        playlistName: 'Recently Watched',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    });
-}
-function getPlatformSystemPlaylist(opts) {
-    const videos = getPages(opts.httpClient, opts.query, opts.operationName, {
-        page: 1,
-        thumbnail_resolution: THUMBNAIL_HEIGHT[opts.thumbnailResolutionIndex]
-    }, opts.usePlatformAuth, (jsonResponse) => jsonResponse?.data?.me, //set root
-    (me) => (me?.[opts.rootObject]?.edges.length ?? 0) > 0 ?? false, //hasNextCallback
-    (me, currentPage) => ++currentPage, //getNextPage
-    (me) => me?.[opts.rootObject]?.edges.map(edge => {
-        return SourceVideoToGrayjayVideo(opts.pluginId, edge.node);
-    }));
-    const collection = {
-        "id": generateUUIDv4(),
-        "name": opts.playlistName,
-        "creator": {}
-    };
-    return SourceCollectionToGrayjayPlaylistDetails(opts.pluginId, collection, videos);
-}
 
 class SearchPagerAll extends VideoPager {
     cb;
@@ -1898,35 +1556,214 @@ class ChannelPlaylistPager extends PlaylistPager {
         super(results, hasMore, { params, page });
         this.cb = cb;
     }
-    nextPage() {
-        this.context.page += 1;
-        return this.cb(this.context.params.url, this.context.page);
+    nextPage() {
+        this.context.page += 1;
+        return this.cb(this.context.params.url, this.context.page);
+    }
+}
+class SearchPlaylistPager extends PlaylistPager {
+    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);
+    }
+}
+
+const SourceChannelToGrayjayChannel = (pluginId, url, sourceChannel) => {
+    const externalLinks = sourceChannel?.externalLinks ?? {};
+    const links = Object.keys(externalLinks).reduce((acc, key) => {
+        if (externalLinks[key]) {
+            acc[key.replace('URL', '')] = externalLinks[key];
+        }
+        return acc;
+    }, {});
+    return new PlatformChannel({
+        id: new PlatformID(PLATFORM, sourceChannel?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
+        name: sourceChannel?.displayName ?? "",
+        thumbnail: sourceChannel?.avatar?.url ?? "",
+        banner: sourceChannel.banner?.url ?? "",
+        subscribers: sourceChannel?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0,
+        description: sourceChannel?.description ?? "",
+        url,
+        links
+    });
+};
+const SourceAuthorToGrayjayPlatformAuthorLink = (pluginId, creator) => {
+    return new PlatformAuthorLink(new PlatformID(PLATFORM, creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), creator?.displayName ?? "", creator?.name ? `${BASE_URL}/${creator?.name}` : "", creator?.avatar?.url ?? "", creator?.followers?.totalCount ?? creator?.stats?.followers?.total ?? creator?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0);
+};
+const SourceVideoToGrayjayVideo = (pluginId, sourceVideo) => {
+    const isLive = getIsLive(sourceVideo);
+    const viewCount = getViewCount(sourceVideo);
+    const video = {
+        id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
+        description: sourceVideo?.description ?? '',
+        name: sourceVideo?.title ?? "",
+        thumbnails: new Thumbnails([
+            new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0)
+        ]),
+        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator),
+        uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
+        datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
+        url: `${BASE_URL_VIDEO}/${sourceVideo?.xid}`,
+        duration: sourceVideo?.duration ?? 0,
+        viewCount,
+        isLive
+    };
+    return new PlatformVideo(video);
+};
+const SourceCollectionToGrayjayPlaylistDetails = (pluginId, sourceCollection, videos = []) => {
+    return new PlatformPlaylistDetails({
+        url: sourceCollection?.xid ? `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}` : "",
+        id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE),
+        author: sourceCollection?.creator ? SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection?.creator) : {},
+        name: sourceCollection.name,
+        thumbnail: sourceCollection?.thumbnail?.url,
+        videoCount: videos.length ?? 0,
+        contents: new VideoPager(videos)
+    });
+};
+const SourceCollectionToGrayjayPlaylist = (pluginId, sourceCollection) => {
+    return new PlatformPlaylist({
+        url: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`,
+        id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE),
+        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceCollection?.creator),
+        name: sourceCollection?.name,
+        thumbnail: sourceCollection?.thumbnail?.url,
+        videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total,
+    });
+};
+const getIsLive = (sourceVideo) => {
+    return sourceVideo?.isOnAir === true || sourceVideo?.duration == undefined;
+};
+const getViewCount = (sourceVideo) => {
+    let viewCount = 0;
+    if (getIsLive(sourceVideo)) {
+        viewCount = sourceVideo?.audienceCount ?? sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0;
+    }
+    else {
+        viewCount = sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0;
     }
-}
-class SearchPlaylistPager extends PlaylistPager {
-    cb;
-    constructor(results, hasMore, params, page, cb) {
-        super(results, hasMore, { params, page });
-        this.cb = cb;
+    return viewCount;
+};
+const SourceVideoToPlatformVideoDetailsDef = (pluginId, sourceVideo, sources, sourceSubtitle) => {
+    let positiveRatingCount = 0;
+    let negativeRatingCount = 0;
+    const ratings = sourceVideo?.metrics?.engagement?.likes?.edges ?? [];
+    for (const edge of ratings) {
+        const ratingName = edge?.node?.rating;
+        const ratingTotal = edge?.node?.total;
+        if (POSITIVE_RATINGS_LABELS.includes(ratingName)) {
+            positiveRatingCount += ratingTotal;
+        }
+        else if (NEGATIVE_RATINGS_LABELS.includes(ratingName)) {
+            negativeRatingCount += ratingTotal;
+        }
     }
-    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);
+    const isLive = getIsLive(sourceVideo);
+    const viewCount = getViewCount(sourceVideo);
+    const duration = isLive ? 0 : sourceVideo?.duration ?? 0;
+    const platformVideoDetails = {
+        id: new PlatformID(PLATFORM, sourceVideo?.id ?? "", pluginId, PLATFORM_CLAIMTYPE),
+        name: sourceVideo?.title ?? "",
+        thumbnails: new Thumbnails([new Thumbnail(sourceVideo?.thumbnail?.url ?? "", 0)]),
+        author: SourceAuthorToGrayjayPlatformAuthorLink(pluginId, sourceVideo?.creator),
+        uploadDate: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
+        datetime: Math.floor(new Date(sourceVideo?.createdAt).getTime() / 1000),
+        duration,
+        // viewCount,
+        viewCount,
+        url: sourceVideo?.xid ? `${BASE_URL_VIDEO}/${sourceVideo.xid}` : "",
+        isLive,
+        description: sourceVideo?.description ?? "",
+        video: new VideoSourceDescriptor(sources),
+        rating: new RatingLikesDislikes(positiveRatingCount, negativeRatingCount),
+        dash: null,
+        live: null,
+        hls: null,
+        subtitles: []
+    };
+    if (sourceSubtitle?.enable && sourceSubtitle?.data) {
+        Object.keys(sourceSubtitle.data).forEach(key => {
+            const subtitleData = sourceSubtitle.data[key];
+            if (subtitleData) {
+                const subtitleUrl = subtitleData.urls[0];
+                platformVideoDetails.subtitles.push({
+                    name: subtitleData.label,
+                    url: subtitleUrl,
+                    format: "text/vtt",
+                    getSubtitles() {
+                        try {
+                            const subResp = http.GET(subtitleUrl, {});
+                            if (!subResp.isOk) {
+                                if (IS_TESTING) {
+                                    bridge.log(`Failed to fetch subtitles from ${subtitleUrl}`);
+                                }
+                                return "";
+                            }
+                            return convertSRTtoVTT(subResp.body);
+                        }
+                        catch (error) {
+                            if (IS_TESTING) {
+                                bridge.log(`Error fetching subtitles: ${error?.message}`);
+                            }
+                            return "";
+                        }
+                    }
+                });
+            }
+        });
     }
-}
+    return platformVideoDetails;
+};
+/**
+ * Converts SRT subtitle format to VTT format.
+ *
+ * @param {string} srt - The SRT subtitle string.
+ * @returns {string} - The converted VTT subtitle string.
+ */
+const convertSRTtoVTT = (srt) => {
+    // Initialize the VTT output with the required header
+    const vtt = ['WEBVTT\n\n'];
+    // Split the SRT input into blocks based on double newlines
+    const srtBlocks = srt.split('\n\n');
+    // Process each block individually
+    srtBlocks.forEach((block) => {
+        // Split each block into lines
+        const lines = block.split('\n');
+        if (lines.length >= 3) {
+            // Extract and convert the timestamp line
+            const timestamp = lines[1].replace(/,/g, '.');
+            // Extract the subtitle text lines
+            const subtitleText = lines.slice(2).join('\n');
+            // Add the converted block to the VTT output
+            vtt.push(`${timestamp}\n${subtitleText}\n\n`);
+        }
+    });
+    // Join the VTT array into a single string and return it
+    return vtt.join('');
+};
 
 let config;
 let _settings;
-const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST_ID";
-const FAVORITES_PLAYLIST_ID = "FAVORITES_PLAYLIST_ID";
-const RECENTLY_WATCHED_PLAYLIST_ID = "RECENTLY_WATCHED_PLAYLIST_ID";
+const state = {
+    anonymousUserAuthorizationToken: "",
+    anonymousUserAuthorizationTokenExpirationDate: 0
+};
+const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST";
+const FAVORITES_PLAYLIST_ID = "FAVORITES_PLAYLIST";
+const RECENTLY_WATCHED_PLAYLIST_ID = "RECENTLY_WATCHED_PLAYLIST";
 let httpClientAnonymous = http.newClient(false);
+let httpClientRequestToken = http.newClient(false);
 // Will be used to store private playlists that require authentication
 const authenticatedPlaylistCollection = [];
 source.setSettings = function (settings) {
@@ -1946,9 +1783,64 @@ source.enable = function (conf, settings, saveStateStr) {
         _settings.playlistsPerPageOptionIndex = 0;
         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;
+                if (!isTokenValid()) {
+                    log("Token expired. Fetching a new one.");
+                }
+                else {
+                    didSaveState = true;
+                    log("Using save state");
+                }
+            }
+        }
+    }
+    catch (ex) {
+        log("Failed to parse saveState:" + ex);
+        didSaveState = false;
+    }
+    if (!didSaveState) {
+        log("Getting a new token");
+        const body = objectToUrlEncodedString({
+            client_id: CLIENT_ID,
+            client_secret: CLIENT_SECRET,
+            grant_type: 'client_credentials'
+        });
+        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);
+        if (res.code !== 200) {
+            console.error('Failed to get token', res);
+            throw new ScriptException("", "Failed to get token: " + res.code + " - " + res.body);
+        }
+        const json = JSON.parse(res.body);
+        if (!json.token_type || !json.access_token) {
+            console.error('Invalid token response', res);
+            throw new ScriptException("", 'Invalid token response: ' + res.body);
+        }
+        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}`);
+    }
 };
 source.getHome = function () {
-    getAnonymousUserTokenSingleton();
     return getVideoPager({}, 0);
 };
 source.searchSuggestions = function (query) {
@@ -2017,6 +1909,12 @@ source.isContentDetailsUrl = function (url) {
 source.getContentDetails = function (url) {
     return getSavedVideo(url, false);
 };
+source.saveState = () => {
+    return JSON.stringify({
+        anonymousUserAuthorizationToken: state.anonymousUserAuthorizationToken,
+        anonymousUserAuthorizationTokenExpirationDate: state.anonymousUserAuthorizationTokenExpirationDate
+    });
+};
 //Playlist
 source.isPlaylistUrl = (url) => {
     return url.startsWith(BASE_URL_PLAYLIST) ||
@@ -2059,7 +1957,7 @@ source.getPlaylist = (url) => {
 };
 source.getUserSubscriptions = () => {
     if (!bridge.isLoggedIn()) {
-        bridge.log("Failed to retrieve subscriptions page because not logged in.");
+        log("Failed to retrieve subscriptions page because not logged in.");
         throw new ScriptException("Not logged in");
     }
     const headers = {
@@ -2115,7 +2013,7 @@ source.getUserSubscriptions = () => {
 };
 source.getUserPlaylists = () => {
     if (!bridge.isLoggedIn()) {
-        bridge.log("Failed to retrieve subscriptions page because not logged in.");
+        log("Failed to retrieve subscriptions page because not logged in.");
         throw new ScriptException("Not logged in");
     }
     const headers = {
@@ -2275,7 +2173,7 @@ function getChannelContentsPager(url, page, type, order, filters) {
     const shouldLoadVideos = type === Type.Feed.Mixed || type === Type.Feed.Videos;
     const shouldLoadLives = type === Type.Feed.Mixed || type === Type.Feed.Streams || type === Type.Feed.Live;
     if (IS_TESTING) {
-        bridge.log(`Getting channel contents for ${url}, page: ${page}, type: ${type}, order: ${order}, shouldLoadVideos: ${shouldLoadVideos}, shouldLoadLives: ${shouldLoadLives}, filters: ${JSON.stringify(filters)}`);
+        log(`Getting channel contents for ${url}, page: ${page}, type: ${type}, order: ${order}, shouldLoadVideos: ${shouldLoadVideos}, shouldLoadLives: ${shouldLoadLives}, filters: ${JSON.stringify(filters)}`);
     }
     /**
         Recent = Sort liked medias by most recent.
@@ -2418,7 +2316,7 @@ function getSavedVideo(url, usePlatformAuth = false) {
         "Cache-Control": "no-cache"
     };
     if (!usePlatformAuth) {
-        videoDetailsRequestHeaders.Authorization = getAnonymousUserTokenSingleton();
+        videoDetailsRequestHeaders.Authorization = state.anonymousUserAuthorizationToken;
     }
     const variables = {
         "xid": id,
@@ -2517,6 +2415,128 @@ function getChannelPlaylists(url, page = 1) {
     const hasMore = channel?.collections?.pageInfo?.hasNextPage ?? false;
     return new ChannelPlaylistPager(content, hasMore, params, page, getChannelPlaylists);
 }
+function isTokenValid() {
+    const currentTime = Date.now();
+    return state.anonymousUserAuthorizationTokenExpirationDate > currentTime;
+}
+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 = state.anonymousUserAuthorizationToken;
+    }
+    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;
+}
+function getPages(httpClient, query, operationName, variables, usePlatformAuth, setRoot, hasNextCallback, getNextPage, map) {
+    let all = [];
+    if (!hasNextCallback) {
+        hasNextCallback = () => false;
+    }
+    let hasNext = true;
+    let nextPage = 1;
+    do {
+        variables = { ...variables, page: nextPage };
+        const jsonResponse = executeGqlQuery(httpClient, {
+            operationName,
+            variables,
+            query,
+            usePlatformAuth
+        });
+        const root = setRoot(jsonResponse);
+        nextPage = getNextPage(root, nextPage);
+        const items = map(root);
+        hasNext = hasNextCallback(root);
+        all = all.concat(items);
+    } while (hasNext);
+    return all;
+}
+function getLikePlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
+    return getPlatformSystemPlaylist({
+        pluginId,
+        httpClient,
+        query: USER_LIKED_VIDEOS_QUERY,
+        operationName: 'USER_LIKED_VIDEOS_QUERY',
+        rootObject: 'likedMedias',
+        playlistName: 'Liked Videos',
+        usePlatformAuth,
+        thumbnailResolutionIndex
+    });
+}
+function getFavoritesPlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
+    return getPlatformSystemPlaylist({
+        pluginId,
+        httpClient,
+        query: USER_WATCH_LATER_VIDEOS_QUERY,
+        operationName: 'USER_WATCH_LATER_VIDEOS_QUERY',
+        rootObject: 'watchLaterMedias',
+        playlistName: 'Favorites',
+        usePlatformAuth,
+        thumbnailResolutionIndex
+    });
+}
+function getRecentlyWatchedPlaylist(pluginId, httpClient, usePlatformAuth = false, thumbnailResolutionIndex = 0) {
+    return getPlatformSystemPlaylist({
+        pluginId,
+        httpClient,
+        query: USER_WATCHED_VIDEOS_QUERY,
+        operationName: 'USER_WATCHED_VIDEOS_QUERY',
+        rootObject: 'watchedVideos',
+        playlistName: 'Recently Watched',
+        usePlatformAuth,
+        thumbnailResolutionIndex
+    });
+}
+function getPlatformSystemPlaylist(opts) {
+    const videos = getPages(opts.httpClient, opts.query, opts.operationName, {
+        page: 1,
+        thumbnail_resolution: THUMBNAIL_HEIGHT[opts.thumbnailResolutionIndex]
+    }, opts.usePlatformAuth, (jsonResponse) => jsonResponse?.data?.me, //set root
+    (me) => (me?.[opts.rootObject]?.edges.length ?? 0) > 0 ?? false, //hasNextCallback
+    (me, currentPage) => ++currentPage, //getNextPage
+    (me) => me?.[opts.rootObject]?.edges.map(edge => {
+        return SourceVideoToGrayjayVideo(opts.pluginId, edge.node);
+    }));
+    const collection = {
+        "id": generateUUIDv4(),
+        "name": opts.playlistName,
+        "creator": {}
+    };
+    return SourceCollectionToGrayjayPlaylistDetails(opts.pluginId, collection, videos);
+}
 function getHttpContext(opts = { usePlatformAuth: false }) {
     return opts.usePlatformAuth ? http : httpClientAnonymous;
 }
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index d7988cf25e6763d77748c217e4429c4bece7c334..d9a10feeb33db8c3f2ff63faaab0fc05b2c8c3a5 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -9,8 +9,7 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
-        "axios": "1.7.2",
-        "dotenv": "16.4.5"
+        "axios": "1.7.2"
       },
       "devDependencies": {
         "@graphql-codegen/cli": "5.0.2",
@@ -25,7 +24,7 @@
         "graphql": "16.8.1",
         "rollup": "4.18.0",
         "rollup-plugin-copy": "3.5.0",
-        "rollup-plugin-delete": "^2.0.0",
+        "rollup-plugin-delete": "2.0.0",
         "typescript": "5.4.5"
       },
       "engines": {
@@ -3390,6 +3389,7 @@
       "version": "16.4.5",
       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
       "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+      "dev": true,
       "engines": {
         "node": ">=12"
       },
diff --git a/package.json b/package.json
index a856208ea1d196a06bc18d1a6d6b297562dcab48..c2909ea3b8e0ca082d99968cfe9b99bfdc324486 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,6 @@
   "scripts": {
     "build": "rollup -c",
     "start": "rollup -c -w",
-    "get-token": "node ./scripts/get-token.js",
     "codegen": "graphql-codegen --config ./scripts/codegen.ts"
   },
   "engines": {
@@ -28,12 +27,11 @@
     "@rollup/plugin-typescript": "11.1.6",
     "graphql": "16.8.1",
     "rollup": "4.18.0",
-    "rollup-plugin-delete": "^2.0.0",
-    "typescript": "5.4.5",
-    "rollup-plugin-copy": "3.5.0"
+    "rollup-plugin-copy": "3.5.0",
+    "rollup-plugin-delete": "2.0.0",
+    "typescript": "5.4.5"
   },
   "dependencies": {
-    "axios": "1.7.2",
-    "dotenv": "16.4.5"
+    "axios": "1.7.2"
   }
-}
\ No newline at end of file
+}
diff --git a/scripts/codegen.ts b/scripts/codegen.ts
index a13e0a8f2a64cf62c0b79c91684bcedbb0ca6544..8113db3a6e6444b0223cd6a61a4487336bd6f491 100644
--- a/scripts/codegen.ts
+++ b/scripts/codegen.ts
@@ -1,48 +1,104 @@
-import type { CodegenConfig } from '@graphql-codegen/cli';
-const path = require('path');
-
-const currentDirectory = process.cwd();
-
-require('dotenv').config({ path: path.join(currentDirectory, 'scripts', '.env')});
-
-const config: CodegenConfig = {
-  overwrite: true,
-  schema: {
-    // URL of the GraphQL endpoint
-    "https://graphql.api.dailymotion.com": {
-      // Headers to be sent with the request
-      headers: {
-        // Authorization header with the token from environment variables
-        'Authorization': `Bearer ${process.env.GRAPHQL_ACCESS_TOKEN}`,
-        'Content-Type': 'application/json',
-        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
-        'Accept': '*/*',
-        'Accept-Language': 'en-GB,en;q=0.5',
-        'Accept-Encoding': 'gzip, deflate, br, zstd',
-        'Origin': 'https://www.dailymotion.com',
-        'DNT': '1',
-        'Sec-GPC': '1',
-        'Connection': 'keep-alive',
-        'Sec-Fetch-Dest': 'empty',
-        'Sec-Fetch-Mode': 'cors',
-        'Sec-Fetch-Site': 'same-site',
-        'Priority': 'u=4',
-        'Cache-Control': 'no-cache'
-      },
+const axios = require("axios").default;
+
+const client_id = 'f1a362d288c1b98099c7';
+const client_secret = 'eea605b96e01c796ff369935357eca920c5da4c5';
+const grant_type = 'client_credentials';
+
+// Function to fetch and save the token
+async function fetchAndSaveToken() {
+  const options = {
+    method: 'POST',
+    url: 'https://graphql.api.dailymotion.com/oauth/token',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
+      Accept: '*/*',
+      'Accept-Language': 'en-GB,en;q=0.5',
+      'Accept-Encoding': 'gzip, deflate, br, zstd',
+      Origin: 'https://www.dailymotion.com',
+      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'
     },
-  },
-  generates: {
-    "./types/CodeGenDailymotion.d.ts": {
-      plugins: [
-        "typescript",
-        "typescript-operations",
-        "typescript-resolvers",
-      ],
+    data: {
+      client_id,
+      client_secret,
+      grant_type
+    }
+  };
+
+  try {
+    const response = await axios.request(options);
+    const token = response.data.access_token;
+    console.log('Token fetched ');
+    return token;
+  } catch (error) {
+    console.error('Error fetching the token:', error);
+    throw error;
+  }
+}
+
+// Main function to setup GraphQL Codegen config
+async function setupCodegenConfig() {
+  
+  const token = await fetchAndSaveToken();
+
+  const config = {
+    overwrite: true,
+    schema: {
+      // URL of the GraphQL endpoint
+      "https://graphql.api.dailymotion.com": {
+        // Headers to be sent with the request
+        headers: {
+          // Authorization header with the token
+          'Authorization': `Bearer ${token}`,
+          'Content-Type': 'application/json',
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
+          'Accept': '*/*',
+          'Accept-Language': 'en-GB,en;q=0.5',
+          'Accept-Encoding': 'gzip, deflate, br, zstd',
+          'Origin': 'https://www.dailymotion.com',
+          'DNT': '1',
+          'Sec-GPC': '1',
+          'Connection': 'keep-alive',
+          'Sec-Fetch-Dest': 'empty',
+          'Sec-Fetch-Mode': 'cors',
+          'Sec-Fetch-Site': 'same-site',
+          'Priority': 'u=4',
+          'Cache-Control': 'no-cache'
+        },
+      },
     },
-    "./types/CodeGenDailymotion.schema.json": {
-      plugins: ["introspection"],
+    generates: {
+      "./types/CodeGenDailymotion.d.ts": {
+        plugins: [
+          "typescript",
+          "typescript-operations",
+          "typescript-resolvers",
+        ],
+      },
+      "./types/CodeGenDailymotion.schema.json": {
+        plugins: ["introspection"],
+      },
     },
-  },
-};
+  };
+
+  return config;
+}
+
+export default new Promise((resolve, reject) => {
+  setupCodegenConfig()
+    .then(config => {
+      resolve(config);
+    }).catch(error => {
+      console.error('Failed to setup GraphQL Codegen config:', error);
+      reject(error);
+    });
 
-export default config;
+})
diff --git a/scripts/get-token.js b/scripts/get-token.js
deleted file mode 100644
index 97c0cf44a99cb5f046cc84af5acc8b3fb52437be..0000000000000000000000000000000000000000
--- a/scripts/get-token.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const axios = require("axios").default;
-const fs = require('fs');
-const path = require("path");
-
-const options = {
-    method: 'POST',
-    url: 'https://graphql.api.dailymotion.com/oauth/token',
-    headers: {
-        cookie: 'dmvk=664c425a1edc4; ts=351870; v1st=15af1984-8adb-4541-9020-b5712535e57b',
-        'Content-Type': 'application/x-www-form-urlencoded',
-        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
-        Accept: '*/*',
-        'Accept-Language': 'en-GB,en;q=0.5',
-        'Accept-Encoding': 'gzip, deflate, br, zstd',
-        Origin: 'https://www.dailymotion.com',
-        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'
-    },
-    data: {
-        client_id: 'f1a362d288c1b98099c7',
-        client_secret: 'eea605b96e01c796ff369935357eca920c5da4c5',
-        grant_type: 'client_credentials'
-    }
-};
-
-axios.request(options).then(function (response) {
-    const token = response.data.access_token;
-    const filepath = path.join('./scripts', '.env');
-    fs.writeFileSync(filepath, `GRAPHQL_ACCESS_TOKEN=${token}\n`);
-}).catch(function (error) {
-    console.error(error);
-});
\ No newline at end of file
diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts
index 9ddc3d6571c6624d176fefb4b5e003e890b28fe9..369cd9885499a540f251812cc6714cf85a2076c8 100644
--- a/src/DailymotionScript.ts
+++ b/src/DailymotionScript.ts
@@ -1,9 +1,14 @@
 let config: Config;
 let _settings: IDailymotionPluginSettings;
 
-const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST_ID";
-const FAVORITES_PLAYLIST_ID = "FAVORITES_PLAYLIST_ID";
-const RECENTLY_WATCHED_PLAYLIST_ID = "RECENTLY_WATCHED_PLAYLIST_ID";
+const state = {
+	anonymousUserAuthorizationToken: "",
+	anonymousUserAuthorizationTokenExpirationDate: 0
+};
+
+const LIKE_PLAYLIST_ID = "LIKE_PLAYLIST";
+const FAVORITES_PLAYLIST_ID = "FAVORITES_PLAYLIST";
+const RECENTLY_WATCHED_PLAYLIST_ID = "RECENTLY_WATCHED_PLAYLIST";
 
 
 import {
@@ -23,7 +28,10 @@ import {
 	ERROR_TYPES,
 	LikedMediaSort,
 	VIDEOS_PER_PAGE_OPTIONS,
-	PLAYLISTS_PER_PAGE_OPTIONS
+	PLAYLISTS_PER_PAGE_OPTIONS,
+	CLIENT_ID,
+	CLIENT_SECRET,
+	BASE_URL_API_AUTH
 } from './constants';
 
 import {
@@ -38,19 +46,19 @@ import {
 	SEARCH_CHANNEL,
 	CHANNEL_PLAYLISTS_QUERY,
 	SUBSCRIPTIONS_QUERY,
-	GET_CHANNEL_PLAYLISTS_XID
+	GET_CHANNEL_PLAYLISTS_XID,
+	USER_LIKED_VIDEOS_QUERY,
+	USER_WATCHED_VIDEOS_QUERY,
+	USER_WATCH_LATER_VIDEOS_QUERY
 } from './gqlQueries';
 
 import {
 	getChannelNameFromUrl,
 	isUsernameUrl,
-	executeGqlQuery,
 	getPreferredCountry,
-	getAnonymousUserTokenSingleton,
 	getQuery,
-	getLikePlaylist,
-	getFavoritesPlaylist,
-	getRecentlyWatchedPlaylist
+	objectToUrlEncodedString,
+	generateUUIDv4
 } from './util';
 
 import {
@@ -60,7 +68,9 @@ import {
 	Live,
 	LiveConnection,
 	LiveEdge,
+	Maybe,
 	SuggestionConnection,
+	User,
 	Video,
 	VideoConnection,
 	VideoEdge
@@ -86,6 +96,7 @@ import {
 
 
 let httpClientAnonymous: IHttp = http.newClient(false);
+let httpClientRequestToken: IHttp = http.newClient(false);
 
 
 // Will be used to store private playlists that require authentication
@@ -114,13 +125,76 @@ 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;
+
+				if (!isTokenValid()) {
+					log("Token expired. Fetching a new one.");
+				} else {
+					didSaveState = true;
+					log("Using save state");
+				}
+			}
+		}
+	} catch (ex) {
+		log("Failed to parse saveState:" + ex);
+		didSaveState = false;
+	}
 
-source.getHome = function () {
+	if (!didSaveState) {
+
+		log("Getting a new token");
+
+		const body = objectToUrlEncodedString({
+			client_id: CLIENT_ID,
+			client_secret: CLIENT_SECRET,
+			grant_type: 'client_credentials'
+		});
+
+		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);
+
+		if (res.code !== 200) {
+			console.error('Failed to get token', res);
+			throw new ScriptException("", "Failed to get token: " + res.code + " - " + res.body);
+		}
 
-	getAnonymousUserTokenSingleton();
+		const json = JSON.parse(res.body);
 
+		if (!json.token_type || !json.access_token) {
+			console.error('Invalid token response', res);
+			throw new ScriptException("", 'Invalid token response: ' + res.body);
+		}
+
+		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}`);
+	}
+
+}
+
+
+source.getHome = function () {
 	return getVideoPager({}, 0);
 };
 
@@ -219,6 +293,13 @@ source.getContentDetails = function (url) {
 	return getSavedVideo(url, false);
 };
 
+source.saveState = () => {
+	return JSON.stringify({
+		anonymousUserAuthorizationToken: state.anonymousUserAuthorizationToken,
+		anonymousUserAuthorizationTokenExpirationDate: state.anonymousUserAuthorizationTokenExpirationDate
+	});
+};
+
 //Playlist
 source.isPlaylistUrl = (url): boolean => {
 	return url.startsWith(BASE_URL_PLAYLIST) ||
@@ -279,7 +360,7 @@ source.getPlaylist = (url: string): PlatformPlaylistDetails => {
 source.getUserSubscriptions = (): string[] => {
 
 	if (!bridge.isLoggedIn()) {
-		bridge.log("Failed to retrieve subscriptions page because not logged in.");
+		log("Failed to retrieve subscriptions page because not logged in.");
 		throw new ScriptException("Not logged in");
 	}
 
@@ -351,7 +432,7 @@ source.getUserSubscriptions = (): string[] => {
 source.getUserPlaylists = (): string[] => {
 
 	if (!bridge.isLoggedIn()) {
-		bridge.log("Failed to retrieve subscriptions page because not logged in.");
+		log("Failed to retrieve subscriptions page because not logged in.");
 		throw new ScriptException("Not logged in");
 	}
 
@@ -567,7 +648,7 @@ function getChannelContentsPager(url, page, type, order, filters) {
 	const shouldLoadLives = type === Type.Feed.Mixed || type === Type.Feed.Streams || type === Type.Feed.Live;
 
 	if (IS_TESTING) {
-		bridge.log(`Getting channel contents for ${url}, page: ${page}, type: ${type}, order: ${order}, shouldLoadVideos: ${shouldLoadVideos}, shouldLoadLives: ${shouldLoadLives}, filters: ${JSON.stringify(filters)}`);
+		log(`Getting channel contents for ${url}, page: ${page}, type: ${type}, order: ${order}, shouldLoadVideos: ${shouldLoadVideos}, shouldLoadLives: ${shouldLoadLives}, filters: ${JSON.stringify(filters)}`);
 	}
 
 	/** 
@@ -745,7 +826,7 @@ function getSavedVideo(url, usePlatformAuth = false) {
 	};
 
 	if (!usePlatformAuth) {
-		videoDetailsRequestHeaders.Authorization = getAnonymousUserTokenSingleton();
+		videoDetailsRequestHeaders.Authorization = state.anonymousUserAuthorizationToken;
 	}
 
 	const variables = {
@@ -878,6 +959,184 @@ function getChannelPlaylists(url: string, page: number = 1): SearchPlaylistPager
 	return new ChannelPlaylistPager(content, hasMore, params, page, getChannelPlaylists);
 }
 
+function isTokenValid() {
+    const currentTime = Date.now();
+    return state.anonymousUserAuthorizationTokenExpirationDate > currentTime;
+}
+
+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 = state.anonymousUserAuthorizationToken;
+	}
+
+	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;
+}
+
+
+function getPages<TI, TO>(
+	httpClient: IHttp,
+	query: string,
+	operationName: string,
+	variables: any,
+	usePlatformAuth: boolean,
+	setRoot: (jsonResponse: any) => TI,
+	hasNextCallback: (page: TI) => boolean,
+	getNextPage: (page: TI, currentPage) => number,
+	map: (page: any) => TO[]
+
+): TO[] {
+
+	let all: TO[] = [];
+
+	if (!hasNextCallback) {
+		hasNextCallback = () => false;
+	}
+
+	let hasNext = true;
+	let nextPage = 1;
+
+	do {
+
+		variables = { ...variables, page: nextPage };
+
+		const jsonResponse = executeGqlQuery(
+			httpClient,
+			{
+				operationName,
+				variables,
+				query,
+				usePlatformAuth
+			});
+
+		const root = setRoot(jsonResponse);
+
+		nextPage = getNextPage(root, nextPage);
+
+		const items = map(root);
+
+		hasNext = hasNextCallback(root);
+
+		all = all.concat(items);
+
+	} while (hasNext);
+
+	return all;
+}
+
+function getLikePlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
+	return getPlatformSystemPlaylist({
+		pluginId,
+		httpClient,
+		query: USER_LIKED_VIDEOS_QUERY,
+		operationName: 'USER_LIKED_VIDEOS_QUERY',
+		rootObject: 'likedMedias',
+		playlistName: 'Liked Videos',
+		usePlatformAuth,
+		thumbnailResolutionIndex
+	});
+
+}
+
+function getFavoritesPlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
+	return getPlatformSystemPlaylist({
+		pluginId,
+		httpClient,
+		query: USER_WATCH_LATER_VIDEOS_QUERY,
+		operationName: 'USER_WATCH_LATER_VIDEOS_QUERY',
+		rootObject: 'watchLaterMedias',
+		playlistName: 'Favorites',
+		usePlatformAuth,
+		thumbnailResolutionIndex
+	})
+}
+
+function getRecentlyWatchedPlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
+	return getPlatformSystemPlaylist({
+		pluginId,
+		httpClient,
+		query: USER_WATCHED_VIDEOS_QUERY,
+		operationName: 'USER_WATCHED_VIDEOS_QUERY',
+		rootObject: 'watchedVideos',
+		playlistName: 'Recently Watched',
+		usePlatformAuth,
+		thumbnailResolutionIndex
+
+	});
+}
+
+function getPlatformSystemPlaylist(opts: IPlatformSystemPlaylist): PlatformPlaylistDetails {
+
+	const videos: PlatformVideo[] = getPages<Maybe<User>, PlatformVideo>(
+		opts.httpClient,
+		opts.query,
+		opts.operationName,
+		{
+			page: 1,
+			thumbnail_resolution: THUMBNAIL_HEIGHT[opts.thumbnailResolutionIndex]
+		},
+		opts.usePlatformAuth,
+		(jsonResponse) => jsonResponse?.data?.me,//set root
+		(me) => (me?.[opts.rootObject]?.edges.length ?? 0) > 0 ?? false,//hasNextCallback
+		(me, currentPage) => ++currentPage, //getNextPage
+		(me) => me?.[opts.rootObject]?.edges.map(edge => {
+			return SourceVideoToGrayjayVideo(opts.pluginId, edge.node as Video);
+		}));
+
+	const collection = {
+		"id": generateUUIDv4(),
+		"name": opts.playlistName,
+		"creator": {}
+	}
+
+	return SourceCollectionToGrayjayPlaylistDetails(opts.pluginId, collection as Collection, videos);
+}
+
+
 function getHttpContext(opts: { usePlatformAuth: boolean } = { usePlatformAuth: false }): IHttp {
 	return opts.usePlatformAuth ? http : httpClientAnonymous;
 }
diff --git a/src/util.ts b/src/util.ts
index 9a53b1504552fc94f22a0b35e91c606ce492c159..890ec20b83b3fe0fb8cc7dd662d420ac0740bff0 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,22 +1,9 @@
-let AUTHORIZATION_TOKEN_ANONYMOUS_USER: string = "";
-let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE: number;
-let httpClientRequestToken: IHttp = http.newClient(false);
-
-import { Collection, Maybe, User, Video } from '../types/CodeGenDailymotion';
-import { SourceCollectionToGrayjayPlaylistDetails, SourceVideoToGrayjayVideo } from './Mappers';
 import {
     BASE_URL,
-    USER_AGENT,
-    BASE_URL_API,
     COUNTRY_NAMES,
     COUNTRY_NAMES_TO_CODE,
-    CLIENT_ID,
-    CLIENT_SECRET,
-    BASE_URL_API_AUTH,
-    DURATION_THRESHOLDS,
-    THUMBNAIL_HEIGHT,
+    DURATION_THRESHOLDS
 } from './constants'
-import { USER_WATCH_LATER_VIDEOS_QUERY, USER_LIKED_VIDEOS_QUERY, USER_WATCHED_VIDEOS_QUERY } from './gqlQueries';
 
 export function getPreferredCountry(preferredCountryIndex) {
     const countryName = COUNTRY_NAMES[preferredCountryIndex];
@@ -53,120 +40,6 @@ export function isUsernameUrl(url) {
     return regex.test(url);
 }
 
-
-// TODO: save to state
-export function getAnonymousUserTokenSingleton() {
-    // Check if the anonymous user token is available and not expired
-    if (AUTHORIZATION_TOKEN_ANONYMOUS_USER) {
-
-        const isTokenValid = AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE && new Date().getTime() < AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE;
-
-        if (isTokenValid) {
-            return AUTHORIZATION_TOKEN_ANONYMOUS_USER;
-        }
-    }
-
-    // Prepare the request body for obtaining a new token
-    const body = objectToUrlEncodedString({
-        client_id: CLIENT_ID,
-        client_secret: CLIENT_SECRET,
-        grant_type: 'client_credentials'
-    });
-
-    // Make the HTTP POST request to the authorization API
-    const res = httpClientRequestToken.POST(`${BASE_URL_API_AUTH}`, body, {
-        'User-Agent': USER_AGENT,
-        'Content-Type': 'application/x-www-form-urlencoded',
-        'Origin': BASE_URL,
-        'DNT': '1',
-        'Sec-GPC': '1',
-        'Connection': 'keep-alive',
-        'Sec-Fetch-Dest': 'empty',
-        'Sec-Fetch-Mode': 'cors',
-        'Sec-Fetch-Site': 'same-site',
-        'Priority': 'u=4',
-        'Pragma': 'no-cache',
-        'Cache-Control': 'no-cache'
-    }, false);
-
-    // Check if the response code indicates success
-    if (res.code !== 200) {
-        console.error('Failed to get token', res);
-        throw new ScriptException("", "Failed to get token: " + res.code + " - " + res.body);
-    }
-
-    // Parse the response JSON to extract the token information
-    const json = JSON.parse(res.body);
-
-    // Ensure the response contains the necessary token information
-    if (!json.token_type || !json.access_token) {
-        console.error('Invalid token response', res);
-        throw new ScriptException("", 'Invalid token response: ' + res.body);
-    }
-
-    // Store the token and its expiration date
-    AUTHORIZATION_TOKEN_ANONYMOUS_USER = `${json.token_type} ${json.access_token}`;
-    AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE = new Date().getTime() + (json.expires_in * 1000);
-
-    return AUTHORIZATION_TOKEN_ANONYMOUS_USER;
-}
-
-
-export function executeGqlQuery(httpClient, requestOptions) {
-
-    const headersToAdd = requestOptions.headers || {
-        "User-Agent": USER_AGENT,
-        "Accept": "*/*",
-        // "Accept-Language": Accept_Language,
-        "Referer": BASE_URL,
-        "Origin": BASE_URL,
-        "DNT": "1",
-        "Connection": "keep-alive",
-        "Sec-Fetch-Dest": "empty",
-        "Sec-Fetch-Mode": "cors",
-        "Sec-Fetch-Site": "same-site",
-        "Pragma": "no-cache",
-        "Cache-Control": "no-cache"
-    }
-
-
-    const gql = JSON.stringify({
-        operationName: requestOptions.operationName,
-        variables: requestOptions.variables,
-        query: requestOptions.query,
-    });
-
-    const usePlatformAuth = requestOptions.usePlatformAuth == undefined ? false : requestOptions.usePlatformAuth;
-    const throwOnError = requestOptions.throwOnError == undefined ? true : requestOptions.throwOnError;
-
-    if (!usePlatformAuth) {
-        headersToAdd.Authorization = getAnonymousUserTokenSingleton();
-    }
-
-    const res = httpClient.POST(BASE_URL_API, gql, headersToAdd, usePlatformAuth);
-
-    if (!res.isOk) {
-        console.error('Failed to get token', res);
-        if (throwOnError) {
-            throw new ScriptException("Failed to get token", res);
-        }
-    }
-
-    const body = JSON.parse(res.body);
-
-    // some errors may be returned in the body with a status code 200
-    if (body.errors) {
-        const message = body.errors.map(e => e.message).join(', ');
-
-        if (throwOnError) {
-            throw new UnavailableException(message);
-        }
-    }
-
-    return body;
-}
-
-
 export const parseUploadDateFilter = (filter) => {
     let createdAfterVideos;
 
@@ -250,124 +123,4 @@ export function generateUUIDv4() {
         const v = c === 'x' ? r : (r & 0x3 | 0x8);
         return v.toString(16);
     });
-}
-
-
-export function getPages<TI, TO>(
-    httpClient: IHttp,
-    query: string,
-    operationName: string,
-    variables: any,
-    usePlatformAuth: boolean,
-    setRoot: (jsonResponse: any) => TI,
-    hasNextCallback: (page: TI) => boolean,
-    getNextPage: (page: TI, currentPage) => number,
-    map: (page: any) => TO[]
-
-): TO[] {
-
-    let all: TO[] = [];
-
-    if (!hasNextCallback) {
-        hasNextCallback = () => false;
-    }
-
-    let hasNext = true;
-    let nextPage = 1;
-
-    do {
-
-        variables = { ...variables, page: nextPage };
-
-        const jsonResponse = executeGqlQuery(
-            httpClient,
-            {
-                operationName,
-                variables,
-                query,
-                usePlatformAuth
-            });
-
-        const root = setRoot(jsonResponse);
-
-        nextPage = getNextPage(root, nextPage);
-
-        const items = map(root);
-
-        hasNext = hasNextCallback(root);
-
-        all = all.concat(items);
-
-    } while (hasNext);
-
-    return all;
-}
-
-
-export function getLikePlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_LIKED_VIDEOS_QUERY,
-        operationName: 'USER_LIKED_VIDEOS_QUERY',
-        rootObject: 'likedMedias',
-        playlistName: 'Liked Videos',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    });
-
-}
-
-export function getFavoritesPlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_WATCH_LATER_VIDEOS_QUERY,
-        operationName: 'USER_WATCH_LATER_VIDEOS_QUERY',
-        rootObject: 'watchLaterMedias',
-        playlistName: 'Favorites',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    })
-}
-
-export function getRecentlyWatchedPlaylist(pluginId: string, httpClient: IHttp, usePlatformAuth: boolean = false, thumbnailResolutionIndex: number = 0): PlatformPlaylistDetails {
-    return getPlatformSystemPlaylist({
-        pluginId,
-        httpClient,
-        query: USER_WATCHED_VIDEOS_QUERY,
-        operationName: 'USER_WATCHED_VIDEOS_QUERY',
-        rootObject: 'watchedVideos',
-        playlistName: 'Recently Watched',
-        usePlatformAuth,
-        thumbnailResolutionIndex
-    
-    });
-}
-
-export function getPlatformSystemPlaylist(opts: IPlatformSystemPlaylist): PlatformPlaylistDetails {
-
-    const videos: PlatformVideo[] = getPages<Maybe<User>, PlatformVideo>(
-        opts.httpClient,
-        opts.query,
-        opts.operationName,
-        {
-            page: 1,
-            thumbnail_resolution: THUMBNAIL_HEIGHT[opts.thumbnailResolutionIndex]
-        },
-        opts.usePlatformAuth,
-        (jsonResponse) => jsonResponse?.data?.me,//set root
-        (me) => (me?.[opts.rootObject]?.edges.length ?? 0) > 0 ?? false,//hasNextCallback
-        (me, currentPage) => ++currentPage, //getNextPage
-        (me) => me?.[opts.rootObject]?.edges.map(edge => {
-            return SourceVideoToGrayjayVideo(opts.pluginId, edge.node as Video);
-        }));
-
-    const collection = {
-        "id": generateUUIDv4(),
-        "name": opts.playlistName,
-        "creator": {}
-    }
-
-    return SourceCollectionToGrayjayPlaylistDetails(opts.pluginId, collection as Collection, videos);
-}
+}
\ No newline at end of file
diff --git a/types/CodeGenDailymotion.d.ts b/types/CodeGenDailymotion.d.ts
index 83782e5f1ea2d0a1455fa7c35f0743c69e8dfbc5..2c4835c17b1d82147560e53ab4d3feaf830b4fa1 100644
--- a/types/CodeGenDailymotion.d.ts
+++ b/types/CodeGenDailymotion.d.ts
@@ -1772,7 +1772,9 @@ export type CreateCommentInput = {
   /** The ID generated for the client performing the mutation. */
   clientMutationId?: InputMaybe<Scalars['String']['input']>;
   /** The ID of the post that the comment is created for. */
-  postId: Scalars['ID']['input'];
+  postId?: InputMaybe<Scalars['ID']['input']>;
+  /** The ID of the story that the comment is created for. */
+  storyId?: InputMaybe<Scalars['ID']['input']>;
   /** The text on the comment. */
   text: Scalars['String']['input'];
 };
@@ -2737,11 +2739,11 @@ export type IdOperator = {
   /** Short for equal, must match the given data exactly. */
   eq?: InputMaybe<Scalars['ID']['input']>;
   /** Short for in array, must NOT be an element of the array. */
-  in?: InputMaybe<Array<InputMaybe<Scalars['ID']['input']>>>;
+  in?: InputMaybe<Array<Scalars['ID']['input']>>;
   /** Short for not equal, must be different from the given data. */
   ne?: InputMaybe<Scalars['ID']['input']>;
   /** Short for not in array, must be an element of the array. */
-  nin?: InputMaybe<Array<InputMaybe<Scalars['ID']['input']>>>;
+  nin?: InputMaybe<Array<Scalars['ID']['input']>>;
 };
 
 /** Information of an Image. */
@@ -5077,7 +5079,7 @@ export type PostOperator = {
   /** Short for equal, must match the given data exactly. */
   eq?: InputMaybe<PostTypename>;
   /** Short for in array, must be an element of the array. */
-  in?: InputMaybe<Array<InputMaybe<PostTypename>>>;
+  in?: InputMaybe<Array<PostTypename>>;
 };
 
 /** The possible values for a PostStatus. */
diff --git a/types/CodeGenDailymotion.schema.json b/types/CodeGenDailymotion.schema.json
index 18ba2e7f5589fc1ea09f6f6541681b7a27310ce1..e22caed6225fe8f9f6001c4fe563a743c0c9fca5 100644
--- a/types/CodeGenDailymotion.schema.json
+++ b/types/CodeGenDailymotion.schema.json
@@ -8626,13 +8626,21 @@
             "name": "postId",
             "description": "The ID of the post that the comment is created for.",
             "type": {
-              "kind": "NON_NULL",
-              "name": null,
-              "ofType": {
-                "kind": "SCALAR",
-                "name": "ID",
-                "ofType": null
-              }
+              "kind": "SCALAR",
+              "name": "ID",
+              "ofType": null
+            },
+            "defaultValue": null,
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "storyId",
+            "description": "The ID of the story that the comment is created for.",
+            "type": {
+              "kind": "SCALAR",
+              "name": "ID",
+              "ofType": null
             },
             "defaultValue": null,
             "isDeprecated": false,
@@ -13279,9 +13287,13 @@
               "kind": "LIST",
               "name": null,
               "ofType": {
-                "kind": "SCALAR",
-                "name": "ID",
-                "ofType": null
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "ID",
+                  "ofType": null
+                }
               }
             },
             "defaultValue": null,
@@ -13307,9 +13319,13 @@
               "kind": "LIST",
               "name": null,
               "ofType": {
-                "kind": "SCALAR",
-                "name": "ID",
-                "ofType": null
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "ID",
+                  "ofType": null
+                }
               }
             },
             "defaultValue": null,
@@ -23611,9 +23627,13 @@
               "kind": "LIST",
               "name": null,
               "ofType": {
-                "kind": "ENUM",
-                "name": "PostTypename",
-                "ofType": null
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "ENUM",
+                  "name": "PostTypename",
+                  "ofType": null
+                }
               }
             },
             "defaultValue": null,
diff --git a/types/plugin.d.ts b/types/plugin.d.ts
index 2b99504dea32dbeb6baba4ea29985ac637274f7d..f6c210772fd9b4be8d7d55faa6ba8f7d6c532973 100644
--- a/types/plugin.d.ts
+++ b/types/plugin.d.ts
@@ -1148,13 +1148,13 @@ function parseSettings(settings) {
     return newSettings;
 }
 
-function log(str: string) {
-    if (str) {
-        console.log(str);
-        if (typeof str == "string")
-            bridge.log(str);
+function log(obj: string | object) {
+    if (obj) {
+        console.log(obj);
+        if (typeof obj == "string")
+            bridge.log(obj);
         else
-            bridge.log(JSON.stringify(str, null, 4));
+            bridge.log(JSON.stringify(obj, null, 4));
     }
 }