From ae51a7be540150d2b48fb8bdbf3071190830ed74 Mon Sep 17 00:00:00 2001 From: Stefan Cruz <17972991+stefancruz@users.noreply.github.com> Date: Sat, 8 Jun 2024 06:09:45 +0100 Subject: [PATCH] perfom: improve load, refact requests to not do additional request to get views from video. --- build/DailymotionScript.js | 864 +++++++++++++------------------------ build/Readme.md | 4 + scripts/get-token.js | 2 - src/DailymotionScript.ts | 473 ++++---------------- src/Mappers.ts | 98 +++++ src/Pagers.ts | 1 - src/constants.ts | 50 ++- src/gqlQueries.ts | 310 ++++--------- src/util.ts | 93 +++- types/plugin.d.ts | 4 +- 10 files changed, 702 insertions(+), 1197 deletions(-) create mode 100644 src/Mappers.ts diff --git a/build/DailymotionScript.js b/build/DailymotionScript.js index ce7847d..14aada2 100644 --- a/build/DailymotionScript.js +++ b/build/DailymotionScript.js @@ -31,7 +31,7 @@ DURATION_THRESHOLDS[ONE_TO_FIVE_MINUTES] = { min: 60, max: 300 }; DURATION_THRESHOLDS[FIVE_TO_THIRTY_MINUTES] = { min: 300, max: 1800 }; DURATION_THRESHOLDS[THIRTY_TO_ONE_HOUR] = { min: 1800, max: 3600 }; DURATION_THRESHOLDS[MORE_THAN_ONE_HOUR] = { min: 3600, max: null }; -const countryNamesToCode = { +const COUNTRY_NAMES_TO_CODE = { "": "", "Afghanistan": "AF", "Aland Islands": "AX", @@ -283,7 +283,7 @@ const countryNamesToCode = { "Zambia": "ZM", "Zimbabwe": "ZW" }; -const creatorAvatarHeight = [ +const CREATOR_AVATAR_HEIGHT = [ "SQUARE_25", "SQUARE_60", "SQUARE_80", @@ -294,7 +294,7 @@ const creatorAvatarHeight = [ "SQUARE_480", "SQUARE_720" ]; -const thumbnailHeight = [ +const THUMBNAIL_HEIGHT = [ "PORTRAIT_60", "PORTRAIT_120", "PORTRAIT_180", @@ -304,8 +304,8 @@ const thumbnailHeight = [ "PORTRAIT_720", "PORTRAIT_1080" ]; -const countryNames = Object.keys(countryNamesToCode); -const errorTypes = { +const COUNTRY_NAMES = Object.keys(COUNTRY_NAMES_TO_CODE); +const ERROR_TYPES = { "DM001": "No video has been specified, you need to specify one.", "DM002": "Content has been deleted.", "DM003": "Live content is not available, i.e. it may not have started yet.", @@ -324,6 +324,42 @@ const errorTypes = { "DM016": "Content not available on this website, it can only be watched on Dailymotion", "DM019": "This content has been uploaded by an inactive channel and its access is limited" }; +const SEARCH_CAPABILITIES = { + types: [ + Type.Feed.Videos, + Type.Feed.Live + ], + sorts: [ + "Most Recent", + "Most Viewed", + "Most Relevant" + ], + filters: [ + { + id: "uploaddate", + name: "Upload Date", + isMultiSelect: false, + filters: [ + { name: "Today", value: "today" }, + { name: "Past week", value: "thisweek" }, + { name: "Past month", value: "thismonth" }, + { name: "Past year", value: "thisyear" } + ] + }, + { + id: "duration", + name: "Duration", + isMultiSelect: false, + filters: [ + { name: "< 1 min", value: LESS_THAN_MINUTE }, + { name: "1 - 5 min", value: ONE_TO_FIVE_MINUTES }, + { name: "5 - 30 min", value: FIVE_TO_THIRTY_MINUTES }, + { name: "30 min - 1 hour", value: THIRTY_TO_ONE_HOUR }, + { name: "> 1 hour", value: MORE_THAN_ONE_HOUR } + ] + } + ] +}; const SEARCH_SUGGESTIONS_QUERY = ` query AUTOCOMPLETE_QUERY($query: String!) { @@ -360,10 +396,6 @@ query CHANNEL_QUERY_DESKTOP( url } tagline - country { - id - codeAlpha2 - } metrics { engagement { followers { @@ -409,7 +441,6 @@ fragment SEARCH_DISCOVERY_VIDEO_FRAGMENT on Video { xid title isPublished - embedURL thumbnail(height:$thumbnail_resolution) { url } @@ -424,10 +455,15 @@ fragment SEARCH_DISCOVERY_VIDEO_FRAGMENT on Video { } } duration - + viewCount + stats { + views { + total + } + } } -query SEACH_DISCOVERY_QUERY($shouldQueryPromotedHashtag: Boolean!, $avatar_size: AvatarHeight!, $thumbnail_resolution: ThumbnailHeight!) { +query SEACH_DISCOVERY_QUERY($avatar_size: AvatarHeight!, $thumbnail_resolution: ThumbnailHeight!) { home: views { id neon { @@ -457,105 +493,74 @@ query SEACH_DISCOVERY_QUERY($shouldQueryPromotedHashtag: Boolean!, $avatar_size: } } } - featuredContent { - id - channels(first: 10) { - edges { - node { - id - xid - displayName - name - logoURL(size: "x120") - stats { - id - followers { - id - total - } - } - } - } - } - } - conversations( - filter: { story: { eq: HASHTAG }, algorithm: { eq: SPONSORED } } - first: 1 - ) @include(if: $shouldQueryPromotedHashtag) { - edges { - node { - id - story { - ... on Hashtag { - id - name - } - } - } - } - } } + `; const CHANNEL_VIDEOS_BY_CHANNEL_NAME = ` query CHANNEL_VIDEOS_QUERY( - $channel_name: String! - $first: Int! - $sort: String - $page: Int! - $allowExplicit: Boolean - $avatar_size: AvatarHeight! - $thumbnail_resolution: ThumbnailHeight! + $channel_name: String! + $first: Int! + $sort: String + $page: Int! + $allowExplicit: Boolean + $avatar_size: AvatarHeight! + $thumbnail_resolution: ThumbnailHeight! ) { - channel(name: $channel_name) { - id - xid - channel_videos_all_videos: videos( - sort: $sort - page: $page - first: $first - allowExplicit: $allowExplicit - ) { - pageInfo { - hasNextPage - nextPage - } - edges { - node { - id - xid - title - thumbnail(height:$thumbnail_resolution) { - url - } - bestAvailableQuality - duration - createdAt - creator { - id - name - displayName - avatar(height:$avatar_size) { - url - } - - } - metrics { - engagement { - likes { - totalCount - } - } - } + channel(name: $channel_name) { + id + xid + videos( + sort: $sort + page: $page + first: $first + allowExplicit: $allowExplicit + ) { + pageInfo { + hasNextPage + nextPage + } + edges { + node { + id + xid + title + thumbnail(height: $thumbnail_resolution) { + url + } + bestAvailableQuality + duration + createdAt + creator { + id + name + displayName + avatar(height:$avatar_size) { + url + } - } - } - } - } + } + metrics { + engagement { + likes { + totalCount + } + } + } + viewCount + stats { + views { + total + } + } + } + } + } + } } - - + + `; const MAIN_SEARCH_QUERY = ` fragment VIDEO_BASE_FRAGMENT on Video { @@ -665,38 +670,11 @@ const MAIN_SEARCH_QUERY = ` } } - fragment TOPIC_BASE_FRAG on Topic { - id - xid - name - videos(sort: "recent", first: 5) { - pageInfo { - hasNextPage - nextPage - } - edges { - node { - id - ...VIDEO_BASE_FRAGMENT - ...VIDEO_FAVORITES_FRAGMENT - } - } - } - stats { - id - videos { - id - total - } - } - } - query SEARCH_QUERY( $query: String! $shouldIncludeVideos: Boolean! $shouldIncludeChannels: Boolean! $shouldIncludePlaylists: Boolean! - $shouldIncludeTopics: Boolean! $shouldIncludeLives: Boolean! $page: Int $limit: Int @@ -796,20 +774,6 @@ const MAIN_SEARCH_QUERY = ` } } } - topics(query: $query, first: $limit, page: $page) - @include(if: $shouldIncludeTopics) { - pageInfo { - hasNextPage - nextPage - } - totalCount - edges { - node { - id - ...TOPIC_BASE_FRAG - } - } - } } } `; @@ -862,7 +826,6 @@ fragment VIDEO_FRAGMENT on Video { height width } - coverURLx375: coverURL(size: "x375") stats { id views { @@ -878,60 +841,6 @@ fragment VIDEO_FRAGMENT on Video { total } } - country { - id - codeAlpha2 - } - organization @skip(if: $isSEO) { - id - xid - owner { - id - xid - } - } - } - language { - id - codeAlpha2 - } - tags { - edges { - node { - id - label - } - } - } - moderation { - id - reviewedAt - } - topics(whitelistedOnly: true, first: 3, page: 1) { - edges { - node { - id - xid - name - names { - edges { - node { - id - name - language { - id - codeAlpha2 - } - } - } - } - } - } - } - geoblockedCountries { - id - allowed - denied } } @@ -1006,14 +915,6 @@ fragment LIVE_FRAGMENT on Live { id codeAlpha2 } - organization @skip(if: $isSEO) { - id - xid - owner { - id - xid - } - } } language { id @@ -1061,7 +962,6 @@ fragment LIVE_FRAGMENT on Live { query WATCHING_VIDEO( $xid: String! - $isSEO: Boolean! $avatar_size: AvatarHeight! $thumbnail_resolution: ThumbnailHeight! ) { @@ -1200,19 +1100,6 @@ query PLAYLIST_VIDEO_QUERY($xid: String!, $numberOfVideos: Int = 100, $avatar_si } } `; -const GET_VIDEO_EXTRA_DETAILS = ` -query WATCHING_VIDEO($xid: String!) { - video: media(xid: $xid) { - ... on Video { - stats { - views { - total - } - } - } - } -} - `; const GET_USER_SUBSCRIPTIONS = ` query SUBSCRIPTIONS_QUERY($first: Int, $page: Int) { me { @@ -1239,8 +1126,6 @@ query CHANNEL_PLAYLISTS_QUERY( $first: Int! ) { channel(name: $channel_name) { - id - xid channel_playlist_collections: collections( sort: $sort page: $page @@ -1252,26 +1137,21 @@ query CHANNEL_PLAYLISTS_QUERY( } edges { node { - id xid - updatedAt - createdAt - name - description - thumbnailx60: thumbnailURL(size: "x60") - thumbnailx120: thumbnailURL(size: "x120") - thumbnailx240: thumbnailURL(size: "x240") - thumbnailx720: thumbnailURL(size: "x720") - stats { - id - videos { - id - total + } } } } } +`; +const SUBSCRIPTIONS_QUERY = ` +query SUBSCRIPTIONS_QUERY { + me { + xid + channel { + name + } } } `; @@ -1280,8 +1160,8 @@ let AUTHORIZATION_TOKEN_ANONYMOUS_USER = ""; let AUTHORIZATION_TOKEN_ANONYMOUS_USER_EXPIRATION_DATE; let httpClientRequestToken = http.newClient(false); function getPreferredCountry(preferredCountryIndex) { - const countryName = countryNames[preferredCountryIndex]; - const code = countryNamesToCode[countryName]; + const countryName = COUNTRY_NAMES[preferredCountryIndex]; + const code = COUNTRY_NAMES_TO_CODE[countryName]; const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); return preferredCountry; } @@ -1301,7 +1181,7 @@ function getChannelNameFromUrl(url) { return channel_name; } function isUsernameUrl(url) { - var regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); + const regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); return regex.test(url); } function getAnonymousUserTokenSingleton() { @@ -1419,6 +1299,72 @@ const convertSRTtoVTT = (srt) => { // Join the VTT array into a single string and return it return vtt.join(''); }; +const parseUploadDateFilter = (filter) => { + let createdAfterVideos; + const now = new Date(); + switch (filter) { + case "today": + // Last 24 hours from now + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + createdAfterVideos = yesterday.toISOString(); + break; + case "thisweek": + // Adjusts to the start of the current week (assuming week starts on Sunday) + const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); + createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); + break; + case "thismonth": + // Adjusts to the start of the month + createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); + break; + case "thisyear": + // Adjusts to the start of the year + createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); + break; + default: + createdAfterVideos = null; + } + return createdAfterVideos; +}; +const parseSort = (order) => { + let sort; + switch (order) { + //TODO: refact this to use constants + case "Most Recent": + sort = "RECENT"; + break; + case "Most Viewed": + sort = "VIEW_COUNT"; + break; + case "Most Relevant": + sort = "RELEVANCE"; + break; + default: + sort = order; // Default to the original order if no match + } + return sort; +}; +const getQuery = (context) => { + context.sort = parseSort(context.order); + if (!context.filters) { + context.filters = {}; + } + if (!context.page) { + context.page = 1; + } + if (context?.filters.duration) { + context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; + context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; + } + else { + context.filters.durationMinVideos = null; + context.filters.durationMaxVideos = null; + } + if (context.filters.uploaddate) { + context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); + } + return context; +}; class SearchPagerAll extends VideoPager { /** @@ -1485,13 +1431,78 @@ class SearchPlaylistPager extends VideoPager { page: this.context.page, filters: this.context.params.filters }; - // return searchPlaylists(opts); return this.cb(opts); } } -var config; -var _settings; +const SourceChannelToGrayjayChannel = (pluginId, url, sourceChannel) => { + const externalLinks = sourceChannel?.externalLinks ?? {}; + const links = {}; + Object + .keys(externalLinks) + .forEach(key => { + if (externalLinks[key]) { + links[key.replace('URL', '')] = externalLinks[key]; + } + }); + 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 SourceVideoToGrayjayVideo = (pluginId, sourceVideo) => { + // const metadata = GetVideoExtraDetails(anonymousHttpClient, sv.xid); + // const viewCount = metadata.views ?? 0; + const isLive = sourceVideo?.isOnAir == true; + const viewCount = isLive ? (sourceVideo?.audienceCount ?? 0) : (sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0); + // const url = sourceVideo?.url ?? `${BASE_URL_VIDEO}/${sourceVideo?.xid}`; + 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: new PlatformAuthorLink(new PlatformID(PLATFORM, sourceVideo?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), sourceVideo?.creator?.displayName ?? "", `${BASE_URL}/${sourceVideo?.creator?.name}`, sourceVideo?.creator?.avatar?.url ?? "", 0), + uploadDate: parseInt(new Date(sourceVideo.createdAt).getTime() / 1000), + datetime: parseInt(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: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`, + id: new PlatformID(PLATFORM, sourceCollection?.xid, pluginId, PLATFORM_CLAIMTYPE), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, sourceCollection?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), sourceCollection?.creator?.displayName ?? "", `${BASE_URL}/${sourceCollection?.creator?.name}`, sourceCollection?.creator?.avatar?.url ?? "", 0), + name: sourceCollection.name, + thumbnail: sourceCollection?.thumbnail?.url, + videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total, + 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: new PlatformAuthorLink(new PlatformID(PLATFORM, sourceCollection?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), sourceCollection?.creator?.displayName ?? "", `${BASE_URL}/${sourceCollection?.creator?.name}`, sourceCollection?.creator?.avatar?.url ?? "", 0), + name: sourceCollection?.name, + thumbnail: sourceCollection?.thumbnail?.url, + videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total, + }); +}; + +let config; +let _settings; let httpClientAnonymous = http.newClient(false); // Will be used to store playlists that require authentication const authenticatedPlaylistCollection = []; @@ -1519,13 +1530,12 @@ source.getHome = function () { return getVideoPager({}, 0); }; source.searchSuggestions = function (query) { - const variables = { - "query": query - }; try { const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'AUTOCOMPLETE_QUERY', - variables: variables, + variables: { + query + }, query: SEARCH_SUGGESTIONS_QUERY }); return jsonResponse?.data?.search?.suggestedVideos?.edges?.map(edge => edge?.node?.name ?? "") ?? []; @@ -1535,45 +1545,7 @@ source.searchSuggestions = function (query) { return []; } }; -source.getSearchCapabilities = () => { - //TODO: refact this to use more constants - return { - types: [ - Type.Feed.Videos, - Type.Feed.Live - ], - sorts: [ - "Most Recent", - "Most Viewed", - "Most Relevant" - ], - filters: [ - { - id: "uploaddate", - name: "Upload Date", - isMultiSelect: false, - filters: [ - { name: "Today", value: "today" }, - { name: "Past week", value: "thisweek" }, - { name: "Past month", value: "thismonth" }, - { name: "Past year", value: "thisyear" } - ] - }, - { - id: "duration", - name: "Duration", - isMultiSelect: false, - filters: [ - { name: "< 1 min", value: LESS_THAN_MINUTE }, - { name: "1 - 5 min", value: ONE_TO_FIVE_MINUTES }, - { name: "5 - 30 min", value: FIVE_TO_THIRTY_MINUTES }, - { name: "30 min - 1 hour", value: THIRTY_TO_ONE_HOUR }, - { name: "> 1 hour", value: MORE_THAN_ONE_HOUR } - ] - } - ] - }; -}; +source.getSearchCapabilities = () => SEARCH_CAPABILITIES; source.search = function (query, type, order, filters) { return getSearchPagerAll({ q: query, page: 1, type, order, filters }); }; @@ -1589,31 +1561,12 @@ source.getChannel = function (url) { const channelDetails = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'CHANNEL_QUERY_DESKTOP', variables: { - channel_name: channel_name, - avatar_size: creatorAvatarHeight[_settings?.avatarSize] + channel_name, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] }, query: CHANNEL_BY_URL_QUERY }); - const channel = channelDetails.data.channel; - const externalLinks = channel?.externalLinks ?? {}; - const links = {}; - Object - .keys(externalLinks) - .forEach(key => { - if (externalLinks[key]) { - links[key.replace('URL', '')] = externalLinks[key]; - } - }); - return new PlatformChannel({ - id: new PlatformID(PLATFORM, channel?.id, config.id, PLATFORM_CLAIMTYPE), - name: channel?.displayName ?? "", - thumbnail: channel?.avatar?.url ?? "", - banner: channel.banner?.url ?? "", - subscribers: channel?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, - description: channel?.description ?? "", - url, - links, - }); + return SourceChannelToGrayjayChannel(config.id, url, channelDetails.data.channel); }; source.getChannelContents = function (url) { return getChannelPager({ url, page_size: ITEMS_PER_PAGE, page: 1 }); @@ -1636,8 +1589,8 @@ source.getPlaylist = (url) => { const xid = url.split('/').pop(); const variables = { xid, - avatar_size: creatorAvatarHeight[_settings.avatarSize], - thumbnail_resolution: thumbnailHeight[_settings.thumbnailResolution], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], }; const usePlatformAuth = authenticatedPlaylistCollection.includes(url); let jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth }), { @@ -1647,33 +1600,9 @@ source.getPlaylist = (url) => { usePlatformAuth }); const videos = jsonResponse?.data?.collection?.videos?.edges.map(edge => { - const resource = edge.node; - const opts = { - id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), - name: resource.title ?? "", - thumbnails: new Thumbnails([ - new Thumbnail(resource?.thumbnail?.url ?? "", 0) - ]), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, resource?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), resource?.creator?.displayName ?? "", `${BASE_URL}/${resource?.creator?.name}`, resource?.creator?.avatar?.url ?? "", 0), - uploadDate: parseInt(new Date(resource.createdAt).getTime() / 1000), - datetime: parseInt(new Date(resource.createdAt).getTime() / 1000), - url: resource.url ?? "", - duration: resource.duration ?? 0, - viewCount: resource?.viewCount ?? 0, - isLive: false - }; - return opts; - }); - const playlist = jsonResponse?.data?.collection; - return new PlatformPlaylistDetails({ - url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid, config.id, PLATFORM_CLAIMTYPE), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), playlist?.creator?.displayName ?? "", `${BASE_URL}/${playlist?.creator?.name}`, playlist?.creator?.avatar?.url ?? "", 0), - name: playlist.name, - thumbnail: playlist?.thumbnail?.url, - videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, - contents: new VideoPager(videos) + return SourceVideoToGrayjayVideo(config.id, edge.node); }); + return SourceCollectionToGrayjayPlaylistDetails(config.id, jsonResponse?.data?.collection, videos); }; source.getUserSubscriptions = () => { if (!bridge.isLoggedIn()) { @@ -1708,7 +1637,7 @@ source.getUserSubscriptions = () => { variables: { first: first, page: page, - avatar_size: creatorAvatarHeight[_settings?.avatarSize], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], }, headers, query: GET_USER_SUBSCRIPTIONS, @@ -1757,20 +1686,10 @@ source.getUserPlaylists = () => { Pragma: 'no-cache', 'Cache-Control': 'no-cache', }; - const userInfoQuery = ` - query SUBSCRIPTIONS_QUERY { - me { - xid - channel { - name - } - } - } - `; const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: true }), { operationName: 'SUBSCRIPTIONS_QUERY', headers, - query: userInfoQuery, + query: SUBSCRIPTIONS_QUERY, usePlatformAuth: true }); const userName = jsonResponse?.data?.me?.channel?.name; @@ -1798,27 +1717,6 @@ function getPlaylistsByUsername(userName, headers, usePlatformAuth = false) { }); return playlists; } -function getQuery(context) { - context.sort = parseSort(context.order); - if (!context.filters) { - context.filters = {}; - } - if (!context.page) { - context.page = 1; - } - if (context?.filters.duration) { - context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; - context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; - } - else { - context.filters.durationMinVideos = null; - context.filters.durationMaxVideos = null; - } - if (context.filters.uploaddate) { - context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); - } - return context; -} function searchPlaylists(contextQuery) { const context = getQuery(contextQuery); const variables = { @@ -1829,13 +1727,12 @@ function searchPlaylists(contextQuery) { "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 "shouldIncludeChannels": false, "shouldIncludePlaylists": true, - "shouldIncludeTopics": false, "shouldIncludeVideos": false, "shouldIncludeLives": false, "page": context.page, "limit": ITEMS_PER_PAGE, - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], }; const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'SEARCH_QUERY', @@ -1844,22 +1741,14 @@ function searchPlaylists(contextQuery) { headers: undefined }); const playlistConnection = jsonResponse?.data?.search?.playlists; - var searchResults = playlistConnection?.edges?.map(edge => { - const playlist = edge?.node; - return new PlatformPlaylist({ - url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid ?? "", config.id, PLATFORM_CLAIMTYPE), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), playlist?.creator?.displayName ?? "", `${BASE_URL}/${playlist?.creator?.name}`, playlist?.creator?.avatar?.url ?? "", 0), - name: playlist?.name, - thumbnail: playlist?.thumbnail?.url, - videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, - }); + const searchResults = playlistConnection?.edges?.map(edge => { + return SourceCollectionToGrayjayPlaylist(config.id, edge?.node); }); const hasMore = playlistConnection?.pageInfo?.hasNextPage; - if (!searchResults || !searchResults?.length) { + if (!searchResults || searchResults.length === 0) { return new PlaylistPager([]); } - var params = { + const params = { query: context.q, sort: context.sort, filters: context.filters, @@ -1875,7 +1764,6 @@ function getVideoPager(params, page) { params = { ...params, count }; const headersToAdd = { "User-Agent": USER_AGENT, - // "Accept-Language": Accept_Language, "Referer": BASE_URL, "Content-Type": "application/json", "X-DM-AppInfo-Id": X_DM_AppInfo_Id, @@ -1891,13 +1779,13 @@ function getVideoPager(params, page) { "Cache-Control": "no-cache" }; let obj; + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); try { - obj = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { + obj = executeGqlQuery(anonymousHttpClient, { operationName: 'SEACH_DISCOVERY_QUERY', variables: { - shouldQueryPromotedHashtag: false, - avatar_size: creatorAvatarHeight[_settings?.avatarSize], - thumbnail_resolution: thumbnailHeight[_settings?.thumbnailResolution], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], }, query: HOME_QUERY, headers: headersToAdd, @@ -1906,46 +1794,19 @@ function getVideoPager(params, page) { catch (error) { return new VideoPager([], false, { params }); } - var results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges - // ?.filter(edge => edge?.node?.__typename === 'Video') + const results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges ?.filter(edge => edge?.node?.id) ?.map(edge => { - const v = edge.node; - const metadata = GetVideoExtraDetails(v.xid); - return ToPlatformVideo({ - id: v.id, - name: v.title ?? "", - thumbnail: v.thumbnail?.url ?? "", - createdAt: v.createdAt, - creatorId: v?.creator?.id, - creatorName: v?.creator?.name, - creatorDisplayName: v.creator?.displayName, - creatorAvatar: v?.creator?.avatar?.url ?? "", - creatorUrl: `${BASE_URL}/${v.creator?.name}`, - duration: v.duration, - viewCount: metadata.views ?? 0, - url: `${BASE_URL_VIDEO}/${v.xid}`, - isLive: false, - description: v?.description ?? '', - }); + return SourceVideoToGrayjayVideo(config.id, edge.node); }); const hasMore = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.pageInfo?.hasNextPage ?? false; return new SearchPagerAll(results, hasMore, params, page, getVideoPager); } -function GetVideoExtraDetails(xid) { - const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { - operationName: 'WATCHING_VIDEO', - variables: { xid }, - query: GET_VIDEO_EXTRA_DETAILS - }); - return { - views: json?.data?.video?.stats?.views?.total - }; -} function getChannelPager(context) { const url = context.url; const channel_name = getChannelNameFromUrl(url); - const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); + const json = executeGqlQuery(anonymousHttpClient, { operationName: 'CHANNEL_VIDEOS_QUERY', variables: { "channel_name": channel_name, @@ -1953,93 +1814,19 @@ function getChannelPager(context) { "page": context.page ?? 1, "allowExplicit": !_settings.hideSensitiveContent, "first": context.page_size ?? ITEMS_PER_PAGE, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], }, query: CHANNEL_VIDEOS_BY_CHANNEL_NAME }); - const edges = json?.data?.channel?.channel_videos_all_videos?.edges ?? []; + const edges = json?.data?.channel?.videos?.edges ?? []; let videos = edges.map((edge) => { - const video = edge.node; - const metadata = GetVideoExtraDetails(video.xid); - return ToPlatformVideo({ - id: video.id, - name: video.title, - thumbnail: video?.thumbnail?.url ?? "", - createdAt: video?.createdAt, - creatorId: video?.creator?.id, - creatorDisplayName: video?.creator?.displayName, - creatorName: video?.creator?.name, - creatorAvatar: video?.creator?.avatar?.url, - creatorUrl: `${BASE_URL}/${video?.creator?.name}`, - duration: video.duration, - url: `${BASE_URL_VIDEO}/${video?.xid}`, - viewCount: metadata.views ?? 0, - isLive: false - }); + return SourceVideoToGrayjayVideo(config.id, edge.node); }); if (edges.length > 0) { context.page++; } - return new ChannelVideoPager(context, videos, json?.data?.channel?.channel_videos_all_videos?.pageInfo?.hasNextPage, getChannelPager); -} -function ToPlatformVideo(resource) { - return new PlatformVideo({ - id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), - name: resource.name, - thumbnails: new Thumbnails([new Thumbnail(resource.thumbnail, 0)]), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, resource.creatorId, config.id, PLATFORM_CLAIMTYPE), resource.creatorDisplayName, resource.creatorUrl, resource.creatorAvatar ?? "", 0), - uploadDate: parseInt(new Date(resource.createdAt).getTime() / 1000), - url: resource.url, - duration: resource.duration, - viewCount: resource.viewCount, - isLive: resource.isLive - }); -} -function parseSort(order) { - let sort; - switch (order) { - //TODO: refact this to use constants - case "Most Recent": - sort = "RECENT"; - break; - case "Most Viewed": - sort = "VIEW_COUNT"; - break; - case "Most Relevant": - sort = "RELEVANCE"; - break; - default: - sort = order; // Default to the original order if no match - } - return sort; -} -function parseUploadDateFilter(filter) { - let createdAfterVideos; - const now = new Date(); - switch (filter) { - case "today": - // Last 24 hours from now - const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); - createdAfterVideos = yesterday.toISOString(); - break; - case "thisweek": - // Adjusts to the start of the current week (assuming week starts on Sunday) - const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); - createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); - break; - case "thismonth": - // Adjusts to the start of the month - createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); - break; - case "thisyear": - // Adjusts to the start of the year - createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); - break; - default: - createdAfterVideos = null; - } - return createdAfterVideos; + return new ChannelVideoPager(context, videos, json?.data?.channel?.videos?.pageInfo?.hasNextPage, getChannelPager); } function getSearchPagerAll(contextQuery) { const context = getQuery(contextQuery); @@ -2051,13 +1838,12 @@ function getSearchPagerAll(contextQuery) { "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 "shouldIncludeChannels": false, "shouldIncludePlaylists": false, - "shouldIncludeTopics": false, "shouldIncludeVideos": true, "shouldIncludeLives": true, "page": context.page ?? 1, "limit": ITEMS_PER_PAGE, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] }; const jsonResponse = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: 'SEARCH_QUERY', @@ -2073,29 +1859,11 @@ function getSearchPagerAll(contextQuery) { ...(liveConnection?.edges ?? []) ]; for (const edge of all) { - const sv = edge?.node; - const isLive = sv?.isOnAir == true; - const viewCount = isLive ? sv?.audienceCount : sv?.stats?.views?.total; - var video = ToPlatformVideo({ - id: sv?.id, - name: sv?.title, - thumbnail: sv?.thumbnail?.url, - createdAt: sv?.createdAt, - creatorId: sv?.creator?.id, - creatorName: sv?.creator?.name, - creatorDisplayName: sv?.creator?.displayName, - creatorUrl: `${BASE_URL}/${sv?.creator?.name}`, - creatorAvatar: sv?.creator?.avatar?.url ?? "", - duration: sv?.duration, - viewCount, - url: `${BASE_URL_VIDEO}/${sv?.xid}`, - isLive, - description: sv?.description ?? '', - }); + const video = SourceVideoToGrayjayVideo(config.id, edge?.node); results.push(video); } //results, hasMore, path, params, page - var params = { + const params = { query: context.q, sort: context.sort, filters: context.filters, @@ -2105,10 +1873,9 @@ function getSearchPagerAll(contextQuery) { function getSavedVideo(url, usePlatformAuth = false) { const id = url.split('/').pop(); const player_metadata_url = `${BASE_URL_METADATA}/${id}?embedder=https%3A%2F%2Fwww.dailymotion.com%2Fvideo%2Fx8yb2e8&geo=1&player-id=xjnde&locale=en-GB&dmV1st=ce2035cd-bdca-4d7b-baa4-127a17490ca5&dmTs=747022&is_native_app=0&app=com.dailymotion.neon&client_type=webapp§ion_type=player&component_style=_`; - var headers1 = { + const headers1 = { "User-Agent": USER_AGENT, "Accept": "*/*", - // "Accept-Language": Accept_Language, // "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "https://geo.dailymotion.com/", "Origin": "https://geo.dailymotion.com", @@ -2126,23 +1893,21 @@ function getSavedVideo(url, usePlatformAuth = false) { else { headers1["Cookie"] = "ff=off"; } - var player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); + const player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); if (!player_metadataResponse.isOk) { throw new UnavailableException('Unable to get player metadata'); } - var player_metadata = JSON.parse(player_metadataResponse.body); + const player_metadata = JSON.parse(player_metadataResponse.body); if (player_metadata.error) { - if (player_metadata.error.code && errorTypes[player_metadata.error.code] !== undefined) { - throw new UnavailableException(errorTypes[player_metadata.error.code]); + if (player_metadata.error.code && ERROR_TYPES[player_metadata.error.code] !== undefined) { + throw new UnavailableException(ERROR_TYPES[player_metadata.error.code]); } throw new UnavailableException('This content is not available'); } - const hls_url = player_metadata?.qualities?.auto[0]?.url; const videoDetailsRequestHeaders = { "Content-Type": "application/json", "User-Agent": USER_AGENT, "Accept": "*/*, */*", - // "Accept-Language": Accept_Language, "Referer": `${BASE_URL_VIDEO}/${id}`, "X-DM-AppInfo-Id": X_DM_AppInfo_Id, "X-DM-AppInfo-Type": X_DM_AppInfo_Type, @@ -2164,9 +1929,8 @@ function getSavedVideo(url, usePlatformAuth = false) { } const variables = { "xid": id, - "isSEO": false, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] }; const videoDetailsRequestBody = JSON.stringify({ operationName: "WATCHING_VIDEO", @@ -2182,7 +1946,7 @@ function getSavedVideo(url, usePlatformAuth = false) { new HLSSource({ name: 'source', duration: player_metadata?.duration, - url: hls_url, + url: player_metadata?.qualities?.auto[0]?.url, // priority: true, }) ]; @@ -2211,19 +1975,20 @@ function getSavedVideo(url, usePlatformAuth = false) { negativeRatingCount += ratingTotal; } } - var platformVideoDetails = { + const platformVideoDetails = { id: new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), - name: video.title, - thumbnails: new Thumbnails([new Thumbnail(video.thumbnail.url, 0)]), - author: new PlatformAuthorLink(new PlatformID(PLATFORM, video?.creator?.id, config.id, PLATFORM_CLAIMTYPE), video?.creator?.displayName, `${BASE_URL}/${video?.creator?.name}`, `${video?.creator?.avatar?.url}`, 0 //subscribers + name: video?.title ?? "", + thumbnails: new Thumbnails([new Thumbnail(video?.thumbnail?.url ?? "", 0)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, video?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), video?.creator?.displayName ?? "", `${BASE_URL}/${video?.creator?.name}`, `${video?.creator?.avatar?.url}`, 0 //subscribers ), // datetime: new Date(video?.createdAt).getTime(), - uploadDate: parseInt(new Date(video.createdAt).getTime() / 1000), - duration: video?.duration, - viewCount: video?.stats?.views?.total, + uploadDate: parseInt(new Date(video?.createdAt).getTime() / 1000), + datetime: parseInt(new Date(video?.createdAt).getTime() / 1000), + duration: video?.duration ?? 0, + viewCount: video?.stats?.views?.total ?? 0, url: `${BASE_URL_VIDEO}/${id}`, isLive: video?.duration == undefined, - description: video?.description, + description: video?.description ?? "", video: new VideoSourceDescriptor(sources), rating: new RatingLikesDislikes(positiveRatingCount, negativeRatingCount), dash: null, @@ -2270,7 +2035,7 @@ function getSearchChannelPager(context) { query: context.q, page: context.page ?? 1, limit: ITEMS_PER_PAGE, - avatar_size: creatorAvatarHeight[_settings?.avatarSize] + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] }; const json = executeGqlQuery(getHttpContext({ usePlatformAuth: false }), { operationName: "SEARCH_QUERY", @@ -2278,19 +2043,10 @@ function getSearchChannelPager(context) { query: SEARCH_CHANNEL }); const results = json?.data?.search?.channels?.edges.map(edge => { - const c = edge.node; - return new PlatformChannel({ - id: new PlatformID(PLATFORM, c.id, config.id, PLATFORM_CLAIMTYPE), - name: c.displayName, - thumbnail: c?.avatar?.url, - subscribers: c?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, - url: `${BASE_URL}/${c.name}`, - links: [], - banner: "", - description: c.description, - }); + const channel = edge.node; + return SourceChannelToGrayjayChannel(config.id, `${BASE_URL}/${channel.name}`, channel); }); - var params = { + const params = { query: context.q, }; return new SearchChannelPager(results, json?.data?.search?.channels?.pageInfo?.hasNextPage, params, context.page, getSearchChannelPager); diff --git a/build/Readme.md b/build/Readme.md index d4324fd..6d982a5 100644 --- a/build/Readme.md +++ b/build/Readme.md @@ -5,10 +5,13 @@ Click [here](https://stefancruz.github.io/GrayjayDailymotion/index.html) to inst ## Features - [x] - Home - [x] - Home search +- [x] - Live videos from home search - [x] - Search autocomplete +- [x] - Video details (likes, dislikes, views, publish date) - [x] - Subscriptions - [x] - Channel search - [x] - Channel Details +- [x] - Downloads - [x] - Playlists search - [x] - Settings / Thumbnail resolution - [x] - Settings / Creator avatar resolution @@ -16,6 +19,7 @@ Click [here](https://stefancruz.github.io/GrayjayDailymotion/index.html) to inst - [x] - Settings / Prefered Country - [x] - Sign in (import subscriptions and playlists) - [x] - Policentric Comments +- [x] - Subtitles diff --git a/scripts/get-token.js b/scripts/get-token.js index aac7a13..97c0cf4 100644 --- a/scripts/get-token.js +++ b/scripts/get-token.js @@ -1,7 +1,6 @@ const axios = require("axios").default; const fs = require('fs'); const path = require("path"); -const currentDirectory = process.cwd(); const options = { method: 'POST', @@ -32,7 +31,6 @@ const options = { }; axios.request(options).then(function (response) { - // console.log(response.data); const token = response.data.access_token; const filepath = path.join('./scripts', '.env'); fs.writeFileSync(filepath, `GRAPHQL_ACCESS_TOKEN=${token}\n`); diff --git a/src/DailymotionScript.ts b/src/DailymotionScript.ts index 435e55e..f911ac6 100644 --- a/src/DailymotionScript.ts +++ b/src/DailymotionScript.ts @@ -1,16 +1,12 @@ -var config: Config; -var _settings: DailymotionPluginSettings; +let config: Config; +let _settings: DailymotionPluginSettings; import { - creatorAvatarHeight, - thumbnailHeight, + CREATOR_AVATAR_HEIGHT, + THUMBNAIL_HEIGHT, BASE_URL, - LESS_THAN_MINUTE, - ONE_TO_FIVE_MINUTES, - FIVE_TO_THIRTY_MINUTES, - THIRTY_TO_ONE_HOUR, - MORE_THAN_ONE_HOUR, + SEARCH_CAPABILITIES, PLATFORM, PLATFORM_CLAIMTYPE, ITEMS_PER_PAGE, @@ -21,10 +17,9 @@ import { X_DM_AppInfo_Type, X_DM_AppInfo_Version, X_DM_Neon_SSR, - DURATION_THRESHOLDS, BASE_URL_API, BASE_URL_METADATA, - errorTypes, + ERROR_TYPES, } from './constants'; import { @@ -34,11 +29,11 @@ import { GET_USER_SUBSCRIPTIONS, MAIN_SEARCH_QUERY, HOME_QUERY, - GET_VIDEO_EXTRA_DETAILS, CHANNEL_VIDEOS_BY_CHANNEL_NAME, VIDEO_DETAILS_QUERY, SEARCH_CHANNEL, - GET_CHANNEL_PLAYLISTS + GET_CHANNEL_PLAYLISTS, + SUBSCRIPTIONS_QUERY } from './gqlQueries'; import { @@ -47,7 +42,8 @@ import { executeGqlQuery, getPreferredCountry, getAnonymousUserTokenSingleton, - convertSRTtoVTT + convertSRTtoVTT, + getQuery } from './util'; import { @@ -70,6 +66,13 @@ import { } from './Pagers'; +import { + SourceChannelToGrayjayChannel, + SourceCollectionToGrayjayPlaylist, + SourceCollectionToGrayjayPlaylistDetails, + SourceVideoToGrayjayVideo +} from './Mappers'; + let httpClientAnonymous: IHttp = http.newClient(false); @@ -110,17 +113,15 @@ source.getHome = function () { source.searchSuggestions = function (query): string[] { - const variables = { - "query": query - } - try { const jsonResponse = executeGqlQuery( getHttpContext({ usePlatformAuth: false }), { operationName: 'AUTOCOMPLETE_QUERY', - variables: variables, + variables: { + query + }, query: SEARCH_SUGGESTIONS_QUERY }); @@ -132,46 +133,7 @@ source.searchSuggestions = function (query): string[] { }; -source.getSearchCapabilities = () => { - //TODO: refact this to use more constants - return { - types: [ - Type.Feed.Videos, - Type.Feed.Live - ], - sorts: [ - "Most Recent", - "Most Viewed", - "Most Relevant" - ], - filters: [ - { - id: "uploaddate", - name: "Upload Date", - isMultiSelect: false, - filters: [ - { name: "Today", value: "today" }, - { name: "Past week", value: "thisweek" }, - { name: "Past month", value: "thismonth" }, - { name: "Past year", value: "thisyear" } - ] - }, - { - id: "duration", - name: "Duration", - isMultiSelect: false, - filters: [ - { name: "< 1 min", value: LESS_THAN_MINUTE }, - { name: "1 - 5 min", value: ONE_TO_FIVE_MINUTES }, - { name: "5 - 30 min", value: FIVE_TO_THIRTY_MINUTES }, - { name: "30 min - 1 hour", value: THIRTY_TO_ONE_HOUR }, - { name: "> 1 hour", value: MORE_THAN_ONE_HOUR } - ] - } - ] - }; - -} +source.getSearchCapabilities = () => SEARCH_CAPABILITIES; source.search = function (query: string, type: string, order: string, filters) { @@ -196,36 +158,13 @@ source.getChannel = function (url) { { operationName: 'CHANNEL_QUERY_DESKTOP', variables: { - channel_name: channel_name, - avatar_size: creatorAvatarHeight[_settings?.avatarSize] + channel_name, + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] }, query: CHANNEL_BY_URL_QUERY }); - const channel: Channel = channelDetails.data.channel; - - const externalLinks = channel?.externalLinks ?? {}; - - const links = {}; - - Object - .keys(externalLinks) - .forEach(key => { - if (externalLinks[key]) { - links[key.replace('URL', '')] = externalLinks[key]; - } - }); - - return new PlatformChannel({ - id: new PlatformID(PLATFORM, channel?.id, config.id, PLATFORM_CLAIMTYPE), - name: channel?.displayName ?? "", - thumbnail: channel?.avatar?.url ?? "", - banner: channel.banner?.url ?? "", - subscribers: channel?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, - description: channel?.description ?? "", - url, - links, - }) + return SourceChannelToGrayjayChannel(config.id, url, channelDetails.data.channel as Channel); }; @@ -258,8 +197,8 @@ source.getPlaylist = (url: string): PlatformPlaylistDetails => { const variables = { xid, - avatar_size: creatorAvatarHeight[_settings.avatarSize], - thumbnail_resolution: thumbnailHeight[_settings.thumbnailResolution], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings.thumbnailResolution], } const usePlatformAuth = authenticatedPlaylistCollection.includes(url); @@ -273,49 +212,11 @@ source.getPlaylist = (url: string): PlatformPlaylistDetails => { usePlatformAuth }); - const videos = jsonResponse?.data?.collection?.videos?.edges.map(edge => { - const resource = edge.node as Video; - const opts: PlatformVideoDef = { - id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), - name: resource.title ?? "", - thumbnails: new Thumbnails([ - new Thumbnail(resource?.thumbnail?.url ?? "", 0) - ]), - author: new PlatformAuthorLink( - new PlatformID(PLATFORM, resource?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), - resource?.creator?.displayName ?? "", - `${BASE_URL}/${resource?.creator?.name}`, - resource?.creator?.avatar?.url ?? "", - 0 - ), - uploadDate: parseInt(new Date(resource.createdAt).getTime() / 1000), - datetime: parseInt(new Date(resource.createdAt).getTime() / 1000), - url: resource.url ?? "", - duration: resource.duration ?? 0, - viewCount: resource?.viewCount ?? 0, - isLive: false - }; - - return opts; + const videos: PlatformVideoDef[] = jsonResponse?.data?.collection?.videos?.edges.map(edge => { + return SourceVideoToGrayjayVideo(config.id, edge.node as Video); }); - const playlist = jsonResponse?.data?.collection as Collection; - - return new PlatformPlaylistDetails({ - url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid, config.id, PLATFORM_CLAIMTYPE), - author: new PlatformAuthorLink( - new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), - playlist?.creator?.displayName ?? "", - `${BASE_URL}/${playlist?.creator?.name}`, - playlist?.creator?.avatar?.url ?? "", - 0 - ), - name: playlist.name, - thumbnail: playlist?.thumbnail?.url, - videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, - contents: new VideoPager(videos) - }); + return SourceCollectionToGrayjayPlaylistDetails(config.id, jsonResponse?.data?.collection as Collection, videos); } @@ -358,7 +259,7 @@ source.getUserSubscriptions = (): string[] => { variables: { first: first, page: page, - avatar_size: creatorAvatarHeight[_settings?.avatarSize], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], }, headers, query: GET_USER_SUBSCRIPTIONS, @@ -420,16 +321,6 @@ source.getUserPlaylists = (): string[] => { 'Cache-Control': 'no-cache', } - const userInfoQuery = ` - query SUBSCRIPTIONS_QUERY { - me { - xid - channel { - name - } - } - } - `; const jsonResponse = executeGqlQuery( @@ -437,7 +328,7 @@ source.getUserPlaylists = (): string[] => { { operationName: 'SUBSCRIPTIONS_QUERY', headers, - query: userInfoQuery, + query: SUBSCRIPTIONS_QUERY, usePlatformAuth: true }); @@ -479,32 +370,6 @@ function getPlaylistsByUsername(userName, headers, usePlatformAuth = false) { } -function getQuery(context) { - context.sort = parseSort(context.order); - - if (!context.filters) { - context.filters = {}; - } - - if (!context.page) { - context.page = 1; - } - - if (context?.filters.duration) { - context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; - context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; - } else { - context.filters.durationMinVideos = null; - context.filters.durationMaxVideos = null; - } - - if (context.filters.uploaddate) { - context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); - } - - return context; -} - function searchPlaylists(contextQuery) { @@ -518,13 +383,12 @@ function searchPlaylists(contextQuery) { "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 "shouldIncludeChannels": false, "shouldIncludePlaylists": true, - "shouldIncludeTopics": false, "shouldIncludeVideos": false, "shouldIncludeLives": false, "page": context.page, "limit": ITEMS_PER_PAGE, - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], } @@ -539,33 +403,17 @@ function searchPlaylists(contextQuery) { const playlistConnection = jsonResponse?.data?.search?.playlists as CollectionConnection; - var searchResults = playlistConnection?.edges?.map(edge => { - - const playlist = edge?.node; - - return new PlatformPlaylist({ - url: `${BASE_URL_PLAYLIST}/${playlist?.xid}`, - id: new PlatformID(PLATFORM, playlist?.xid ?? "", config.id, PLATFORM_CLAIMTYPE), - author: new PlatformAuthorLink( - new PlatformID(PLATFORM, playlist?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), - playlist?.creator?.displayName ?? "", - `${BASE_URL}/${playlist?.creator?.name}`, - playlist?.creator?.avatar?.url ?? "", - 0 - ), - name: playlist?.name, - thumbnail: playlist?.thumbnail?.url, - videoCount: playlist?.metrics?.engagement?.videos?.edges[0]?.node?.total, - }); + const searchResults = playlistConnection?.edges?.map(edge => { + return SourceCollectionToGrayjayPlaylist(config.id, edge?.node); }); const hasMore = playlistConnection?.pageInfo?.hasNextPage; - if (!searchResults || !searchResults?.length) { + if (!searchResults || searchResults.length === 0) { return new PlaylistPager([]); } - var params = { + const params = { query: context.q, sort: context.sort, filters: context.filters, @@ -581,7 +429,6 @@ function searchPlaylists(contextQuery) { function getVideoPager(params, page) { const count = ITEMS_PER_PAGE; - const start = (page ?? 0) * count; if (!params) { params = {}; @@ -592,7 +439,6 @@ function getVideoPager(params, page) { const headersToAdd = { "User-Agent": USER_AGENT, - // "Accept-Language": Accept_Language, "Referer": BASE_URL, "Content-Type": "application/json", "X-DM-AppInfo-Id": X_DM_AppInfo_Id, @@ -611,15 +457,16 @@ function getVideoPager(params, page) { let obj; + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); + try { obj = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), + anonymousHttpClient, { operationName: 'SEACH_DISCOVERY_QUERY', variables: { - shouldQueryPromotedHashtag: false, - avatar_size: creatorAvatarHeight[_settings?.avatarSize], - thumbnail_resolution: thumbnailHeight[_settings?.thumbnailResolution], + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + thumbnail_resolution: THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], }, query: HOME_QUERY, headers: headersToAdd, @@ -629,31 +476,11 @@ function getVideoPager(params, page) { return new VideoPager([], false, { params }); } - var results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges - // ?.filter(edge => edge?.node?.__typename === 'Video') + const results = obj?.data?.home?.neon?.sections?.edges[0]?.node?.components?.edges ?.filter(edge => edge?.node?.id) ?.map(edge => { - const v = edge.node as Video; - - const metadata = GetVideoExtraDetails(v.xid); - - return ToPlatformVideo({ - id: v.id, - name: v.title ?? "", - thumbnail: v.thumbnail?.url ?? "", - createdAt: v.createdAt, - creatorId: v?.creator?.id, - creatorName: v?.creator?.name, - creatorDisplayName: v.creator?.displayName, - creatorAvatar: v?.creator?.avatar?.url ?? "", - creatorUrl: `${BASE_URL}/${v.creator?.name}`, - duration: v.duration, - viewCount: metadata.views ?? 0, - url: `${BASE_URL_VIDEO}/${v.xid}`, - isLive: false, - description: v?.description ?? '', - }); + return SourceVideoToGrayjayVideo(config.id, edge.node as Video); }) @@ -661,21 +488,6 @@ function getVideoPager(params, page) { return new SearchPagerAll(results, hasMore, params, page, getVideoPager); } -function GetVideoExtraDetails(xid) { - - - const json = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), { - operationName: 'WATCHING_VIDEO', - variables: { xid }, - query: GET_VIDEO_EXTRA_DETAILS - }); - - - return { - views: json?.data?.video?.stats?.views?.total - } -} function getChannelPager(context) { @@ -684,8 +496,9 @@ function getChannelPager(context) { const channel_name = getChannelNameFromUrl(url); + const anonymousHttpClient = getHttpContext({ usePlatformAuth: false }); const json = executeGqlQuery( - getHttpContext({ usePlatformAuth: false }), + anonymousHttpClient, { operationName: 'CHANNEL_VIDEOS_QUERY', variables: { @@ -694,36 +507,17 @@ function getChannelPager(context) { "page": context.page ?? 1, "allowExplicit": !_settings.hideSensitiveContent, "first": context.page_size ?? ITEMS_PER_PAGE, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution], + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution], }, query: CHANNEL_VIDEOS_BY_CHANNEL_NAME }); - const edges = json?.data?.channel?.channel_videos_all_videos?.edges ?? []; - let videos = edges.map((edge) => { + const edges = json?.data?.channel?.videos?.edges ?? []; - const video: Video = edge.node; - - const metadata = GetVideoExtraDetails(video.xid); - - - return ToPlatformVideo({ - id: video.id, - name: video.title, - thumbnail: video?.thumbnail?.url ?? "", - createdAt: video?.createdAt, - creatorId: video?.creator?.id, - creatorDisplayName: video?.creator?.displayName, - creatorName: video?.creator?.name, - creatorAvatar: video?.creator?.avatar?.url, - creatorUrl: `${BASE_URL}/${video?.creator?.name}`, - duration: video.duration, - url: `${BASE_URL_VIDEO}/${video?.xid}`, - viewCount: metadata.views ?? 0, - isLive: false - }); + let videos = edges.map((edge) => { + return SourceVideoToGrayjayVideo(config.id, edge.node as Video); }) @@ -731,78 +525,8 @@ function getChannelPager(context) { context.page++; } - return new ChannelVideoPager(context, videos, json?.data?.channel?.channel_videos_all_videos?.pageInfo?.hasNextPage, getChannelPager); -} - -function ToPlatformVideo(resource) { - - return new PlatformVideo({ - id: new PlatformID(PLATFORM, resource.id, config.id, PLATFORM_CLAIMTYPE), - name: resource.name, - thumbnails: new Thumbnails([new Thumbnail(resource.thumbnail, 0)]), - author: new PlatformAuthorLink( - new PlatformID(PLATFORM, resource.creatorId, config.id, PLATFORM_CLAIMTYPE), - resource.creatorDisplayName, - resource.creatorUrl, - resource.creatorAvatar ?? "", - 0 - ), - uploadDate: parseInt(new Date(resource.createdAt).getTime() / 1000), - url: resource.url, - duration: resource.duration, - viewCount: resource.viewCount, - isLive: resource.isLive - }) - -} - -function parseSort(order) { - let sort; - switch (order) { - //TODO: refact this to use constants - case "Most Recent": - sort = "RECENT"; - break; - case "Most Viewed": - sort = "VIEW_COUNT"; - break; - case "Most Relevant": - sort = "RELEVANCE"; - break; - default: - sort = order; // Default to the original order if no match - } - return sort -} -function parseUploadDateFilter(filter) { - let createdAfterVideos; - - const now = new Date(); - - switch (filter) { - case "today": - // Last 24 hours from now - const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); - createdAfterVideos = yesterday.toISOString(); - break; - case "thisweek": - // Adjusts to the start of the current week (assuming week starts on Sunday) - const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); - createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); - break; - case "thismonth": - // Adjusts to the start of the month - createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); - break; - case "thisyear": - // Adjusts to the start of the year - createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); - break; - default: - createdAfterVideos = null; - } - return createdAfterVideos; + return new ChannelVideoPager(context, videos, json?.data?.channel?.videos?.pageInfo?.hasNextPage, getChannelPager); } function getSearchPagerAll(contextQuery): VideoPager { @@ -817,13 +541,12 @@ function getSearchPagerAll(contextQuery): VideoPager { "createdAfterVideos": context.filters?.createdAfterVideos, //Represents a DateTime value as specified by iso8601 "shouldIncludeChannels": false, "shouldIncludePlaylists": false, - "shouldIncludeTopics": false, "shouldIncludeVideos": true, "shouldIncludeLives": true, "page": context.page ?? 1, "limit": ITEMS_PER_PAGE, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] } @@ -847,34 +570,12 @@ function getSearchPagerAll(contextQuery): VideoPager { ] for (const edge of all) { - - const sv = edge?.node; - - const isLive = sv?.isOnAir == true; - const viewCount = isLive ? sv?.audienceCount : sv?.stats?.views?.total; - - var video = ToPlatformVideo({ - id: sv?.id, - name: sv?.title, - thumbnail: sv?.thumbnail?.url, - createdAt: sv?.createdAt, - creatorId: sv?.creator?.id, - creatorName: sv?.creator?.name, - creatorDisplayName: sv?.creator?.displayName, - creatorUrl: `${BASE_URL}/${sv?.creator?.name}`, - creatorAvatar: sv?.creator?.avatar?.url ?? "", - duration: sv?.duration, - viewCount, - url: `${BASE_URL_VIDEO}/${sv?.xid}`, - isLive, - description: sv?.description ?? '', - }); - + const video = SourceVideoToGrayjayVideo(config.id, edge?.node as Video); results.push(video) } //results, hasMore, path, params, page - var params = { + const params = { query: context.q, sort: context.sort, filters: context.filters, @@ -889,10 +590,9 @@ function getSavedVideo(url, usePlatformAuth = false) { const player_metadata_url = `${BASE_URL_METADATA}/${id}?embedder=https%3A%2F%2Fwww.dailymotion.com%2Fvideo%2Fx8yb2e8&geo=1&player-id=xjnde&locale=en-GB&dmV1st=ce2035cd-bdca-4d7b-baa4-127a17490ca5&dmTs=747022&is_native_app=0&app=com.dailymotion.neon&client_type=webapp§ion_type=player&component_style=_`; - var headers1 = { + const headers1 = { "User-Agent": USER_AGENT, "Accept": "*/*", - // "Accept-Language": Accept_Language, // "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "https://geo.dailymotion.com/", "Origin": "https://geo.dailymotion.com", @@ -911,30 +611,27 @@ function getSavedVideo(url, usePlatformAuth = false) { headers1["Cookie"] = "ff=off" } - var player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); + const player_metadataResponse = getHttpContext({ usePlatformAuth }).GET(player_metadata_url, headers1, usePlatformAuth); if (!player_metadataResponse.isOk) { throw new UnavailableException('Unable to get player metadata'); } - var player_metadata = JSON.parse(player_metadataResponse.body); + const player_metadata = JSON.parse(player_metadataResponse.body); if (player_metadata.error) { - if (player_metadata.error.code && errorTypes[player_metadata.error.code] !== undefined) { - throw new UnavailableException(errorTypes[player_metadata.error.code]); + if (player_metadata.error.code && ERROR_TYPES[player_metadata.error.code] !== undefined) { + throw new UnavailableException(ERROR_TYPES[player_metadata.error.code]); } throw new UnavailableException('This content is not available'); } - const hls_url = player_metadata?.qualities?.auto[0]?.url; - const videoDetailsRequestHeaders = { "Content-Type": "application/json", "User-Agent": USER_AGENT, "Accept": "*/*, */*", - // "Accept-Language": Accept_Language, "Referer": `${BASE_URL_VIDEO}/${id}`, "X-DM-AppInfo-Id": X_DM_AppInfo_Id, "X-DM-AppInfo-Type": X_DM_AppInfo_Type, @@ -958,9 +655,8 @@ function getSavedVideo(url, usePlatformAuth = false) { const variables = { "xid": id, - "isSEO": false, - "avatar_size": creatorAvatarHeight[_settings?.avatarSize], - "thumbnail_resolution": thumbnailHeight[_settings?.thumbnailResolution] + "avatar_size": CREATOR_AVATAR_HEIGHT[_settings?.avatarSize], + "thumbnail_resolution": THUMBNAIL_HEIGHT[_settings?.thumbnailResolution] }; const videoDetailsRequestBody = JSON.stringify( @@ -983,18 +679,17 @@ function getSavedVideo(url, usePlatformAuth = false) { { name: 'source', duration: player_metadata?.duration, - url: hls_url, + url: player_metadata?.qualities?.auto[0]?.url, // priority: true, } ) ] - const video = video_details?.data?.video; + const video = video_details?.data?.video as Video; // This platform uses a scale system for rating the videos. // Ratings are grouped into positive and negative to calculate likes and dislikes. - const positiveRatings = [ "STAR_STRUCK", // amazing "SMILING_FACE_WITH_SUNGLASSES", // cool @@ -1022,24 +717,25 @@ function getSavedVideo(url, usePlatformAuth = false) { } } - var platformVideoDetails: PlatformVideoDetailsDef = { + const platformVideoDetails: PlatformVideoDetailsDef = { id: new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), - name: video.title, - thumbnails: new Thumbnails([new Thumbnail(video.thumbnail.url, 0)]), + name: video?.title ?? "", + thumbnails: new Thumbnails([new Thumbnail(video?.thumbnail?.url ?? "", 0)]), author: new PlatformAuthorLink( - new PlatformID(PLATFORM, video?.creator?.id, config.id, PLATFORM_CLAIMTYPE), - video?.creator?.displayName, + new PlatformID(PLATFORM, video?.creator?.id ?? "", config.id, PLATFORM_CLAIMTYPE), + video?.creator?.displayName ?? "", `${BASE_URL}/${video?.creator?.name}`, `${video?.creator?.avatar?.url}`, 0 //subscribers ), // datetime: new Date(video?.createdAt).getTime(), - uploadDate: parseInt(new Date(video.createdAt).getTime() / 1000), - duration: video?.duration, - viewCount: video?.stats?.views?.total, + uploadDate: parseInt(new Date(video?.createdAt).getTime() / 1000), + datetime: parseInt(new Date(video?.createdAt).getTime() / 1000), + duration: video?.duration ?? 0, + viewCount: video?.stats?.views?.total ?? 0, url: `${BASE_URL_VIDEO}/${id}`, isLive: video?.duration == undefined, - description: video?.description, + description: video?.description ?? "", video: new VideoSourceDescriptor(sources), rating: new RatingLikesDislikes(positiveRatingCount, negativeRatingCount), dash: null, @@ -1093,7 +789,7 @@ function getSearchChannelPager(context) { query: context.q, page: context.page ?? 1, limit: ITEMS_PER_PAGE, - avatar_size: creatorAvatarHeight[_settings?.avatarSize] + avatar_size: CREATOR_AVATAR_HEIGHT[_settings?.avatarSize] }; const json = executeGqlQuery( @@ -1104,20 +800,11 @@ function getSearchChannelPager(context) { }); const results = json?.data?.search?.channels?.edges.map(edge => { - const c = edge.node; - return new PlatformChannel({ - id: new PlatformID(PLATFORM, c.id, config.id, PLATFORM_CLAIMTYPE), - name: c.displayName, - thumbnail: c?.avatar?.url, - subscribers: c?.metrics?.engagement?.followers?.edges[0]?.node?.total ?? 0, - url: `${BASE_URL}/${c.name}`, - links: [], - banner: "", - description: c.description, - }); + const channel = edge.node as Channel; + return SourceChannelToGrayjayChannel(config.id, `${BASE_URL}/${channel.name}`, channel); }); - var params = { + const params = { query: context.q, } @@ -1125,7 +812,7 @@ function getSearchChannelPager(context) { } -function getHttpContext(opts: { usePlatformAuth: Boolean } = { usePlatformAuth: false }): IHttp { +function getHttpContext(opts: { usePlatformAuth: boolean } = { usePlatformAuth: false }): IHttp { return opts.usePlatformAuth ? http : httpClientAnonymous; } diff --git a/src/Mappers.ts b/src/Mappers.ts new file mode 100644 index 0000000..cfa3ad2 --- /dev/null +++ b/src/Mappers.ts @@ -0,0 +1,98 @@ +import { Channel, Collection, Video } from "../types/CodeGenDailymotion"; +import { BASE_URL, BASE_URL_PLAYLIST, BASE_URL_VIDEO, PLATFORM, PLATFORM_CLAIMTYPE } from "./constants"; + +export const SourceChannelToGrayjayChannel = (pluginId: string, url: string, sourceChannel: Channel): PlatformChannel => { + + const externalLinks = sourceChannel?.externalLinks ?? {}; + + const links = {}; + + Object + .keys(externalLinks) + .forEach(key => { + if (externalLinks[key]) { + links[key.replace('URL', '')] = externalLinks[key]; + } + }); + + 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 + }) +} + +export const SourceVideoToGrayjayVideo = (pluginId: string, sourceVideo: Video): PlatformVideo => { + + // const metadata = GetVideoExtraDetails(anonymousHttpClient, sv.xid); + // const viewCount = metadata.views ?? 0; + + const isLive = sourceVideo?.isOnAir == true; + const viewCount = isLive ? (sourceVideo?.audienceCount ?? 0) : (sourceVideo?.viewCount ?? sourceVideo?.stats?.views?.total ?? 0); + // const url = sourceVideo?.url ?? `${BASE_URL_VIDEO}/${sourceVideo?.xid}`; + const video: PlatformVideoDef = { + 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: new PlatformAuthorLink( + new PlatformID(PLATFORM, sourceVideo?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), + sourceVideo?.creator?.displayName ?? "", + `${BASE_URL}/${sourceVideo?.creator?.name}`, + sourceVideo?.creator?.avatar?.url ?? "", + 0 + ), + uploadDate: parseInt(new Date(sourceVideo.createdAt).getTime() / 1000), + datetime: parseInt(new Date(sourceVideo.createdAt).getTime() / 1000), + url: `${BASE_URL_VIDEO}/${sourceVideo?.xid}`, + duration: sourceVideo?.duration ?? 0, + viewCount, + isLive + }; + + return new PlatformVideo(video); +} + +export const SourceCollectionToGrayjayPlaylistDetails = (pluginId: string, sourceCollection: Collection, videos: PlatformVideo[] = []): PlatformPlaylistDetails => { + + return new PlatformPlaylistDetails({ + url: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`, + id: new PlatformID(PLATFORM, sourceCollection?.xid, pluginId, PLATFORM_CLAIMTYPE), + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, sourceCollection?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), + sourceCollection?.creator?.displayName ?? "", + `${BASE_URL}/${sourceCollection?.creator?.name}`, + sourceCollection?.creator?.avatar?.url ?? "", + 0 + ), + name: sourceCollection.name, + thumbnail: sourceCollection?.thumbnail?.url, + videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total, + contents: new VideoPager(videos) + }); + +} + +export const SourceCollectionToGrayjayPlaylist = (pluginId: string, sourceCollection: Collection): PlatformPlaylist => { + return new PlatformPlaylist({ + url: `${BASE_URL_PLAYLIST}/${sourceCollection?.xid}`, + id: new PlatformID(PLATFORM, sourceCollection?.xid ?? "", pluginId, PLATFORM_CLAIMTYPE), + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, sourceCollection?.creator?.id ?? "", pluginId, PLATFORM_CLAIMTYPE), + sourceCollection?.creator?.displayName ?? "", + `${BASE_URL}/${sourceCollection?.creator?.name}`, + sourceCollection?.creator?.avatar?.url ?? "", + 0 + ), + name: sourceCollection?.name, + thumbnail: sourceCollection?.thumbnail?.url, + videoCount: sourceCollection?.metrics?.engagement?.videos?.edges[0]?.node?.total, + }); +} \ No newline at end of file diff --git a/src/Pagers.ts b/src/Pagers.ts index 70e6cb1..9ddb5b5 100644 --- a/src/Pagers.ts +++ b/src/Pagers.ts @@ -79,7 +79,6 @@ export class SearchPlaylistPager extends VideoPager { filters: this.context.params.filters }; - // return searchPlaylists(opts); return this.cb(opts) } } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index c26cd75..f01ef2a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -38,7 +38,7 @@ DURATION_THRESHOLDS[FIVE_TO_THIRTY_MINUTES] = { min: 300, max: 1800 }; DURATION_THRESHOLDS[THIRTY_TO_ONE_HOUR] = { min: 1800, max: 3600 }; DURATION_THRESHOLDS[MORE_THAN_ONE_HOUR] = { min: 3600, max: null }; -export const countryNamesToCode = { +export const COUNTRY_NAMES_TO_CODE = { "": "", "Afghanistan": "AF", "Aland Islands": "AX", @@ -291,7 +291,7 @@ export const countryNamesToCode = { "Zimbabwe": "ZW" } -export const creatorAvatarHeight = [ +export const CREATOR_AVATAR_HEIGHT = [ "SQUARE_25", "SQUARE_60", "SQUARE_80", @@ -303,7 +303,7 @@ export const creatorAvatarHeight = [ "SQUARE_720" ] -export const thumbnailHeight = [ +export const THUMBNAIL_HEIGHT = [ "PORTRAIT_60", "PORTRAIT_120", "PORTRAIT_180", @@ -314,9 +314,9 @@ export const thumbnailHeight = [ "PORTRAIT_1080" ] -export const countryNames = Object.keys(countryNamesToCode); +export const COUNTRY_NAMES = Object.keys(COUNTRY_NAMES_TO_CODE); -export const errorTypes = { +export const ERROR_TYPES = { "DM001": "No video has been specified, you need to specify one.", "DM002": "Content has been deleted.", "DM003": "Live content is not available, i.e. it may not have started yet.", @@ -334,4 +334,42 @@ export const errorTypes = { "DM015": "Kids host error", "DM016": "Content not available on this website, it can only be watched on Dailymotion", "DM019": "This content has been uploaded by an inactive channel and its access is limited" -}; \ No newline at end of file +}; + + +export const SEARCH_CAPABILITIES = { + types: [ + Type.Feed.Videos, + Type.Feed.Live + ], + sorts: [ + "Most Recent", + "Most Viewed", + "Most Relevant" + ], + filters: [ + { + id: "uploaddate", + name: "Upload Date", + isMultiSelect: false, + filters: [ + { name: "Today", value: "today" }, + { name: "Past week", value: "thisweek" }, + { name: "Past month", value: "thismonth" }, + { name: "Past year", value: "thisyear" } + ] + }, + { + id: "duration", + name: "Duration", + isMultiSelect: false, + filters: [ + { name: "< 1 min", value: LESS_THAN_MINUTE }, + { name: "1 - 5 min", value: ONE_TO_FIVE_MINUTES }, + { name: "5 - 30 min", value: FIVE_TO_THIRTY_MINUTES }, + { name: "30 min - 1 hour", value: THIRTY_TO_ONE_HOUR }, + { name: "> 1 hour", value: MORE_THAN_ONE_HOUR } + ] + } + ] +} \ No newline at end of file diff --git a/src/gqlQueries.ts b/src/gqlQueries.ts index 245e6b5..8d00e2d 100644 --- a/src/gqlQueries.ts +++ b/src/gqlQueries.ts @@ -33,10 +33,6 @@ query CHANNEL_QUERY_DESKTOP( url } tagline - country { - id - codeAlpha2 - } metrics { engagement { followers { @@ -82,7 +78,6 @@ fragment SEARCH_DISCOVERY_VIDEO_FRAGMENT on Video { xid title isPublished - embedURL thumbnail(height:$thumbnail_resolution) { url } @@ -97,10 +92,15 @@ fragment SEARCH_DISCOVERY_VIDEO_FRAGMENT on Video { } } duration - + viewCount + stats { + views { + total + } + } } -query SEACH_DISCOVERY_QUERY($shouldQueryPromotedHashtag: Boolean!, $avatar_size: AvatarHeight!, $thumbnail_resolution: ThumbnailHeight!) { +query SEACH_DISCOVERY_QUERY($avatar_size: AvatarHeight!, $thumbnail_resolution: ThumbnailHeight!) { home: views { id neon { @@ -130,107 +130,76 @@ query SEACH_DISCOVERY_QUERY($shouldQueryPromotedHashtag: Boolean!, $avatar_size: } } } - featuredContent { - id - channels(first: 10) { - edges { - node { - id - xid - displayName - name - logoURL(size: "x120") - stats { - id - followers { - id - total - } - } - } - } - } - } - conversations( - filter: { story: { eq: HASHTAG }, algorithm: { eq: SPONSORED } } - first: 1 - ) @include(if: $shouldQueryPromotedHashtag) { - edges { - node { - id - story { - ... on Hashtag { - id - name - } - } - } - } - } } + ` export const CHANNEL_VIDEOS_BY_CHANNEL_NAME = ` query CHANNEL_VIDEOS_QUERY( - $channel_name: String! - $first: Int! - $sort: String - $page: Int! - $allowExplicit: Boolean - $avatar_size: AvatarHeight! - $thumbnail_resolution: ThumbnailHeight! + $channel_name: String! + $first: Int! + $sort: String + $page: Int! + $allowExplicit: Boolean + $avatar_size: AvatarHeight! + $thumbnail_resolution: ThumbnailHeight! ) { - channel(name: $channel_name) { - id - xid - channel_videos_all_videos: videos( - sort: $sort - page: $page - first: $first - allowExplicit: $allowExplicit - ) { - pageInfo { - hasNextPage - nextPage - } - edges { - node { - id - xid - title - thumbnail(height:$thumbnail_resolution) { - url - } - bestAvailableQuality - duration - createdAt - creator { - id - name - displayName - avatar(height:$avatar_size) { - url - } - - } - metrics { - engagement { - likes { - totalCount - } - } - } + channel(name: $channel_name) { + id + xid + videos( + sort: $sort + page: $page + first: $first + allowExplicit: $allowExplicit + ) { + pageInfo { + hasNextPage + nextPage + } + edges { + node { + id + xid + title + thumbnail(height: $thumbnail_resolution) { + url + } + bestAvailableQuality + duration + createdAt + creator { + id + name + displayName + avatar(height:$avatar_size) { + url + } - } - } - } - } + } + metrics { + engagement { + likes { + totalCount + } + } + } + viewCount + stats { + views { + total + } + } + } + } + } + } } - - + + ` export const MAIN_SEARCH_QUERY = ` @@ -341,38 +310,11 @@ export const MAIN_SEARCH_QUERY = ` } } - fragment TOPIC_BASE_FRAG on Topic { - id - xid - name - videos(sort: "recent", first: 5) { - pageInfo { - hasNextPage - nextPage - } - edges { - node { - id - ...VIDEO_BASE_FRAGMENT - ...VIDEO_FAVORITES_FRAGMENT - } - } - } - stats { - id - videos { - id - total - } - } - } - query SEARCH_QUERY( $query: String! $shouldIncludeVideos: Boolean! $shouldIncludeChannels: Boolean! $shouldIncludePlaylists: Boolean! - $shouldIncludeTopics: Boolean! $shouldIncludeLives: Boolean! $page: Int $limit: Int @@ -472,20 +414,6 @@ export const MAIN_SEARCH_QUERY = ` } } } - topics(query: $query, first: $limit, page: $page) - @include(if: $shouldIncludeTopics) { - pageInfo { - hasNextPage - nextPage - } - totalCount - edges { - node { - id - ...TOPIC_BASE_FRAG - } - } - } } } `; @@ -540,7 +468,6 @@ fragment VIDEO_FRAGMENT on Video { height width } - coverURLx375: coverURL(size: "x375") stats { id views { @@ -556,60 +483,6 @@ fragment VIDEO_FRAGMENT on Video { total } } - country { - id - codeAlpha2 - } - organization @skip(if: $isSEO) { - id - xid - owner { - id - xid - } - } - } - language { - id - codeAlpha2 - } - tags { - edges { - node { - id - label - } - } - } - moderation { - id - reviewedAt - } - topics(whitelistedOnly: true, first: 3, page: 1) { - edges { - node { - id - xid - name - names { - edges { - node { - id - name - language { - id - codeAlpha2 - } - } - } - } - } - } - } - geoblockedCountries { - id - allowed - denied } } @@ -684,14 +557,6 @@ fragment LIVE_FRAGMENT on Live { id codeAlpha2 } - organization @skip(if: $isSEO) { - id - xid - owner { - id - xid - } - } } language { id @@ -739,7 +604,6 @@ fragment LIVE_FRAGMENT on Live { query WATCHING_VIDEO( $xid: String! - $isSEO: Boolean! $avatar_size: AvatarHeight! $thumbnail_resolution: ThumbnailHeight! ) { @@ -883,20 +747,6 @@ query PLAYLIST_VIDEO_QUERY($xid: String!, $numberOfVideos: Int = 100, $avatar_si } ` -export const GET_VIDEO_EXTRA_DETAILS = ` -query WATCHING_VIDEO($xid: String!) { - video: media(xid: $xid) { - ... on Video { - stats { - views { - total - } - } - } - } -} - ` - export const GET_USER_SUBSCRIPTIONS = ` query SUBSCRIPTIONS_QUERY($first: Int, $page: Int) { me { @@ -925,8 +775,6 @@ query CHANNEL_PLAYLISTS_QUERY( $first: Int! ) { channel(name: $channel_name) { - id - xid channel_playlist_collections: collections( sort: $sort page: $page @@ -938,26 +786,22 @@ query CHANNEL_PLAYLISTS_QUERY( } edges { node { - id xid - updatedAt - createdAt - name - description - thumbnailx60: thumbnailURL(size: "x60") - thumbnailx120: thumbnailURL(size: "x120") - thumbnailx240: thumbnailURL(size: "x240") - thumbnailx720: thumbnailURL(size: "x720") - stats { - id - videos { - id - total + } } } } } +` + +export const SUBSCRIPTIONS_QUERY = ` +query SUBSCRIPTIONS_QUERY { + me { + xid + channel { + name + } } } -` \ No newline at end of file +`; \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 5491163..05875dc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,16 +7,17 @@ import { USER_AGENT, BASE_URL_API, X_DM_Preferred_Country, - countryNames, - countryNamesToCode, + COUNTRY_NAMES, + COUNTRY_NAMES_TO_CODE, CLIENT_ID, CLIENT_SECRET, BASE_URL_API_AUTH, + DURATION_THRESHOLDS, } from './constants' export function getPreferredCountry(preferredCountryIndex) { - const countryName = countryNames[preferredCountryIndex]; - const code = countryNamesToCode[countryName]; + const countryName = COUNTRY_NAMES[preferredCountryIndex]; + const code = COUNTRY_NAMES_TO_CODE[countryName]; const preferredCountry = (code || X_DM_Preferred_Country || '').toLowerCase(); return preferredCountry; } @@ -45,7 +46,7 @@ export function getChannelNameFromUrl(url) { export function isUsernameUrl(url) { - var regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); + const regex = new RegExp('^' + BASE_URL.replace(/\./g, '\\.') + '/[^/]+$'); return regex.test(url); } @@ -135,8 +136,8 @@ export function executeGqlQuery(httpClient, requestOptions) { const usePlatformAuth = requestOptions.usePlatformAuth == undefined ? false : requestOptions.usePlatformAuth; const throwOnError = requestOptions.throwOnError == undefined ? true : requestOptions.throwOnError; - if(!usePlatformAuth){ - headersToAdd.Authorization = getAnonymousUserTokenSingleton(); + if (!usePlatformAuth) { + headersToAdd.Authorization = getAnonymousUserTokenSingleton(); } const res = httpClient.POST(BASE_URL_API, gql, headersToAdd, usePlatformAuth); @@ -192,4 +193,82 @@ export const convertSRTtoVTT = (srt) => { // Join the VTT array into a single string and return it return vtt.join(''); +} + + + +export const parseUploadDateFilter = (filter) => { + let createdAfterVideos; + + const now = new Date(); + + switch (filter) { + case "today": + // Last 24 hours from now + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + createdAfterVideos = yesterday.toISOString(); + break; + case "thisweek": + // Adjusts to the start of the current week (assuming week starts on Sunday) + const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); + createdAfterVideos = new Date(startOfWeek.getFullYear(), startOfWeek.getMonth(), startOfWeek.getDate()).toISOString(); + break; + case "thismonth": + // Adjusts to the start of the month + createdAfterVideos = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); + break; + case "thisyear": + // Adjusts to the start of the year + createdAfterVideos = new Date(now.getFullYear(), 0, 1).toISOString(); + break; + default: + createdAfterVideos = null; + } + return createdAfterVideos; +} + + +export const parseSort = (order) => { + let sort; + switch (order) { + //TODO: refact this to use constants + case "Most Recent": + sort = "RECENT"; + break; + case "Most Viewed": + sort = "VIEW_COUNT"; + break; + case "Most Relevant": + sort = "RELEVANCE"; + break; + default: + sort = order; // Default to the original order if no match + } + return sort +} + +export const getQuery = (context) => { + context.sort = parseSort(context.order); + + if (!context.filters) { + context.filters = {}; + } + + if (!context.page) { + context.page = 1; + } + + if (context?.filters.duration) { + context.filters.durationMinVideos = DURATION_THRESHOLDS[context.filters.duration].min; + context.filters.durationMaxVideos = DURATION_THRESHOLDS[context.filters.duration].max; + } else { + context.filters.durationMinVideos = null; + context.filters.durationMaxVideos = null; + } + + if (context.filters.uploaddate) { + context.filters.createdAfterVideos = parseUploadDateFilter(context.filters.uploaddate[0]); + } + + return context; } \ No newline at end of file diff --git a/types/plugin.d.ts b/types/plugin.d.ts index d08534a..a8e21ee 100644 --- a/types/plugin.d.ts +++ b/types/plugin.d.ts @@ -585,6 +585,7 @@ declare class PlatformAuthorMembershipLink { declare interface PlatformVideoDef { id: PlatformID, name: string, + description: string, thumbnails: Thumbnails, author: PlatformAuthorLink, uploadDate: number, @@ -592,7 +593,8 @@ declare interface PlatformVideoDef { url: string, duration: number, viewCount: number, - isLive: boolean + isLive: boolean, + shareUrl?: any } declare class PlatformVideo extends PlatformContent { -- GitLab