diff --git a/migrations/0052_playlist_medium.sql b/migrations/0052_playlist_medium.sql new file mode 100644 index 00000000..db420e11 --- /dev/null +++ b/migrations/0052_playlist_medium.sql @@ -0,0 +1,24 @@ +ALTER TABLE ONLY public."playlists" + ADD COLUMN "isDefault" boolean DEFAULT false NOT NULL; + +CREATE TYPE public.playlists_medium_enum AS ENUM +( + 'podcast', + 'music', + 'video', + 'film', + 'audiobook', + 'newsletter', + 'blog', + 'music-video', + 'mixed' +); + +ALTER TYPE public.playlists_medium_enum OWNER TO postgres; + +ALTER TABLE ONLY public."playlists" + ADD COLUMN medium public.playlists_medium_enum DEFAULT 'mixed'::public.playlists_medium_enum NOT NULL; + +CREATE UNIQUE INDEX playlists_owner_isDefault_medium_unique_idx + ON public."playlists" ("ownerId", "isDefault", "medium") + WHERE "playlists"."isDefault" IS TRUE; diff --git a/migrations/0053_podcast_medium_indexes.sql b/migrations/0053_podcast_medium_indexes.sql new file mode 100644 index 00000000..80b6f9d8 --- /dev/null +++ b/migrations/0053_podcast_medium_indexes.sql @@ -0,0 +1,27 @@ +CREATE INDEX CONCURRENTLY "podcasts_medium_index" ON "podcasts" ("medium"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastAllTimeTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastAllTimeTotalUniquePageviews"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastHourTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastHourTotalUniquePageviews"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastDayTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastDayTotalUniquePageviews"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastWeekTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastWeekTotalUniquePageviews"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastMonthTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastMonthTotalUniquePageviews"); + +CREATE INDEX CONCURRENTLY "podcasts_medium_pastYearTotalUniquePageviews_index" ON public.podcasts USING btree +("medium", "pastYearTotalUniquePageviews"); + +-- + +CREATE INDEX CONCURRENTLY "playlists_medium_index" ON "playlists" ("medium"); + +-- + +ALTER TYPE podcasts_medium_enum ADD VALUE 'mixed'; diff --git a/package.json b/package.json index c3a0b63d..f9e03709 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "podverse-api", - "version": "4.14.13", + "version": "4.15.0", "description": "Data API, database migration scripts, and backend services for all Podverse models.", "contributors": [ "Mitch Downey" @@ -153,7 +153,7 @@ "archiver": "^5.3.1", "awesome-typescript-loader": "5.2.1", "aws-sdk": "2.814.0", - "axios": "0.21.2", + "axios": "1.6.0", "bcryptjs": "2.4.3", "chai": "4.2.0", "chai-http": "4.3.0", @@ -200,7 +200,7 @@ "paypal-rest-sdk": "2.0.0-rc.2", "pg": "8.7.3", "podcast-partytime": "4.6.2", - "podverse-shared": "^4.14.9", + "podverse-shared": "^4.14.15", "reflect-metadata": "0.1.13", "request": "^2.88.2", "request-promise-native": "1.0.8", diff --git a/src/app.ts b/src/app.ts index 6c5222ee..1a8de5bf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,7 @@ import { podcastRouter, podcastIndexRouter, podpingRouter, + secondaryQueueRouter, upDeviceRouter, urlResolverRouter, userHistoryItemRouter, @@ -197,6 +198,9 @@ export const createApp = async (conn: Connection) => { app.use(podpingRouter.routes()) app.use(podpingRouter.allowedMethods()) + app.use(secondaryQueueRouter.routes()) + app.use(secondaryQueueRouter.allowedMethods()) + app.use(toolsRouter.routes()) app.use(toolsRouter.allowedMethods()) diff --git a/src/controllers/episode.ts b/src/controllers/episode.ts index c8fb925f..159db002 100644 --- a/src/controllers/episode.ts +++ b/src/controllers/episode.ts @@ -152,7 +152,9 @@ const generateEpisodeSelects = ( sincePubDate = '', hasVideo, shouldUseEpisodesMostRecent, - liveItemStatus + liveItemStatus, + isMusic, + podcastsOnly ) => { const table = shouldUseEpisodesMostRecent ? EpisodeMostRecent : Episode @@ -197,6 +199,14 @@ const generateEpisodeSelects = ( qb.andWhere(`episode."mediaType" LIKE 'video%'`) } + if (isMusic) { + qb.andWhere(`podcast.medium = 'music'`) + } + + if (podcastsOnly) { + qb.andWhere(`podcast.medium = 'podcast'`) + } + if (liveItemStatus) { qb.andWhere('"liveItem" IS NOT null AND "liveItem".status = :liveItemStatus', { liveItemStatus }) } else if (!shouldUseEpisodesMostRecent) { @@ -259,7 +269,19 @@ const getEpisodesFromSearchEngine = async (query) => { } const getEpisodes = async (query, isFromManticoreSearch?, totalOverride?) => { - const { episodeId, hasVideo, includePodcast, liveItemStatus, searchTitle, sincePubDate, skip, sort, take } = query + const { + episodeId, + hasVideo, + includePodcast, + isMusic, + liveItemStatus, + podcastsOnly, + searchTitle, + sincePubDate, + skip, + sort, + take + } = query const episodeIds = (episodeId && episodeId.split(',')) || [] const shouldUseEpisodesMostRecent = false @@ -269,7 +291,9 @@ const getEpisodes = async (query, isFromManticoreSearch?, totalOverride?) => { sincePubDate, hasVideo, shouldUseEpisodesMostRecent, - liveItemStatus + liveItemStatus, + isMusic, + podcastsOnly ) // const shouldLimit = true // qb = limitEpisodesQuerySize(qb, shouldLimit, sort) @@ -292,7 +316,19 @@ const getEpisodes = async (query, isFromManticoreSearch?, totalOverride?) => { } const getEpisodesByCategoryIds = async (query) => { - const { categories, hasVideo, includePodcast, liveItemStatus, searchTitle, sincePubDate, skip, sort, take } = query + const { + categories, + hasVideo, + includePodcast, + isMusic, + liveItemStatus, + podcastsOnly, + searchTitle, + sincePubDate, + skip, + sort, + take + } = query const categoriesIds = (categories && categories.split(',')) || [] const shouldUseEpisodesMostRecent = sort === 'most-recent' @@ -302,7 +338,9 @@ const getEpisodesByCategoryIds = async (query) => { sincePubDate, hasVideo, shouldUseEpisodesMostRecent, - liveItemStatus + liveItemStatus, + isMusic, + podcastsOnly ) qb.innerJoin('podcast.categories', 'categories', 'categories.id IN (:...categoriesIds)', { categoriesIds }) @@ -335,7 +373,9 @@ const getEpisodesByPodcastIdWithSeasons = async ({ sincePubDate, hasVideo, itunesFeedType, - podcastId + podcastId, + isMusic, + podcastsOnly }) => { const includePodcast = false const shouldUseEpisodesMostRecent = false @@ -346,7 +386,9 @@ const getEpisodesByPodcastIdWithSeasons = async ({ sincePubDate, hasVideo, shouldUseEpisodesMostRecent, - liveItemStatus + liveItemStatus, + isMusic, + podcastsOnly ) const isSerial = itunesFeedType === 'serial' @@ -371,9 +413,11 @@ const getEpisodesByPodcastIds = async (query) => { const { hasVideo, includePodcast, + isMusic, liveItemStatus, maxResults, podcastId, + podcastsOnly, searchTitle, sincePubDate, skip, @@ -389,7 +433,9 @@ const getEpisodesByPodcastIds = async (query) => { sincePubDate, hasVideo, shouldUseEpisodesMostRecent, - liveItemStatus + liveItemStatus, + isMusic, + podcastsOnly ) if (podcastIds.length === 1) { @@ -401,7 +447,9 @@ const getEpisodesByPodcastIds = async (query) => { sincePubDate, hasVideo, itunesFeedType: podcast.itunesFeedType, - podcastId + podcastId, + isMusic, + podcastsOnly }) } else { return getEpisodesByPodcastId(query, qb, podcastIds) diff --git a/src/controllers/mediaRef.ts b/src/controllers/mediaRef.ts index e4323342..f9b21953 100644 --- a/src/controllers/mediaRef.ts +++ b/src/controllers/mediaRef.ts @@ -150,7 +150,7 @@ const getMediaRefs = async (query, isFromManticoreSearch?, totalOverride?) => { const podcastIds = (query.podcastId && query.podcastId.split(',')) || [] const episodeIds = (query.episodeId && query.episodeId.split(',')) || [] const categoriesIds = (query.categories && query.categories.split(',')) || [] - const { hasVideo, includeEpisode, includePodcast, skip, take } = query + const { hasVideo, includeEpisode, includePodcast, isMusic, podcastsOnly, skip, take } = query const repositoryName = hasVideo ? MediaRefVideos : MediaRef const repository = getRepository(repositoryName) @@ -209,6 +209,14 @@ const getMediaRefs = async (query, isFromManticoreSearch?, totalOverride?) => { qb.andWhere('mediaRef.id IN (:...mediaRefIds)', { mediaRefIds }) } + if (isMusic) { + qb.andWhere(`podcast.medium = 'music'`) + } + + if (podcastsOnly) { + qb.andWhere(`podcast.medium = 'podcast'`) + } + const allowRandom = podcastIds.length > 0 || episodeIds.length > 0 qb = addOrderByToQuery(qb, 'mediaRef', query.sort, 'createdAt', allowRandom, isFromManticoreSearch) diff --git a/src/controllers/playlist.ts b/src/controllers/playlist.ts index 09d2c101..07605159 100644 --- a/src/controllers/playlist.ts +++ b/src/controllers/playlist.ts @@ -1,9 +1,32 @@ import { getRepository } from 'typeorm' -import { Episode, Playlist, MediaRef, User } from '~/entities' +import { Playlist, User } from '~/entities' import { validateClassOrThrow } from '~/lib/errors' import { getUserSubscribedPlaylistIds } from './user' +import { getMediaRef } from './mediaRef' +import { getEpisode } from './episode' const createError = require('http-errors') +// medium = podcast are always be put in the general "mixed" category for playlists +const getPlaylistMedium = (medium) => { + if (!medium || medium === 'podcast') { + medium = 'mixed' + } + + return medium +} + +const playlistDefaultTitles = { + // 'podcast': 'Favorites', + music: 'Favorite Music', + video: 'Favorite Videos', + film: 'Favorite Films', + audiobook: 'Favorite Audiobooks', + newsletter: 'Favorite Newsletters', + blog: 'Favorite Blogs', + 'music-video': 'Favorite Music Videos', + mixed: 'Favorites' +} + const createPlaylist = async (obj) => { const repository = getRepository(Playlist) const playlist = new Playlist() @@ -80,9 +103,11 @@ const getPlaylists = async (query) => { .createQueryBuilder('playlist') .select('playlist.id') .addSelect('playlist.description') + .addSelect('playlist.isDefault') .addSelect('playlist.isPublic') .addSelect('playlist.itemCount') .addSelect('playlist.itemsOrder') + .addSelect('playlist.medium') .addSelect('playlist.title') .addSelect('playlist.createdAt') .addSelect('playlist.updatedAt') @@ -97,6 +122,7 @@ const getPlaylists = async (query) => { } const updatePlaylist = async (obj, loggedInUserId) => { + // Make sure medium and isDefault is preserved after update const relations = [ 'episodes', 'episodes.podcast', @@ -131,6 +157,74 @@ const updatePlaylist = async (obj, loggedInUserId) => { return newPlaylist } +const getOrCreateDefaultPlaylist = async (medium, loggedInUserId) => { + const filteredMedium = getPlaylistMedium(medium) + let playlist = await getDefaultPlaylist(filteredMedium, loggedInUserId) + + if (!playlist) { + const newDefaultPlaylistData = { + owner: loggedInUserId, + description: '', + isPublic: false, + itemsOrder: [], + medium: filteredMedium, + title: playlistDefaultTitles[filteredMedium] + } + playlist = await createPlaylist(newDefaultPlaylistData) + } + + if (!playlist) { + throw new createError.NotFound('Default playlist not created') + } + + return playlist +} + +const getDefaultPlaylist = async (filteredMedium, loggedInUserId) => { + const repository = getRepository(Playlist) + const playlist = await repository.findOne({ + where: { + owner: loggedInUserId, + isDefault: true, + medium: filteredMedium + } + }) + + return playlist +} + +const addOrRemovePlaylistItemToDefaultPlaylist = async (mediaRefId, episodeId, loggedInUserId) => { + if (mediaRefId) { + const mediaRef = await getMediaRef(mediaRefId) + + if (!mediaRef) { + throw new createError.NotFound('MediaRef not found') + } + + const filteredMedium = getPlaylistMedium(mediaRef.episode.podcast.medium) + const defaultPlaylist = await getOrCreateDefaultPlaylist(filteredMedium, loggedInUserId) + + const playlistId = defaultPlaylist.id + const episodeId = null + return await addOrRemovePlaylistItem(playlistId, mediaRefId, episodeId, loggedInUserId) + } else if (episodeId) { + const episode = await getEpisode(episodeId) + + if (!episode) { + throw new createError.NotFound('Episode not found') + } + + const filteredMedium = getPlaylistMedium(episode.podcast.medium) + const defaultPlaylist = await getOrCreateDefaultPlaylist(filteredMedium, loggedInUserId) + + const playlistId = defaultPlaylist.id + const mediaRefId = null + return await addOrRemovePlaylistItem(playlistId, mediaRefId, episodeId, loggedInUserId) + } + + throw new createError.NotFound('Could not update default playlist') +} + const addOrRemovePlaylistItem = async (playlistId, mediaRefId, episodeId, loggedInUserId) => { const relations = [ 'episodes', @@ -165,9 +259,14 @@ const addOrRemovePlaylistItem = async (playlistId, mediaRefId, episodeId, logged const filteredMediaRefs = playlist.mediaRefs.filter((x) => x.id !== mediaRefId) if (filteredMediaRefs.length === playlist.mediaRefs.length) { - const mediaRefRepository = getRepository(MediaRef) - const mediaRef = await mediaRefRepository.findOne({ id: mediaRefId }) + const mediaRef = await getMediaRef(mediaRefId) + if (mediaRef) { + const filteredMedium = getPlaylistMedium(mediaRef.episode.podcast.medium) + if (playlist.medium !== 'mixed' && playlist.medium !== filteredMedium) { + throw new createError.NotFound('Item can not be added to this type of playlist') + } + playlist.mediaRefs.push(mediaRef) actionTaken = 'added' } else { @@ -184,9 +283,14 @@ const addOrRemovePlaylistItem = async (playlistId, mediaRefId, episodeId, logged const filteredEpisodes = playlist.episodes.filter((x) => x.id !== episodeId) if (filteredEpisodes.length === playlist.episodes.length) { - const episodeRepository = getRepository(Episode) - const episode = await episodeRepository.findOne({ id: episodeId }) + const episode = await getEpisode(episodeId) + if (episode) { + const filteredMedium = getPlaylistMedium(episode.podcast.medium) + if (playlist.medium !== 'mixed' && playlist.medium !== filteredMedium) { + throw new createError.NotFound('Item can not be added to this type of playlist') + } + playlist.episodes.push(episode) actionTaken = 'added' } else { @@ -249,6 +353,7 @@ const toggleSubscribeToPlaylist = async (playlistId, loggedInUserId) => { export { addOrRemovePlaylistItem, + addOrRemovePlaylistItemToDefaultPlaylist, createPlaylist, deletePlaylist, getPlaylist, diff --git a/src/controllers/podcast.ts b/src/controllers/podcast.ts index 9970272f..472e43f4 100644 --- a/src/controllers/podcast.ts +++ b/src/controllers/podcast.ts @@ -135,18 +135,28 @@ const getSubscribedPodcasts = async (query, loggedInUserId) => { } const getPodcastsFromSearchEngine = async (query) => { - const { searchTitle, skip, sort, take } = query + const { hasVideo, isMusic, podcastsOnly, searchTitle, skip, sort, take } = query const { orderByColumnName, orderByDirection } = getManticoreOrderByColumnName(sort) const cleanedSearchTitle = removeAllSpaces(searchTitle) if (!cleanedSearchTitle) throw new Error('Must provide a searchTitle.') const titleWithWildcards = manticoreWildcardSpecialCharacters(cleanedSearchTitle) + let extraQuery = `` + if (hasVideo) { + extraQuery = `AND hasvideo = 1` + } else if (isMusic) { + extraQuery = `AND medium = 'music'` + } else if (podcastsOnly) { + extraQuery = `AND medium = 'podcast'` + } + const safeSqlString = SqlString.format( ` SELECT * FROM idx_podcast WHERE match(?) + ${extraQuery} ORDER BY weight() DESC, ${orderByColumnName} ${orderByDirection} LIMIT ?,?; `, @@ -182,8 +192,10 @@ const getPodcasts = async (query, countOverride?, isFromManticoreSearch?) => { hasVideo, includeAuthors, includeCategories, + isMusic, maxResults, podcastId, + podcastsOnly, searchAuthor, skip, sort, @@ -254,6 +266,14 @@ const getPodcasts = async (query, countOverride?, isFromManticoreSearch?) => { qb.andWhere('podcast."hasVideo" IS true') } + if (isMusic) { + qb.andWhere(`podcast.medium = 'music'`) + } + + if (podcastsOnly) { + qb.andWhere(`podcast.medium = 'podcast'`) + } + const allowRandom = !!podcastIds qb = addOrderByToQuery(qb, 'podcast', sort, 'lastEpisodePubDate', allowRandom, isFromManticoreSearch) @@ -328,7 +348,7 @@ const getMetadata = async (query) => { .andWhere('"isPublic" = true') try { - const podcasts = await qb.take(500).getManyAndCount() + const podcasts = await qb.take(1000).getManyAndCount() return podcasts } catch (error) { diff --git a/src/controllers/secondaryQueue.ts b/src/controllers/secondaryQueue.ts new file mode 100644 index 00000000..9f1865ff --- /dev/null +++ b/src/controllers/secondaryQueue.ts @@ -0,0 +1,74 @@ +import { LessThan, MoreThan, getRepository } from 'typeorm' +import { Episode, Podcast } from '~/entities' +const createError = require('http-errors') + +const relations = ['liveItem', 'podcast'] + +type SecondaryQueueResponseData = { + previousEpisodes: Episode[] + nextEpisodes: Episode[] + inheritedPodcast: Podcast +} + +export const getSecondaryQueueEpisodesForPodcastId = async ( + episodeId: string, + podcastId: string +): Promise => { + const repository = getRepository(Episode) + const currentEpisode = await repository.findOne( + { + isPublic: true, + id: episodeId + }, + { relations } + ) + + if (!currentEpisode || currentEpisode.liveItem) { + throw new createError.NotFound('Episode not found') + } + + const inheritedPodcast = currentEpisode.podcast + if (!inheritedPodcast) { + throw new createError.NotFound('Podcast not found') + } + + const { itunesEpisode, pubDate } = currentEpisode + const take = currentEpisode.podcast.medium === 'music' ? 50 : 5 + + const previousEpisodesAndWhere = + currentEpisode.podcast.medium === 'music' + ? { itunesEpisode: LessThan(itunesEpisode) } + : { pubDate: MoreThan(pubDate) } + const previousEpisodesOrder = + currentEpisode.podcast.medium === 'music' ? ['itunesEpisode', 'ASC'] : ['pubDate', 'DESC'] + const previousEpisodes = await repository.find({ + where: { + isPublic: true, + podcastId, + ...previousEpisodesAndWhere + }, + order: { + [previousEpisodesOrder[0]]: previousEpisodesOrder[1] + }, + take + }) + + const nextEpisodesAndWhere = + currentEpisode.podcast.medium === 'music' + ? { itunesEpisode: MoreThan(itunesEpisode) } + : { pubDate: LessThan(pubDate) } + const nextEpisodesOrder = currentEpisode.podcast.medium === 'music' ? ['itunesEpisode', 'ASC'] : ['pubDate', 'DESC'] + const nextEpisodes = await repository.find({ + where: { + isPublic: true, + podcastId, + ...nextEpisodesAndWhere + }, + order: { + [nextEpisodesOrder[0]]: nextEpisodesOrder[1] + }, + take + }) + + return { previousEpisodes, nextEpisodes, inheritedPodcast } +} diff --git a/src/controllers/user.ts b/src/controllers/user.ts index d7ec343e..bddcb7d7 100644 --- a/src/controllers/user.ts +++ b/src/controllers/user.ts @@ -277,9 +277,11 @@ const getLoggedInUserPlaylistsCombined = async (loggedInUserId) => { .createQueryBuilder('playlist') .select('playlist.id') .addSelect('playlist.description') + .addSelect('playlist.isDefault') .addSelect('playlist.isPublic') .addSelect('playlist.itemCount') .addSelect('playlist.itemsOrder') + .addSelect('playlist.medium') .addSelect('playlist.title') .addSelect('playlist.createdAt') .addSelect('playlist.updatedAt') @@ -314,9 +316,11 @@ const getUserPlaylists = async (query, ownerId) => { .createQueryBuilder('playlist') .select('playlist.id') .addSelect('playlist.description') + .addSelect('playlist.isDefault') .addSelect('playlist.isPublic') .addSelect('playlist.itemCount') .addSelect('playlist.itemsOrder') + .addSelect('playlist.medium') .addSelect('playlist.title') .addSelect('playlist.createdAt') .addSelect('playlist.updatedAt') diff --git a/src/controllers/userHistoryItem.ts b/src/controllers/userHistoryItem.ts index ecb31e65..c494e784 100644 --- a/src/controllers/userHistoryItem.ts +++ b/src/controllers/userHistoryItem.ts @@ -28,10 +28,11 @@ export const cleanUserItemResult = (result) => { episodeValue: parseProp(result, 'clipEpisodeValue', []), id: result.id, podcastFunding: parseProp(result, 'clipPodcastFunding', []), + podcastGuid: result.clipPodcastGuid, podcastId: result.clipPodcastId, podcastImageUrl: result.clipPodcastImageUrl, podcastIndexPodcastId: result.clipPodcastIndexId, - podcastGuid: result.clipPodcastGuid, + podcastMedium: result.clipPodcastMedium, podcastShrunkImageUrl: result.clipPodcastShrunkImageUrl, podcastTitle: result.clipPodcastTitle, podcastValue: parseProp(result, 'clipPodcastValue', []), @@ -67,10 +68,11 @@ export const cleanUserItemResult = (result) => { id: result.id, ...(liveItem ? { liveItem } : {}), podcastFunding: parseProp(result, 'podcastFunding', []), + podcastGuid: result.podcastGuid, podcastId: result.podcastId, podcastImageUrl: result.podcastImageUrl, podcastIndexPodcastId: result.podcastPodcastIndexId, - podcastGuid: result.podcastGuid, + podcastMedium: result.podcastMedium, podcastShrunkImageUrl: result.podcastShrunkImageUrl, podcastTitle: result.podcastTitle, podcastValue: parseProp(result, 'podcastValue', []), @@ -104,8 +106,7 @@ export const generateGetUserItemsQuery = (table, tableName, loggedInUserId) => { qb.addSelect(`${tableName}.queuePosition`, 'queuePosition') } - return qb - .addSelect('mediaRef.id', 'clipId') + qb.addSelect('mediaRef.id', 'clipId') .addSelect('mediaRef.title', 'clipTitle') .addSelect('mediaRef.startTime', 'clipStartTime') .addSelect('mediaRef.endTime', 'clipEndTime') @@ -136,6 +137,7 @@ export const generateGetUserItemsQuery = (table, tableName, loggedInUserId) => { .addSelect('podcast.podcastIndexId', 'podcastPodcastIndexId') .addSelect('podcast.podcastGuid', 'podcastGuid') .addSelect('podcast.itunesFeedType', 'podcastItunesFeedType') + .addSelect('podcast.medium', 'podcastMedium') .addSelect('podcast.shrunkImageUrl', 'podcastShrunkImageUrl') .addSelect('podcast.title', 'podcastTitle') .addSelect('podcast.value', 'podcastValue') @@ -164,6 +166,7 @@ export const generateGetUserItemsQuery = (table, tableName, loggedInUserId) => { .addSelect('clipPodcast.hasSeasons', 'clipPodcastHasSeasons') .addSelect('clipPodcast.imageUrl', 'clipPodcastImageUrl') .addSelect('clipPodcast.itunesFeedType', 'clipPodcastItunesFeedType') + .addSelect('clipPodcast.medium', 'clipPodcastMedium') .addSelect('clipPodcast.podcastGuid', 'clipPodcastGuid') .addSelect('clipPodcast.podcastIndexId', 'clipPodcastIndexId') .addSelect('clipPodcast.shrunkImageUrl', 'clipPodcastShrunkImageUrl') @@ -176,7 +179,9 @@ export const generateGetUserItemsQuery = (table, tableName, loggedInUserId) => { .leftJoin('mediaRef.episode', 'clipEpisode') .leftJoin('clipEpisode.podcast', 'clipPodcast') .leftJoin(`${tableName}.owner`, 'owner') - .where('owner.id = :loggedInUserId', { loggedInUserId }) as any + .where('owner.id = :loggedInUserId', { loggedInUserId }) + + return qb as any } export const getUserHistoryItems = async (loggedInUserId, query) => { diff --git a/src/controllers/userQueueItem.ts b/src/controllers/userQueueItem.ts index dcb4da45..c6135cba 100644 --- a/src/controllers/userQueueItem.ts +++ b/src/controllers/userQueueItem.ts @@ -144,7 +144,9 @@ export const removeAllUserQueueItems = async (loggedInUserId) => { const repository = getRepository(UserQueueItem) const userQueueItems = await repository.find({ - where: { owner: loggedInUserId } + where: { + owner: loggedInUserId + } }) return repository.remove(userQueueItems) diff --git a/src/entities/playlist.ts b/src/entities/playlist.ts index a4ac57f9..50c3bedd 100644 --- a/src/entities/playlist.ts +++ b/src/entities/playlist.ts @@ -16,6 +16,8 @@ import { UpdateDateColumn } from 'typeorm' import { generateShortId } from '~/lib/utility' +import { PodcastMedium } from 'podverse-shared' +import { podcastMediumAllowedValues } from '~/lib/constants' @Entity('playlists') export class Playlist { @@ -33,6 +35,9 @@ export class Playlist { @Column({ nullable: true }) description?: string + @Column({ default: false }) + isDefault: boolean + @Index() @Column({ default: false }) isPublic: boolean @@ -43,6 +48,13 @@ export class Playlist { @Column('varchar', { array: true }) itemsOrder: string[] + @Column({ + type: 'enum', + enum: podcastMediumAllowedValues, + default: 'mixed' + }) + medium: PodcastMedium + @Index() @Column({ nullable: true }) title?: string diff --git a/src/entities/podcast.ts b/src/entities/podcast.ts index 0fc58022..73b02fef 100644 --- a/src/entities/podcast.ts +++ b/src/entities/podcast.ts @@ -51,6 +51,12 @@ export const podcastItunesTypeDefaultValue = 'episodic' @Index(['hasVideo', 'pastWeekTotalUniquePageviews']) @Index(['hasVideo', 'pastMonthTotalUniquePageviews']) @Index(['hasVideo', 'pastYearTotalUniquePageviews']) +@Index(['medium', 'pastAllTimeTotalUniquePageviews']) +@Index(['medium', 'pastHourTotalUniquePageviews']) +@Index(['medium', 'pastDayTotalUniquePageviews']) +@Index(['medium', 'pastWeekTotalUniquePageviews']) +@Index(['medium', 'pastMonthTotalUniquePageviews']) +@Index(['medium', 'pastYearTotalUniquePageviews']) @Entity('podcasts') export class Podcast { @PrimaryColumn('varchar', { @@ -162,6 +168,9 @@ export class Podcast { @Column({ nullable: true }) linkUrl?: string + // TODO: the Podcast.medium enum is currently missing the "mixed" value. + // It is also missing Medium Lists values (podcastL, musicL, etc.), + // but I don't know how those would fit into our UX yet. @Index() @Column({ type: 'enum', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6e6e3cef..c9ccbbc7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -5,3 +5,15 @@ export const authExpires = () => { authExpires.setDate(authExpires.getDate() + 365) return authExpires } + +export const podcastMediumAllowedValues = [ + 'podcast', + 'music', + 'video', + 'film', + 'audiobook', + 'newsletter', + 'blog', + 'music-video', + 'mixed' +] diff --git a/src/lib/utility/index.ts b/src/lib/utility/index.ts index d17b92c6..d623ea4b 100644 --- a/src/lib/utility/index.ts +++ b/src/lib/utility/index.ts @@ -162,6 +162,8 @@ export const addOrderByToQuery = (qb, type, sort, sortDateKey, allowRandom, isFr qb.orderBy(`${type}.startTime`, ascKey) } else if (sort === 'createdAt') { qb.orderBy(`${type}.createdAt`, descKey) + } else if (sort === 'episode-number-asc') { + qb.orderBy(`${type}.itunesEpisode`, ascKey) } else { // sort = top-past-week qb.orderBy(`${type}.pastWeekTotalUniquePageviews`, descKey) diff --git a/src/middleware/hasMediumQueryParam.ts b/src/middleware/hasMediumQueryParam.ts new file mode 100644 index 00000000..45dbafad --- /dev/null +++ b/src/middleware/hasMediumQueryParam.ts @@ -0,0 +1,25 @@ +import { podcastMediumAllowedValues } from '~/lib/constants' +const createError = require('http-errors') + +export const requireAndParseMediumQueryParam = async (ctx, next) => { + const query = ctx.request.query + + const medium = query?.medium + + if (!medium) { + throw new createError.BadRequest( + `A "medium" query param must be provided with one of the following values: ${podcastMediumAllowedValues}` + ) + } + + if (!podcastMediumAllowedValues.includes(medium)) { + throw new createError.BadRequest('Invalid medium passed as query param') + } + + ctx.state.query = { + ...ctx.state.query, + medium + } + + await next(ctx) +} diff --git a/src/middleware/parseQueryPageOptions.ts b/src/middleware/parseQueryPageOptions.ts index 4c03ed09..653d6047 100644 --- a/src/middleware/parseQueryPageOptions.ts +++ b/src/middleware/parseQueryPageOptions.ts @@ -11,13 +11,16 @@ export const parseQueryPageOptions = async (ctx, next, type = '') => { includeCategories, includeEpisode, includePodcast, + isMusic, liveItemStatus, maxResults, mediaRefId, + medium, name, page, playlistId, podcastId, + podcastsOnly, searchAuthor, searchTitle, sincePubDate, @@ -34,45 +37,31 @@ export const parseQueryPageOptions = async (ctx, next, type = '') => { ...(categories ? { categories } : {}), ...(episodeId ? { episodeId } : {}), ...(hasVideo ? { hasVideo } : {}), + ...(id ? { id } : {}), ...(includeAuthors ? { includeAuthors: includeAuthors === 'true' } : {}), ...(includeCategories ? { includeCategories: includeCategories === 'true' } : {}), ...(includeEpisode ? { includeEpisode: includeEpisode === 'true' } : {}), ...(includePodcast ? { includePodcast: includePodcast === 'true' } : {}), + ...(isMusic ? { isMusic } : {}), ...(liveItemStatus ? { liveItemStatus } : {}), ...(maxResults ? { maxResults: true } : {}), ...(mediaRefId ? { mediaRefId } : {}), + ...(medium ? { medium } : {}), + ...(name ? { name } : {}), ...(playlistId ? { playlistId } : {}), ...(podcastId ? { podcastId } : {}), + ...(podcastsOnly ? { podcastsOnly } : {}), ...(searchTitle ? { searchTitle } : {}), ...(searchAuthor ? { searchAuthor } : {}), ...(searchTitle ? { searchTitle } : {}), ...(sincePubDate ? { sincePubDate } : {}), + ...(slug ? { slug } : {}), + ...(sort ? { sort } : {}), + ...(title ? { title } : {}), ...(topLevelCategories ? { topLevelCategories: topLevelCategories === 'true' } : {}), ...(userIds ? { userIds } : {}) } as any - // NOTE: for some reason when I use more than ~8 spread operators, the src/server.ts - // takes forever to start :( I'd like to understand why this is happening... - if (id) { - options.id = id - } - - if (name) { - options.name = name - } - - if (slug) { - options.slug = slug - } - - if (title) { - options.title = title - } - - if (sort) { - options.sort = sort - } - if (sincePubDate) { // Use a larger than normal limit for sincePubDate requests options.take = 50 diff --git a/src/middleware/queryValidation/create.ts b/src/middleware/queryValidation/create.ts index d9357c19..b5761a74 100644 --- a/src/middleware/queryValidation/create.ts +++ b/src/middleware/queryValidation/create.ts @@ -1,4 +1,5 @@ const Joi = require('joi') +import { podcastMediumAllowedValues } from '~/lib/constants' import { validateBaseBody } from './base' const validateAppStorePurchaseCreate = async (ctx, next) => { @@ -62,6 +63,7 @@ const validatePlaylistCreate = async (ctx, next) => { isPublic: Joi.boolean(), itemsOrder: Joi.array().items(Joi.string()), mediaRefs: Joi.array().items(Joi.string()), + medium: Joi.string().allow(null).allow('').valid(podcastMediumAllowedValues), title: Joi.string().allow(null).allow('') }) diff --git a/src/middleware/queryValidation/search.ts b/src/middleware/queryValidation/search.ts index fdb309ca..97e594da 100644 --- a/src/middleware/queryValidation/search.ts +++ b/src/middleware/queryValidation/search.ts @@ -37,6 +37,7 @@ const validateEpisodeSearch = async (ctx, next) => { const schema = Joi.object().keys({ hasVideo: Joi.boolean(), includePodcast: Joi.boolean(), + isMusic: Joi.boolean(), maxResults: Joi.boolean(), podcastId: Joi.string(), searchTitle: Joi.string().min(2), @@ -71,6 +72,7 @@ const validateLiveItemSearch = async (ctx, next) => { liveItemStatus: Joi.string().valid(...liveItemStatuses), hasVideo: Joi.boolean(), includePodcast: Joi.boolean(), + isMusic: Joi.boolean(), maxResults: Joi.boolean(), podcastId: Joi.string(), searchTitle: Joi.string().min(2), @@ -93,6 +95,7 @@ const validateMediaRefSearch = async (ctx, next) => { hasVideo: Joi.boolean(), includeEpisode: Joi.boolean(), includePodcast: Joi.boolean(), + isMusic: Joi.boolean(), podcastId: Joi.string(), searchTitle: Joi.string().min(2), page: Joi.number().integer().min(0), @@ -122,8 +125,10 @@ const validatePodcastSearch = async (ctx, next) => { hasVideo: Joi.boolean(), includeAuthors: Joi.boolean(), includeCategories: Joi.boolean(), + isMusic: Joi.boolean(), maxResults: Joi.boolean(), podcastId: Joi.string(), + podcastsOnly: Joi.boolean(), searchAuthor: Joi.string().min(2), searchTitle: Joi.string().min(2), page: Joi.number().integer().min(0), diff --git a/src/routes/index.ts b/src/routes/index.ts index c32cce4f..0e4c5fc4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -20,6 +20,7 @@ export { playlistRouter } from '~/routes/playlist' export { podcastRouter } from '~/routes/podcast' export { podcastIndexRouter } from '~/routes/podcastIndex' export { podpingRouter } from '~/routes/podping' +export { secondaryQueueRouter } from '~/routes/secondaryQueue' export { toolsRouter } from '~/routes/tools' export { upDeviceRouter } from '~/routes/upDevice' export { userRouter } from '~/routes/user' diff --git a/src/routes/playlist.ts b/src/routes/playlist.ts index 6d566c9e..d7ee4ed2 100644 --- a/src/routes/playlist.ts +++ b/src/routes/playlist.ts @@ -5,6 +5,7 @@ import { emitRouterError } from '~/lib/errors' import { delimitQueryValues } from '~/lib/utility' import { addOrRemovePlaylistItem, + addOrRemovePlaylistItemToDefaultPlaylist, createPlaylist, deletePlaylist, getPlaylist, @@ -128,7 +129,6 @@ router.delete('/:id', jwtAuth, async (ctx) => { } }) -// Add/remove mediaRef/episode to/from playlist const addOrRemovePlaylistLimiter = RateLimit.middleware({ interval: 1 * 60 * 1000, max: rateLimiterMaxOverride || 30, @@ -136,6 +136,26 @@ const addOrRemovePlaylistLimiter = RateLimit.middleware({ prefixKey: 'patch/add-or-remove' }) +// Add/remove mediaRef/episode to/from default playlist +router.patch('/default/add-or-remove', addOrRemovePlaylistLimiter, jwtAuth, hasValidMembership, async (ctx) => { + try { + const body: any = ctx.request.body + const { episodeId, mediaRefId } = body + + const results = await addOrRemovePlaylistItemToDefaultPlaylist(mediaRefId, episodeId, ctx.state.user.id) + const updatedPlaylist = results[0] as any + const actionTaken = results[1] + ctx.body = { + playlistId: updatedPlaylist.id, + playlistItemCount: updatedPlaylist.itemCount, + actionTaken + } + } catch (error) { + emitRouterError(error, ctx) + } +}) + +// Add/remove mediaRef/episode to/from playlist router.patch('/add-or-remove', addOrRemovePlaylistLimiter, jwtAuth, hasValidMembership, async (ctx) => { try { const body: any = ctx.request.body diff --git a/src/routes/secondaryQueue.ts b/src/routes/secondaryQueue.ts new file mode 100644 index 00000000..41715c9d --- /dev/null +++ b/src/routes/secondaryQueue.ts @@ -0,0 +1,19 @@ +import * as Router from 'koa-router' +import { config } from '~/config' +import { getSecondaryQueueEpisodesForPodcastId } from '~/controllers/secondaryQueue' +import { emitRouterError } from '~/lib/errors' +import { parseNSFWHeader } from '~/middleware/parseNSFWHeader' + +const router = new Router({ prefix: `${config.apiPrefix}${config.apiVersion}/secondary-queue` }) + +// Get episodes that are adjacent to a podcast +router.get('/episode/:episodeId/podcast/:podcastId', parseNSFWHeader, async (ctx) => { + try { + const data = await getSecondaryQueueEpisodesForPodcastId(ctx.params.episodeId, ctx.params.podcastId) + ctx.body = data + } catch (error) { + emitRouterError(error, ctx) + } +}) + +export const secondaryQueueRouter = router diff --git a/src/seeds/qa/feeds.ts b/src/seeds/qa/feeds.ts index 676bbc5d..1af79366 100644 --- a/src/seeds/qa/feeds.ts +++ b/src/seeds/qa/feeds.ts @@ -1,11 +1,14 @@ +import { getLightningKeysendValueItem } from 'podverse-shared' import { Connection } from 'typeorm' +import { getEpisodesByPodcastIds } from '~/controllers/episode' +import { getPodcastByPodcastIndexId } from '~/controllers/podcast' import { podcastIndexIds, podcastIndexIdsQuick, podcastIndexIdsWithLiveItems, podcastIndexIdsWithLiveItemsQuick } from '~/seeds/qa/podcastIndexIds' -import { addOrUpdatePodcastFromPodcastIndex } from '~/services/podcastIndex' +import { addOrUpdatePodcastFromPodcastIndex, getPodcastFromPodcastIndexByGuid } from '~/services/podcastIndex' export const parseQAFeeds = async (connection: Connection, isQuickRun: boolean) => { const pIds = isQuickRun ? podcastIndexIdsQuick : podcastIndexIds @@ -17,4 +20,43 @@ export const parseQAFeeds = async (connection: Connection, isQuickRun: boolean) for (const id of pIdsLive) { await addOrUpdatePodcastFromPodcastIndex(connection, id.toString()) } + + /* + Boostagram Ball is 6524027 and we handle it as a special podcast during the qa script. + We use it as an example of a feed that has value time splits with remoteItem, + but remoteItems will only work if we also parse the remote RSS feeds those + remoteItem tags reference. In order to automatically populate some remoteItem data + we can use in testing, the seed script will grab the value time splits for + the latest episode of Boostagram Ball, and parse and save + each remoteItem's RSS feed as well. + */ + const boostagramBallPodcastIndexId = 6524027 + const boostagramBall = await getPodcastByPodcastIndexId(boostagramBallPodcastIndexId) + if (boostagramBall?.id) { + const boostagramBallEpisodes = await getEpisodesByPodcastIds({ podcastId: boostagramBall.id, sort: 'most-recent' }) + const latestBoostagramBallEpisode = boostagramBallEpisodes?.[0]?.[0] + if (latestBoostagramBallEpisode) { + const valueTags = latestBoostagramBallEpisode.value + if (valueTags && valueTags.length > 0) { + const lightningKeysendValueItem = getLightningKeysendValueItem(valueTags) + if (lightningKeysendValueItem) { + const valueTimeSplits = lightningKeysendValueItem.valueTimeSplits + if (valueTimeSplits) { + for (const valueTaggg of valueTimeSplits) { + try { + const valueTag: any = valueTaggg + const podcastIndexPodcast = await getPodcastFromPodcastIndexByGuid(valueTag?.remoteItem?.feedGuid) + const podcastIndexId = podcastIndexPodcast?.feed?.id + if (podcastIndexId) { + await addOrUpdatePodcastFromPodcastIndex(connection, podcastIndexId) + } + } catch (error) { + // assume a 404 + } + } + } + } + } + } + } } diff --git a/src/seeds/qa/playlists.ts b/src/seeds/qa/playlists.ts index a5c4016e..22391b4a 100644 --- a/src/seeds/qa/playlists.ts +++ b/src/seeds/qa/playlists.ts @@ -1,6 +1,10 @@ import { faker } from '@faker-js/faker' import { _logEnd, _logStart, logPerformance } from '~/lib/utility' -import { addOrRemovePlaylistItem, createPlaylist } from '~/controllers/playlist' +import { + addOrRemovePlaylistItem, + addOrRemovePlaylistItemToDefaultPlaylist, + createPlaylist +} from '~/controllers/playlist' import { getRandomMediaRefIds } from './mediaRefs' import { getRandomEpisodeIds } from './episodes' import { generateQAItemsForUsers, getQABatchRange } from './utility' @@ -39,6 +43,16 @@ const generatePlaylistsForUser = async (userId: string) => { await addOrRemovePlaylistItem(playlist.id, mediaRefIds[i], '', userId) } } + + for (let j = 5; j < 10; j++) { + const episodeIds = getQABatchRange(episodeIdsFull, j) + const mediaRefIds = getQABatchRange(mediaRefIdsFull, j) + + for (let j = 5; j < 10; j++) { + await addOrRemovePlaylistItemToDefaultPlaylist('', episodeIds[j], userId) + await addOrRemovePlaylistItemToDefaultPlaylist(mediaRefIds[j], '', userId) + } + } } const getRandomTitle = () => { diff --git a/src/seeds/qa/podcastIndexIds.ts b/src/seeds/qa/podcastIndexIds.ts index eb4464cf..1c009d77 100644 --- a/src/seeds/qa/podcastIndexIds.ts +++ b/src/seeds/qa/podcastIndexIds.ts @@ -15,4 +15,4 @@ export const podcastIndexIdsWithLiveItems = [ 288180, 955598, 6524027 ] -export const podcastIndexIdsWithLiveItemsQuick = [4935828, 5495489, 162612, 5461087] +export const podcastIndexIdsWithLiveItemsQuick = [4935828, 5495489, 162612, 5461087, 6524027] diff --git a/src/services/parser.ts b/src/services/parser.ts index 18650d49..4fc6d624 100644 --- a/src/services/parser.ts +++ b/src/services/parser.ts @@ -225,9 +225,9 @@ export const parseFeedUrl = async (feedUrl, forceReparsing = false, cacheBust = // funding: Array.isArray(episode.podcastFunding) ? episode.podcastFunding?.map((f) => fundingCompat(f)) : [], guid: episode.guid, imageURL: episode.image, - itunesEpisode: episode.itunesEpisode, + itunesEpisode: episode.podcastEpisode?.number || episode.itunesEpisode, itunesEpisodeType: episode.itunesEpisodeType, - itunesSeason: episode.itunesSeason, + itunesSeason: episode.podcastSeason?.number || episode.itunesSeason, link: episode.link, pubDate: episode.pubDate, socialInteraction: episode.podcastSocialInteraction ?? [], diff --git a/src/services/podcastIndex.ts b/src/services/podcastIndex.ts index 53677c64..5392b44e 100644 --- a/src/services/podcastIndex.ts +++ b/src/services/podcastIndex.ts @@ -195,6 +195,23 @@ export const getPodcastValueTagForPodcastIndexId = async (id: string) => { return pvValueTagArray } +export const getPodcastFromPodcastIndexByGuid = async (podcastGuid: string) => { + const url = `${podcastIndexConfig.baseUrl}/podcasts/byguid?guid=${podcastGuid}` + let podcastIndexPodcast: any = null + try { + const response = await axiosRequest(url) + podcastIndexPodcast = response.data + } catch (error) { + // assume a 404 + } + + if (!podcastIndexPodcast) { + throw new createError.NotFound('Podcast not found in Podcast Index') + } + + return podcastIndexPodcast +} + // These getValueTagFor* services were intended for getting "value tag" info from Podcast Index, // but at this point they more broadly is for retrieving the "remote item" data // our client side apps need. The most common use case involves needing value tags diff --git a/src/services/stats.ts b/src/services/stats.ts index 9016d294..988d4f58 100644 --- a/src/services/stats.ts +++ b/src/services/stats.ts @@ -9,7 +9,11 @@ const moment = require('moment') enum PagePaths { clips = 'clip', episodes = 'episode', - podcasts = 'podcast' + podcasts = 'podcast', + albums = 'albums', + tracks = 'tracks', + channels = 'channels', + videos = 'videos' } enum StartDateOffset { @@ -35,12 +39,12 @@ enum TimeRanges { allTime = 'pastAllTimeTotalUniquePageviews' } -export const queryUniquePageviews = async (pagePath, timeRange) => { +export const queryUniquePageviews = async (pagePath: PagePaths, timeRange) => { const startDateOffset = parseInt(StartDateOffset[timeRange], 10) if (!Object.keys(PagePaths).includes(pagePath)) { console.log('A valid pagePath must be provided in the first parameter.') - console.log('Valid options are: podcasts, episodes, clips') + console.log('Valid options are: podcasts, episodes, clips, albums, tracks, channels, videos') return } @@ -81,31 +85,57 @@ export const queryUniquePageviews = async (pagePath, timeRange) => { } const podcastLimit = 42 // https://podverse.fm/podcast/12345678901234 - const episodeLimit = 42 // https://podverse.fm/podcast/12345678901234 + const episodeLimit = 42 // https://podverse.fm/episode/12345678901234 const clipLimit = 39 // https://podverse.fm/clip/12345678901234 + const albumLimit = 40 // https://podverse.fm/album/12345678901234 + const trackLimit = 40 // https://podverse.fm/track/12345678901234 + const channelLimit = 42 // https://podverse.fm/channel/12345678901234 + const videoLimit = 40 // https://podverse.fm/video/12345678901234 let filteredData: any[] = [] - if (pagePath === 'podcasts') { + if (pagePath === PagePaths.podcasts) { filteredData = filterCustomFeedUrls(data, podcastLimit) - } else if (pagePath === 'episodes') { + } else if (pagePath === PagePaths.episodes) { filteredData = filterCustomFeedUrls(data, episodeLimit) - } else if (pagePath === 'clips') { + } else if (pagePath === PagePaths.clips) { filteredData = filterCustomFeedUrls(data, clipLimit) + } else if (pagePath === PagePaths.albums) { + filteredData = filterCustomFeedUrls(data, albumLimit) + } else if (pagePath === PagePaths.tracks) { + filteredData = filterCustomFeedUrls(data, trackLimit) + } else if (pagePath === PagePaths.channels) { + filteredData = filterCustomFeedUrls(data, channelLimit) + } else if (pagePath === PagePaths.videos) { + filteredData = filterCustomFeedUrls(data, videoLimit) } await savePageviewsToDatabase(pagePath, timeRange, filteredData) } -const savePageviewsToDatabase = async (pagePath, timeRange, data) => { +const getTableName = (pagePath: PagePaths) => { + let tableName = TableNames[pagePath] + if (pagePath === PagePaths.albums) { + tableName = TableNames['podcasts'] + } else if (pagePath === PagePaths.tracks) { + tableName = TableNames['episodes'] + } else if (pagePath === PagePaths.channels) { + tableName = TableNames['podcasts'] + } else if (pagePath === PagePaths.videos) { + tableName = TableNames['episodes'] + } + return tableName +} + +const savePageviewsToDatabase = async (pagePath: PagePaths, timeRange, data) => { await connectToDb() const matomoDataRows = data - const tableName = TableNames[pagePath] + const tableName = getTableName(pagePath) console.log('savePageviewsToDatabase') console.log('pagePath', pagePath) + console.log('tableName', tableName) console.log('timeRange', timeRange) console.log('matomoDataRows.length', matomoDataRows.length) - console.log('tableName', tableName) console.log('TimeRange', TimeRanges[timeRange]) /* diff --git a/yarn.lock b/yarn.lock index bc0d9b9b..631e8ba8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1928,12 +1928,14 @@ axe-core@^4.4.3: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f" integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w== -axios@0.21.2: - version "0.21.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" - integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== +axios@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" axobject-query@^2.2.0: version "2.2.0" @@ -4319,10 +4321,10 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.14.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== for-in@^1.0.2: version "1.0.2" @@ -4352,6 +4354,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -8179,15 +8190,16 @@ podcast-partytime@4.6.2: ramda "^0.27.1" tiny-invariant "^1.2.0" -podverse-shared@^4.14.9: - version "4.14.9" - resolved "https://registry.yarnpkg.com/podverse-shared/-/podverse-shared-4.14.9.tgz#19e3823cbe9f18fcb43daa93e56ca92b9be2797d" - integrity sha512-AZplb5KhkM41Y4+fSHeLvKu24T2TMcUgA0eFT3p7cVfPODGYiAkUgJPo4UXydMKJ5oOI/WOzyrYvWdw+0iHDnQ== +podverse-shared@^4.14.15: + version "4.14.15" + resolved "https://registry.yarnpkg.com/podverse-shared/-/podverse-shared-4.14.15.tgz#c6a9ce60a0e4a5c03ccabf4e9fe0949b1d1a2682" + integrity sha512-aZ6x8DzB9wvLRBgRP8X+/4zoLRG/p3eoEyjJ1wOQhPAVKbPN2q1E/nQ55bHkdFQP+wLcAdGDZ8JPpyvIXIEpug== dependencies: "@types/lodash" "4.14.172" he "^1.2.0" html-entities "^2.3.2" lodash "4.17.21" + moment "^2.29.4" striptags "^3.2.0" typescript "3.9.7" @@ -8313,6 +8325,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"