diff --git a/README.md b/README.md index f6f3ee4c..af2c02cc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Materialious

- Modern material design for Invidious. + Modern material design for YouTube and Invidious. @@ -16,6 +16,7 @@ # Features +- Support for using without a Invidious instance (With experimental Youtubejs backend). - [Invidious companion support.](./docs/DOCKER.md#invidious-companion-support) - [Invidious API extended integration.](https://github.com/Materialious/api-extended) - [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android. diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 6051fe2b..f5ca278a 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Modern material design for Invidious \ No newline at end of file +Modern material design for YouTube and Invidious. diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 703f47a5..d885f88f 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 203 - versionName "1.13.19" + versionCode 204 + versionName "1.14.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.desktop b/materialious/electron/materialious.desktop index 35d99ac1..ce9dea9b 100644 --- a/materialious/electron/materialious.desktop +++ b/materialious/electron/materialious.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Name=Materialious -Comment=Modern material design for Invidious. +Comment=Modern material design for YouTube and Invidious. GenericName=Materialious Exec=run.sh %u Icon=us.materialio.Materialious diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 8f8f3e7c..835956c6 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -102,7 +102,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.14.0 + + https://github.com/Materialious/Materialious/releases/tag/1.13.19 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index a40e535a..b5175bc0 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.13.19", + "version": "1.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.13.19", + "version": "1.14.0", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 360b7000..6fbd8259 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,7 +1,7 @@ { "name": "Materialious", - "version": "1.13.19", - "description": "Modern material design for Invidious.", + "version": "1.14.0", + "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", "email": "wardpearce@pm.me" @@ -45,4 +45,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/materialious/package.json b/materialious/package.json index caf7742d..f34cd510 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.13.19", + "version": "1.14.0", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/materialious/src/app.html b/materialious/src/app.html index 6f41c801..0fd22130 100644 --- a/materialious/src/app.html +++ b/materialious/src/app.html @@ -3,7 +3,7 @@ - + } { } export async function getPopular(fetchOptions?: RequestInit): Promise { + // Doesn't exist in YTjs. + if (isYTBackend()) { + return []; + } + const resp = await fetchErrorHandle(await fetch(buildPath('popular'), fetchOptions)); return await resp.json(); } export async function getResolveUrl(url: string): Promise { + if (isYTBackend() || useEngineFallback('ResolveUrl')) { + return await getResolveUrlYTjs(url); + } + const resp = await fetchErrorHandle(await fetch(`${buildPath('resolveurl')}?url=${url}`)); return await resp.json(); } @@ -85,14 +109,18 @@ export async function getVideo( local: boolean = false, fetchOptions?: RequestInit ): Promise { - if (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) { - return await getVideoTYjs(videoId); + if ( + (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) || + isYTBackend() || + useEngineFallback('Video') + ) { + return await getVideoYTjs(videoId); } const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)), fetchOptions); if (!resp.ok && get(playerYouTubeJsFallback) && Capacitor.isNativePlatform()) { - return await getVideoTYjs(videoId); + return await getVideoYTjs(videoId); } else { await fetchErrorHandle(resp); } @@ -111,23 +139,17 @@ export async function getDislikes( export async function getComments( videoId: string, - parameters: { - sort_by?: 'top' | 'new'; - source?: 'youtube' | 'reddit'; - continuation?: string; - }, + options: CommentsOptions, fetchOptions?: RequestInit ): Promise { - if (typeof parameters.sort_by === 'undefined') { - parameters.sort_by = 'top'; - } + commentsSetDefaults(options); - if (typeof parameters.source === 'undefined') { - parameters.source = 'youtube'; + if (isYTBackend() || useEngineFallback('Comments')) { + return await getCommentsYTjs(videoId, options); } const path = buildPath(`comments/${videoId}`); - path.search = new URLSearchParams(parameters).toString(); + path.search = new URLSearchParams(options).toString(); const resp = await fetchErrorHandle(await fetch(path, fetchOptions)); return await resp.json(); } @@ -136,32 +158,32 @@ export async function getChannel( channelId: string, fetchOptions?: RequestInit ): Promise { + if (isYTBackend() || useEngineFallback('Channel')) { + return getChannelYTjs(channelId); + } const resp = await fetchErrorHandle( await fetch(buildPath(`channels/${channelId}`), fetchOptions) ); return await resp.json(); } -export type channelSortBy = 'oldest' | 'newest' | 'popular'; -export type channelContentTypes = 'videos' | 'playlists' | 'streams' | 'shorts'; - export async function getChannelContent( channelId: string, - parameters: { - type?: channelContentTypes; - continuation?: string; - sortBy?: channelSortBy; - }, + options: ChannelOptions, fetchOptions?: RequestInit -): Promise { - if (typeof parameters.type === 'undefined') parameters.type = 'videos'; +): Promise { + if (typeof options.type === 'undefined') options.type = 'videos'; - const url = buildPath(`channels/${channelId}/${parameters.type}`); + const url = buildPath(`channels/${channelId}/${options.type}`); - if (typeof parameters.continuation !== 'undefined') - url.searchParams.set('continuation', parameters.continuation); + if (typeof options.continuation !== 'undefined') + url.searchParams.set('continuation', options.continuation); - if (typeof parameters.sortBy !== 'undefined') url.searchParams.set('sort_by', parameters.sortBy); + if (typeof options.sortBy !== 'undefined') url.searchParams.set('sort_by', options.sortBy); + + if (isYTBackend() || useEngineFallback('ChannelContent')) { + return await getChannelContentYTjs(channelId, options); + } const resp = await fetchErrorHandle(await fetch(url.toString(), fetchOptions)); return await resp.json(); @@ -171,7 +193,14 @@ export async function searchChannelContent( channelId: string, search: string, fetchOptions?: RequestInit -) { +): Promise { + // Not Implemented in YTjs + if (isYTBackend()) { + return { + videos: [] + }; + } + const path = buildPath(`channel/${channelId}/search`); path.search = new URLSearchParams({ q: search }).toString(); const resp = await fetchErrorHandle(await fetch(path, fetchOptions)); @@ -182,6 +211,10 @@ export async function getSearchSuggestions( search: string, fetchOptions?: RequestInit ): Promise { + if (isYTBackend() || useEngineFallback('SearchSuggestions')) { + return getSearchSuggestionsYTjs(search); + } + const path = buildPath('search/suggestions'); path.search = new URLSearchParams({ q: search }).toString(); const resp = await fetchErrorHandle(await fetch(path, fetchOptions)); @@ -189,34 +222,22 @@ export async function getSearchSuggestions( } export async function getHashtag(tag: string, page: number = 0): Promise<{ results: Video[] }> { + // TODO: Implement in YTjs + if (isYTBackend()) return { results: [] }; + const resp = await fetchErrorHandle(await fetch(buildPath(`hashtag/${tag}?page=${page}`))); return await resp.json(); } -export interface SearchOptions { - sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; - type?: 'video' | 'playlist' | 'channel' | 'all'; - duration?: 'short' | 'medium' | 'long'; - date?: 'hour' | 'today' | 'week' | 'month' | 'year'; - features?: string; - page?: string; -} - export async function getSearch( search: string, options: SearchOptions, fetchOptions?: RequestInit -): Promise<(Channel | Video | Playlist | HashTag)[]> { - if (typeof options.sort_by === 'undefined') { - options.sort_by = 'relevance'; - } +): Promise { + searchSetDefaults(options); - if (typeof options.type === 'undefined') { - options.type = 'all'; - } - - if (typeof options.page === 'undefined') { - options.page = '1'; + if (isYTBackend() || useEngineFallback('Search')) { + return await getSearchYTjs(search, options); } const path = buildPath('search'); @@ -230,6 +251,10 @@ export async function getFeed( page: number, fetchOptions: RequestInit = {} ): Promise { + if (isYTBackend()) { + return getFeedYTjs(maxResults, page); + } + const path = buildPath('auth/feed'); path.search = new URLSearchParams({ max_results: maxResults.toString(), @@ -241,7 +266,18 @@ export async function getFeed( return await resp.json(); } +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 })); +} + export async function getSubscriptions(fetchOptions: RequestInit = {}): Promise { + if (isYTBackend()) { + return getSubscriptionsYTjs(); + } const resp = await fetchErrorHandle( await fetch(buildPath('auth/subscriptions'), { ...buildAuthHeaders(), ...fetchOptions }) ); @@ -252,6 +288,10 @@ export async function amSubscribed( authorId: string, fetchOptions: RequestInit = {} ): Promise { + if (isYTBackend()) { + return amSubscribedYTjs(authorId); + } + if (!get(authStore)) return false; try { @@ -265,6 +305,10 @@ export async function amSubscribed( } export async function postSubscribe(authorId: string, fetchOptions: RequestInit = {}) { + if (isYTBackend()) { + return postSubscribeYTjs(authorId); + } + await fetchErrorHandle( await fetch(buildPath(`auth/subscriptions/${authorId}`), { method: 'POST', @@ -275,6 +319,10 @@ export async function postSubscribe(authorId: string, fetchOptions: RequestInit } export async function deleteUnsubscribe(authorId: string, fetchOptions: RequestInit = {}) { + if (isYTBackend()) { + return deleteUnsubscribeYTjs(authorId); + } + await fetchErrorHandle( await fetch(buildPath(`auth/subscriptions/${authorId}`), { method: 'DELETE', @@ -289,6 +337,11 @@ export async function getHistory( maxResults: number = 20, fetchOptions: RequestInit = {} ): Promise { + // Not supported functionality of YTjs. + if (isYTBackend()) { + return []; + } + const resp = await fetchErrorHandle( await fetch(buildPath(`auth/history?page=${page}&max_results=${maxResults}`), { ...buildAuthHeaders(), @@ -302,6 +355,8 @@ export async function deleteHistory( videoId: string | undefined = undefined, fetchOptions: RequestInit = {} ) { + if (isYTBackend()) return; + let url = '/api/v1/auth/history'; if (typeof videoId !== 'undefined') { url += `/${videoId}`; @@ -317,6 +372,8 @@ export async function deleteHistory( } export async function postHistory(videoId: string, fetchOptions: RequestInit = {}) { + if (isYTBackend()) return; + await fetchErrorHandle( await fetch(buildPath(`auth/history/${videoId}`), { method: 'POST', @@ -331,6 +388,10 @@ export async function getPlaylist( page: number = 1, fetchOptions: RequestInit = {} ): Promise { + if (isYTBackend() || useEngineFallback('Playlist')) { + return await getPlaylistYTjs(playlistId); + } + let resp; if (get(authStore)) { @@ -348,6 +409,8 @@ export async function getPlaylist( export async function getPersonalPlaylists( fetchOptions: RequestInit = {} ): Promise { + if (isYTBackend()) return []; + const resp = await fetchErrorHandle( await fetch(buildPath('auth/playlists'), { ...buildAuthHeaders(), ...fetchOptions }) ); @@ -355,6 +418,8 @@ export async function getPersonalPlaylists( } export async function deletePersonalPlaylist(playlistId: string) { + if (isYTBackend()) return; + await fetchErrorHandle( await fetch(buildPath(`auth/playlists/${playlistId}`), { method: 'DELETE', @@ -368,6 +433,8 @@ export async function postPersonalPlaylist( privacy: 'public' | 'private' | 'unlisted', fetchOptions: RequestInit = {} ) { + if (isYTBackend()) return; + const headers: Record> = buildAuthHeaders(); headers['headers']['Content-type'] = 'application/json'; @@ -389,6 +456,8 @@ export async function addPlaylistVideo( videoId: string, fetchOptions: RequestInit = {} ) { + if (isYTBackend()) return; + const headers: Record> = buildAuthHeaders(); headers['headers']['Content-type'] = 'application/json'; @@ -409,6 +478,8 @@ export async function removePlaylistVideo( indexId: string, fetchOptions: RequestInit = {} ) { + if (isYTBackend()) return; + await fetchErrorHandle( await fetch(buildPath(`auth/playlists/${playlistId}/videos/${indexId}`), { method: 'DELETE', diff --git a/materialious/src/lib/api/misc.ts b/materialious/src/lib/api/misc.ts new file mode 100644 index 00000000..30e4cb8e --- /dev/null +++ b/materialious/src/lib/api/misc.ts @@ -0,0 +1,38 @@ +import { get } from 'svelte/store'; +import type { CommentsOptions, SearchOptions } from './model'; +import { engineFallbacksStore } from '$lib/store'; +import { Capacitor } from '@capacitor/core'; + +export function searchSetDefaults(options: SearchOptions) { + if (typeof options.sort_by === 'undefined') { + options.sort_by = 'relevance'; + } + + if (typeof options.type === 'undefined') { + options.type = 'all'; + } + + if (typeof options.page === 'undefined') { + options.page = '1'; + } +} + +export function commentsSetDefaults(options: CommentsOptions) { + if (typeof options.sort_by === 'undefined') { + options.sort_by = 'top'; + } +} + +export type EngineFallback = + | 'ResolveUrl' + | 'Video' + | 'Comments' + | 'Channel' + | 'ChannelContent' + | 'SearchSuggestions' + | 'Search' + | 'Playlist'; + +export function useEngineFallback(fallback: EngineFallback): boolean { + return get(engineFallbacksStore).includes(fallback) && Capacitor.isNativePlatform(); +} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 63e68398..7902c11c 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -1,5 +1,14 @@ import type { ApiResponse, Innertube, YT } from 'youtubei.js'; +export interface SearchOptions { + sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; + type?: 'video' | 'playlist' | 'channel' | 'all'; + duration?: 'short' | 'medium' | 'long'; + date?: 'hour' | 'today' | 'week' | 'month' | 'year'; + features?: string; + page?: string; +} + export interface Image { url: string; width: number; @@ -7,7 +16,6 @@ export interface Image { } export interface Thumbnail { - quality: string; url: string; width: number; height: number; @@ -160,6 +168,7 @@ export interface Comment { replyCount: number; continuation: string; }; + getReplies?: () => Promise; } export interface Comments { @@ -167,6 +176,7 @@ export interface Comments { videoId: string; continuation?: string; comments: Comment[]; + getContinuation?: () => Promise; } export interface Channel { @@ -217,12 +227,14 @@ export interface PlaylistPageVideo extends Omit { export interface ChannelContentVideos { videos: Video[]; - continuation: string; + continuation?: string; + getContinuation?: () => Promise; } export interface ChannelContentPlaylists { playlists: PlaylistPage[]; - continuation: string; + continuation?: string; + getContinuation?: () => Promise; } export interface PlaylistPage extends Omit { @@ -232,6 +244,7 @@ export interface PlaylistPage extends Omit { updated: number; isListed: boolean; videos: PlaylistPageVideo[]; + getContinuation?: () => Promise; } export interface ChannelPage extends Channel { @@ -295,3 +308,23 @@ export interface ApiExntendedProgressModel { export interface SynciousSaveProgressModel { time: number; } + +export type SearchResults = (Channel | Video | Playlist | HashTag)[] & { + getContinuation?: () => Promise; +}; + +export type ChannelContent = ChannelContentVideos | ChannelContentPlaylists; + +export type CommentsOptions = { + sort_by?: 'top' | 'new'; + continuation?: string; +}; + +export type ChannelSortBy = 'oldest' | 'newest' | 'popular'; +export type ChannelContentTypes = 'videos' | 'playlists' | 'streams' | 'shorts'; + +export type ChannelOptions = { + type?: ChannelContentTypes; + continuation?: string; + sortBy?: ChannelSortBy; +}; diff --git a/materialious/src/lib/api/youtubejs/channel.ts b/materialious/src/lib/api/youtubejs/channel.ts new file mode 100644 index 00000000..b0156378 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/channel.ts @@ -0,0 +1,135 @@ +import { YT, YTNodes } from 'youtubei.js'; +import { getInnertube } from '.'; +import type { + ChannelContent, + ChannelContentVideos, + ChannelOptions, + ChannelPage, + Image, + Video +} from '../model'; +import { invidiousItemSchema } from './schema'; + +export async function getChannelYTjs(channelId: string): Promise { + const innertube = await getInnertube(); + + const innerResults = await innertube.getChannel(channelId); + + const authorId = innerResults.metadata.url?.split('/')[4] ?? ''; + + let authorBanners: Image[] = []; + if (innerResults.header?.is(YTNodes.PageHeaderView)) { + authorBanners = innerResults.header.banner?.image ?? []; + } + + return { + type: 'channel', + allowedRegions: innerResults.metadata.available_countries ?? [], + tabs: innerResults.tabs.map((tab) => tab.toLowerCase()), + latestVideos: [], + isFamilyFriendly: true, + author: innerResults.metadata.title ?? '', + authorThumbnails: innerResults.metadata.avatar ?? [], + authorId: authorId, + authorBanners, + joined: 0, + authorVerified: true, + totalViews: 0, + authorUrl: `/channel/${authorId}`, + subCount: 0, + autoGenerated: false, + description: '', + descriptionHml: '' + }; +} + +export function invidiousChannelContentSchema( + innerResults: YT.Channel | YT.ChannelListContinuation, + author: string +) { + const videos: Video[] = []; + + let contents; + if ( + innerResults instanceof YT.Channel && + innerResults.current_tab?.content?.is(YTNodes.RichGrid) + ) { + contents = innerResults.current_tab.content.contents; + } else if ( + innerResults instanceof YT.ChannelListContinuation && + innerResults.contents?.is(YTNodes.AppendContinuationItemsAction) + ) { + contents = innerResults.contents.contents; + } + + if (!contents) return videos; + + contents.forEach((item) => { + if (item.is(YTNodes.RichItem)) { + const invidiousSchema = invidiousItemSchema(item.content); + if (invidiousSchema?.type === 'video') { + invidiousSchema.author = author; + videos.push(invidiousSchema); + } + } + }); + + return videos; +} + +function fetchChannelContentVideosWithContinuation( + innerResults: YT.ChannelListContinuation, + author: string +): () => Promise { + return async () => { + const continuation = await innerResults.getContinuation(); + return { + videos: invidiousChannelContentSchema(continuation, author), + getContinuation: fetchChannelContentVideosWithContinuation(continuation, author) + }; + }; +} + +async function fetchChannelContentWithContinuation( + channelId: string, + innerResults: YT.Channel | YT.ChannelListContinuation, + author: string +): Promise { + const channelContent: ChannelContent = { + continuation: innerResults.has_continuation ? 'logicalPlaceholder' : undefined, + videos: invidiousChannelContentSchema(innerResults, author) + }; + + if (channelContent.continuation) { + channelContent.getContinuation = fetchChannelContentVideosWithContinuation( + innerResults, + author + ); + } + + return channelContent; +} + +export async function getChannelContentYTjs( + channelId: string, + options: ChannelOptions +): Promise { + const innertube = await getInnertube(); + + const channel = await innertube.getChannel(channelId); + + const author = channel.metadata.title ?? ''; + + let innerResults: YT.Channel | YT.ChannelListContinuation; + if (options.type === 'videos') { + innerResults = await channel.getVideos(); + } else if (options.type === 'playlists') { + innerResults = await channel.getPlaylists(); + } else if (options.type === 'shorts') { + innerResults = await channel.getShorts(); + } else { + innerResults = await channel.getLiveStreams(); + } + + return fetchChannelContentWithContinuation(channelId, innerResults, author); +} diff --git a/materialious/src/lib/api/youtubejs/comments.ts b/materialious/src/lib/api/youtubejs/comments.ts new file mode 100644 index 00000000..942bd634 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/comments.ts @@ -0,0 +1,98 @@ +import { extractNumber } from '$lib/numbers'; +import { YT, YTNodes } from 'youtubei.js'; +import { getInnertube } from '.'; +import type { Comment, Comments, CommentsOptions, Thumbnail } from '../model'; + +function invidiousCommentContentSchema(result: YTNodes.CommentView): Comment | undefined { + if (!result.author) return; + + return { + author: result.author.name ?? '', + authorId: result.author.id ?? '', + authorUrl: `/channel/${result.author.id}`, + authorThumbnails: result.author.thumbnails as Thumbnail[], + isEdited: false, + isPinned: result.is_pinned === true, + content: result.content?.text ?? '', + contentHtml: result.content?.toHTML() ?? '', + published: 0, + publishedText: result.published_time ?? '', + likeCount: extractNumber(result.like_count ?? '0'), + authorIsChannelOwner: result.author_is_channel_owner === true, + creatorHeart: { + creatorName: result.heart_active_tooltip?.split('@')[1] ?? '', + creatorThumbnail: result.creator_thumbnail_url ?? '' + }, + replies: { + replyCount: extractNumber(result.reply_count ?? '0'), + continuation: '' + } + }; +} + +function invidiousCommentSchema(innerResults: YT.Comments, videoId: string) { + const comment: Comment[] = []; + + innerResults.contents.forEach(async (result) => { + if (result.comment) { + const invidiousResult = invidiousCommentContentSchema(result.comment); + if (invidiousResult) { + if (result.has_replies) + invidiousResult.getReplies = async (): Promise => { + const replies = await result.getReplies(); + if (!replies.replies) return { videoId: '', comments: [], commentCount: 0 }; + + const comments: Comment[] = replies.replies + .map((reply) => invidiousCommentContentSchema(reply)) + .filter((comment): comment is Comment => !!comment); + + return { + videoId: videoId, + comments: comments, + commentCount: 0 + }; + }; + + comment.push(invidiousResult); + } + } + }); + + return comment; +} + +async function fetchCommentsWithContinuation( + videoId: string, + innerResults: YT.Comments, + options: CommentsOptions +): Promise { + const comments: Comments = { + videoId: videoId, + comments: invidiousCommentSchema(innerResults, videoId), + commentCount: extractNumber(innerResults.header?.comments_count.text ?? '0'), + continuation: innerResults.has_continuation ? 'logicalPlaceholder' : undefined + }; + + if (comments.continuation) { + comments.getContinuation = async () => { + const continuation = await innerResults.getContinuation(); + return fetchCommentsWithContinuation(videoId, continuation, options); + }; + } + + return comments; +} + +export async function getCommentsYTjs( + videoId: string, + options: CommentsOptions +): Promise { + const innertube = await getInnertube(); + + const innerResults = await innertube.getComments( + videoId, + options.sort_by === 'top' ? 'TOP_COMMENTS' : 'NEWEST_FIRST' + ); + + return fetchCommentsWithContinuation(videoId, innerResults, options); +} diff --git a/materialious/src/lib/api/youtubejs/index.ts b/materialious/src/lib/api/youtubejs/index.ts new file mode 100644 index 00000000..b8a45735 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/index.ts @@ -0,0 +1,19 @@ +import { interfaceRegionStore } from '$lib/store'; +import { USER_AGENT } from 'bgutils-js'; +import { get } from 'svelte/store'; +import Innertube, { UniversalCache } from 'youtubei.js'; + +let innertube: Innertube | undefined; + +export async function getInnertube(): Promise { + if (innertube) return innertube; + + innertube = await Innertube.create({ + fetch: fetch, + cache: new UniversalCache(true), + location: get(interfaceRegionStore), + user_agent: USER_AGENT + }); + + return innertube; +} diff --git a/materialious/src/lib/api/youtubejs/misc.ts b/materialious/src/lib/api/youtubejs/misc.ts new file mode 100644 index 00000000..5219be15 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/misc.ts @@ -0,0 +1,14 @@ +import { getInnertube } from '.'; +import type { ResolvedUrl } from '../model'; + +export async function getResolveUrlYTjs(url: string): Promise { + const innertube = await getInnertube(); + + const resloved = await innertube.resolveURL(url); + + return { + pageType: resloved.metadata.page_type ?? '', + params: '', + ucid: resloved.metadata.url ? resloved.metadata.url?.split('/')[2] : '' + }; +} diff --git a/materialious/src/lib/api/youtubejs/playlist.ts b/materialious/src/lib/api/youtubejs/playlist.ts new file mode 100644 index 00000000..35f475de --- /dev/null +++ b/materialious/src/lib/api/youtubejs/playlist.ts @@ -0,0 +1,62 @@ +import { extractNumber } from '$lib/numbers'; +import { YT, YTNodes } from 'youtubei.js'; +import { getInnertube } from '.'; +import type { PlaylistPage, PlaylistPageVideo } from '../model'; + +async function fetchPlaylistWithContinuation( + playlist: YT.Playlist, + playlistId: string +): Promise { + const videos: PlaylistPageVideo[] = []; + + playlist.videos.forEach((video) => { + if (video.is(YTNodes.PlaylistVideo)) { + const videoIndex = video.index.text ?? '0'; + videos.push({ + type: 'video', + author: video.author.name, + authorId: video.author.id, + index: extractNumber(videoIndex), + indexId: videoIndex, + viewCount: 0, + title: video.title.text ?? '', + videoId: video.id ?? '', + lengthSeconds: video.duration.seconds, + videoThumbnails: video.thumbnails + }); + } + }); + + const playlistPage: PlaylistPage = { + type: 'playlist', + title: playlist.info.title ?? '', + description: playlist.info.description ?? '', + descriptionHtml: playlist.info.description ?? '', + viewCount: extractNumber(playlist.info.views ?? '0'), + updated: 0, + isListed: true, + videos: videos, + playlistId, + videoCount: playlist.videos.length, + author: playlist.info.author.name, + authorId: playlist.info.author.id, + authorVerified: true, + playlistThumbnail: playlist.info.thumbnails[0].url ?? '' + }; + + if (playlist) { + playlistPage.getContinuation = async () => { + const continuation = await playlist.getContinuation(); + return fetchPlaylistWithContinuation(continuation, playlistId); + }; + } + + return playlistPage; +} + +export async function getPlaylistYTjs(playlistId: string): Promise { + const innertube = await getInnertube(); + const playlist = await innertube.getPlaylist(playlistId); + + return fetchPlaylistWithContinuation(playlist, playlistId); +} diff --git a/materialious/src/lib/api/youtubejs/schema.ts b/materialious/src/lib/api/youtubejs/schema.ts new file mode 100644 index 00000000..ba673718 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/schema.ts @@ -0,0 +1,65 @@ +import { cleanNumber, extractNumber } from '$lib/numbers'; +import { convertToSeconds } from '$lib/time'; +import type { Channel, Playlist, Thumbnail, Video } from '../model'; +import { Helpers, YTNodes } from 'youtubei.js'; + +export function invidiousItemSchema(item: Helpers.YTNode): Video | Channel | Playlist | undefined { + if (item.is(YTNodes.Video)) { + const views = extractNumber(item.view_count?.toString() || ''); + return { + type: 'video', + title: item.title.toString(), + videoId: item.video_id, + viewCountText: cleanNumber(views), + viewCount: views, + videoThumbnails: item.thumbnails as Thumbnail[], + published: 0, + publishedText: item.published?.toString() || '', + description: '', + descriptionHtml: '', + authorUrl: `/channel/${item.author.id}`, + authorId: item.author.id, + authorVerified: false, + liveNow: false, + isUpcoming: false, + premium: false, + author: item.author.name, + lengthSeconds: item.length_text?.text ? convertToSeconds(item.length_text.text) : 0 + }; + } else if (item.is(YTNodes.Channel)) { + return { + type: 'channel', + authorId: item.id, + author: item.author.name, + authorUrl: `/channel/${item.id}`, + authorVerified: item.author.is_verified === true, + subCount: item.video_count.text ? extractNumber(item.video_count.text) : 0, + totalViews: 0, + autoGenerated: false, + description: item.description_snippet.text ?? '', + descriptionHml: item.description_snippet.toHTML() ?? '', + authorThumbnails: item.author.thumbnails as Thumbnail[] + }; + } else if (item.is(YTNodes.LockupView) && item.content_type === 'PLAYLIST') { + let author = ''; + const metadataRows = item.metadata?.metadata?.metadata_rows[0]; + if (metadataRows && metadataRows.metadata_parts) { + author = metadataRows.metadata_parts[0].text?.text ?? ''; + } + return { + type: 'playlist', + title: item.metadata?.title.text ?? '', + playlistId: item.content_id, + playlistThumbnail: item.content_image?.is(YTNodes.CollectionThumbnailView) + ? item.content_image.primary_thumbnail?.is(YTNodes.ThumbnailView) + ? item.content_image.primary_thumbnail.image[0].url + : '' + : '', + authorVerified: false, + author, + authorId: '', + videoCount: 0, + videos: [] + }; + } +} diff --git a/materialious/src/lib/api/youtubejs/search.ts b/materialious/src/lib/api/youtubejs/search.ts new file mode 100644 index 00000000..4917c8ce --- /dev/null +++ b/materialious/src/lib/api/youtubejs/search.ts @@ -0,0 +1,44 @@ +import { getInnertube } from '.'; +import type { SearchOptions, SearchResults } from '../model'; +import { type Types, YT } from 'youtubei.js'; +import { invidiousItemSchema } from './schema'; + +function invidiousSchema(innerResults: YT.Search): SearchResults { + const searchResults: SearchResults = []; + + innerResults.results.forEach((result) => { + const item = invidiousItemSchema(result); + if (item) searchResults.push(item); + }); + + return searchResults; +} + +async function fetchSearchWithContinuation(innerResults: YT.Search): Promise { + const searchResults = invidiousSchema(innerResults); + + if (innerResults.has_continuation) { + searchResults.getContinuation = async () => { + const continuation = await innerResults.getContinuation(); + return fetchSearchWithContinuation(continuation); + }; + } + + return searchResults; +} + +export async function getSearchYTjs( + search: string, + options: SearchOptions +): Promise { + const innertube = await getInnertube(); + + const innerResults = await innertube.search(search, { + sort_by: options.sort_by, + duration: options.duration, + features: [options.features] as Types.Feature[], + upload_date: options.date + }); + + return fetchSearchWithContinuation(innerResults); +} diff --git a/materialious/src/lib/api/youtubejs/searchSuggestions.ts b/materialious/src/lib/api/youtubejs/searchSuggestions.ts new file mode 100644 index 00000000..078fe57e --- /dev/null +++ b/materialious/src/lib/api/youtubejs/searchSuggestions.ts @@ -0,0 +1,13 @@ +import { getInnertube } from '.'; +import type { SearchSuggestion } from '../model'; + +export async function getSearchSuggestionsYTjs(search: string): Promise { + const innertube = await getInnertube(); + + const searchSuggestions = await innertube.getSearchSuggestions(search); + + return { + query: search, + suggestions: searchSuggestions + }; +} diff --git a/materialious/src/lib/api/youtubejs/subscriptions.ts b/materialious/src/lib/api/youtubejs/subscriptions.ts new file mode 100644 index 00000000..66d513ed --- /dev/null +++ b/materialious/src/lib/api/youtubejs/subscriptions.ts @@ -0,0 +1,168 @@ +import { localDb } from '$lib/dexie'; +import { logoutStores } from '$lib/misc'; +import { cleanNumber } from '$lib/numbers'; +import { relativeTimestamp } from '$lib/time'; +import { get } from 'svelte/store'; +import type { Feed, Subscription, Thumbnail } from '../model'; +import { getChannelYTjs } from './channel'; +import { engineCooldownYTStore, engineCullYTStore } from '$lib/store'; + +export async function getSubscriptionsYTjs(): Promise { + const subscriptions: Subscription[] = []; + + await localDb.channelSubscriptions.each((subscription) => { + subscriptions.push({ + author: subscription.channelName, + authorId: subscription.channelId + }); + }); + + return subscriptions; +} + +export async function amSubscribedYTjs(authorId: string): Promise { + return (await localDb.channelSubscriptions.where('channelId').equals(authorId).count()) > 0; +} + +export async function postSubscribeYTjs(authorId: string) { + const channel = await getChannelYTjs(authorId); + + try { + await localDb.channelSubscriptions.add({ + channelId: authorId, + channelName: channel.author, + lastRSSFetch: new Date(0) + }); + } catch { + // Continue regardless + } + + // Store channel into db in background + parseChannelRSS(authorId); +} + +export async function deleteUnsubscribeYTjs(authorId: string) { + await localDb.channelSubscriptions.where('channelId').equals(authorId).delete(); + await localDb.subscriptionFeed.where('authorId').equals(authorId).delete(); + logoutStores(); +} + +export async function parseChannelRSS(channelId: string): Promise { + const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`; + + const response = await fetch(feedUrl); + const text = await response.text(); + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(text, 'text/xml'); + const entries = xmlDoc.getElementsByTagName('entry'); + + for (const entry of entries) { + const videoId = entry.getElementsByTagName('yt:videoId')[0]?.textContent || ''; + const title = entry.getElementsByTagName('title')[0]?.textContent || 'Untitled'; + const description = + entry.getElementsByTagName('media:description')[0]?.textContent || 'No description available'; + const descriptionHtml = description; + const publishedAt = new Date( + entry.getElementsByTagName('published')[0]?.textContent || new Date() + ); + const published = publishedAt.getTime(); + const publishedText = relativeTimestamp(published, false); + const author = + entry.getElementsByTagName('author')[0]?.getElementsByTagName('name')[0]?.textContent || + 'Unknown Author'; + const authorId = + entry + .getElementsByTagName('author')[0] + ?.getElementsByTagName('uri')[0] + ?.textContent.split('/')[4] || ''; + + const authorUrl = authorId ? `/channel/${authorId}` : ''; + const authorVerified = false; + + // Extracting video thumbnails + const videoThumbnails: Thumbnail[] = Array.from( + entry.getElementsByTagName('media:group')[0]?.getElementsByTagName('media:thumbnail') || [] + ).map((thumbElement) => ({ + url: thumbElement.getAttribute('url') || '', + width: parseInt(thumbElement.getAttribute('width') || '0'), + height: parseInt(thumbElement.getAttribute('height') || '0') + })); + + const liveNow = false; + const isUpcoming = false; + const premium = false; + const lengthSeconds = 0; + const viewCountText = + entry.getElementsByTagName('media:statistics')[0]?.getAttribute('views') || '0'; + const viewCount = parseInt(viewCountText) || 0; + + try { + await localDb.subscriptionFeed.add({ + type: 'video', + videoId, + title, + videoThumbnails, + author, + authorId, + description, + descriptionHtml, + published, + publishedText, + liveNow, + premium, + isUpcoming, + lengthSeconds, + viewCountText: cleanNumber(viewCount), + viewCount, + authorUrl, + authorVerified + }); + } catch { + // Continue regardless of error + } + + await localDb.channelSubscriptions.where('channelId').equals(channelId).modify({ + lastRSSFetch: new Date() + }); + } +} + +// Ignored to match non ytjs version of getfeed. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function getFeedYTjs(maxResults: number, page: number): Promise { + const channelSubscriptions = await localDb.channelSubscriptions.toArray(); + + const toUpdatePromises: Promise[] = []; + + const now = new Date(); + for (const channel of channelSubscriptions) { + const lastRSSFetch = new Date(channel.lastRSSFetch); + const timeDifference = now.getTime() - lastRSSFetch.getTime(); + const cooldownTime = get(engineCooldownYTStore) * 60 * 60 * 1000; + if (timeDifference > cooldownTime) { + toUpdatePromises.push(parseChannelRSS(channel.channelId)); + } + } + + if (toUpdatePromises) { + await Promise.all(toUpdatePromises); + } + + const videos = await localDb.subscriptionFeed.toArray(); + videos.sort((a, b) => b.published - a.published); + + const cullAfter = get(engineCullYTStore); + + if (videos.length > cullAfter) { + const videosToDelete = videos.slice(cullAfter); + const videoIdsToDelete = videosToDelete.map((video) => video.videoId); + + await localDb.subscriptionFeed.where('id').anyOf(videoIdsToDelete).delete(); + } + + return { + notifications: [], + videos: videos + }; +} diff --git a/materialious/src/lib/api/youtubejs.ts b/materialious/src/lib/api/youtubejs/video.ts similarity index 88% rename from materialious/src/lib/api/youtubejs.ts rename to materialious/src/lib/api/youtubejs/video.ts index e484441b..992b5a82 100644 --- a/materialious/src/lib/api/youtubejs.ts +++ b/materialious/src/lib/api/youtubejs/video.ts @@ -8,13 +8,14 @@ import type { VideoBase, VideoPlay } from '$lib/api/model'; -import { interfaceRegionStore, poTokenCacheStore } from '$lib/store'; +import { poTokenCacheStore } from '$lib/store'; import { convertToSeconds } from '$lib/time'; import { Capacitor } from '@capacitor/core'; -import { USER_AGENT } from 'bgutils-js'; import { get } from 'svelte/store'; import type { Types } from 'youtubei.js'; -import { Innertube, UniversalCache, Utils, YT, YTNodes, Platform } from 'youtubei.js'; +import { Utils, YT, YTNodes, Platform } from 'youtubei.js'; +import { getInnertube } from '.'; +import { cleanNumber, extractNumber } from '$lib/numbers'; Platform.shim.eval = async ( data: Types.BuildScriptResult, @@ -35,17 +36,12 @@ Platform.shim.eval = async ( return new Function(code)(); }; -export async function getVideoTYjs(videoId: string): Promise { +export async function getVideoYTjs(videoId: string): Promise { if (!Capacitor.isNativePlatform()) { throw new Error('Platform not supported'); } - const youtube = await Innertube.create({ - fetch: fetch, - cache: new UniversalCache(false), - location: get(interfaceRegionStore), - user_agent: USER_AGENT - }); + const innertube = await getInnertube(); const requestKey = 'O43z0dpjhgX20SCx4KAo'; @@ -57,7 +53,7 @@ export async function getVideoTYjs(videoId: string): Promise { const clientPlaybackNonce = Utils.generateRandomString(16); const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } }); - const rawPlayerResponse = await watchEndpoint.call(youtube.actions, { + const rawPlayerResponse = await watchEndpoint.call(innertube.actions, { contentCheckOk: true, racyCheckOk: true, playbackContext: { @@ -65,16 +61,16 @@ export async function getVideoTYjs(videoId: string): Promise { pyv: true }, contentPlaybackContext: { - signatureTimestamp: youtube.session.player?.signature_timestamp + signatureTimestamp: innertube.session.player?.signature_timestamp } } }); - const rawNextResponse = await watchEndpoint.call(youtube.actions, { + const rawNextResponse = await watchEndpoint.call(innertube.actions, { override_endpoint: '/next' }); const video = new YT.VideoInfo( [rawPlayerResponse, rawNextResponse], - youtube.actions, + innertube.actions, clientPlaybackNonce ); @@ -82,7 +78,7 @@ export async function getVideoTYjs(videoId: string): Promise { throw new Error('Unable to pull video info from youtube.js'); } - const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'); + const challengeResponse = await innertube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'); poTokenCacheStore.set(await platformMinter(requestKey, videoId, challengeResponse)); let dashUri: string | undefined; @@ -139,7 +135,7 @@ export async function getVideoTYjs(videoId: string): Promise { let authorThumbnails: Image[]; if (video.basic_info.channel_id) { - const channel = await youtube.getChannel(video.basic_info.channel_id); + const channel = await innertube.getChannel(video.basic_info.channel_id); authorThumbnails = channel.metadata.avatar as Image[]; } else { authorThumbnails = []; @@ -151,7 +147,7 @@ export async function getVideoTYjs(videoId: string): Promise { url.searchParams.set('potc', '1'); url.searchParams.set('pot', get(poTokenCacheStore) ?? ''); - url.searchParams.set('c', youtube.session.context.client.clientName); + url.searchParams.set('c', innertube.session.context.client.clientName); url.searchParams.set('fmt', 'vtt'); // Remove &xosf=1 as it adds `position:63% line:0%` to the subtitle lines @@ -190,10 +186,13 @@ export async function getVideoTYjs(videoId: string): Promise { videoThumbnails: (recommended?.content_image.image as Thumbnail[]) || [], videoId: recommended.content_id, title: recommended.metadata.title.toString(), - viewCountText: - (recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text ?? '') - .toString() - .replace('views', '') || '', + viewCountText: cleanNumber( + extractNumber( + ( + recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text ?? '' + ).toString() + ) + ), author: ( recommended.metadata.metadata.metadata_rows[0]?.metadata_parts?.[0]?.text ?? '' @@ -271,7 +270,7 @@ export async function getVideoTYjs(videoId: string): Promise { keywords: video.basic_info.keywords || [], allowedRegions: [], ytjs: { - innertube: youtube, + innertube: innertube, video: video, clientPlaybackNonce: clientPlaybackNonce, rawApiResponse: rawPlayerResponse diff --git a/materialious/src/lib/components/Author.svelte b/materialious/src/lib/components/Author.svelte index 6242d9e8..5bced802 100644 --- a/materialious/src/lib/components/Author.svelte +++ b/materialious/src/lib/components/Author.svelte @@ -3,7 +3,7 @@ import { amSubscribed, deleteUnsubscribe, postSubscribe } from '$lib/api'; import type { Image } from '$lib/api/model'; import { getBestThumbnail, proxyGoogleImage } from '$lib/images'; - import { truncate } from '$lib/misc'; + import { isYTBackend, truncate } from '$lib/misc'; import { authStore, interfaceLowBandwidthMode, isAndroidTvStore } from '$lib/store'; import { _ } from '$lib/i18n'; import { localDb } from '$lib/dexie'; @@ -71,12 +71,12 @@ {#if !hideSubscribe} - {#if $authStore} + {#if $authStore || isYTBackend()} {:else} - {/if} - {#if playerElement && hasWebkitShowPlaybackTargetPicker(playerElement)} - {/if} {#if document.pictureInPictureEnabled} {/if} - {:else} - diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 249f1ccc..4dec2928 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -68,7 +68,7 @@ nav.bottom { } dialog { - padding-top: calc(var(--safe-area-inset-top) + 1rem) !important; + border-radius: 0.25rem !important; } main.root { @@ -156,4 +156,8 @@ menu { width: 100% !important; border-radius: 0 !important; } + + dialog { + padding-top: calc(var(--safe-area-inset-top) + 1rem) !important; + } } diff --git a/materialious/src/lib/dexie.ts b/materialious/src/lib/dexie.ts index f2501806..090585c9 100644 --- a/materialious/src/lib/dexie.ts +++ b/materialious/src/lib/dexie.ts @@ -1,18 +1,29 @@ import type { Table } from 'dexie'; import Dexie from 'dexie'; +import type { Video } from './api/model'; export interface FavouriteChannels { channelId: string; created: Date; } +export interface ChannelSubscriptions { + channelId: string; + channelName: string; + lastRSSFetch: Date; +} + export class MaterialiousDb extends Dexie { favouriteChannels!: Table; + channelSubscriptions!: Table; + subscriptionFeed!: Table
{$_('layout.settings')}
- {#if !isLoggedIn} - - login -
{$_('layout.login')}
-
- {:else} - - logout -
{$_('layout.logout')}
-
+ {#if !isYTBackend()} + {#if !isLoggedIn} + + login +
{$_('layout.login')}
+
+ {:else} + + logout +
{$_('layout.logout')}
+
+ {/if} {/if} {/if} @@ -350,7 +346,12 @@
{$_('layout.syncParty')}
{#if isLoggedIn} - - {#if !isLoggedIn} - - {:else} - + {#if !isYTBackend()} + {#if !isLoggedIn} + + {:else} + + {/if} {/if} {/if} diff --git a/materialious/src/routes/(app)/+page.ts b/materialious/src/routes/(app)/+page.ts index 14feb4fb..1a74d6b2 100644 --- a/materialious/src/routes/(app)/+page.ts +++ b/materialious/src/routes/(app)/+page.ts @@ -1,9 +1,17 @@ +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; import { getPopular } from '$lib/api/index'; +import { isYTBackend } from '$lib/misc'; import { feedCacheStore } from '$lib/store'; import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; export async function load() { + if (isYTBackend()) { + goto(resolve('/subscriptions', {}), { replaceState: true }); + return; + } + let popular = get(feedCacheStore).popular; let popularDisabled: boolean = false; diff --git a/materialious/src/routes/(app)/channel/[slug]/+page.svelte b/materialious/src/routes/(app)/channel/[slug]/+page.svelte index fded0d42..0a54dbb5 100644 --- a/materialious/src/routes/(app)/channel/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/channel/[slug]/+page.svelte @@ -1,11 +1,6 @@ diff --git a/materialious/src/routes/(app)/subscriptions/+page.svelte b/materialious/src/routes/(app)/subscriptions/+page.svelte index 09ee3c00..95dd99c1 100644 --- a/materialious/src/routes/(app)/subscriptions/+page.svelte +++ b/materialious/src/routes/(app)/subscriptions/+page.svelte @@ -6,11 +6,15 @@ import ItemsList from '$lib/components/ItemsList.svelte'; import { resolve } from '$app/paths'; import { _ } from '$lib/i18n'; + import { isYTBackend } from '$lib/misc'; let currentPage = 1; let videos: (VideoBase | Video | PlaylistPageVideo)[] = $state($feedCacheStore.subscription); async function loadMore(event: InfiniteEvent) { + // Not supported or needed on YT backend + if (isYTBackend()) return; + currentPage++; const feed = await getFeed(100, currentPage); if (feed.videos.length === 0) { @@ -24,7 +28,7 @@ diff --git a/materialious/src/routes/(app)/subscriptions/+page.ts b/materialious/src/routes/(app)/subscriptions/+page.ts index fcf60edf..6b2e3953 100644 --- a/materialious/src/routes/(app)/subscriptions/+page.ts +++ b/materialious/src/routes/(app)/subscriptions/+page.ts @@ -1,7 +1,7 @@ import { getFeed } from '$lib/api/index'; import type { PlaylistPageVideo, Video, VideoBase } from '$lib/api/model'; import { localDb } from '$lib/dexie'; -import { excludeDuplicateFeeds } from '$lib/misc'; +import { authProtected, excludeDuplicateFeeds } from '$lib/misc'; import { feedCacheStore } from '$lib/store'; import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; @@ -35,6 +35,8 @@ async function sortVideosByFavourites(videos: supportedVideos): Promise 0) { + comments.continuation = loadedComments.continuation; + comments.comments = [...comments.comments, ...loadedComments.comments]; + } + } catch (error) { + console.error('Error loading more comments:', error); + } } function toggleTheatreMode() { @@ -371,8 +384,6 @@ pauseTimerSeconds = 0; clearTimeout(pauseTimeout); }, pauseTimerSeconds * 1000); - - ui('#pause-timer'); } @@ -407,7 +418,7 @@ - {#if personalPlaylists && personalPlaylists.length > 0} - {:else} - - - - + + + +
diff --git a/materialious/static/localApi/ghContributors.json b/materialious/static/localApi/ghContributors.json index 381e2261..ad59e267 100644 --- a/materialious/static/localApi/ghContributors.json +++ b/materialious/static/localApi/ghContributors.json @@ -1 +1 @@ -[{"login":"WardPearce","id":27844174,"node_id":"MDQ6VXNlcjI3ODQ0MTc0","avatar_url":"https://avatars.githubusercontent.com/u/27844174?v=4","gravatar_id":"","url":"https://api.github.com/users/WardPearce","html_url":"https://github.com/WardPearce","followers_url":"https://api.github.com/users/WardPearce/followers","following_url":"https://api.github.com/users/WardPearce/following{/other_user}","gists_url":"https://api.github.com/users/WardPearce/gists{/gist_id}","starred_url":"https://api.github.com/users/WardPearce/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/WardPearce/subscriptions","organizations_url":"https://api.github.com/users/WardPearce/orgs","repos_url":"https://api.github.com/users/WardPearce/repos","events_url":"https://api.github.com/users/WardPearce/events{/privacy}","received_events_url":"https://api.github.com/users/WardPearce/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1901},{"login":"dependabot[bot]","id":49699333,"node_id":"MDM6Qm90NDk2OTkzMzM=","avatar_url":"https://avatars.githubusercontent.com/in/29110?v=4","gravatar_id":"","url":"https://api.github.com/users/dependabot%5Bbot%5D","html_url":"https://github.com/apps/dependabot","followers_url":"https://api.github.com/users/dependabot%5Bbot%5D/followers","following_url":"https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dependabot%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/dependabot%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/dependabot%5Bbot%5D/repos","events_url":"https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/dependabot%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false,"contributions":224},{"login":"Lezurex","id":48829956,"node_id":"MDQ6VXNlcjQ4ODI5OTU2","avatar_url":"https://avatars.githubusercontent.com/u/48829956?v=4","gravatar_id":"","url":"https://api.github.com/users/Lezurex","html_url":"https://github.com/Lezurex","followers_url":"https://api.github.com/users/Lezurex/followers","following_url":"https://api.github.com/users/Lezurex/following{/other_user}","gists_url":"https://api.github.com/users/Lezurex/gists{/gist_id}","starred_url":"https://api.github.com/users/Lezurex/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lezurex/subscriptions","organizations_url":"https://api.github.com/users/Lezurex/orgs","repos_url":"https://api.github.com/users/Lezurex/repos","events_url":"https://api.github.com/users/Lezurex/events{/privacy}","received_events_url":"https://api.github.com/users/Lezurex/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":39},{"login":"Coool","id":8421903,"node_id":"MDQ6VXNlcjg0MjE5MDM=","avatar_url":"https://avatars.githubusercontent.com/u/8421903?v=4","gravatar_id":"","url":"https://api.github.com/users/Coool","html_url":"https://github.com/Coool","followers_url":"https://api.github.com/users/Coool/followers","following_url":"https://api.github.com/users/Coool/following{/other_user}","gists_url":"https://api.github.com/users/Coool/gists{/gist_id}","starred_url":"https://api.github.com/users/Coool/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Coool/subscriptions","organizations_url":"https://api.github.com/users/Coool/orgs","repos_url":"https://api.github.com/users/Coool/repos","events_url":"https://api.github.com/users/Coool/events{/privacy}","received_events_url":"https://api.github.com/users/Coool/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":15},{"login":"araujosemacento","id":60983392,"node_id":"MDQ6VXNlcjYwOTgzMzky","avatar_url":"https://avatars.githubusercontent.com/u/60983392?v=4","gravatar_id":"","url":"https://api.github.com/users/araujosemacento","html_url":"https://github.com/araujosemacento","followers_url":"https://api.github.com/users/araujosemacento/followers","following_url":"https://api.github.com/users/araujosemacento/following{/other_user}","gists_url":"https://api.github.com/users/araujosemacento/gists{/gist_id}","starred_url":"https://api.github.com/users/araujosemacento/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/araujosemacento/subscriptions","organizations_url":"https://api.github.com/users/araujosemacento/orgs","repos_url":"https://api.github.com/users/araujosemacento/repos","events_url":"https://api.github.com/users/araujosemacento/events{/privacy}","received_events_url":"https://api.github.com/users/araujosemacento/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":14},{"login":"ghostofsparta222","id":251893418,"node_id":"U_kgDODwOWqg","avatar_url":"https://avatars.githubusercontent.com/u/251893418?v=4","gravatar_id":"","url":"https://api.github.com/users/ghostofsparta222","html_url":"https://github.com/ghostofsparta222","followers_url":"https://api.github.com/users/ghostofsparta222/followers","following_url":"https://api.github.com/users/ghostofsparta222/following{/other_user}","gists_url":"https://api.github.com/users/ghostofsparta222/gists{/gist_id}","starred_url":"https://api.github.com/users/ghostofsparta222/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ghostofsparta222/subscriptions","organizations_url":"https://api.github.com/users/ghostofsparta222/orgs","repos_url":"https://api.github.com/users/ghostofsparta222/repos","events_url":"https://api.github.com/users/ghostofsparta222/events{/privacy}","received_events_url":"https://api.github.com/users/ghostofsparta222/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":11},{"login":"toolatebot","id":169580663,"node_id":"U_kgDOChuYdw","avatar_url":"https://avatars.githubusercontent.com/u/169580663?v=4","gravatar_id":"","url":"https://api.github.com/users/toolatebot","html_url":"https://github.com/toolatebot","followers_url":"https://api.github.com/users/toolatebot/followers","following_url":"https://api.github.com/users/toolatebot/following{/other_user}","gists_url":"https://api.github.com/users/toolatebot/gists{/gist_id}","starred_url":"https://api.github.com/users/toolatebot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/toolatebot/subscriptions","organizations_url":"https://api.github.com/users/toolatebot/orgs","repos_url":"https://api.github.com/users/toolatebot/repos","events_url":"https://api.github.com/users/toolatebot/events{/privacy}","received_events_url":"https://api.github.com/users/toolatebot/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":9},{"login":"Precific","id":30053082,"node_id":"MDQ6VXNlcjMwMDUzMDgy","avatar_url":"https://avatars.githubusercontent.com/u/30053082?v=4","gravatar_id":"","url":"https://api.github.com/users/Precific","html_url":"https://github.com/Precific","followers_url":"https://api.github.com/users/Precific/followers","following_url":"https://api.github.com/users/Precific/following{/other_user}","gists_url":"https://api.github.com/users/Precific/gists{/gist_id}","starred_url":"https://api.github.com/users/Precific/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Precific/subscriptions","organizations_url":"https://api.github.com/users/Precific/orgs","repos_url":"https://api.github.com/users/Precific/repos","events_url":"https://api.github.com/users/Precific/events{/privacy}","received_events_url":"https://api.github.com/users/Precific/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":9},{"login":"yzqzss","id":30341059,"node_id":"MDQ6VXNlcjMwMzQxMDU5","avatar_url":"https://avatars.githubusercontent.com/u/30341059?v=4","gravatar_id":"","url":"https://api.github.com/users/yzqzss","html_url":"https://github.com/yzqzss","followers_url":"https://api.github.com/users/yzqzss/followers","following_url":"https://api.github.com/users/yzqzss/following{/other_user}","gists_url":"https://api.github.com/users/yzqzss/gists{/gist_id}","starred_url":"https://api.github.com/users/yzqzss/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yzqzss/subscriptions","organizations_url":"https://api.github.com/users/yzqzss/orgs","repos_url":"https://api.github.com/users/yzqzss/repos","events_url":"https://api.github.com/users/yzqzss/events{/privacy}","received_events_url":"https://api.github.com/users/yzqzss/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"thejenja","id":65224669,"node_id":"MDQ6VXNlcjY1MjI0NjY5","avatar_url":"https://avatars.githubusercontent.com/u/65224669?v=4","gravatar_id":"","url":"https://api.github.com/users/thejenja","html_url":"https://github.com/thejenja","followers_url":"https://api.github.com/users/thejenja/followers","following_url":"https://api.github.com/users/thejenja/following{/other_user}","gists_url":"https://api.github.com/users/thejenja/gists{/gist_id}","starred_url":"https://api.github.com/users/thejenja/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thejenja/subscriptions","organizations_url":"https://api.github.com/users/thejenja/orgs","repos_url":"https://api.github.com/users/thejenja/repos","events_url":"https://api.github.com/users/thejenja/events{/privacy}","received_events_url":"https://api.github.com/users/thejenja/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"cookiennn","id":114363954,"node_id":"U_kgDOBtEOMg","avatar_url":"https://avatars.githubusercontent.com/u/114363954?v=4","gravatar_id":"","url":"https://api.github.com/users/cookiennn","html_url":"https://github.com/cookiennn","followers_url":"https://api.github.com/users/cookiennn/followers","following_url":"https://api.github.com/users/cookiennn/following{/other_user}","gists_url":"https://api.github.com/users/cookiennn/gists{/gist_id}","starred_url":"https://api.github.com/users/cookiennn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cookiennn/subscriptions","organizations_url":"https://api.github.com/users/cookiennn/orgs","repos_url":"https://api.github.com/users/cookiennn/repos","events_url":"https://api.github.com/users/cookiennn/events{/privacy}","received_events_url":"https://api.github.com/users/cookiennn/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"SecularSteve","id":33793273,"node_id":"MDQ6VXNlcjMzNzkzMjcz","avatar_url":"https://avatars.githubusercontent.com/u/33793273?v=4","gravatar_id":"","url":"https://api.github.com/users/SecularSteve","html_url":"https://github.com/SecularSteve","followers_url":"https://api.github.com/users/SecularSteve/followers","following_url":"https://api.github.com/users/SecularSteve/following{/other_user}","gists_url":"https://api.github.com/users/SecularSteve/gists{/gist_id}","starred_url":"https://api.github.com/users/SecularSteve/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/SecularSteve/subscriptions","organizations_url":"https://api.github.com/users/SecularSteve/orgs","repos_url":"https://api.github.com/users/SecularSteve/repos","events_url":"https://api.github.com/users/SecularSteve/events{/privacy}","received_events_url":"https://api.github.com/users/SecularSteve/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"R0GGER","id":8298741,"node_id":"MDQ6VXNlcjgyOTg3NDE=","avatar_url":"https://avatars.githubusercontent.com/u/8298741?v=4","gravatar_id":"","url":"https://api.github.com/users/R0GGER","html_url":"https://github.com/R0GGER","followers_url":"https://api.github.com/users/R0GGER/followers","following_url":"https://api.github.com/users/R0GGER/following{/other_user}","gists_url":"https://api.github.com/users/R0GGER/gists{/gist_id}","starred_url":"https://api.github.com/users/R0GGER/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/R0GGER/subscriptions","organizations_url":"https://api.github.com/users/R0GGER/orgs","repos_url":"https://api.github.com/users/R0GGER/repos","events_url":"https://api.github.com/users/R0GGER/events{/privacy}","received_events_url":"https://api.github.com/users/R0GGER/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"arcoast","id":81871508,"node_id":"MDQ6VXNlcjgxODcxNTA4","avatar_url":"https://avatars.githubusercontent.com/u/81871508?v=4","gravatar_id":"","url":"https://api.github.com/users/arcoast","html_url":"https://github.com/arcoast","followers_url":"https://api.github.com/users/arcoast/followers","following_url":"https://api.github.com/users/arcoast/following{/other_user}","gists_url":"https://api.github.com/users/arcoast/gists{/gist_id}","starred_url":"https://api.github.com/users/arcoast/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arcoast/subscriptions","organizations_url":"https://api.github.com/users/arcoast/orgs","repos_url":"https://api.github.com/users/arcoast/repos","events_url":"https://api.github.com/users/arcoast/events{/privacy}","received_events_url":"https://api.github.com/users/arcoast/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"maboroshin","id":41102508,"node_id":"MDQ6VXNlcjQxMTAyNTA4","avatar_url":"https://avatars.githubusercontent.com/u/41102508?v=4","gravatar_id":"","url":"https://api.github.com/users/maboroshin","html_url":"https://github.com/maboroshin","followers_url":"https://api.github.com/users/maboroshin/followers","following_url":"https://api.github.com/users/maboroshin/following{/other_user}","gists_url":"https://api.github.com/users/maboroshin/gists{/gist_id}","starred_url":"https://api.github.com/users/maboroshin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboroshin/subscriptions","organizations_url":"https://api.github.com/users/maboroshin/orgs","repos_url":"https://api.github.com/users/maboroshin/repos","events_url":"https://api.github.com/users/maboroshin/events{/privacy}","received_events_url":"https://api.github.com/users/maboroshin/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"lamarios","id":1192563,"node_id":"MDQ6VXNlcjExOTI1NjM=","avatar_url":"https://avatars.githubusercontent.com/u/1192563?v=4","gravatar_id":"","url":"https://api.github.com/users/lamarios","html_url":"https://github.com/lamarios","followers_url":"https://api.github.com/users/lamarios/followers","following_url":"https://api.github.com/users/lamarios/following{/other_user}","gists_url":"https://api.github.com/users/lamarios/gists{/gist_id}","starred_url":"https://api.github.com/users/lamarios/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lamarios/subscriptions","organizations_url":"https://api.github.com/users/lamarios/orgs","repos_url":"https://api.github.com/users/lamarios/repos","events_url":"https://api.github.com/users/lamarios/events{/privacy}","received_events_url":"https://api.github.com/users/lamarios/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"bonswouar","id":615053,"node_id":"MDQ6VXNlcjYxNTA1Mw==","avatar_url":"https://avatars.githubusercontent.com/u/615053?v=4","gravatar_id":"","url":"https://api.github.com/users/bonswouar","html_url":"https://github.com/bonswouar","followers_url":"https://api.github.com/users/bonswouar/followers","following_url":"https://api.github.com/users/bonswouar/following{/other_user}","gists_url":"https://api.github.com/users/bonswouar/gists{/gist_id}","starred_url":"https://api.github.com/users/bonswouar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bonswouar/subscriptions","organizations_url":"https://api.github.com/users/bonswouar/orgs","repos_url":"https://api.github.com/users/bonswouar/repos","events_url":"https://api.github.com/users/bonswouar/events{/privacy}","received_events_url":"https://api.github.com/users/bonswouar/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"yurtpage","id":155876573,"node_id":"U_kgDOCUp83Q","avatar_url":"https://avatars.githubusercontent.com/u/155876573?v=4","gravatar_id":"","url":"https://api.github.com/users/yurtpage","html_url":"https://github.com/yurtpage","followers_url":"https://api.github.com/users/yurtpage/followers","following_url":"https://api.github.com/users/yurtpage/following{/other_user}","gists_url":"https://api.github.com/users/yurtpage/gists{/gist_id}","starred_url":"https://api.github.com/users/yurtpage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yurtpage/subscriptions","organizations_url":"https://api.github.com/users/yurtpage/orgs","repos_url":"https://api.github.com/users/yurtpage/repos","events_url":"https://api.github.com/users/yurtpage/events{/privacy}","received_events_url":"https://api.github.com/users/yurtpage/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Vladik01-11","id":70571714,"node_id":"MDQ6VXNlcjcwNTcxNzE0","avatar_url":"https://avatars.githubusercontent.com/u/70571714?v=4","gravatar_id":"","url":"https://api.github.com/users/Vladik01-11","html_url":"https://github.com/Vladik01-11","followers_url":"https://api.github.com/users/Vladik01-11/followers","following_url":"https://api.github.com/users/Vladik01-11/following{/other_user}","gists_url":"https://api.github.com/users/Vladik01-11/gists{/gist_id}","starred_url":"https://api.github.com/users/Vladik01-11/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Vladik01-11/subscriptions","organizations_url":"https://api.github.com/users/Vladik01-11/orgs","repos_url":"https://api.github.com/users/Vladik01-11/repos","events_url":"https://api.github.com/users/Vladik01-11/events{/privacy}","received_events_url":"https://api.github.com/users/Vladik01-11/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Midou36O","id":45198486,"node_id":"MDQ6VXNlcjQ1MTk4NDg2","avatar_url":"https://avatars.githubusercontent.com/u/45198486?v=4","gravatar_id":"","url":"https://api.github.com/users/Midou36O","html_url":"https://github.com/Midou36O","followers_url":"https://api.github.com/users/Midou36O/followers","following_url":"https://api.github.com/users/Midou36O/following{/other_user}","gists_url":"https://api.github.com/users/Midou36O/gists{/gist_id}","starred_url":"https://api.github.com/users/Midou36O/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Midou36O/subscriptions","organizations_url":"https://api.github.com/users/Midou36O/orgs","repos_url":"https://api.github.com/users/Midou36O/repos","events_url":"https://api.github.com/users/Midou36O/events{/privacy}","received_events_url":"https://api.github.com/users/Midou36O/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Makhuta","id":23267923,"node_id":"MDQ6VXNlcjIzMjY3OTIz","avatar_url":"https://avatars.githubusercontent.com/u/23267923?v=4","gravatar_id":"","url":"https://api.github.com/users/Makhuta","html_url":"https://github.com/Makhuta","followers_url":"https://api.github.com/users/Makhuta/followers","following_url":"https://api.github.com/users/Makhuta/following{/other_user}","gists_url":"https://api.github.com/users/Makhuta/gists{/gist_id}","starred_url":"https://api.github.com/users/Makhuta/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Makhuta/subscriptions","organizations_url":"https://api.github.com/users/Makhuta/orgs","repos_url":"https://api.github.com/users/Makhuta/repos","events_url":"https://api.github.com/users/Makhuta/events{/privacy}","received_events_url":"https://api.github.com/users/Makhuta/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"viraj-sh","id":214743952,"node_id":"U_kgDODMy7kA","avatar_url":"https://avatars.githubusercontent.com/u/214743952?v=4","gravatar_id":"","url":"https://api.github.com/users/viraj-sh","html_url":"https://github.com/viraj-sh","followers_url":"https://api.github.com/users/viraj-sh/followers","following_url":"https://api.github.com/users/viraj-sh/following{/other_user}","gists_url":"https://api.github.com/users/viraj-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/viraj-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/viraj-sh/subscriptions","organizations_url":"https://api.github.com/users/viraj-sh/orgs","repos_url":"https://api.github.com/users/viraj-sh/repos","events_url":"https://api.github.com/users/viraj-sh/events{/privacy}","received_events_url":"https://api.github.com/users/viraj-sh/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Sphet98","id":260131232,"node_id":"U_kgDOD4FJoA","avatar_url":"https://avatars.githubusercontent.com/u/260131232?v=4","gravatar_id":"","url":"https://api.github.com/users/Sphet98","html_url":"https://github.com/Sphet98","followers_url":"https://api.github.com/users/Sphet98/followers","following_url":"https://api.github.com/users/Sphet98/following{/other_user}","gists_url":"https://api.github.com/users/Sphet98/gists{/gist_id}","starred_url":"https://api.github.com/users/Sphet98/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Sphet98/subscriptions","organizations_url":"https://api.github.com/users/Sphet98/orgs","repos_url":"https://api.github.com/users/Sphet98/repos","events_url":"https://api.github.com/users/Sphet98/events{/privacy}","received_events_url":"https://api.github.com/users/Sphet98/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"simplylu","id":55274241,"node_id":"MDQ6VXNlcjU1Mjc0MjQx","avatar_url":"https://avatars.githubusercontent.com/u/55274241?v=4","gravatar_id":"","url":"https://api.github.com/users/simplylu","html_url":"https://github.com/simplylu","followers_url":"https://api.github.com/users/simplylu/followers","following_url":"https://api.github.com/users/simplylu/following{/other_user}","gists_url":"https://api.github.com/users/simplylu/gists{/gist_id}","starred_url":"https://api.github.com/users/simplylu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/simplylu/subscriptions","organizations_url":"https://api.github.com/users/simplylu/orgs","repos_url":"https://api.github.com/users/simplylu/repos","events_url":"https://api.github.com/users/simplylu/events{/privacy}","received_events_url":"https://api.github.com/users/simplylu/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"ramazansancar","id":8878055,"node_id":"MDQ6VXNlcjg4NzgwNTU=","avatar_url":"https://avatars.githubusercontent.com/u/8878055?v=4","gravatar_id":"","url":"https://api.github.com/users/ramazansancar","html_url":"https://github.com/ramazansancar","followers_url":"https://api.github.com/users/ramazansancar/followers","following_url":"https://api.github.com/users/ramazansancar/following{/other_user}","gists_url":"https://api.github.com/users/ramazansancar/gists{/gist_id}","starred_url":"https://api.github.com/users/ramazansancar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ramazansancar/subscriptions","organizations_url":"https://api.github.com/users/ramazansancar/orgs","repos_url":"https://api.github.com/users/ramazansancar/repos","events_url":"https://api.github.com/users/ramazansancar/events{/privacy}","received_events_url":"https://api.github.com/users/ramazansancar/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"ptah-alexs","id":705210,"node_id":"MDQ6VXNlcjcwNTIxMA==","avatar_url":"https://avatars.githubusercontent.com/u/705210?v=4","gravatar_id":"","url":"https://api.github.com/users/ptah-alexs","html_url":"https://github.com/ptah-alexs","followers_url":"https://api.github.com/users/ptah-alexs/followers","following_url":"https://api.github.com/users/ptah-alexs/following{/other_user}","gists_url":"https://api.github.com/users/ptah-alexs/gists{/gist_id}","starred_url":"https://api.github.com/users/ptah-alexs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ptah-alexs/subscriptions","organizations_url":"https://api.github.com/users/ptah-alexs/orgs","repos_url":"https://api.github.com/users/ptah-alexs/repos","events_url":"https://api.github.com/users/ptah-alexs/events{/privacy}","received_events_url":"https://api.github.com/users/ptah-alexs/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"polarwood","id":199823813,"node_id":"U_kgDOC-kRxQ","avatar_url":"https://avatars.githubusercontent.com/u/199823813?v=4","gravatar_id":"","url":"https://api.github.com/users/polarwood","html_url":"https://github.com/polarwood","followers_url":"https://api.github.com/users/polarwood/followers","following_url":"https://api.github.com/users/polarwood/following{/other_user}","gists_url":"https://api.github.com/users/polarwood/gists{/gist_id}","starred_url":"https://api.github.com/users/polarwood/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/polarwood/subscriptions","organizations_url":"https://api.github.com/users/polarwood/orgs","repos_url":"https://api.github.com/users/polarwood/repos","events_url":"https://api.github.com/users/polarwood/events{/privacy}","received_events_url":"https://api.github.com/users/polarwood/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"beboprocky","id":110792083,"node_id":"U_kgDOBpqNkw","avatar_url":"https://avatars.githubusercontent.com/u/110792083?v=4","gravatar_id":"","url":"https://api.github.com/users/beboprocky","html_url":"https://github.com/beboprocky","followers_url":"https://api.github.com/users/beboprocky/followers","following_url":"https://api.github.com/users/beboprocky/following{/other_user}","gists_url":"https://api.github.com/users/beboprocky/gists{/gist_id}","starred_url":"https://api.github.com/users/beboprocky/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/beboprocky/subscriptions","organizations_url":"https://api.github.com/users/beboprocky/orgs","repos_url":"https://api.github.com/users/beboprocky/repos","events_url":"https://api.github.com/users/beboprocky/events{/privacy}","received_events_url":"https://api.github.com/users/beboprocky/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Poussinou","id":19646549,"node_id":"MDQ6VXNlcjE5NjQ2NTQ5","avatar_url":"https://avatars.githubusercontent.com/u/19646549?v=4","gravatar_id":"","url":"https://api.github.com/users/Poussinou","html_url":"https://github.com/Poussinou","followers_url":"https://api.github.com/users/Poussinou/followers","following_url":"https://api.github.com/users/Poussinou/following{/other_user}","gists_url":"https://api.github.com/users/Poussinou/gists{/gist_id}","starred_url":"https://api.github.com/users/Poussinou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Poussinou/subscriptions","organizations_url":"https://api.github.com/users/Poussinou/orgs","repos_url":"https://api.github.com/users/Poussinou/repos","events_url":"https://api.github.com/users/Poussinou/events{/privacy}","received_events_url":"https://api.github.com/users/Poussinou/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Spaenny","id":5008508,"node_id":"MDQ6VXNlcjUwMDg1MDg=","avatar_url":"https://avatars.githubusercontent.com/u/5008508?v=4","gravatar_id":"","url":"https://api.github.com/users/Spaenny","html_url":"https://github.com/Spaenny","followers_url":"https://api.github.com/users/Spaenny/followers","following_url":"https://api.github.com/users/Spaenny/following{/other_user}","gists_url":"https://api.github.com/users/Spaenny/gists{/gist_id}","starred_url":"https://api.github.com/users/Spaenny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Spaenny/subscriptions","organizations_url":"https://api.github.com/users/Spaenny/orgs","repos_url":"https://api.github.com/users/Spaenny/repos","events_url":"https://api.github.com/users/Spaenny/events{/privacy}","received_events_url":"https://api.github.com/users/Spaenny/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1}] \ No newline at end of file +[{"login":"WardPearce","id":27844174,"node_id":"MDQ6VXNlcjI3ODQ0MTc0","avatar_url":"https://avatars.githubusercontent.com/u/27844174?v=4","gravatar_id":"","url":"https://api.github.com/users/WardPearce","html_url":"https://github.com/WardPearce","followers_url":"https://api.github.com/users/WardPearce/followers","following_url":"https://api.github.com/users/WardPearce/following{/other_user}","gists_url":"https://api.github.com/users/WardPearce/gists{/gist_id}","starred_url":"https://api.github.com/users/WardPearce/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/WardPearce/subscriptions","organizations_url":"https://api.github.com/users/WardPearce/orgs","repos_url":"https://api.github.com/users/WardPearce/repos","events_url":"https://api.github.com/users/WardPearce/events{/privacy}","received_events_url":"https://api.github.com/users/WardPearce/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1910},{"login":"dependabot[bot]","id":49699333,"node_id":"MDM6Qm90NDk2OTkzMzM=","avatar_url":"https://avatars.githubusercontent.com/in/29110?v=4","gravatar_id":"","url":"https://api.github.com/users/dependabot%5Bbot%5D","html_url":"https://github.com/apps/dependabot","followers_url":"https://api.github.com/users/dependabot%5Bbot%5D/followers","following_url":"https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dependabot%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/dependabot%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/dependabot%5Bbot%5D/repos","events_url":"https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/dependabot%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false,"contributions":224},{"login":"Lezurex","id":48829956,"node_id":"MDQ6VXNlcjQ4ODI5OTU2","avatar_url":"https://avatars.githubusercontent.com/u/48829956?v=4","gravatar_id":"","url":"https://api.github.com/users/Lezurex","html_url":"https://github.com/Lezurex","followers_url":"https://api.github.com/users/Lezurex/followers","following_url":"https://api.github.com/users/Lezurex/following{/other_user}","gists_url":"https://api.github.com/users/Lezurex/gists{/gist_id}","starred_url":"https://api.github.com/users/Lezurex/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lezurex/subscriptions","organizations_url":"https://api.github.com/users/Lezurex/orgs","repos_url":"https://api.github.com/users/Lezurex/repos","events_url":"https://api.github.com/users/Lezurex/events{/privacy}","received_events_url":"https://api.github.com/users/Lezurex/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":39},{"login":"Coool","id":8421903,"node_id":"MDQ6VXNlcjg0MjE5MDM=","avatar_url":"https://avatars.githubusercontent.com/u/8421903?v=4","gravatar_id":"","url":"https://api.github.com/users/Coool","html_url":"https://github.com/Coool","followers_url":"https://api.github.com/users/Coool/followers","following_url":"https://api.github.com/users/Coool/following{/other_user}","gists_url":"https://api.github.com/users/Coool/gists{/gist_id}","starred_url":"https://api.github.com/users/Coool/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Coool/subscriptions","organizations_url":"https://api.github.com/users/Coool/orgs","repos_url":"https://api.github.com/users/Coool/repos","events_url":"https://api.github.com/users/Coool/events{/privacy}","received_events_url":"https://api.github.com/users/Coool/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":15},{"login":"araujosemacento","id":60983392,"node_id":"MDQ6VXNlcjYwOTgzMzky","avatar_url":"https://avatars.githubusercontent.com/u/60983392?v=4","gravatar_id":"","url":"https://api.github.com/users/araujosemacento","html_url":"https://github.com/araujosemacento","followers_url":"https://api.github.com/users/araujosemacento/followers","following_url":"https://api.github.com/users/araujosemacento/following{/other_user}","gists_url":"https://api.github.com/users/araujosemacento/gists{/gist_id}","starred_url":"https://api.github.com/users/araujosemacento/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/araujosemacento/subscriptions","organizations_url":"https://api.github.com/users/araujosemacento/orgs","repos_url":"https://api.github.com/users/araujosemacento/repos","events_url":"https://api.github.com/users/araujosemacento/events{/privacy}","received_events_url":"https://api.github.com/users/araujosemacento/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":14},{"login":"ghostofsparta222","id":251893418,"node_id":"U_kgDODwOWqg","avatar_url":"https://avatars.githubusercontent.com/u/251893418?v=4","gravatar_id":"","url":"https://api.github.com/users/ghostofsparta222","html_url":"https://github.com/ghostofsparta222","followers_url":"https://api.github.com/users/ghostofsparta222/followers","following_url":"https://api.github.com/users/ghostofsparta222/following{/other_user}","gists_url":"https://api.github.com/users/ghostofsparta222/gists{/gist_id}","starred_url":"https://api.github.com/users/ghostofsparta222/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ghostofsparta222/subscriptions","organizations_url":"https://api.github.com/users/ghostofsparta222/orgs","repos_url":"https://api.github.com/users/ghostofsparta222/repos","events_url":"https://api.github.com/users/ghostofsparta222/events{/privacy}","received_events_url":"https://api.github.com/users/ghostofsparta222/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":11},{"login":"toolatebot","id":169580663,"node_id":"U_kgDOChuYdw","avatar_url":"https://avatars.githubusercontent.com/u/169580663?v=4","gravatar_id":"","url":"https://api.github.com/users/toolatebot","html_url":"https://github.com/toolatebot","followers_url":"https://api.github.com/users/toolatebot/followers","following_url":"https://api.github.com/users/toolatebot/following{/other_user}","gists_url":"https://api.github.com/users/toolatebot/gists{/gist_id}","starred_url":"https://api.github.com/users/toolatebot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/toolatebot/subscriptions","organizations_url":"https://api.github.com/users/toolatebot/orgs","repos_url":"https://api.github.com/users/toolatebot/repos","events_url":"https://api.github.com/users/toolatebot/events{/privacy}","received_events_url":"https://api.github.com/users/toolatebot/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":9},{"login":"Precific","id":30053082,"node_id":"MDQ6VXNlcjMwMDUzMDgy","avatar_url":"https://avatars.githubusercontent.com/u/30053082?v=4","gravatar_id":"","url":"https://api.github.com/users/Precific","html_url":"https://github.com/Precific","followers_url":"https://api.github.com/users/Precific/followers","following_url":"https://api.github.com/users/Precific/following{/other_user}","gists_url":"https://api.github.com/users/Precific/gists{/gist_id}","starred_url":"https://api.github.com/users/Precific/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Precific/subscriptions","organizations_url":"https://api.github.com/users/Precific/orgs","repos_url":"https://api.github.com/users/Precific/repos","events_url":"https://api.github.com/users/Precific/events{/privacy}","received_events_url":"https://api.github.com/users/Precific/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":9},{"login":"yzqzss","id":30341059,"node_id":"MDQ6VXNlcjMwMzQxMDU5","avatar_url":"https://avatars.githubusercontent.com/u/30341059?v=4","gravatar_id":"","url":"https://api.github.com/users/yzqzss","html_url":"https://github.com/yzqzss","followers_url":"https://api.github.com/users/yzqzss/followers","following_url":"https://api.github.com/users/yzqzss/following{/other_user}","gists_url":"https://api.github.com/users/yzqzss/gists{/gist_id}","starred_url":"https://api.github.com/users/yzqzss/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yzqzss/subscriptions","organizations_url":"https://api.github.com/users/yzqzss/orgs","repos_url":"https://api.github.com/users/yzqzss/repos","events_url":"https://api.github.com/users/yzqzss/events{/privacy}","received_events_url":"https://api.github.com/users/yzqzss/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"thejenja","id":65224669,"node_id":"MDQ6VXNlcjY1MjI0NjY5","avatar_url":"https://avatars.githubusercontent.com/u/65224669?v=4","gravatar_id":"","url":"https://api.github.com/users/thejenja","html_url":"https://github.com/thejenja","followers_url":"https://api.github.com/users/thejenja/followers","following_url":"https://api.github.com/users/thejenja/following{/other_user}","gists_url":"https://api.github.com/users/thejenja/gists{/gist_id}","starred_url":"https://api.github.com/users/thejenja/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thejenja/subscriptions","organizations_url":"https://api.github.com/users/thejenja/orgs","repos_url":"https://api.github.com/users/thejenja/repos","events_url":"https://api.github.com/users/thejenja/events{/privacy}","received_events_url":"https://api.github.com/users/thejenja/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"cookiennn","id":114363954,"node_id":"U_kgDOBtEOMg","avatar_url":"https://avatars.githubusercontent.com/u/114363954?v=4","gravatar_id":"","url":"https://api.github.com/users/cookiennn","html_url":"https://github.com/cookiennn","followers_url":"https://api.github.com/users/cookiennn/followers","following_url":"https://api.github.com/users/cookiennn/following{/other_user}","gists_url":"https://api.github.com/users/cookiennn/gists{/gist_id}","starred_url":"https://api.github.com/users/cookiennn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cookiennn/subscriptions","organizations_url":"https://api.github.com/users/cookiennn/orgs","repos_url":"https://api.github.com/users/cookiennn/repos","events_url":"https://api.github.com/users/cookiennn/events{/privacy}","received_events_url":"https://api.github.com/users/cookiennn/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"SecularSteve","id":33793273,"node_id":"MDQ6VXNlcjMzNzkzMjcz","avatar_url":"https://avatars.githubusercontent.com/u/33793273?v=4","gravatar_id":"","url":"https://api.github.com/users/SecularSteve","html_url":"https://github.com/SecularSteve","followers_url":"https://api.github.com/users/SecularSteve/followers","following_url":"https://api.github.com/users/SecularSteve/following{/other_user}","gists_url":"https://api.github.com/users/SecularSteve/gists{/gist_id}","starred_url":"https://api.github.com/users/SecularSteve/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/SecularSteve/subscriptions","organizations_url":"https://api.github.com/users/SecularSteve/orgs","repos_url":"https://api.github.com/users/SecularSteve/repos","events_url":"https://api.github.com/users/SecularSteve/events{/privacy}","received_events_url":"https://api.github.com/users/SecularSteve/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":4},{"login":"R0GGER","id":8298741,"node_id":"MDQ6VXNlcjgyOTg3NDE=","avatar_url":"https://avatars.githubusercontent.com/u/8298741?v=4","gravatar_id":"","url":"https://api.github.com/users/R0GGER","html_url":"https://github.com/R0GGER","followers_url":"https://api.github.com/users/R0GGER/followers","following_url":"https://api.github.com/users/R0GGER/following{/other_user}","gists_url":"https://api.github.com/users/R0GGER/gists{/gist_id}","starred_url":"https://api.github.com/users/R0GGER/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/R0GGER/subscriptions","organizations_url":"https://api.github.com/users/R0GGER/orgs","repos_url":"https://api.github.com/users/R0GGER/repos","events_url":"https://api.github.com/users/R0GGER/events{/privacy}","received_events_url":"https://api.github.com/users/R0GGER/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"arcoast","id":81871508,"node_id":"MDQ6VXNlcjgxODcxNTA4","avatar_url":"https://avatars.githubusercontent.com/u/81871508?v=4","gravatar_id":"","url":"https://api.github.com/users/arcoast","html_url":"https://github.com/arcoast","followers_url":"https://api.github.com/users/arcoast/followers","following_url":"https://api.github.com/users/arcoast/following{/other_user}","gists_url":"https://api.github.com/users/arcoast/gists{/gist_id}","starred_url":"https://api.github.com/users/arcoast/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arcoast/subscriptions","organizations_url":"https://api.github.com/users/arcoast/orgs","repos_url":"https://api.github.com/users/arcoast/repos","events_url":"https://api.github.com/users/arcoast/events{/privacy}","received_events_url":"https://api.github.com/users/arcoast/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"maboroshin","id":41102508,"node_id":"MDQ6VXNlcjQxMTAyNTA4","avatar_url":"https://avatars.githubusercontent.com/u/41102508?v=4","gravatar_id":"","url":"https://api.github.com/users/maboroshin","html_url":"https://github.com/maboroshin","followers_url":"https://api.github.com/users/maboroshin/followers","following_url":"https://api.github.com/users/maboroshin/following{/other_user}","gists_url":"https://api.github.com/users/maboroshin/gists{/gist_id}","starred_url":"https://api.github.com/users/maboroshin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboroshin/subscriptions","organizations_url":"https://api.github.com/users/maboroshin/orgs","repos_url":"https://api.github.com/users/maboroshin/repos","events_url":"https://api.github.com/users/maboroshin/events{/privacy}","received_events_url":"https://api.github.com/users/maboroshin/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":3},{"login":"lamarios","id":1192563,"node_id":"MDQ6VXNlcjExOTI1NjM=","avatar_url":"https://avatars.githubusercontent.com/u/1192563?v=4","gravatar_id":"","url":"https://api.github.com/users/lamarios","html_url":"https://github.com/lamarios","followers_url":"https://api.github.com/users/lamarios/followers","following_url":"https://api.github.com/users/lamarios/following{/other_user}","gists_url":"https://api.github.com/users/lamarios/gists{/gist_id}","starred_url":"https://api.github.com/users/lamarios/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lamarios/subscriptions","organizations_url":"https://api.github.com/users/lamarios/orgs","repos_url":"https://api.github.com/users/lamarios/repos","events_url":"https://api.github.com/users/lamarios/events{/privacy}","received_events_url":"https://api.github.com/users/lamarios/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"bonswouar","id":615053,"node_id":"MDQ6VXNlcjYxNTA1Mw==","avatar_url":"https://avatars.githubusercontent.com/u/615053?v=4","gravatar_id":"","url":"https://api.github.com/users/bonswouar","html_url":"https://github.com/bonswouar","followers_url":"https://api.github.com/users/bonswouar/followers","following_url":"https://api.github.com/users/bonswouar/following{/other_user}","gists_url":"https://api.github.com/users/bonswouar/gists{/gist_id}","starred_url":"https://api.github.com/users/bonswouar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bonswouar/subscriptions","organizations_url":"https://api.github.com/users/bonswouar/orgs","repos_url":"https://api.github.com/users/bonswouar/repos","events_url":"https://api.github.com/users/bonswouar/events{/privacy}","received_events_url":"https://api.github.com/users/bonswouar/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"yurtpage","id":155876573,"node_id":"U_kgDOCUp83Q","avatar_url":"https://avatars.githubusercontent.com/u/155876573?v=4","gravatar_id":"","url":"https://api.github.com/users/yurtpage","html_url":"https://github.com/yurtpage","followers_url":"https://api.github.com/users/yurtpage/followers","following_url":"https://api.github.com/users/yurtpage/following{/other_user}","gists_url":"https://api.github.com/users/yurtpage/gists{/gist_id}","starred_url":"https://api.github.com/users/yurtpage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yurtpage/subscriptions","organizations_url":"https://api.github.com/users/yurtpage/orgs","repos_url":"https://api.github.com/users/yurtpage/repos","events_url":"https://api.github.com/users/yurtpage/events{/privacy}","received_events_url":"https://api.github.com/users/yurtpage/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Vladik01-11","id":70571714,"node_id":"MDQ6VXNlcjcwNTcxNzE0","avatar_url":"https://avatars.githubusercontent.com/u/70571714?v=4","gravatar_id":"","url":"https://api.github.com/users/Vladik01-11","html_url":"https://github.com/Vladik01-11","followers_url":"https://api.github.com/users/Vladik01-11/followers","following_url":"https://api.github.com/users/Vladik01-11/following{/other_user}","gists_url":"https://api.github.com/users/Vladik01-11/gists{/gist_id}","starred_url":"https://api.github.com/users/Vladik01-11/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Vladik01-11/subscriptions","organizations_url":"https://api.github.com/users/Vladik01-11/orgs","repos_url":"https://api.github.com/users/Vladik01-11/repos","events_url":"https://api.github.com/users/Vladik01-11/events{/privacy}","received_events_url":"https://api.github.com/users/Vladik01-11/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Midou36O","id":45198486,"node_id":"MDQ6VXNlcjQ1MTk4NDg2","avatar_url":"https://avatars.githubusercontent.com/u/45198486?v=4","gravatar_id":"","url":"https://api.github.com/users/Midou36O","html_url":"https://github.com/Midou36O","followers_url":"https://api.github.com/users/Midou36O/followers","following_url":"https://api.github.com/users/Midou36O/following{/other_user}","gists_url":"https://api.github.com/users/Midou36O/gists{/gist_id}","starred_url":"https://api.github.com/users/Midou36O/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Midou36O/subscriptions","organizations_url":"https://api.github.com/users/Midou36O/orgs","repos_url":"https://api.github.com/users/Midou36O/repos","events_url":"https://api.github.com/users/Midou36O/events{/privacy}","received_events_url":"https://api.github.com/users/Midou36O/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"Makhuta","id":23267923,"node_id":"MDQ6VXNlcjIzMjY3OTIz","avatar_url":"https://avatars.githubusercontent.com/u/23267923?v=4","gravatar_id":"","url":"https://api.github.com/users/Makhuta","html_url":"https://github.com/Makhuta","followers_url":"https://api.github.com/users/Makhuta/followers","following_url":"https://api.github.com/users/Makhuta/following{/other_user}","gists_url":"https://api.github.com/users/Makhuta/gists{/gist_id}","starred_url":"https://api.github.com/users/Makhuta/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Makhuta/subscriptions","organizations_url":"https://api.github.com/users/Makhuta/orgs","repos_url":"https://api.github.com/users/Makhuta/repos","events_url":"https://api.github.com/users/Makhuta/events{/privacy}","received_events_url":"https://api.github.com/users/Makhuta/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":2},{"login":"viraj-sh","id":214743952,"node_id":"U_kgDODMy7kA","avatar_url":"https://avatars.githubusercontent.com/u/214743952?v=4","gravatar_id":"","url":"https://api.github.com/users/viraj-sh","html_url":"https://github.com/viraj-sh","followers_url":"https://api.github.com/users/viraj-sh/followers","following_url":"https://api.github.com/users/viraj-sh/following{/other_user}","gists_url":"https://api.github.com/users/viraj-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/viraj-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/viraj-sh/subscriptions","organizations_url":"https://api.github.com/users/viraj-sh/orgs","repos_url":"https://api.github.com/users/viraj-sh/repos","events_url":"https://api.github.com/users/viraj-sh/events{/privacy}","received_events_url":"https://api.github.com/users/viraj-sh/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Sphet98","id":260131232,"node_id":"U_kgDOD4FJoA","avatar_url":"https://avatars.githubusercontent.com/u/260131232?v=4","gravatar_id":"","url":"https://api.github.com/users/Sphet98","html_url":"https://github.com/Sphet98","followers_url":"https://api.github.com/users/Sphet98/followers","following_url":"https://api.github.com/users/Sphet98/following{/other_user}","gists_url":"https://api.github.com/users/Sphet98/gists{/gist_id}","starred_url":"https://api.github.com/users/Sphet98/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Sphet98/subscriptions","organizations_url":"https://api.github.com/users/Sphet98/orgs","repos_url":"https://api.github.com/users/Sphet98/repos","events_url":"https://api.github.com/users/Sphet98/events{/privacy}","received_events_url":"https://api.github.com/users/Sphet98/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"simplylu","id":55274241,"node_id":"MDQ6VXNlcjU1Mjc0MjQx","avatar_url":"https://avatars.githubusercontent.com/u/55274241?v=4","gravatar_id":"","url":"https://api.github.com/users/simplylu","html_url":"https://github.com/simplylu","followers_url":"https://api.github.com/users/simplylu/followers","following_url":"https://api.github.com/users/simplylu/following{/other_user}","gists_url":"https://api.github.com/users/simplylu/gists{/gist_id}","starred_url":"https://api.github.com/users/simplylu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/simplylu/subscriptions","organizations_url":"https://api.github.com/users/simplylu/orgs","repos_url":"https://api.github.com/users/simplylu/repos","events_url":"https://api.github.com/users/simplylu/events{/privacy}","received_events_url":"https://api.github.com/users/simplylu/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"ramazansancar","id":8878055,"node_id":"MDQ6VXNlcjg4NzgwNTU=","avatar_url":"https://avatars.githubusercontent.com/u/8878055?v=4","gravatar_id":"","url":"https://api.github.com/users/ramazansancar","html_url":"https://github.com/ramazansancar","followers_url":"https://api.github.com/users/ramazansancar/followers","following_url":"https://api.github.com/users/ramazansancar/following{/other_user}","gists_url":"https://api.github.com/users/ramazansancar/gists{/gist_id}","starred_url":"https://api.github.com/users/ramazansancar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ramazansancar/subscriptions","organizations_url":"https://api.github.com/users/ramazansancar/orgs","repos_url":"https://api.github.com/users/ramazansancar/repos","events_url":"https://api.github.com/users/ramazansancar/events{/privacy}","received_events_url":"https://api.github.com/users/ramazansancar/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"ptah-alexs","id":705210,"node_id":"MDQ6VXNlcjcwNTIxMA==","avatar_url":"https://avatars.githubusercontent.com/u/705210?v=4","gravatar_id":"","url":"https://api.github.com/users/ptah-alexs","html_url":"https://github.com/ptah-alexs","followers_url":"https://api.github.com/users/ptah-alexs/followers","following_url":"https://api.github.com/users/ptah-alexs/following{/other_user}","gists_url":"https://api.github.com/users/ptah-alexs/gists{/gist_id}","starred_url":"https://api.github.com/users/ptah-alexs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ptah-alexs/subscriptions","organizations_url":"https://api.github.com/users/ptah-alexs/orgs","repos_url":"https://api.github.com/users/ptah-alexs/repos","events_url":"https://api.github.com/users/ptah-alexs/events{/privacy}","received_events_url":"https://api.github.com/users/ptah-alexs/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"polarwood","id":199823813,"node_id":"U_kgDOC-kRxQ","avatar_url":"https://avatars.githubusercontent.com/u/199823813?v=4","gravatar_id":"","url":"https://api.github.com/users/polarwood","html_url":"https://github.com/polarwood","followers_url":"https://api.github.com/users/polarwood/followers","following_url":"https://api.github.com/users/polarwood/following{/other_user}","gists_url":"https://api.github.com/users/polarwood/gists{/gist_id}","starred_url":"https://api.github.com/users/polarwood/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/polarwood/subscriptions","organizations_url":"https://api.github.com/users/polarwood/orgs","repos_url":"https://api.github.com/users/polarwood/repos","events_url":"https://api.github.com/users/polarwood/events{/privacy}","received_events_url":"https://api.github.com/users/polarwood/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"beboprocky","id":110792083,"node_id":"U_kgDOBpqNkw","avatar_url":"https://avatars.githubusercontent.com/u/110792083?v=4","gravatar_id":"","url":"https://api.github.com/users/beboprocky","html_url":"https://github.com/beboprocky","followers_url":"https://api.github.com/users/beboprocky/followers","following_url":"https://api.github.com/users/beboprocky/following{/other_user}","gists_url":"https://api.github.com/users/beboprocky/gists{/gist_id}","starred_url":"https://api.github.com/users/beboprocky/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/beboprocky/subscriptions","organizations_url":"https://api.github.com/users/beboprocky/orgs","repos_url":"https://api.github.com/users/beboprocky/repos","events_url":"https://api.github.com/users/beboprocky/events{/privacy}","received_events_url":"https://api.github.com/users/beboprocky/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Poussinou","id":19646549,"node_id":"MDQ6VXNlcjE5NjQ2NTQ5","avatar_url":"https://avatars.githubusercontent.com/u/19646549?v=4","gravatar_id":"","url":"https://api.github.com/users/Poussinou","html_url":"https://github.com/Poussinou","followers_url":"https://api.github.com/users/Poussinou/followers","following_url":"https://api.github.com/users/Poussinou/following{/other_user}","gists_url":"https://api.github.com/users/Poussinou/gists{/gist_id}","starred_url":"https://api.github.com/users/Poussinou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Poussinou/subscriptions","organizations_url":"https://api.github.com/users/Poussinou/orgs","repos_url":"https://api.github.com/users/Poussinou/repos","events_url":"https://api.github.com/users/Poussinou/events{/privacy}","received_events_url":"https://api.github.com/users/Poussinou/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1},{"login":"Spaenny","id":5008508,"node_id":"MDQ6VXNlcjUwMDg1MDg=","avatar_url":"https://avatars.githubusercontent.com/u/5008508?v=4","gravatar_id":"","url":"https://api.github.com/users/Spaenny","html_url":"https://github.com/Spaenny","followers_url":"https://api.github.com/users/Spaenny/followers","following_url":"https://api.github.com/users/Spaenny/following{/other_user}","gists_url":"https://api.github.com/users/Spaenny/gists{/gist_id}","starred_url":"https://api.github.com/users/Spaenny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Spaenny/subscriptions","organizations_url":"https://api.github.com/users/Spaenny/orgs","repos_url":"https://api.github.com/users/Spaenny/repos","events_url":"https://api.github.com/users/Spaenny/events{/privacy}","received_events_url":"https://api.github.com/users/Spaenny/received_events","type":"User","user_view_type":"public","site_admin":false,"contributions":1}] \ No newline at end of file diff --git a/materialious/vite.config.ts b/materialious/vite.config.ts index 1d0a685e..6bdd6b83 100644 --- a/materialious/vite.config.ts +++ b/materialious/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ maximumFileSizeToCacheInBytes: 4000000 }, manifest: { - description: 'Modern material design for Invidious.', + description: 'Modern material design for YouTube and Invidious.', theme_color: '#8936FF', background_color: '#1e1b1e', icons: [ diff --git a/previews/chapter-previews.png b/previews/chapter-previews.png index 9801d06e..7c23af5d 100644 Binary files a/previews/chapter-previews.png and b/previews/chapter-previews.png differ diff --git a/previews/player-preview.png b/previews/player-preview.png index 2328f8dc..7a931e8e 100644 Binary files a/previews/player-preview.png and b/previews/player-preview.png differ diff --git a/previews/setting-preview.png b/previews/setting-preview.png index eb79e2dd..7b69231c 100644 Binary files a/previews/setting-preview.png and b/previews/setting-preview.png differ diff --git a/update_versions.py b/update_versions.py index 728e87b0..ab145056 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.13.19" +LATEST_VERSION = "1.14.0" RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")