Skip to content

Commit

Permalink
everything
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAlan404 committed May 3, 2024
1 parent d8d4fcf commit f5ff9d5
Show file tree
Hide file tree
Showing 25 changed files with 356 additions and 126 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# NekoTube

WIP

## [Try it out here](https://tube.deniz.blue)

## TODO

- [x] LightTube support
- [x] Invidious support
- [ ] PokeTube support
- [ ] Redirector
- [x] Custom Instances
- [x] Search
- [ ] Search continuation
- [x] Search Suggestions
- [x] Comments
- [x] Comments continuation
- [x] Chapters from description
- [x] Chapters from comments
- [ ] User defined chapters
- [ ] SponsorBlock segments
- [ ] SponsorBlock skipping
- [ ] DeArrow implementation
- [ ] Progressbar hover tooltip
- [ ] Storyboards
- [ ] DASH playback
- [ ] HLS playback
- [ ] Captions
- [ ] End screen items
- [ ] Timestamp hovering
12 changes: 12 additions & 0 deletions src/api/context/APIController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { LTAPIProvider } from "../platforms/lt/lighttube";
import { fetchInvidiousPublicInstances, fetchLightTubePublicInstances } from "../platforms/public";
import { InvidiousAPIProvider } from "../platforms/invid/invidious";
import { useLocalStorage } from "@mantine/hooks";
import { DislikeAPI } from "../platforms/ryd/dislikeapi";
import { SponsorBlockAPI } from "../platforms/sponsorblock/sponsorblock";

const CUSTOM_INSTANCES: Instance[] = [

Expand All @@ -31,6 +33,8 @@ export interface APIController {
removeFavourite: (i: Instance) => void;

api: APIProvider;
dislikesApi: DislikeAPI;
sponsorBlockApi: SponsorBlockAPI;
};

export const APIContext = createContext<APIController>({
Expand All @@ -45,6 +49,9 @@ export const APIContext = createContext<APIController>({
customInstance: false,
favourited: [],
isRefreshing: false,

dislikesApi: new DislikeAPI(),
sponsorBlockApi: new SponsorBlockAPI(),
});

export const APIControllerProvider = ({ children }: React.PropsWithChildren) => {
Expand Down Expand Up @@ -89,6 +96,9 @@ export const APIControllerProvider = ({ children }: React.PropsWithChildren) =>
throw new Error("uhhh");
}
}, [currentInstance]);

const dislikesApi = useMemo(() => new DislikeAPI(), []);
const sponsorBlockApi = useMemo(() => new SponsorBlockAPI(), []);

return (
<APIContext.Provider
Expand All @@ -108,6 +118,8 @@ export const APIControllerProvider = ({ children }: React.PropsWithChildren) =>
setFavourited(v => v.filter(x => x.url !== i.url));
},
api,
dislikesApi,
sponsorBlockApi,
}}
>
{children}
Expand Down
31 changes: 24 additions & 7 deletions src/api/context/VideoPlayerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import { useVideoEventListener } from "../../hooks/useVideoEventListener";
import { parseChapters } from "../../utils/parseChapters";
import { clamp, useLocalStorage } from "@mantine/hooks";
import { SponsorBlockAPI } from "../platforms/sponsorblock/sponsorblock";
import { PreferencesContext } from "../pref/Preferences";

export const VideoPlayerProvider = ({
children
}: React.PropsWithChildren) => {
//const sourceElement = useRef<HTMLSourceElement>(null);
const videoElement = useMemo(() => {
let el = document.createElement("video");
el.style.width = "100%";
el.style.height = "100%";
return el;
}, []);
const { api, currentInstance } = useContext(APIContext);
const {
api,
currentInstance,
dislikesApi,
sponsorBlockApi,
} = useContext(APIContext);
const { pref } = useContext(PreferencesContext);

const [videoID, setVideoID] = useState<string | null>(null);
const [videoInfo, setVideoInfo] = useState<VideoData | null>(null);
Expand All @@ -30,10 +36,7 @@ export const VideoPlayerProvider = ({
const [availableFormats, setAvailableFormats] = useState<VideoFormat[]>([]);
const [playState, setPlayState] = useState<PlayState>("loading");
const [error, setError] = useState<any | null>(null);
const [volume, setVolume] = useLocalStorage({
key: "nekotube:volume",
defaultValue: 1,
});
const [volume, setVolume] = useState(JSON.parse(localStorage.getItem("nekotube:volume") ?? "1"));
const [muted, setMuted] = useState(false);

useEffect(() => {
Expand All @@ -49,6 +52,7 @@ export const VideoPlayerProvider = ({

useEffect(() => {
videoElement.volume = volume;
localStorage.setItem("nekotube:volume", JSON.stringify(volume));
}, [volume]);

const fetchVideoInfo = async () => {
Expand All @@ -67,7 +71,20 @@ export const VideoPlayerProvider = ({

try {
let info = await api.getVideoInfo(videoID);
setVideoInfo(info);

let dislikesResponse = pref.useReturnYoutubeDislike ? (
await dislikesApi.getDislikes(videoID)
) : (
null
);

setVideoInfo({
...info,
dislikeCount: dislikesResponse?.dislikes,
published: info.published || dislikesResponse?.dateCreated ? (
new Date(dislikesResponse?.dateCreated)
) : null,
});

console.log("Fetched VideoData", info);

Expand Down
17 changes: 5 additions & 12 deletions src/api/platforms/invid/invidious.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class InvidiousAPIProvider implements APIProvider {
id: d.authorId,
title: d.author,
},
viewCount: d.viewCount,
length: d.lengthSeconds,
} as Renderer;
};

Expand All @@ -61,18 +63,8 @@ export class InvidiousAPIProvider implements APIProvider {

return {
key: null,
results: data.filter(x => x.type == "video").map(result => ({
type: "video",
id: result.videoId,
title: result.title,
description: result.descriptionHtml,
thumbnails: result.videoThumbnails,
channel: {
id: result.authorId,
title: result.author,
thumbnails: result.authorThumbnails,
},
} as Renderer)),
results: data.filter(x => x.type == "video")
.map(this.convertVideoInfo),
};
}

Expand Down Expand Up @@ -102,6 +94,7 @@ export class InvidiousAPIProvider implements APIProvider {
viewCount: v.viewCount,
published: new Date(v.published * 1000),
thumbnails: v.videoThumbnails,
length: v.lengthSeconds,
recommended: v.recommendedVideos
.map(this.convertVideoInfo),

Expand Down
24 changes: 8 additions & 16 deletions src/api/platforms/lt/lighttube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,16 @@ export class LTAPIProvider implements APIProvider {
channel: {
id: d.channel.id,
title: d.channel.title,
thumbnails: [{
url: d.channel.avatar,
width: 176,
height: 176,
}]
},
description: d.description,
thumbnails: d.thumbnails,
published: new Date(d.published),
viewCount: Number(d.viewCount),
} as Renderer;
} else {
return null;
Expand All @@ -77,22 +84,7 @@ export class LTAPIProvider implements APIProvider {
return {
key: res.continuationKey,
results: res.searchResults.filter(x => x.type == "videoRenderer")
.map(v => ({
type: "video",
id: v.id,
title: v.title,
description: v.description,
thumbnails: v.thumbnails,
channel: {
id: v.channel.id,
title: v.channel.title,
thumbnails: [{
url: v.channel.avatar,
width: 176,
height: 176,
}]
},
} as VideoInfo & { type: "video" })),
.map(this.convertRenderer),
};
}

Expand Down
39 changes: 39 additions & 0 deletions src/api/platforms/ryd/dislikeapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const RETURN_YOUTUBE_DISLIKE_API_URL = "https://returnyoutubedislikeapi.com";

export interface DislikeAPIResponse {
id: string;
dateCreated: string;
likes: number;
dislikes: number;
rating: number;
viewCount: number;
deleted: boolean;
};

export class DislikeAPI {
url: string;

constructor(url = RETURN_YOUTUBE_DISLIKE_API_URL) {
this.url = url;
}

request = async <T>(path: string, opts?: {
query: Record<string, string>;
signal?: AbortSignal;
}) => {
let url = new URL(this.url + "/" + path);

for(let [k,v] of Object.entries(opts?.query || {}))
url.searchParams.set(k, v);

let res = await fetch(url, {
signal: opts?.signal,
});

return await res.json() as T;
}

async getDislikes(videoId: string) {
return await this.request("Votes", { query: { videoId } }) as DislikeAPIResponse;
};
};
3 changes: 0 additions & 3 deletions src/api/platforms/sponsorblock/sponsorblock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ export class SponsorBlockAPI {

let res = await fetch(url, {
signal: opts?.signal,
headers: {
"Content-Type": "application/json; utf-8",
},
});

return await res.json() as T;
Expand Down
9 changes: 9 additions & 0 deletions src/api/pref/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Preferences {
watchPageAnimations: boolean;
useSponsorBlock: boolean;
useDeArrow: boolean;
useReturnYoutubeDislike: boolean;
};

export const DefaultPreferences: Preferences = {
Expand All @@ -17,6 +18,7 @@ export const DefaultPreferences: Preferences = {
watchPageAnimations: true,
useSponsorBlock: true,
useDeArrow: false,
useReturnYoutubeDislike: true,
};

export interface PreferencesAPI {
Expand All @@ -33,6 +35,13 @@ export const PreferencesProvider = ({ children }: React.PropsWithChildren) => {
const [preferences, setPreferences] = useLocalStorage({
key: "nekotube:preferences",
defaultValue: DefaultPreferences,
deserialize(value) {
let j: Preferences = JSON.parse(value || JSON.stringify(DefaultPreferences));
for(let [k, v] of Object.entries(DefaultPreferences))
if(typeof v === "boolean" && typeof j[k] != typeof v)
j[k] = v;
return j;
},
});

return (
Expand Down
8 changes: 5 additions & 3 deletions src/api/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ export interface VideoInfo {
title: string;
description: string;
thumbnails: Thumbnail[];
published?: Date;
viewCount?: number;
likeCount?: number;
dislikeCount?: number;
length?: number;
channel: Channel;
};

export interface VideoData extends VideoInfo {
chapters: Chapter[];
published: Date;
keywords: string[];
likeCount?: number;
viewCount?: number;
formats: VideoFormat[];

recommended: Renderer[];
Expand Down
1 change: 0 additions & 1 deletion src/components/cards/DateCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Text, Tooltip } from "@mantine/core";
const automaticRelativeDifference = (d: Date) => {
const diff = -((new Date().getTime() - d.getTime())/1000)|0;
const absDiff = Math.abs(diff);
console.log(diff);
if (absDiff > 86400*30*10) {
return { duration: Math.round(diff/(86400*365)), unit: 'years' };
}
Expand Down
18 changes: 17 additions & 1 deletion src/components/cards/ThumbnailRender.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { AspectRatio, Image } from "@mantine/core";
import { AspectRatio, Box, Flex, Image, Text } from "@mantine/core";
import { Thumbnail } from "../../api/types/video";
import { secondsToTimestamp } from "../../utils/timestamp";

export const ThumbnailRender = ({
thumbnails,
fallback,
length,
}: {
thumbnails: Thumbnail[],
fallback?: string,
length?: number,
}) => {
const scale = 0.5;
return (
Expand All @@ -16,6 +19,19 @@ export const ThumbnailRender = ({
fallbackSrc={fallback}
loading="lazy"
/>
<Flex w="100%" h="100%" align="end" justify="end">
{length && (
<Text
bg="dark"
style={{ borderRadius: "var(--mantine-radius-sm)" }}
px={2}
fz="xs"
m={2}
>
{secondsToTimestamp(length)}
</Text>
)}
</Flex>
</AspectRatio>
);
};
Loading

0 comments on commit f5ff9d5

Please sign in to comment.