Skip to content

Commit

Permalink
YT-43: Page for fetching videos by channel name (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanL123 authored Sep 30, 2024
1 parent f537764 commit 4fb2340
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 17 deletions.
10 changes: 7 additions & 3 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ export const getVideosFromPlaylist = onCall(async (request) => {
});

export const getVideosFromChannel = onCall(async (request) => {
const channelId = request.data.id;
const channelHandle = request.data.id;
const channels = await youtubeClient.channels.list({
forHandle: channelHandle,
part: ["snippet"],
});
const channelVideos = await youtubeClient.search.list({
channelId: channelId,
part: ["contentDetails", "snippet"],
channelId: channels.data?.items?.[0]?.id ?? "",
part: ["snippet"],
maxResults: 50,
});

Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const theme = createTheme({
const root = createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<CssBaseline enableColorScheme />
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<App />
</ThemeProvider>
</React.StrictMode>,
Expand Down
144 changes: 143 additions & 1 deletion src/pages/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,155 @@ Array [
</fieldset>
</div>
</div>
<div
className="MuiBox-root css-8atqhb"
>
<div
className="MuiFormControl-root css-1nrlq1o-MuiFormControl-root"
>
<label
className="MuiFormLabel-root MuiFormLabel-colorPrimary css-u4tvz2-MuiFormLabel-root"
>
Type
</label>
<div
className="MuiFormGroup-root MuiFormGroup-row MuiRadioGroup-root MuiRadioGroup-row css-qfz70r-MuiFormGroup-root"
role="radiogroup"
>
<label
className="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementEnd css-j204z7-MuiFormControlLabel-root"
>
<span
className="MuiButtonBase-root MuiRadio-root MuiRadio-colorPrimary PrivateSwitchBase-root MuiRadio-root MuiRadio-colorPrimary Mui-checked MuiRadio-root MuiRadio-colorPrimary css-vqmohf-MuiButtonBase-root-MuiRadio-root"
onBlur={[Function]}
onContextMenu={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={null}
>
<input
checked={true}
className="PrivateSwitchBase-input css-1m9pwf3"
disabled={false}
name=":r3:"
onChange={[Function]}
required={false}
type="radio"
value="Playlist"
/>
<span
className="css-hyxlzm"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1hbvpl3-MuiSvgIcon-root"
data-testid="RadioButtonUncheckedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-11zohuh-MuiSvgIcon-root"
data-testid="RadioButtonCheckedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</span>
</span>
<span
className="MuiTypography-root MuiTypography-body1 MuiFormControlLabel-label css-ahj2mt-MuiTypography-root"
>
Playlist
</span>
</label>
<label
className="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementEnd css-j204z7-MuiFormControlLabel-root"
>
<span
className="MuiButtonBase-root MuiRadio-root MuiRadio-colorPrimary PrivateSwitchBase-root MuiRadio-root MuiRadio-colorPrimary MuiRadio-root MuiRadio-colorPrimary css-vqmohf-MuiButtonBase-root-MuiRadio-root"
onBlur={[Function]}
onContextMenu={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={null}
>
<input
checked={false}
className="PrivateSwitchBase-input css-1m9pwf3"
disabled={false}
name=":r3:"
onChange={[Function]}
required={false}
type="radio"
value="Channel"
/>
<span
className="css-hyxlzm"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1hbvpl3-MuiSvgIcon-root"
data-testid="RadioButtonUncheckedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1hhw7if-MuiSvgIcon-root"
data-testid="RadioButtonCheckedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</span>
</span>
<span
className="MuiTypography-root MuiTypography-body1 MuiFormControlLabel-label css-ahj2mt-MuiTypography-root"
>
Channel
</span>
</label>
</div>
</div>
</div>
<div
className="MuiBox-root css-2bwllc"
>
<button
className="MuiButtonBase-root MuiButton-root MuiLoadingButton-root MuiButton-outlined MuiButton-outlinedPrimary MuiButton-sizeMedium MuiButton-outlinedSizeMedium MuiButton-colorPrimary MuiButton-root MuiLoadingButton-root MuiButton-outlined MuiButton-outlinedPrimary MuiButton-sizeMedium MuiButton-outlinedSizeMedium MuiButton-colorPrimary css-1pkfx9i-MuiButtonBase-root-MuiButton-root-MuiLoadingButton-root"
disabled={false}
id=":r3:"
id=":r4:"
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
Expand Down
76 changes: 65 additions & 11 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { InfoOutlined, Search as SearchIcon } from "@mui/icons-material";
import { LoadingButton } from "@mui/lab";
import { Box, Button, MenuItem, TextField } from "@mui/material";
import {
Box,
Button,
FormControl,
FormControlLabel,
FormLabel,
MenuItem,
Radio,
RadioGroup,
TextField,
} from "@mui/material";
import { parse, toSeconds } from "iso8601-duration";
import React, { useState } from "react";
import ReactGA from "react-ga4";
import { Link } from "react-router-dom";
import Video from "../components/Video";
import { SortOptions, VideoMetadata } from "../types";
import { findPlaylistById } from "../util/playlistUtil";
import { IdType, SortOptions, VideoMetadata } from "../types";
import {
findVideosByChannelId,
findVideosByPlaylistId,
} from "../util/playlistUtil";

ReactGA.initialize("G-LRVNS567ZT");
ReactGA.send(window.location.pathname + window.location.search);
Expand Down Expand Up @@ -60,6 +73,11 @@ function sortPlaylist(videos: VideoMetadata[], order: SortOptions) {
return ret.sort(compFunction);
}

const placeholderTextByIdType = {
[IdType.PLAYLIST]: "Playlist ID or Link",
[IdType.CHANNEL]: "Channel Handle (e.g. @Apple)",
};

const SearchPanel = ({
setPlaylist,
playlist,
Expand All @@ -70,6 +88,7 @@ const SearchPanel = ({
const [playlistID, setPlaylistID] = useState("");
const [loading, setLoading] = useState(false);
const [order, setOrder] = useState(SortOptions.VIEWS_DESC);
const [idType, setIdType] = useState(IdType.PLAYLIST);

const updateSortOrder = (event) => {
const value = event.target.value;
Expand All @@ -96,14 +115,28 @@ const SearchPanel = ({

setLoading(true);

// extracts playlist ID from a complete link
// if ?list= doesn't exist, assume user pasted in only the ID
const sanitizedPlaylistID =
playlistID.indexOf("list=") === -1
? playlistID
: playlistID.slice(playlistID.indexOf("list=") + "list=".length);
let videoData;

switch (idType) {
case IdType.CHANNEL:
const sanitizedChannelName = playlistID.replace(
new RegExp("@", "g"), // remove all instances of "@"
"",
);
videoData = findVideosByChannelId(sanitizedChannelName);
break;
case IdType.PLAYLIST:
// extracts playlist ID from a complete link
// if ?list= doesn't exist, assume user pasted in only the ID
const sanitizedPlaylistID =
playlistID.indexOf("list=") === -1
? playlistID
: playlistID.slice(playlistID.indexOf("list=") + "list=".length);
videoData = findVideosByPlaylistId(sanitizedPlaylistID);
break;
}

findPlaylistById(sanitizedPlaylistID)
videoData
.then((data) => {
setPlaylist(sortPlaylist(data, order));
})
Expand All @@ -130,7 +163,7 @@ const SearchPanel = ({
<TextField
fullWidth
variant="outlined"
label="Playlist ID or Link"
label={placeholderTextByIdType[idType]}
name="playlistID"
value={playlistID}
onChange={(event) => setPlaylistID(event.target.value)}
Expand All @@ -150,6 +183,27 @@ const SearchPanel = ({
</MenuItem>
))}
</TextField>
<Box width="100%">
<FormControl>
<FormLabel>Type</FormLabel>
<RadioGroup
row
value={idType}
onChange={(e) => setIdType(e.target.value as IdType)}
>
<FormControlLabel
value={IdType.PLAYLIST}
control={<Radio />}
label={IdType.PLAYLIST.toString()}
/>
<FormControlLabel
value={IdType.CHANNEL}
control={<Radio />}
label={IdType.CHANNEL.toString()}
/>
</RadioGroup>
</FormControl>
</Box>
<Box
mt="20px"
display="flex"
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export enum SortOptions {
LONGEST = "Longest",
SHORTEST = "Shortest",
}

export enum IdType {
PLAYLIST = "Playlist",
CHANNEL = "Channel",
}
12 changes: 11 additions & 1 deletion src/util/playlistUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if (
connectFunctionsEmulator(functions, "localhost", 5001);
}

export async function findPlaylistById(
export async function findVideosByPlaylistId(
playlistId: string,
): Promise<VideoMetadata[]> {
const getVideosFromPlaylist = httpsCallable(
Expand All @@ -30,3 +30,13 @@ export async function findPlaylistById(
};
return data.videos;
}

export async function findVideosByChannelId(
channelId: string,
): Promise<VideoMetadata[]> {
const getVideosFromChannel = httpsCallable(functions, "getVideosFromChannel");
const data = (await getVideosFromChannel({ id: channelId })).data as {
videos: VideoMetadata[];
};
return data.videos;
}

0 comments on commit 4fb2340

Please sign in to comment.