Merge pull request #175 from WardPearce/update/syncious-intergration

Update/syncious intergration
This commit is contained in:
Ward
2024-04-28 20:49:49 +12:00
committed by GitHub
8 changed files with 191 additions and 29 deletions
+26 -1
View File
@@ -15,6 +15,8 @@
[Help translate Materialious!](https://fink.inlang.com/github.com/WardPearce/Materialious)
# Features
- [Syncios integration!](https://github.com/WardPearce/syncious)
- Sync your watch progress between Invidious sessions.
- Watch sync parties!
- Sponsorblock built-in.
- Return YouTube dislikes built-in.
@@ -247,7 +249,30 @@ http:
### Step 3:
Modify/add `VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE` for Materialious to be the reverse proxied URL of RYD-Proxy.
## Step 5 (Optional): Self-host PeerJS
## Step 5 (Optional, but recommended): Self-host Syncios
Sync your watch progress between Invidious sessions.
### Step 1: Docker compose
Add the following to your docker compose
```yaml
services:
syncious:
image: wardpearce/syncious:latest
restart: unless-stopped
ports:
- 3004:80
environment:
syncious_postgre: '{"host": "invidious-db", "port": 5432, "database": "invidious", "user": "kemal", "password": "kemal"}'
syncious_allowed_origins: '["https://materialios.example.com"]'
syncious_debug: false
# No trailing backslashes!
syncious_invidious_instance: "https://invidious.example.com"
syncious_production_instance: "https://syncious.example.com"
```
## Step 6 (Optional): Self-host PeerJS
[Read the official guide.](https://github.com/peers/peerjs-server?tab=readme-ov-file#docker)
Add these additional environment variables to Materialious.
+56 -1
View File
@@ -3,7 +3,8 @@ import {
authStore,
deArrowInstanceStore,
deArrowThumbnailInstanceStore,
returnYTDislikesInstanceStore
returnYTDislikesInstanceStore,
synciousInstanceStore
} from '../../store';
import type {
Channel,
@@ -18,6 +19,7 @@ import type {
ReturnYTDislikes,
SearchSuggestion,
Subscription,
SynciousProgressModel,
Video,
VideoPlay
} from './model';
@@ -301,3 +303,56 @@ export async function getThumbnail(videoId: string, time: number): Promise<strin
);
return URL.createObjectURL(await resp.blob());
}
export async function getVideoProgress(videoId: string): Promise<SynciousProgressModel[]> {
const resp = await fetchErrorHandle(
await fetch(
`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`,
buildAuthHeaders()
)
);
return resp.json();
}
export async function saveVideoProgress(videoId: string, time: number) {
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
headers['headers']['Content-type'] = 'application/json';
await fetchErrorHandle(
await fetch(
`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`,
{
...headers,
method: "POST",
body: JSON.stringify({
time: time
})
}
)
);
}
export async function deleteVideoProgress(videoId: string) {
await fetchErrorHandle(
await fetch(
`${get(synciousInstanceStore)}/video/${videoId}`,
{
method: "DELETE",
...buildAuthHeaders()
}
)
);
}
export async function deleteAllVideoProgress() {
await fetchErrorHandle(
await fetch(
`${get(synciousInstanceStore)}/videos`,
{
method: "DELETE",
...buildAuthHeaders()
}
)
);
}
+9
View File
@@ -250,3 +250,12 @@ export interface DeArrow {
randomTime: number;
videoDuration: number;
}
export interface SynciousProgressModel {
time: number;
video_id: string;
}
export interface SynciousSaveProgressModel {
time: number;
}
+34 -15
View File
@@ -8,14 +8,17 @@
import type { MediaTimeUpdateEvent, PlayerSrc } from 'vidstack';
import type { MediaPlayerElement } from 'vidstack/elements';
import {
authStore,
playerAlwaysLoopStore,
playerAutoPlayStore,
playerDashStore,
playerProxyVideosStore,
playerSavePlaybackPositionStore,
sponsorBlockCategoriesStore,
sponsorBlockUrlStore
sponsorBlockUrlStore,
synciousStore
} from '../store';
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from './Api';
import type { VideoPlay } from './Api/model';
import { getBestThumbnail, proxyVideoUrl, videoLength, type PhasedDescription } from './misc';
import { getDynamicTheme } from './theme';
@@ -114,8 +117,8 @@
}
if (get(playerDashStore)) {
player.addEventListener('dash-can-play', () => {
loadPlayerPos();
player.addEventListener('dash-can-play', async () => {
await loadPlayerPos();
});
src = [{ src: data.video.dashUrl + '?local=true', type: 'application/dash+xml' }];
@@ -180,31 +183,47 @@
document.documentElement.style.setProperty('--audio-bg', currentTheme['--surface']);
});
function loadPlayerPos() {
async function loadPlayerPos() {
if (playerPosSet) return;
playerPosSet = true;
if (get(playerSavePlaybackPositionStore)) {
try {
const playerPos = localStorage.getItem(`v_${data.video.videoId}`);
if (playerPos) {
player.currentTime = Number(playerPos);
}
} catch {}
if (get(synciousStore) && get(authStore)) {
try {
player.currentTime = (await getVideoProgress(data.video.videoId))[0].time;
} catch {}
} else {
try {
const playerPos = localStorage.getItem(`v_${data.video.videoId}`);
if (playerPos) {
player.currentTime = Number(playerPos);
}
} catch {}
}
}
}
function savePlayerPos() {
if (data.video.hlsUrl) return;
try {
if (get(playerSavePlaybackPositionStore) && player.currentTime) {
if (player.currentTime < player.duration - 10 && player.currentTime > 10) {
if (get(playerSavePlaybackPositionStore) && player.currentTime) {
if (player.currentTime < player.duration - 10 && player.currentTime > 10) {
try {
localStorage.setItem(`v_${data.video.videoId}`, player.currentTime.toString());
} else {
} catch {}
if (get(synciousStore) && get(authStore)) {
saveVideoProgress(data.video.videoId, player.currentTime);
}
} else {
try {
localStorage.removeItem(`v_${data.video.videoId}`);
} catch {}
if (get(synciousStore) && get(authStore)) {
deleteVideoProgress(data.video.videoId);
}
}
} catch {}
}
}
onDestroy(() => {
+10 -2
View File
@@ -3,14 +3,16 @@
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import {
authStore,
deArrowEnabledStore,
interfacePreviewVideoOnHoverStore,
playerProxyVideosStore,
playerSavePlaybackPositionStore,
syncPartyConnectionsStore,
syncPartyPeerStore
syncPartyPeerStore,
synciousStore
} from '../store';
import { getDeArrow, getThumbnail, getVideo } from './Api';
import { getDeArrow, getThumbnail, getVideo, getVideoProgress } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase, VideoPlay } from './Api/model';
import { cleanNumber, getBestThumbnail, proxyVideoUrl, videoLength } from './misc';
import type { PlayerEvents } from './player';
@@ -128,6 +130,12 @@
img.onerror = () => {
loading = false;
};
if (get(synciousStore) && get(authStore)) {
try {
progress = (await getVideoProgress(video.videoId))[0].time.toString();
} catch {}
}
});
function syncChangeVideo() {
+33
View File
@@ -42,6 +42,8 @@
sponsorBlockUrlStore,
syncPartyConnectionsStore,
syncPartyPeerStore,
synciousInstanceStore,
synciousStore,
themeColorStore
} from '../store';
@@ -61,6 +63,7 @@
let notifications: Notification[] = [];
let sponsorBlockInstance = get(sponsorBlockUrlStore);
let synciousInstance = get(synciousInstanceStore);
let returnYTInstance = get(returnYTDislikesInstanceStore);
let deArrowUrl = get(deArrowInstanceStore);
let deArrowThumbnailUrl = get(deArrowThumbnailInstanceStore);
@@ -604,6 +607,36 @@
</nav>
</div>
<div class="settings">
<h6>Syncious</h6>
<form on:submit|preventDefault={() => synciousInstanceStore.set(synciousInstance)}>
<nav>
<div class="field label border max">
<input bind:value={synciousInstance} name="syncious-instance" type="text" />
<label for="syncious-instance">{$_('layout.instanceUrl')}</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={$synciousStore}
on:click={() => synciousStore.set(!$synciousStore)}
type="checkbox"
/>
<span></span>
</label>
</nav>
</div>
<div class="settings">
<h6>Sponsorblock</h6>
+8 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { deleteHistory, getHistory, getVideo } from '$lib/Api';
import { deleteAllVideoProgress, deleteHistory, getHistory, getVideo } from '$lib/Api';
import type { VideoPlay } from '$lib/Api/model';
import PageLoading from '$lib/PageLoading.svelte';
import VideoList from '$lib/VideoList.svelte';
@@ -7,7 +7,8 @@
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
import { activePageStore } from '../../store';
import { get } from 'svelte/store';
import { activePageStore, synciousStore } from '../../store';
activePageStore.set('history');
@@ -62,6 +63,11 @@
<button
on:click={async () => {
await deleteHistory();
if (get(synciousStore)) {
deleteAllVideoProgress();
}
history = [];
try {
Object.keys(localStorage).forEach((key) => {
+15 -8
View File
@@ -3,10 +3,12 @@ import type { DataConnection } from 'peerjs';
import { persisted } from 'svelte-persisted-store';
import { writable, type Writable } from 'svelte/store';
export const returnYTDislikesInstanceStore: Writable<string | null | undefined> = persisted(
'returnYTDislikesInstance',
import.meta.env.VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE
export const authStore: Writable<null | { username: string; token: string; }> = persisted(
'authToken',
null
);
export const darkModeStore: Writable<null | boolean> = persisted('darkMode', null);
export const themeColorStore: Writable<null | string> = persisted('themeColor', null);
@@ -22,15 +24,20 @@ export const playerTheatreModeByDefaultStore = persisted('theatreModeByDefault',
export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault', false);
export const returnYtDislikesStore = persisted('returnYtDislikes', false);
export const returnYTDislikesInstanceStore: Writable<string | null | undefined> = persisted(
'returnYTDislikesInstance',
import.meta.env.VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE
);
export const synciousStore = persisted('syncious', true);
export const synciousInstanceStore: Writable<string | null | undefined> = persisted(
'synciousInstance',
import.meta.env.VITE_DEFAULT_SYNCIOUS_INSTANCE
);
export const interfaceSearchSuggestionsStore = persisted('searchSuggestions', true);
export const interfacePreviewVideoOnHoverStore = persisted('previewVideoOnHover', true);
export const authStore: Writable<null | { username: string; token: string; }> = persisted(
'authToken',
null
);
export const sponsorBlockStore = persisted('sponsorBlock', true);
export const sponsorBlockUrlStore: Writable<string | null | undefined> = persisted(
'sponsorBlockUrl',