Skip to content

Commit

Permalink
Merge pull request #32 from hiteshchoudhary/feat/youtube-api
Browse files Browse the repository at this point in the history
Quick YouTube public apis without any credentials
- Public YouTube API filled with Hitesh sir's youtube channel data.
- Equivalent to the YouTube's public api, but FREE, requiring no cumbersome API Key or credentials!
- Offering a wide array of endpoints that provide access to video lists, video details (complete with statistics), video comments, related videos, channel playlists, playlist items and channel details (accompanied by comprehensive channel statistics)
- With this wealth of information at fingertips, one can confidently develop their very own YouTube clone without the hassle of dealing with a complex API setup.
  • Loading branch information
wajeshubham authored Jul 25, 2023
2 parents bc38eb7 + a129d7c commit 51f7c05
Show file tree
Hide file tree
Showing 12 changed files with 48,349 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,4 @@ logs
!/public/temp/.gitkeep

NOTES.md
.DS_Store
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import quoteRouter from "./routes/public/quote.routes.js";
import randomjokeRouter from "./routes/public/randomjoke.routes.js";
import randomproductRouter from "./routes/public/randomproduct.routes.js";
import randomuserRouter from "./routes/public/randomuser.routes.js";
import youtubeRouter from "./routes/public/youtube.routes.js";

// * App routes
import userRouter from "./routes/apps/auth/user.routes.js";
Expand Down Expand Up @@ -127,6 +128,7 @@ app.use("/api/v1/public/quotes", quoteRouter);
app.use("/api/v1/public/meals", mealRouter);
app.use("/api/v1/public/dogs", dogRouter);
app.use("/api/v1/public/cats", catRouter);
app.use("/api/v1/public/youtube", youtubeRouter);

// * App apis
app.use("/api/v1/users", userRouter);
Expand Down
12 changes: 12 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export const UserLoginType = {

export const AvailableSocialLogins = Object.values(UserLoginType);

/**
* @type {{ MOST_VIEWED: "mostViewed"; MOST_LIKED: "mostLiked"; LATEST: "latest"; OLDEST: "oldest"} as const}
*/
export const YouTubeFilterEnum = {
MOST_VIEWED: "mostViewed",
MOST_LIKED: "mostLiked",
LATEST: "latest",
OLDEST: "oldest",
};

export const AvailableYouTubeFilters = Object.values(YouTubeFilterEnum);

export const USER_TEMPORARY_TOKEN_EXPIRY = 20 * 60 * 1000; // 20 minutes

export const MAXIMUM_SUB_IMAGE_COUNT = 4;
Expand Down
242 changes: 242 additions & 0 deletions src/controllers/public/youtube.controllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { YouTubeFilterEnum, AvailableYouTubeFilters } from "../../constants.js";
import channelJson from "../../json/youtube/channel.json" assert { type: "json" };
import commentsJson from "../../json/youtube/comments.json" assert { type: "json" };
import playlistItemsJson from "../../json/youtube/playlistitems.json" assert { type: "json" };
import playlistsJson from "../../json/youtube/playlists.json" assert { type: "json" };
import videosJson from "../../json/youtube/videos.json" assert { type: "json" };
import { ApiError } from "../../utils/ApiError.js";
import { ApiResponse } from "../../utils/ApiResponse.js";
import { asyncHandler } from "../../utils/asyncHandler.js";
import { filterObjectKeys, getPaginatedPayload } from "../../utils/helpers.js";

const getChannelDetails = asyncHandler(async (req, res) => {
const channelDetails = channelJson.channel;
return res
.status(200)
.json(
new ApiResponse(
200,
channelDetails,
"Channel details fetched successfully!"
)
);
});

const getPlaylists = asyncHandler(async (req, res) => {
const page = +(req.query.page || 1);
const limit = +(req.query.limit || 10);

const playlists = playlistsJson;

return res
.status(200)
.json(
new ApiResponse(
200,
getPaginatedPayload(playlists, page, limit),
"Playlists fetched successfully"
)
);
});

const getPlaylistById = asyncHandler(async (req, res) => {
const { playlistId } = req.params;

// filter out the playlist by id from json array
const playlist = playlistsJson.find((playlist) => playlist.id === playlistId);

if (!playlist) {
throw new ApiError(
404,
"Playlist with ID " + playlistId + " Does not exist"
);
}

// get the channel info
const channel = {
info: channelJson.channel.info.snippet,
statistics: channelJson.channel.info.statistics,
};

// Get the playlist items in the playlist
const playlistItems = playlistItemsJson.filter(
(item) => item.snippet.playlistId === playlistId
);

return res.status(200).json(
new ApiResponse(
200,
{
channel,
playlist,
playlistItems,
},
"Playlist fetched successfully"
)
);
});

const getVideos = asyncHandler(async (req, res) => {
const page = +(req.query.page || 1);
const limit = +(req.query.limit || 10);

/**
* @type {AvailableYouTubeFilters[0]}
*/
const sortBy = req.query.sortBy;

const query = req.query.query?.toLowerCase(); // search query
const inc = req.query.inc?.split(","); // only include fields mentioned in this query

const videos = videosJson.channelVideos;

let videosArray = query
? structuredClone(videos).filter((video) => {
return (
// Search videos based on title, description and tags
video.items.snippet.title.toLowerCase().includes(query) ||
video.items.snippet.tags?.includes(query) ||
video.items.snippet.description?.includes(query)
);
})
: structuredClone(videos);

if (inc && inc[0]?.trim()) {
videosArray = filterObjectKeys(inc, videosArray);
}

switch (sortBy) {
case YouTubeFilterEnum.LATEST:
// sort by publishedAt key in descending order
videosArray.sort(
(a, b) =>
new Date(b.items.snippet.publishedAt) -
new Date(a.items.snippet.publishedAt)
);
break;
case YouTubeFilterEnum.OLDEST:
// sort by publishedAt key in ascending order
videosArray.sort(
(a, b) =>
new Date(a.items.snippet.publishedAt) -
new Date(b.items.snippet.publishedAt)
);
break;
case YouTubeFilterEnum.MOST_LIKED:
videosArray.sort(
(a, b) => +b.items.statistics.likeCount - +a.items.statistics.likeCount
);
break;
case YouTubeFilterEnum.MOST_VIEWED:
videosArray.sort(
(a, b) => +b.items.statistics.viewCount - +a.items.statistics.viewCount
);
break;
default:
// Return latest videos by default
videosArray.sort(
(a, b) =>
new Date(b.items.snippet.publishedAt) -
new Date(a.items.snippet.publishedAt)
);
break;
}

return res
.status(200)
.json(
new ApiResponse(
200,
getPaginatedPayload(videosArray, page, limit),
"Videos fetched successfully"
)
);
});

const getVideoById = asyncHandler(async (req, res) => {
const { videoId } = req.params;

// get filter based on id
const video = videosJson.channelVideos.find(
(video) => video.items.id === videoId
);

if (!video) {
throw new ApiError(404, "Video with ID " + videoId + " Does not exist");
}

const channel = {
info: channelJson.channel.info.snippet,
statistics: channelJson.channel.info.statistics,
};

return res.status(200).json(
new ApiResponse(
200,
{
channel,
video,
},
"Video fetched successfully"
)
);
});

const getVideoComments = asyncHandler(async (req, res) => {
const { videoId } = req.params;

const video = videosJson.channelVideos.find(
(video) => video.items.id === videoId
);

if (!video) {
throw new ApiError(404, "Video with ID " + videoId + " Does not exist");
}

const comments = commentsJson[videoId]?.items || [];

return res
.status(200)
.json(
new ApiResponse(200, comments, "Video comments fetched successfully")
);
});

const getRelatedVideos = asyncHandler(async (req, res) => {
const { videoId } = req.params;
const page = +(req.query.page || 1);
const limit = +(req.query.limit || 10);

const video = videosJson.channelVideos.find(
(video) => video.items.id === videoId
);

if (!video) {
throw new ApiError(404, "Video with ID " + videoId + " Does not exist");
}

// related videos are all except the selected one
const relatedVideos = videosJson.channelVideos.filter(
(video) => video.items.id !== videoId
);

return res
.status(200)
.json(
new ApiResponse(
200,
getPaginatedPayload(relatedVideos, page, limit),
"Related videos fetched successfully"
)
);
});

export {
getChannelDetails,
getPlaylistById,
getPlaylists,
getRelatedVideos,
getVideoById,
getVideoComments,
getVideos,
};
55 changes: 55 additions & 0 deletions src/json/youtube/channel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"channel": {
"kind": "youtube#channelListResponse",
"info": {
"kind": "youtube#channel",
"id": "UCXgGY0wkgOzynnHvSEVmE3A",
"snippet": {
"title": "Hitesh Choudhary",
"description": "Website: https://hiteshchoudhary.com\nHey there everyone, Hitesh here back again with another video!\nThis means I create a lot of videos, every single week. I cover a wide range of subjects like programming, what's latest in tech, new frameworks, open-source products etc. I keep my interest in a wide area of tech like Javascript, Python, PHP, Machine Learning, etc.\n\nFor the Business purpose, Sponsorships and invitation, reach out at [email protected]\n\nNOTE: Personal questions and code-related questions are not answered at this email. Post them in the course discussion section or react me out at social platforms.\n\n#iWriteCode\n\nInstagram: https://instagram.com/hiteshchoudharyofficial\nFacebook: www.fb.com/HiteshChoudharyPage",
"customUrl": "@hiteshchoudharydotcom",
"publishedAt": "2011-10-24T10:25:16Z",
"thumbnails": {
"default": {
"url": "https://yt3.ggpht.com/ytc/AOPolaTLK52bUQ_YHxgb7RK8GMt_bksIMavy-aEZ9fUOvg=s88-c-k-c0x00ffffff-no-rj",
"width": 88,
"height": 88
},
"medium": {
"url": "https://yt3.ggpht.com/ytc/AOPolaTLK52bUQ_YHxgb7RK8GMt_bksIMavy-aEZ9fUOvg=s240-c-k-c0x00ffffff-no-rj",
"width": 240,
"height": 240
},
"high": {
"url": "https://yt3.ggpht.com/ytc/AOPolaTLK52bUQ_YHxgb7RK8GMt_bksIMavy-aEZ9fUOvg=s800-c-k-c0x00ffffff-no-rj",
"width": 800,
"height": 800
}
},
"localized": {
"title": "Hitesh Choudhary",
"description": "Website: https://hiteshchoudhary.com\nHey there everyone, Hitesh here back again with another video!\nThis means I create a lot of videos, every single week. I cover a wide range of subjects like programming, what's latest in tech, new frameworks, open-source products etc. I keep my interest in a wide area of tech like Javascript, Python, PHP, Machine Learning, etc.\n\nFor the Business purpose, Sponsorships and invitation, reach out at [email protected]\n\nNOTE: Personal questions and code-related questions are not answered at this email. Post them in the course discussion section or react me out at social platforms.\n\n#iWriteCode\n\nInstagram: https://instagram.com/hiteshchoudharyofficial\nFacebook: www.fb.com/HiteshChoudharyPage"
},
"country": "IN"
},
"statistics": {
"viewCount": "54384618",
"subscriberCount": "801000",
"hiddenSubscriberCount": false,
"videoCount": "1442"
},
"brandingSettings": {
"channel": {
"title": "Hitesh Choudhary",
"description": "Website: https://hiteshchoudhary.com\nHey there everyone, Hitesh here back again with another video!\nThis means I create a lot of videos, every single week. I cover a wide range of subjects like programming, what's latest in tech, new frameworks, open-source products etc. I keep my interest in a wide area of tech like Javascript, Python, PHP, Machine Learning, etc.\n\nFor the Business purpose, Sponsorships and invitation, reach out at [email protected]\n\nNOTE: Personal questions and code-related questions are not answered at this email. Post them in the course discussion section or react me out at social platforms.\n\n#iWriteCode\n\nInstagram: https://instagram.com/hiteshchoudharyofficial\nFacebook: www.fb.com/HiteshChoudharyPage",
"keywords": "Programming computers code hitesh udemy Udacity Edx \"machine learning\" python javascript devops cloud",
"unsubscribedTrailer": "xJq0EQMFGyg",
"country": "IN"
},
"image": {
"bannerExternalUrl": "https://yt3.googleusercontent.com/xGlR3Vz-RYHgwRj50-VEdBksVyjyJhvzQUEVttMCd5iRVdw-OXdFkPBPswF2nG_13QR2UfXnCQ"
}
}
}
}
}
Loading

0 comments on commit 51f7c05

Please sign in to comment.