Seperated out the Invidious API funcs
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { fetchErrorHandle } from './invidious/request';
|
||||
import { deArrowInstanceStore, deArrowThumbnailInstanceStore } from '$lib/store';
|
||||
import type { DeArrow } from './model';
|
||||
|
||||
export async function getDeArrow(videoId: string, fetchOptions?: RequestInit): Promise<DeArrow> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(deArrowInstanceStore)}/api/branding?videoID=${videoId}`, fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getThumbnailDeArrow(
|
||||
videoId: string,
|
||||
time: number,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<string> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(
|
||||
`${get(deArrowThumbnailInstanceStore)}/api/v1/getThumbnail?videoID=${videoId}&time=${time}`,
|
||||
fetchOptions
|
||||
)
|
||||
);
|
||||
return URL.createObjectURL(await resp.blob());
|
||||
}
|
||||
@@ -1,24 +1,12 @@
|
||||
import { getVideoYTjs } from '$lib/api/youtubejs/video';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
invidiousAuthStore,
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
invidiousInstanceStore,
|
||||
interfaceRegionStore,
|
||||
playerYouTubeJsAlways,
|
||||
playerYouTubeJsFallback,
|
||||
rawMasterKeyStore,
|
||||
returnYTDislikesInstanceStore
|
||||
} from '../store';
|
||||
import { playerYouTubeJsAlways, rawMasterKeyStore } from '../store';
|
||||
import type {
|
||||
ChannelPage,
|
||||
Comments,
|
||||
DeArrow,
|
||||
Feed,
|
||||
PlaylistPage,
|
||||
ResolvedUrl,
|
||||
ReturnYTDislikes,
|
||||
SearchSuggestion,
|
||||
Subscription,
|
||||
Video,
|
||||
@@ -51,69 +39,33 @@ import {
|
||||
getSubscriptionsBackend,
|
||||
postSubscribeBackend
|
||||
} from './backend/subscriptions';
|
||||
import { getUserLocale } from '$lib/i18n';
|
||||
|
||||
export function buildPath(path: string): URL {
|
||||
return setLocale(new URL(`${get(invidiousInstanceStore)}/api/v1/${path}`));
|
||||
}
|
||||
|
||||
export function setLocale(url: URL): URL {
|
||||
const region = get(interfaceRegionStore);
|
||||
|
||||
if (region) {
|
||||
url.searchParams.set('region', region);
|
||||
}
|
||||
|
||||
const locale = getUserLocale();
|
||||
|
||||
url.searchParams.set('hl', locale);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export class HTTPError {
|
||||
msg: string;
|
||||
response: Response;
|
||||
|
||||
constructor(msg: string, response: Response) {
|
||||
this.msg = msg;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.msg;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchErrorHandle(response: Response): Promise<Response> {
|
||||
if (!response.ok) {
|
||||
let message = 'Internal error';
|
||||
|
||||
// Attempt to parse error.
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.errorBacktrace || json.error || json.message;
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
throw new HTTPError(
|
||||
`${response.status} - ${response.statusText}\n${decodeURIComponent(response.url)}\n${message}`,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export function buildAuthHeaders(): { headers: Record<string, string> } {
|
||||
const authToken = get(invidiousAuthStore)?.token ?? '';
|
||||
if (authToken.startsWith('SID=')) {
|
||||
return { headers: { __sid_auth: authToken } };
|
||||
} else {
|
||||
return { headers: { Authorization: `Bearer ${authToken}` } };
|
||||
}
|
||||
}
|
||||
import { getFeedInvidious, getPopularInvidious, getSubscriptionsInvidious } from './invidious/feed';
|
||||
import { getResolveUrlInvidious } from './invidious/misc';
|
||||
import { getVideoInvidious } from './invidious/video';
|
||||
import { getCommentsInvidious } from './invidious/comments';
|
||||
import {
|
||||
getChannelContentInvidious,
|
||||
getChannelInvidious,
|
||||
searchChannelContentInvidious
|
||||
} from './invidious/channel';
|
||||
import { getSearchSuggestionsInvidious } from './invidious/searchSuggestions';
|
||||
import { getHashtagInvidious } from './invidious/hashtag';
|
||||
import { getSearchInvidious } from './invidious/search';
|
||||
import { notificationsMarkAsReadInvidious } from './invidious/notifcations';
|
||||
import {
|
||||
amSubscribedInvidious,
|
||||
deleteUnsubscribeInvidious,
|
||||
postSubscribeInvidious
|
||||
} from './invidious/subscribe';
|
||||
import { postHistoryInvidious } from './invidious/history';
|
||||
import {
|
||||
addPlaylistVideoInvidious,
|
||||
deletePersonalPlaylistInvidious,
|
||||
getPersonalPlaylistsInvidious,
|
||||
getPlaylistInvidious,
|
||||
postPersonalPlaylistInvidious,
|
||||
removePlaylistVideoInvidious
|
||||
} from './invidious/playlist';
|
||||
|
||||
export async function getPopular(fetchOptions?: RequestInit): Promise<Video[]> {
|
||||
// Doesn't exist in YTjs.
|
||||
@@ -121,20 +73,15 @@ export async function getPopular(fetchOptions?: RequestInit): Promise<Video[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(buildPath('popular'), fetchOptions));
|
||||
return await resp.json();
|
||||
return getPopularInvidious(fetchOptions);
|
||||
}
|
||||
|
||||
export async function getResolveUrl(url: string): Promise<ResolvedUrl> {
|
||||
if (isYTBackend() || useEngineFallback('ResolveUrl')) {
|
||||
return await getResolveUrlYTjs(url);
|
||||
return getResolveUrlYTjs(url);
|
||||
}
|
||||
|
||||
const path = buildPath('resolveurl');
|
||||
path.searchParams.set('url', url);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
return getResolveUrlInvidious(url);
|
||||
}
|
||||
|
||||
export async function getVideo(
|
||||
@@ -147,30 +94,10 @@ export async function getVideo(
|
||||
isYTBackend() ||
|
||||
useEngineFallback('Video')
|
||||
) {
|
||||
return await getVideoYTjs(videoId);
|
||||
return getVideoYTjs(videoId);
|
||||
}
|
||||
|
||||
const path = buildPath(`videos/${videoId}`);
|
||||
path.searchParams.set('local', local.toString());
|
||||
|
||||
const resp = await fetch(path, fetchOptions);
|
||||
|
||||
if (!resp.ok && get(playerYouTubeJsFallback) && isUnrestrictedPlatform()) {
|
||||
return await getVideoYTjs(videoId);
|
||||
} else {
|
||||
await fetchErrorHandle(resp);
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getDislikes(
|
||||
videoId: string,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<ReturnYTDislikes> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(returnYTDislikesInstanceStore)}/votes?videoId=${videoId}`, fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
return getVideoInvidious(videoId, local, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getComments(
|
||||
@@ -181,15 +108,10 @@ export async function getComments(
|
||||
commentsSetDefaults(options);
|
||||
|
||||
if (isYTBackend() || useEngineFallback('Comments')) {
|
||||
return await getCommentsYTjs(videoId, options);
|
||||
return getCommentsYTjs(videoId, options);
|
||||
}
|
||||
|
||||
const path = buildPath(`comments/${videoId}`);
|
||||
if (options.continuation) path.searchParams.set('continuation', options.continuation);
|
||||
if (options.sort_by) path.searchParams.set('sort_by', options.sort_by);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
return getCommentsInvidious(videoId, options, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getChannel(
|
||||
@@ -199,10 +121,8 @@ export async function getChannel(
|
||||
if (isYTBackend() || useEngineFallback('Channel')) {
|
||||
return getChannelYTjs(channelId);
|
||||
}
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath(`channels/${channelId}`), fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
|
||||
return getChannelInvidious(channelId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getChannelContent(
|
||||
@@ -213,18 +133,10 @@ export async function getChannelContent(
|
||||
if (typeof options.type === 'undefined') options.type = 'videos';
|
||||
|
||||
if (isYTBackend() || useEngineFallback('ChannelContent')) {
|
||||
return await getChannelContentYTjs(channelId, options);
|
||||
return getChannelContentYTjs(channelId, options);
|
||||
}
|
||||
|
||||
const url = buildPath(`channels/${channelId}/${options.type}`);
|
||||
|
||||
if (typeof options.continuation !== 'undefined')
|
||||
url.searchParams.set('continuation', options.continuation);
|
||||
|
||||
if (typeof options.sortBy !== 'undefined') url.searchParams.set('sort_by', options.sortBy);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(url.toString(), fetchOptions));
|
||||
return await resp.json();
|
||||
return getChannelContentInvidious(channelId, options, fetchOptions);
|
||||
}
|
||||
|
||||
export async function searchChannelContent(
|
||||
@@ -239,11 +151,7 @@ export async function searchChannelContent(
|
||||
};
|
||||
}
|
||||
|
||||
const path = buildPath(`channel/${channelId}/search`);
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
return searchChannelContentInvidious(channelId, search, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getSearchSuggestions(
|
||||
@@ -254,22 +162,14 @@ export async function getSearchSuggestions(
|
||||
return getSearchSuggestionsYTjs(search);
|
||||
}
|
||||
|
||||
const path = buildPath('search/suggestions');
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
return getSearchSuggestionsInvidious(search, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getHashtag(tag: string, page: number = 0): Promise<{ results: Video[] }> {
|
||||
// TODO: Implement in YTjs
|
||||
if (isYTBackend()) return { results: [] };
|
||||
|
||||
const path = buildPath(`hashtag/${tag}`);
|
||||
path.searchParams.set('page', page.toString());
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
return getHashtagInvidious(tag, page);
|
||||
}
|
||||
|
||||
export async function getSearch(
|
||||
@@ -280,21 +180,10 @@ export async function getSearch(
|
||||
searchSetDefaults(options);
|
||||
|
||||
if (isYTBackend() || useEngineFallback('Search')) {
|
||||
return await getSearchYTjs(search, options);
|
||||
return getSearchYTjs(search, options);
|
||||
}
|
||||
|
||||
const path = buildPath('search');
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
if (options.date) path.searchParams.set('date', options.date);
|
||||
if (options.duration) path.searchParams.set('duration', options.duration);
|
||||
if (options.features) path.searchParams.set('features', options.features);
|
||||
if (options.page) path.searchParams.set('page', options.page);
|
||||
if (options.sort_by) path.searchParams.set('sort_by', options.sort_by);
|
||||
if (options.type) path.searchParams.set('type', options.type);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
return getSearchInvidious(search, options, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getFeed(
|
||||
@@ -306,21 +195,14 @@ export async function getFeed(
|
||||
return getFeedYTjs(maxResults, page);
|
||||
}
|
||||
|
||||
const path = buildPath('auth/feed');
|
||||
path.searchParams.set('max_results', maxResults.toString());
|
||||
path.searchParams.set('page', page.toString());
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(path, { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
return getFeedInvidious(maxResults, page, fetchOptions);
|
||||
}
|
||||
|
||||
export async function notificationsMarkAsRead(fetchOptions: RequestInit = {}) {
|
||||
// Not support functionality of YTjs
|
||||
if (isYTBackend()) return;
|
||||
|
||||
const path = buildPath('auth/notifications');
|
||||
await fetchErrorHandle(await fetch(path, { ...buildAuthHeaders(), ...fetchOptions }));
|
||||
return notificationsMarkAsReadInvidious(fetchOptions);
|
||||
}
|
||||
|
||||
export async function getSubscriptions(
|
||||
@@ -339,10 +221,8 @@ export async function getSubscriptions(
|
||||
|
||||
return getSubscriptionsYTjs();
|
||||
}
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/subscriptions'), { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
|
||||
return getSubscriptionsInvidious(fetchOptions);
|
||||
}
|
||||
|
||||
export async function amSubscribed(
|
||||
@@ -357,16 +237,7 @@ export async function amSubscribed(
|
||||
return amSubscribedYTjs(authorId);
|
||||
}
|
||||
|
||||
if (!get(invidiousAuthStore)) return false;
|
||||
|
||||
try {
|
||||
const subscriptions = (await getSubscriptions(fetchOptions)).filter(
|
||||
(sub) => sub.authorId === authorId
|
||||
);
|
||||
return subscriptions.length === 1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return amSubscribedInvidious(authorId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function postSubscribe(
|
||||
@@ -382,13 +253,7 @@ export async function postSubscribe(
|
||||
return postSubscribeYTjs(authorId);
|
||||
}
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/subscriptions/${authorId}`), {
|
||||
method: 'POST',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return postSubscribeInvidious(authorId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribe(authorId: string, fetchOptions: RequestInit = {}) {
|
||||
@@ -402,68 +267,19 @@ export async function deleteUnsubscribe(authorId: string, fetchOptions: RequestI
|
||||
return deleteUnsubscribeYTjs(authorId);
|
||||
}
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/subscriptions/${authorId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return deleteUnsubscribeInvidious(authorId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getHistory(
|
||||
page: number = 1,
|
||||
maxResults: number = 20,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<string[]> {
|
||||
// Not supported functionality of YTjs.
|
||||
if (isYTBackend()) {
|
||||
return [];
|
||||
}
|
||||
export async function getHistory(page: number = 1, maxResults: number = 20): Promise<string[]> {}
|
||||
|
||||
const path = buildPath(`auth/history`);
|
||||
path.searchParams.set('page', page.toString());
|
||||
path.searchParams.set('max_results', maxResults.toString());
|
||||
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(path, {
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function deleteHistory(
|
||||
videoId: string | undefined = undefined,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
export async function deleteHistory(videoId: string | undefined = undefined) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
let url = '/api/v1/auth/history';
|
||||
if (typeof videoId !== 'undefined') {
|
||||
url += `/${videoId}`;
|
||||
}
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(url), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function postHistory(videoId: string, fetchOptions: RequestInit = {}) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/history/${videoId}`), {
|
||||
method: 'POST',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return postHistoryInvidious(videoId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getPlaylist(
|
||||
@@ -475,21 +291,7 @@ export async function getPlaylist(
|
||||
return await getPlaylistYTjs(playlistId);
|
||||
}
|
||||
|
||||
let resp;
|
||||
|
||||
const path = buildPath(`${get(invidiousAuthStore) ? 'auth/' : ''}playlists/${playlistId}`);
|
||||
path.searchParams.set('page', page.toString());
|
||||
|
||||
if (get(invidiousAuthStore)) {
|
||||
resp = await fetch(path, {
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
});
|
||||
} else {
|
||||
resp = await fetch(path, fetchOptions);
|
||||
}
|
||||
await fetchErrorHandle(resp);
|
||||
return await resp.json();
|
||||
return getPlaylistInvidious(playlistId, page, fetchOptions);
|
||||
}
|
||||
|
||||
export async function getPersonalPlaylists(
|
||||
@@ -497,21 +299,13 @@ export async function getPersonalPlaylists(
|
||||
): Promise<PlaylistPage[]> {
|
||||
if (isYTBackend()) return [];
|
||||
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/playlists'), { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
return getPersonalPlaylistsInvidious(fetchOptions);
|
||||
}
|
||||
|
||||
export async function deletePersonalPlaylist(playlistId: string) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders()
|
||||
})
|
||||
);
|
||||
return deletePersonalPlaylistInvidious(playlistId);
|
||||
}
|
||||
|
||||
export async function postPersonalPlaylist(
|
||||
@@ -520,21 +314,7 @@ export async function postPersonalPlaylist(
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/playlists'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
privacy: privacy
|
||||
}),
|
||||
...headers,
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return postPersonalPlaylistInvidious(title, privacy, fetchOptions);
|
||||
}
|
||||
|
||||
export async function addPlaylistVideo(
|
||||
@@ -543,20 +323,7 @@ export async function addPlaylistVideo(
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}/videos`), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
videoId: videoId
|
||||
}),
|
||||
...headers,
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
return addPlaylistVideoInvidious(playlistId, videoId, fetchOptions);
|
||||
}
|
||||
|
||||
export async function removePlaylistVideo(
|
||||
@@ -566,32 +333,5 @@ export async function removePlaylistVideo(
|
||||
) {
|
||||
if (isYTBackend()) return;
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}/videos/${indexId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDeArrow(videoId: string, fetchOptions?: RequestInit): Promise<DeArrow> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(deArrowInstanceStore)}/api/branding?videoID=${videoId}`, fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getThumbnail(
|
||||
videoId: string,
|
||||
time: number,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<string> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(
|
||||
`${get(deArrowThumbnailInstanceStore)}/api/v1/getThumbnail?videoID=${videoId}&time=${time}`,
|
||||
fetchOptions
|
||||
)
|
||||
);
|
||||
return URL.createObjectURL(await resp.blob());
|
||||
return removePlaylistVideoInvidious(playlistId, indexId, fetchOptions);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ChannelContent, ChannelOptions, ChannelPage } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getChannelInvidious(
|
||||
channelId: string,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<ChannelPage> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath(`channels/${channelId}`), fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getChannelContentInvidious(
|
||||
channelId: string,
|
||||
options: ChannelOptions,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<ChannelContent> {
|
||||
const url = buildPath(`channels/${channelId}/${options.type}`);
|
||||
|
||||
if (typeof options.continuation !== 'undefined')
|
||||
url.searchParams.set('continuation', options.continuation);
|
||||
|
||||
if (typeof options.sortBy !== 'undefined') url.searchParams.set('sort_by', options.sortBy);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(url.toString(), fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function searchChannelContentInvidious(
|
||||
channelId: string,
|
||||
search: string,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<ChannelContent> {
|
||||
const path = buildPath(`channel/${channelId}/search`);
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Comments, CommentsOptions } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getCommentsInvidious(
|
||||
videoId: string,
|
||||
options: CommentsOptions,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<Comments> {
|
||||
const path = buildPath(`comments/${videoId}`);
|
||||
if (options.continuation) path.searchParams.set('continuation', options.continuation);
|
||||
if (options.sort_by) path.searchParams.set('sort_by', options.sort_by);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Feed, Subscription, Video } from '../model';
|
||||
import { buildAuthHeaders, buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getPopularInvidious(fetchOptions?: RequestInit): Promise<Video[]> {
|
||||
const resp = await fetchErrorHandle(await fetch(buildPath('popular'), fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getFeedInvidious(
|
||||
maxResults: number,
|
||||
page: number,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<Feed> {
|
||||
const path = buildPath('auth/feed');
|
||||
path.searchParams.set('max_results', maxResults.toString());
|
||||
path.searchParams.set('page', page.toString());
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(path, { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getSubscriptionsInvidious(
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<Subscription[]> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/subscriptions'), { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Video } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getHashtagInvidious(
|
||||
tag: string,
|
||||
page: number = 0
|
||||
): Promise<{ results: Video[] }> {
|
||||
const path = buildPath(`hashtag/${tag}`);
|
||||
path.searchParams.set('page', page.toString());
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { buildAuthHeaders, buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function postHistoryInvidious(videoId: string, fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/history/${videoId}`), {
|
||||
method: 'POST',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ResolvedUrl } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getResolveUrlInvidious(url: string): Promise<ResolvedUrl> {
|
||||
const path = buildPath('resolveurl');
|
||||
path.searchParams.set('url', url);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { buildAuthHeaders, buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function notificationsMarkAsReadInvidious(fetchOptions: RequestInit = {}) {
|
||||
const path = buildPath('auth/notifications');
|
||||
await fetchErrorHandle(await fetch(path, { ...buildAuthHeaders(), ...fetchOptions }));
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { PlaylistPage } from '../model';
|
||||
import { buildAuthHeaders, buildPath, fetchErrorHandle } from './request';
|
||||
import { invidiousAuthStore } from '$lib/store';
|
||||
|
||||
export async function getPlaylistInvidious(
|
||||
playlistId: string,
|
||||
page: number = 1,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<PlaylistPage> {
|
||||
let resp;
|
||||
|
||||
const path = buildPath(`${get(invidiousAuthStore) ? 'auth/' : ''}playlists/${playlistId}`);
|
||||
path.searchParams.set('page', page.toString());
|
||||
|
||||
if (get(invidiousAuthStore)) {
|
||||
resp = await fetch(path, {
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
});
|
||||
} else {
|
||||
resp = await fetch(path, fetchOptions);
|
||||
}
|
||||
await fetchErrorHandle(resp);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getPersonalPlaylistsInvidious(
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<PlaylistPage[]> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/playlists'), { ...buildAuthHeaders(), ...fetchOptions })
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function deletePersonalPlaylistInvidious(playlistId: string) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function postPersonalPlaylistInvidious(
|
||||
title: string,
|
||||
privacy: 'public' | 'private' | 'unlisted',
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath('auth/playlists'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
privacy: privacy
|
||||
}),
|
||||
...headers,
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function addPlaylistVideoInvidious(
|
||||
playlistId: string,
|
||||
videoId: string,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}/videos`), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
videoId: videoId
|
||||
}),
|
||||
...headers,
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function removePlaylistVideoInvidious(
|
||||
playlistId: string,
|
||||
indexId: string,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/playlists/${playlistId}/videos/${indexId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { getUserLocale } from '$lib/i18n';
|
||||
import { interfaceRegionStore, invidiousAuthStore, invidiousInstanceStore } from '$lib/store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export function buildPath(path: string): URL {
|
||||
return setLocale(new URL(`${get(invidiousInstanceStore)}/api/v1/${path}`));
|
||||
}
|
||||
|
||||
export function setLocale(url: URL): URL {
|
||||
const region = get(interfaceRegionStore);
|
||||
|
||||
if (region) {
|
||||
url.searchParams.set('region', region);
|
||||
}
|
||||
|
||||
const locale = getUserLocale();
|
||||
|
||||
url.searchParams.set('hl', locale);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export class HTTPError {
|
||||
msg: string;
|
||||
response: Response;
|
||||
|
||||
constructor(msg: string, response: Response) {
|
||||
this.msg = msg;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.msg;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchErrorHandle(response: Response): Promise<Response> {
|
||||
if (!response.ok) {
|
||||
let message = 'Internal error';
|
||||
|
||||
// Attempt to parse error.
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.errorBacktrace || json.error || json.message;
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
throw new HTTPError(
|
||||
`${response.status} - ${response.statusText}\n${decodeURIComponent(response.url)}\n${message}`,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export function buildAuthHeaders(): { headers: Record<string, string> } {
|
||||
const authToken = get(invidiousAuthStore)?.token ?? '';
|
||||
if (authToken.startsWith('SID=')) {
|
||||
return { headers: { __sid_auth: authToken } };
|
||||
} else {
|
||||
return { headers: { Authorization: `Bearer ${authToken}` } };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { SearchOptions, SearchResults } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getSearchInvidious(
|
||||
search: string,
|
||||
options: SearchOptions,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<SearchResults> {
|
||||
const path = buildPath('search');
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
if (options.date) path.searchParams.set('date', options.date);
|
||||
if (options.duration) path.searchParams.set('duration', options.duration);
|
||||
if (options.features) path.searchParams.set('features', options.features);
|
||||
if (options.page) path.searchParams.set('page', options.page);
|
||||
if (options.sort_by) path.searchParams.set('sort_by', options.sort_by);
|
||||
if (options.type) path.searchParams.set('type', options.type);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { SearchSuggestion } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function getSearchSuggestionsInvidious(
|
||||
search: string,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<SearchSuggestion> {
|
||||
const path = buildPath('search/suggestions');
|
||||
path.searchParams.set('q', search);
|
||||
|
||||
const resp = await fetchErrorHandle(await fetch(path, fetchOptions));
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { invidiousAuthStore } from '$lib/store';
|
||||
import { get } from 'svelte/store';
|
||||
import { getSubscriptionsInvidious } from './feed';
|
||||
import { buildAuthHeaders, buildPath, fetchErrorHandle } from './request';
|
||||
|
||||
export async function amSubscribedInvidious(
|
||||
authorId: string,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<boolean> {
|
||||
if (!get(invidiousAuthStore)) return false;
|
||||
|
||||
try {
|
||||
const subscriptions = (await getSubscriptionsInvidious(fetchOptions)).filter(
|
||||
(sub) => sub.authorId === authorId
|
||||
);
|
||||
return subscriptions.length === 1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function postSubscribeInvidious(authorId: string, fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/subscriptions/${authorId}`), {
|
||||
method: 'POST',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribeInvidious(authorId: string, fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(buildPath(`auth/subscriptions/${authorId}`), {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { VideoPlay } from '../model';
|
||||
import { buildPath, fetchErrorHandle } from './request';
|
||||
import { getVideoYTjs } from '../youtubejs/video';
|
||||
import { playerYouTubeJsFallback } from '$lib/store';
|
||||
import { isUnrestrictedPlatform } from '$lib/misc';
|
||||
|
||||
export async function getVideoInvidious(
|
||||
videoId: string,
|
||||
local: boolean = false,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<VideoPlay> {
|
||||
const path = buildPath(`videos/${videoId}`);
|
||||
path.searchParams.set('local', local.toString());
|
||||
|
||||
const resp = await fetch(path, fetchOptions);
|
||||
|
||||
if (!resp.ok && get(playerYouTubeJsFallback) && isUnrestrictedPlatform()) {
|
||||
return await getVideoYTjs(videoId);
|
||||
} else {
|
||||
await fetchErrorHandle(resp);
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { ReturnYTDislikes } from './model';
|
||||
import { fetchErrorHandle } from './invidious/request';
|
||||
import { returnYTDislikesInstanceStore } from '$lib/store';
|
||||
|
||||
export async function getDislikesRYD(
|
||||
videoId: string,
|
||||
fetchOptions?: RequestInit
|
||||
): Promise<ReturnYTDislikes> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(returnYTDislikesInstanceStore)}/votes?videoId=${videoId}`, fetchOptions)
|
||||
);
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { getDeArrow, getThumbnail } from '$lib/api';
|
||||
import { Avatar } from 'melt/builders';
|
||||
import type {
|
||||
Notification,
|
||||
@@ -26,6 +25,7 @@
|
||||
import { queueGetWatchHistory } from '$lib/api/backend/historyPool';
|
||||
import { page } from '$app/state';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { getDeArrow, getThumbnailDeArrow } from '$lib/api/dearrow';
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo | VideoWatchHistory;
|
||||
@@ -74,7 +74,7 @@
|
||||
for (const thumbnail of deArrow.thumbnails) {
|
||||
if (thumbnail.locked || thumbnail.original || thumbnail.votes > 0) {
|
||||
if (thumbnail.timestamp !== null) {
|
||||
thumbnailSrc = await getThumbnail(video.videoId, thumbnail.timestamp, {
|
||||
thumbnailSrc = await getThumbnailDeArrow(video.videoId, thumbnail.timestamp, {
|
||||
priority: 'low'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Table } from 'dexie';
|
||||
import Dexie from 'dexie';
|
||||
import type { Video } from './api/model';
|
||||
import type { Video, VideoWatchHistory } from './api/model';
|
||||
|
||||
export interface FavouriteChannels {
|
||||
channelId: string;
|
||||
@@ -17,13 +17,15 @@ export class MaterialiousDb extends Dexie {
|
||||
favouriteChannels!: Table<FavouriteChannels>;
|
||||
channelSubscriptions!: Table<ChannelSubscriptions>;
|
||||
subscriptionFeed!: Table<Video>;
|
||||
watchHistory!: Table<VideoWatchHistory>;
|
||||
|
||||
constructor() {
|
||||
super('materialious');
|
||||
this.version(2).stores({
|
||||
favouriteChannels: 'channelId',
|
||||
channelSubscriptions: 'channelId',
|
||||
subscriptionFeed: 'videoId, authorId, published'
|
||||
subscriptionFeed: 'videoId, authorId, published',
|
||||
watchHistory: 'videoId, id'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
getComments,
|
||||
getDislikes,
|
||||
getPersonalPlaylists,
|
||||
getVideo,
|
||||
postHistory
|
||||
} from '$lib/api/index';
|
||||
import { getComments, getPersonalPlaylists, getVideo } from '$lib/api/index';
|
||||
import { loadEntirePlaylist } from '$lib/playlist';
|
||||
import {
|
||||
invidiousAuthStore,
|
||||
@@ -20,6 +14,7 @@ import { get } from 'svelte/store';
|
||||
import { _ } from './i18n';
|
||||
import { isOwnBackend } from './shared';
|
||||
import { saveWatchHistory } from './api/backend/history';
|
||||
import { getDislikesRYD } from './api/ytd';
|
||||
|
||||
export async function getWatchDetails(videoId: string, url: URL) {
|
||||
const playerStateRetrieved = get(playerState);
|
||||
@@ -42,7 +37,6 @@ export async function getWatchDetails(videoId: string, url: URL) {
|
||||
|
||||
let personalPlaylists;
|
||||
if (get(invidiousAuthStore)) {
|
||||
postHistory(video.videoId);
|
||||
personalPlaylists = getPersonalPlaylists({ priority: 'low' });
|
||||
} else {
|
||||
personalPlaylists = null;
|
||||
@@ -64,7 +58,7 @@ export async function getWatchDetails(videoId: string, url: URL) {
|
||||
if (returnYTDislikesInstance && returnYTDislikesInstance !== '') {
|
||||
try {
|
||||
returnYTDislikes = get(returnYtDislikesStore)
|
||||
? getDislikes(videoId, { priority: 'low' })
|
||||
? getDislikesRYD(videoId, { priority: 'low' })
|
||||
: null;
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
|
||||
Reference in New Issue
Block a user