diff --git a/README.md b/README.md index 29e0e199..47333a37 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/materialious/src/lib/Api/index.ts b/materialious/src/lib/Api/index.ts index 277d0598..4d6fa107 100644 --- a/materialious/src/lib/Api/index.ts +++ b/materialious/src/lib/Api/index.ts @@ -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 { + 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> = 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() + } + ) + ); +} \ No newline at end of file diff --git a/materialious/src/lib/Api/model.ts b/materialious/src/lib/Api/model.ts index dd1604b7..6fe6552f 100644 --- a/materialious/src/lib/Api/model.ts +++ b/materialious/src/lib/Api/model.ts @@ -250,3 +250,12 @@ export interface DeArrow { randomTime: number; videoDuration: number; } + +export interface SynciousProgressModel { + time: number; + video_id: string; +} + +export interface SynciousSaveProgressModel { + time: number; +} diff --git a/materialious/src/lib/Player.svelte b/materialious/src/lib/Player.svelte index 3be87ae7..a39784b8 100644 --- a/materialious/src/lib/Player.svelte +++ b/materialious/src/lib/Player.svelte @@ -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(() => { diff --git a/materialious/src/lib/Thumbnail.svelte b/materialious/src/lib/Thumbnail.svelte index c80b1ca5..311edadd 100644 --- a/materialious/src/lib/Thumbnail.svelte +++ b/materialious/src/lib/Thumbnail.svelte @@ -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() { diff --git a/materialious/src/routes/+layout.svelte b/materialious/src/routes/+layout.svelte index 4752fb0f..cf5791bd 100644 --- a/materialious/src/routes/+layout.svelte +++ b/materialious/src/routes/+layout.svelte @@ -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 @@ +
+
Syncious
+ +
synciousInstanceStore.set(synciousInstance)}> + +
+ + +
+
Sponsorblock
diff --git a/materialious/src/routes/history/+page.svelte b/materialious/src/routes/history/+page.svelte index f25a721b..eaa5b567 100644 --- a/materialious/src/routes/history/+page.svelte +++ b/materialious/src/routes/history/+page.svelte @@ -1,5 +1,5 @@