Merge pull request #175 from WardPearce/update/syncious-intergration
Update/syncious intergration
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -250,3 +250,12 @@ export interface DeArrow {
|
||||
randomTime: number;
|
||||
videoDuration: number;
|
||||
}
|
||||
|
||||
export interface SynciousProgressModel {
|
||||
time: number;
|
||||
video_id: string;
|
||||
}
|
||||
|
||||
export interface SynciousSaveProgressModel {
|
||||
time: number;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user