Added dearrow

This commit is contained in:
WardPearce
2024-04-04 17:03:31 +13:00
parent 12a53c642c
commit e86f75e0fb
9 changed files with 183 additions and 33 deletions
+8
View File
@@ -14,6 +14,7 @@
# Features
- Sponsorblock built-in.
- Return YouTube dislikes built-in.
- DeArrow built-in (With local processing fallback).
- Video progress tracking & resuming.
- No ads.
- No tracking.
@@ -150,6 +151,12 @@ services:
# URL to Sponsorblock
VITE_DEFAULT_SPONSERBLOCK_INSTANCE: "https://sponsor.ajay.app"
# URL to DeArrow
VITE_DEFAULT_DEARROW_INSTANCE: "https://sponsor.ajay.app"
# URL to DeArrow thumbnail instance
VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE: "https://dearrow-thumb.ajay.app"
```
# Previews
@@ -175,6 +182,7 @@ services:
# Special thanks to
- [Invidious](https://github.com/iv-org)
- [Clipious](https://github.com/lamarios/clipious) for inspiration & a good source for learning more about undocumented Invidious routes.
- [Vidstack player](https://github.com/vidstack/player)
- [Beer CSS](https://github.com/beercss/beercss) (Especially the [YouTube template](https://github.com/beercss/beercss/tree/main/src/youtube) what was used as the base for Materialious.)
- Every dependency in [package.json](/materialious/package.json).
+2
View File
@@ -10,4 +10,6 @@ services:
VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE: "https://returnyoutubedislikeapi.com"
VITE_DEFAULT_FRONTEND_URL: "https://materialio.us"
VITE_DEFAULT_SPONSERBLOCK_INSTANCE: "https://sponsor.ajay.app"
VITE_DEFAULT_DEARROW_INSTANCE: "https://sponsor.ajay.app"
VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE: "https://dearrow-thumb.ajay.app"
+17 -2
View File
@@ -1,6 +1,6 @@
import { get } from 'svelte/store';
import { auth, returnYTDislikesInstance } from '../../store';
import type { Channel, ChannelContentPlaylists, ChannelContentVideos, ChannelPage, Comments, Playlist, PlaylistPage, ReturnYTDislikes, SearchSuggestion, Subscription, Video, VideoPlay } from './model';
import { auth, deArrowInstance, deArrowThumbnailInstance, returnYTDislikesInstance } from '../../store';
import type { Channel, ChannelContentPlaylists, ChannelContentVideos, ChannelPage, Comments, DeArrow, Playlist, PlaylistPage, ReturnYTDislikes, SearchSuggestion, Subscription, Video, VideoPlay } from './model';
export function buildPath(path: string): string {
return `${import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE}/api/v1/${path}`;
@@ -221,4 +221,19 @@ export async function addPlaylistVideo(playlistId: string, videoId: string) {
}),
...headers
}));
}
export async function getDeArrow(videoId: string): Promise<DeArrow> {
const resp = await fetchErrorHandle(
await fetch(`${get(deArrowInstance)}/api/branding?videoID=${videoId}`
)
);
return await resp.json();
}
export async function getThumbnail(videoId: string, time: number): Promise<string> {
const resp = await fetchErrorHandle(
await fetch(`${get(deArrowThumbnailInstance)}/api/v1/getThumbnail?videoID=${videoId}&time=${time}`)
);
return URL.createObjectURL(await resp.blob());
}
+19
View File
@@ -230,4 +230,23 @@ export interface Feed {
export interface Subscription {
author: string;
authorId: string;
}
export interface DeArrow {
titles: {
title: string;
original: boolean;
votes: number;
locked: boolean;
UUID: string;
}[];
thumbnails: {
timestamp?: number;
original: boolean;
votes: number;
locked: boolean;
UUID: string;
}[];
randomTime: number;
videoDuration: number;
}
+2 -8
View File
@@ -17,7 +17,7 @@
sponsorBlockUrl
} from '../store';
import type { PlaylistPageVideo, VideoPlay } from './Api/model';
import { videoLength, type PhasedDescription } from './misc';
import { proxyVideoUrl, videoLength, type PhasedDescription } from './misc';
import { getDynamicTheme } from './theme';
export let data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
@@ -138,13 +138,7 @@
let formattedSrc;
src = data.video.formatStreams.map((format) => {
if (proxyVideos) {
const rawSrc = new URL(format.url);
rawSrc.host = import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE.replace(
'http://',
''
).replace('https://', '');
formattedSrc = rawSrc.toString();
formattedSrc = proxyVideoUrl(format.url);
} else {
formattedSrc = format.url;
}
+72 -5
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { playerSavePlaybackPosition } from '../store';
import { deArrowEnabled, playerSavePlaybackPosition } from '../store';
import { getDeArrow, getThumbnail, getVideo } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from './Api/model';
import { cleanNumber, truncate, videoLength } from './misc';
import { cleanNumber, proxyVideoUrl, truncate, videoLength } from './misc';
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
export let playlistId: string = '';
@@ -27,9 +28,72 @@
progress = null;
}
onMount(() => {
onMount(async () => {
let imageSrc = video.videoThumbnails[4].url;
if (get(deArrowEnabled)) {
try {
const deArrow = await getDeArrow(video.videoId);
for (const title of deArrow.titles) {
if (title.locked && !title.original) {
video.title = title.title.replace('>', '');
console.log(video.title);
break;
}
}
let locatedThumbnail = false;
for (const thumbnail of deArrow.thumbnails) {
if (thumbnail.locked || thumbnail.original) {
if (typeof thumbnail.timestamp !== 'undefined') {
imageSrc = await getThumbnail(video.videoId, thumbnail.timestamp);
}
locatedThumbnail = true;
break;
}
}
if (!locatedThumbnail) {
// Process thumbnail locally.
const canvas = document.getElementById('canvas') as HTMLCanvasElement | null;
function generateThumbnail(): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
if (canvas) {
const context = canvas.getContext('2d');
const mockVideo = document.createElement('video');
const videoContainer = document.getElementById('video-container');
videoContainer?.appendChild(mockVideo);
mockVideo.preload = 'auto';
mockVideo.id = 'video';
mockVideo.crossOrigin = import.meta.env.VITE_DEFAULT_FRONTEND_URL;
mockVideo.src = proxyVideoUrl((await getVideo(video.videoId)).formatStreams[0].url);
mockVideo.addEventListener('loadeddata', () => {
mockVideo.currentTime = mockVideo.duration / 100;
mockVideo.addEventListener('seeked', () => {
if (context) {
context.drawImage(mockVideo, 0, 0, 320, 180);
resolve(canvas.toDataURL('image/png'));
mockVideo.preload = 'none';
mockVideo.pause();
videoContainer?.removeChild(mockVideo);
}
});
});
mockVideo.load();
}
});
}
imageSrc = await generateThumbnail();
}
} catch {}
}
img = new Image();
img.src = video.videoThumbnails[4].url;
img.src = imageSrc;
img.onload = () => {
loaded = true;
@@ -53,7 +117,7 @@
{:else if loaded}
<img
class="responsive"
style="max-width: 100%;height: 100%;"
style="max-width: 100%;min-height: 160px;"
src={img.src}
alt="Thumbnail for video"
/>
@@ -97,3 +161,6 @@
</div>
</nav>
</div>
<canvas id="canvas" style="display: none;"></canvas>
<div id="video-container" style="display: none;"></div>
+11 -3
View File
@@ -61,7 +61,7 @@ export function phaseDescription(content: string): PhasedDescription {
const timestamp = timestampMatch[3];
const title = timestampMatch[4] || '';
timestamps.push({
time: parseInt(time),
time: convertToSeconds(time),
title: title,
timePretty: timestamp
});
@@ -77,8 +77,6 @@ export function phaseDescription(content: string): PhasedDescription {
return { description: filteredContent, timestamps: timestamps };
}
function convertToSeconds(time: string): number {
const parts = time.split(':').map(part => parseInt(part));
let seconds = 0;
@@ -93,4 +91,14 @@ function convertToSeconds(time: string): number {
seconds = parts[0];
}
return seconds;
}
export function proxyVideoUrl(source: string): string {
const rawSrc = new URL(source);
rawSrc.host = import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE.replace(
'http://',
''
).replace('https://', '');
return rawSrc.toString();
}
+47 -14
View File
@@ -13,6 +13,9 @@
activePage,
auth,
darkMode,
deArrowEnabled,
deArrowInstance,
deArrowThumbnailInstance,
interfaceSearchSuggestions,
playerAlwaysLoop,
playerAutoPlay,
@@ -31,20 +34,6 @@
let currentPage: string | null = '';
activePage.subscribe((page) => (currentPage = page));
let autoplay = false;
let alwaysLoop = false;
let proxyVideos = false;
let savePlayerPackPos = false;
let dash = false;
let listenByDefault = false;
playerAutoPlay.subscribe((value) => (autoplay = value));
playerAlwaysLoop.subscribe((value) => (alwaysLoop = value));
playerProxyVideos.subscribe((value) => (proxyVideos = value));
playerSavePlaybackPosition.subscribe((value) => (savePlayerPackPos = value));
playerDash.subscribe((value) => (dash = value));
playerListenByDefault.subscribe((value) => (listenByDefault = value));
let searchSuggestions = false;
interfaceSearchSuggestions.subscribe((value) => (searchSuggestions = value));
@@ -60,6 +49,8 @@
let sponsorBlockInstance = get(sponsorBlockUrl);
let returnYTInstance = get(returnYTDislikesInstance);
let deArrowUrl = get(deArrowInstance);
let deArrowThumbnailUrl = get(deArrowThumbnailInstance);
const pages = [
{
@@ -494,6 +485,48 @@
</div>
{/each}
</div>
<div class="settings">
<h6>DeArrow</h6>
<form on:submit|preventDefault={() => deArrowInstance.set(deArrowUrl)}>
<nav>
<div class="field label border">
<input bind:value={deArrowUrl} name="dearrow-instance" type="text" />
<label for="dearrow-instance">Instance URL</label>
</div>
<button class="square round">
<i>done</i>
</button>
</nav>
</form>
<form on:submit|preventDefault={() => deArrowThumbnailInstance.set(deArrowThumbnailUrl)}>
<nav>
<div class="field label border">
<input bind:value={deArrowThumbnailUrl} name="dearrow-thumbnail-instance" type="text" />
<label for="dearrow-thumbnail-instance">Thumbnail instance URL</label>
</div>
<button class="square round">
<i>done</i>
</button>
</nav>
</form>
<nav class="no-padding">
<div class="max">
<p>Enabled</p>
</div>
<label class="switch">
<input
bind:checked={$deArrowEnabled}
on:click={() => deArrowEnabled.set(!$deArrowEnabled)}
type="checkbox"
/>
<span></span>
</label>
</nav>
</div>
</dialog>
<dialog class="right" id="dialog-notifications">
+5 -1
View File
@@ -22,4 +22,8 @@ export const auth: Writable<null | { username: string, token: string; }> = persi
export const sponsorBlock = persisted("sponsorBlock", true);
export const sponsorBlockUrl = persisted("sponsorBlockUrl", import.meta.env.VITE_DEFAULT_SPONSERBLOCK_INSTANCE);
export const sponsorBlockCategories: Writable<string[]> = persisted("sponsorBlockCategories", []);
export const sponsorBlockCategories: Writable<string[]> = persisted("sponsorBlockCategories", []);
export const deArrowInstance = persisted("deArrowInstance", import.meta.env.VITE_DEFAULT_DEARROW_INSTANCE);
export const deArrowEnabled = persisted("deArrowEnabled", false);
export const deArrowThumbnailInstance = persisted("deArrowThumbnailInstance", import.meta.env.VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE);