Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backend and API functionality for uploaded videos #2581

Merged
merged 1 commit into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/api/posts/src/data/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ function ensureFeed(feed) {
return feed instanceof Feed ? Promise.resolve(feed) : Feed.byId(feed);
}

/**
* @param {string} url
dbelokon marked this conversation as resolved.
Show resolved Hide resolved
* @returns {"video" | "blogpost"} the post's type
*/
function determinePostType(url) {
try {
const associatedLink = new URL(url);

if (associatedLink.hostname.includes('youtube.com')) {
dbelokon marked this conversation as resolved.
Show resolved Hide resolved
return 'video';
}
// Assume that we are dealing with a blogpost if we
// are not dealing with videos
return 'blogpost';
} catch {
return 'blogpost';
}
}

class Post {
constructor(title, html, datePublished, dateUpdated, postUrl, guid, feed) {
// Use the post's guid as our unique identifier
Expand All @@ -46,6 +65,7 @@ class Post {
// create an absolute url if postURL is relative
this.url = new URL(postUrl, feed.url).href;
this.guid = guid;
this.type = determinePostType(this.url);
humphd marked this conversation as resolved.
Show resolved Hide resolved

if (!(feed instanceof Feed)) {
throw new Error(`expected feed to be a Feed Object, got '${feed}'`);
Expand Down
44 changes: 42 additions & 2 deletions src/api/posts/test/posts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe('/posts', () => {

describe('test /posts/:id responses', () => {
const missingGuid = 'http://missing-guid';
const youtubeGuid = 'http://youtube.com';
const randomGuid = 'http://random-guid';

const feed1 = new Feed(
Expand All @@ -93,7 +94,16 @@ describe('test /posts/:id responses', () => {
null
);

beforeAll(() => Promise.resolve(addFeed(feed1)));
const youtubeFeed = new Feed(
'YouTube Author',
'http://youtube.com/feed/videos.xml',
'user',
'https://youtube.com/',
null,
null
);

beforeAll(() => Promise.all([addFeed(feed1), addFeed(youtubeFeed)]));

const addedPost1 = new Post(
'Post Title',
Expand All @@ -105,11 +115,23 @@ describe('test /posts/:id responses', () => {
feed1
);

beforeAll(() => Promise.resolve(addPost(addedPost1)));
const addedVideo1 = new Post(
'YouTube Video Title',
'YouTube Video Description',
new Date('2009-09-07T22:20:00.000Z'),
new Date('2009-09-07T22:23:00.000Z'),
'https://youtube.com/watch',
youtubeGuid,
youtubeFeed
);

beforeAll(() => Promise.all([addPost(addedPost1), addPost(addedVideo1)]));

beforeAll(() => {
feed1.save();
youtubeFeed.save();
addedPost1.save();
addedVideo1.save();
});

test('A post with an id should be returned and match the id of a post from redis', async () => {
Expand Down Expand Up @@ -160,4 +182,22 @@ describe('test /posts/:id responses', () => {
expect(res.status).toEqual(404);
expect(res.get('Content-length')).toEqual('46');
});

test('request a post with type equal to "blogpost"', async () => {
const res = await request(app).get(`/${addedPost1.id}`).set('Accept', 'application/json');
const post = await getPost(`${addedPost1.id}`);
expect(res.status).toEqual(200);
expect(res.get('Content-type')).toContain('application/json');
expect(res.body.id).toEqual(post.id);
expect(res.body.type).toEqual('blogpost');
});

test('request a post with type equal to "video"', async () => {
const res = await request(app).get(`/${addedVideo1.id}`).set('Accept', 'application/json');
const post = await getPost(`${addedVideo1.id}`);
expect(res.status).toEqual(200);
expect(res.get('Content-type')).toContain('application/json');
expect(res.body.id).toEqual(post.id);
expect(res.body.type).toEqual('video');
});
});
22 changes: 22 additions & 0 deletions src/backend/data/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ function ensureFeed(feed) {
return feed instanceof Feed ? Promise.resolve(feed) : Feed.byId(feed);
}

/**
* @param {string} url
* @returns {"video" | "blogpost"} the post's type
*/
function determinePostType(url) {
try {
const associatedLink = new URL(url);

if (associatedLink.hostname.includes('youtube.com')) {
return 'video';
}
// Assume that we are dealing with a blogpost if we
// are not dealing with videos
return 'blogpost';
} catch {
return 'blogpost';
}
}

class Post {
constructor(title, html, datePublished, dateUpdated, postUrl, guid, feed) {
// Use the post's guid as our unique identifier
Expand All @@ -50,6 +69,7 @@ class Post {
// create an absolute url if postURL is relative
this.url = new URL(postUrl, feed.url).href;
this.guid = guid;
this.type = determinePostType(this.url);

// We expect to get a real Feed vs. a feed id
if (!(feed instanceof Feed)) {
Expand Down Expand Up @@ -96,6 +116,8 @@ class Post {

if (article.contentEncoded) article.content = article.contentEncoded;

if (article.mediaGroup) article.content = article.mediaGroup['media:description'];

// A valid RSS/Atom feed can have missing fields that we care about.
// Keep track of any that are missing, and throw if necessary.
const missing = [];
Expand Down
6 changes: 6 additions & 0 deletions src/backend/feed/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,19 @@ module.exports = async function processor(job) {
['pubDate', 'pubdate'],
['creator', 'author'],
['content:encoded', 'contentEncoded'],
['updated', 'date'],
['id', 'guid'],
['media:group', 'mediaGroup'],
['published', 'pubdate'],
],
},
},
feed
)
);

const articles = await parser.parseURL(feed.url);

// Transform the list of articles to a list of Post objects
await articlesToPosts(articles.items, feed);

Expand Down
17 changes: 17 additions & 0 deletions test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const getRealWorldRssUri = () => 'https://blog.humphd.org/tag/seneca/rss/';
const getRealWorldRssBody = () =>
fs.readFileSync(path.join(__dirname, './test_files/blog.humphd.org.rss'));

// Use David Humphrey's channel for a realistic test case of YouTube channel
const getRealWorldYouTubeFeedUri = () =>
'https://www.youtube.com/feeds/videos.xml?channel_id=UCqaMbMDf01BLttof1lHAo2A';
const getRealWorldYouTubeFeedBody = () =>
fs.readFileSync(path.join(__dirname, './test_files/humphd-yt-channel.xml'));

// Portion of https://www.feedforall.com/sample.xml
const getValidFeedBody = () =>
`
Expand Down Expand Up @@ -131,6 +137,7 @@ exports.getAtomUri = getAtomUri;
exports.getRssUri = getRssUri;
exports.getHtmlUri = getHtmlUri;
exports.getRealWorldRssUri = getRealWorldRssUri;
exports.getRealWorldYouTubeFeedUri = getRealWorldYouTubeFeedUri;
exports.stripProtocol = stripProtocol;
exports.getInvalidDescription = getInvalidDescription;

Expand Down Expand Up @@ -162,4 +169,14 @@ exports.nockRealWorldRssResponse = function (headers = {}) {
nockResponse(getRealWorldRssUri(), getRealWorldRssBody(), 200, 'application/rss+xml', headers);
};

exports.nockRealWorldYouTubeFeedResponse = function (headers = {}) {
nockResponse(
getRealWorldYouTubeFeedUri(),
getRealWorldYouTubeFeedBody(),
200,
'application/rss+xml',
headers
);
};

exports.createMockJobObjectFromFeedId = (id) => ({ data: { id } });
29 changes: 28 additions & 1 deletion test/post.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ const parse = new Parser({
['pubDate', 'pubdate'],
['creator', 'author'],
['content:encoded', 'contentEncoded'],
['updated', 'date'],
['id', 'guid'],
['media:group', 'mediaGroup'],
['published', 'pubdate'],
],
},
});

const {
nockRealWorldRssResponse,
nockRealWorldYouTubeFeedResponse,
getRealWorldYouTubeFeedUri,
getRealWorldRssUri,
getInvalidDescription,
} = require('./fixtures');
Expand All @@ -32,6 +38,7 @@ describe('Post data class tests', () => {
url: 'https://user.post.com/?post-id=123',
guid: 'https://user.post.com/?post-id=123&guid',
id: hash('https://user.post.com/?post-id=123&guid'),
type: 'blogpost',
};

beforeAll(async () => {
Expand Down Expand Up @@ -175,7 +182,7 @@ describe('Post data class tests', () => {
expect(result).toBe(null);
});

describe('Post.createFromArticle() tests', () => {
describe('Post.createFromArticle() with blog feeds tests', () => {
let articles;
beforeEach(async () => {
nockRealWorldRssResponse();
Expand Down Expand Up @@ -282,4 +289,24 @@ describe('Post data class tests', () => {
await expect(Post.createFromArticle(article, feed)).rejects.toThrow();
});
});

describe('Post.createFromArticle() with youtube feeds tests', () => {
let articles;
beforeEach(async () => {
nockRealWorldYouTubeFeedResponse();
articles = await parse.parseURL(getRealWorldYouTubeFeedUri());
expect(Array.isArray(articles.items)).toBe(true);
expect(articles.items.length).toBe(15);
});

test('Post.createFromArticle() should create Post with YouTube video article', async () => {
const article = articles.items[0];
const id = await Post.createFromArticle(article, feed);
const videoPost = await Post.byId(id);

expect(videoPost.title).toBe('DPS909 OSD600 Week 03 - Fixing a Bug in the Azure JS SDK');
expect(videoPost.url).toBe('https://www.youtube.com/watch?v=mNuHA7vH6Wc');
dbelokon marked this conversation as resolved.
Show resolved Hide resolved
expect(videoPost.type).toBe('video');
});
});
});
Loading