From d9cf0130cc7f5a9809df4e5f901268c530ce909f Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 18:03:11 +1300 Subject: [PATCH 01/29] Bump version --- materialious/android/app/build.gradle | 4 ++-- materialious/electron/materialious.metainfo.xml | 6 +++++- materialious/electron/package.json | 2 +- materialious/package.json | 2 +- update_versions.py | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index e34011ab..694e35a1 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 223 - versionName "1.16.5" + versionCode 224 + versionName "1.16.6" 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.metainfo.xml b/materialious/electron/materialious.metainfo.xml index ee4a6251..8f1b2e56 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -88,7 +88,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.16.6 + + https://github.com/Materialious/Materialious/releases/tag/1.16.5 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 5b43a27b..4dee5076 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.16.5", + "version": "1.16.6", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index a689899d..43f0e116 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.16.5", + "version": "1.16.6", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/update_versions.py b/update_versions.py index 551eb373..85380a7f 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.16.5" +LATEST_VERSION = "1.16.6" 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") From f20a11dbeb316843d90e9260df818ade19274fba Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 18:05:24 +1300 Subject: [PATCH 02/29] Update +page.svelte --- .../src/routes/(app)/watch/[slug]/+page.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 6e1d9316..327b296d 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -451,25 +451,25 @@ }, { type: 'invidious', - path: `/watch?=${data.video.videoId}`, + path: `/watch?v=${data.video.videoId}`, param: { - key: 'v', + key: 't', value: () => Math.round(playerCurrentTime) } }, { type: 'invidious redirect', - path: `/watch?=${data.video.videoId}`, + path: `/watch?v=${data.video.videoId}`, param: { - key: 'v', + key: 't', value: () => Math.round(playerCurrentTime) } }, { type: 'youtube', - path: `/watch?=${data.video.videoId}`, + path: `/watch?v=${data.video.videoId}`, param: { - key: 'v', + key: 't', value: () => Math.round(playerCurrentTime) } } From b759ab318c2c236b58da3528e5dc8d400cff1cfb Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 18:21:32 +1300 Subject: [PATCH 03/29] Fix mobile setting categories getting cut off --- materialious/src/lib/components/settings/Settings.svelte | 5 +++++ materialious/static/localApi/ghContributors.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/materialious/src/lib/components/settings/Settings.svelte b/materialious/src/lib/components/settings/Settings.svelte index 7d7f4415..3d645904 100644 --- a/materialious/src/lib/components/settings/Settings.svelte +++ b/materialious/src/lib/components/settings/Settings.svelte @@ -244,5 +244,10 @@ .settings { height: 100%; } + + #tab-menu { + height: fit-content !important; + max-height: fit-content !important; + } } diff --git a/materialious/static/localApi/ghContributors.json b/materialious/static/localApi/ghContributors.json index 39019fba..ef4306ef 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":2156},{"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":230},{"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":13},{"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":"coolvitto","id":160416811,"node_id":"U_kgDOCY_EKw","avatar_url":"https://avatars.githubusercontent.com/u/160416811?v=4","gravatar_id":"","url":"https://api.github.com/users/coolvitto","html_url":"https://github.com/coolvitto","followers_url":"https://api.github.com/users/coolvitto/followers","following_url":"https://api.github.com/users/coolvitto/following{/other_user}","gists_url":"https://api.github.com/users/coolvitto/gists{/gist_id}","starred_url":"https://api.github.com/users/coolvitto/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coolvitto/subscriptions","organizations_url":"https://api.github.com/users/coolvitto/orgs","repos_url":"https://api.github.com/users/coolvitto/repos","events_url":"https://api.github.com/users/coolvitto/events{/privacy}","received_events_url":"https://api.github.com/users/coolvitto/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}] \ 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":2161},{"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":230},{"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":13},{"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":"coolvitto","id":160416811,"node_id":"U_kgDOCY_EKw","avatar_url":"https://avatars.githubusercontent.com/u/160416811?v=4","gravatar_id":"","url":"https://api.github.com/users/coolvitto","html_url":"https://github.com/coolvitto","followers_url":"https://api.github.com/users/coolvitto/followers","following_url":"https://api.github.com/users/coolvitto/following{/other_user}","gists_url":"https://api.github.com/users/coolvitto/gists{/gist_id}","starred_url":"https://api.github.com/users/coolvitto/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coolvitto/subscriptions","organizations_url":"https://api.github.com/users/coolvitto/orgs","repos_url":"https://api.github.com/users/coolvitto/repos","events_url":"https://api.github.com/users/coolvitto/events{/privacy}","received_events_url":"https://api.github.com/users/coolvitto/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}] \ No newline at end of file From 739a3f00a17f397c7d71300d8ed9732699ef7af4 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 19:15:15 +1300 Subject: [PATCH 04/29] Started Materialious watch history --- materialious/src/lib/api/backend.ts | 53 ++++++++- materialious/src/lib/server/database.ts | 57 +++++++++- materialious/src/lib/watch.ts | 7 ++ .../src/routes/api/user/history/+server.ts | 106 ++++++++++++++++++ .../api/user/history/[videoHash]/+server.ts | 47 ++++++++ 5 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 materialious/src/routes/api/user/history/+server.ts create mode 100644 materialious/src/routes/api/user/history/[videoHash]/+server.ts diff --git a/materialious/src/lib/api/backend.ts b/materialious/src/lib/api/backend.ts index 9f25ab1a..c18b82f1 100644 --- a/materialious/src/lib/api/backend.ts +++ b/materialious/src/lib/api/backend.ts @@ -4,11 +4,14 @@ import { get } from 'svelte/store'; import { parseChannelRSS } from './youtubejs/subscriptions'; import { getChannelYTjs } from './youtubejs/channel'; import type { ChannelSubscriptions } from '$lib/dexie'; +import type { VideoPlay } from './model'; +import { vi } from 'zod/v4/locales'; +import { getBestThumbnail } from '$lib/images'; -async function getInternalAuthorId(authorId: string, rawKey: Uint8Array): Promise { +async function getSecureHash(toHash: string, rawKey: Uint8Array): Promise { await sodium.ready; return sodium.to_base64( - sodium.crypto_generichash(sodium.crypto_generichash_BYTES, authorId, rawKey) + sodium.crypto_generichash(sodium.crypto_generichash_BYTES, toHash, rawKey) ); } @@ -48,7 +51,7 @@ export async function updateRSSLastUpdated(authorId: string) { const rawKey = await getRawKey(); if (!rawKey) return false; - const internalAuthorId = await getInternalAuthorId(authorId, rawKey); + const internalAuthorId = await getSecureHash(authorId, rawKey); await fetch(`/api/user/subscriptions/${internalAuthorId}`, { method: 'PATCH', @@ -60,7 +63,7 @@ export async function amSubscribedBackend(authorId: string): Promise { const rawKey = await getRawKey(); if (!rawKey) return false; - const internalAuthorId = await getInternalAuthorId(authorId, rawKey); + const internalAuthorId = await getSecureHash(authorId, rawKey); const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, { method: 'GET', @@ -77,7 +80,7 @@ export async function deleteUnsubscribeBackend(authorId: string) { const rawKey = await getRawKey(); if (!rawKey) return false; - const internalAuthorId = await getInternalAuthorId(authorId, rawKey); + const internalAuthorId = await getSecureHash(authorId, rawKey); await fetch(`/api/user/subscriptions/${internalAuthorId}`, { method: 'DELETE', @@ -92,7 +95,7 @@ export async function postSubscribeBackend( const rawKey = await getRawKey(); if (!rawKey) return; - const internalAuthorId = await getInternalAuthorId(authorId, rawKey); + const internalAuthorId = await getSecureHash(authorId, rawKey); if (!authorName) { const channel = await getChannelYTjs(authorId); @@ -260,6 +263,44 @@ export async function getKeyValue(key: string): Promise { return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null; } +export async function saveHistoryToBackend(video: VideoPlay, progress: number = 0) { + await sodium.ready; + const rawKey = await getRawKey(); + if (!rawKey) return; + + const videoHash = await getSecureHash(video.videoId, rawKey); + + const title = await encryptWithMasterKey(video.title); + const author = await encryptWithMasterKey(video.author); + const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails)); + const duration = await encryptWithMasterKey(video.lengthSeconds.toString()); + + await fetch('/api/user/history', { + method: 'POST', + body: JSON.stringify({ + id: videoHash, + watched: new Date(), + progress: progress, + title: { + cipher: title?.cipher, + nonce: title?.nonce + }, + author: { + cipher: author?.cipher, + nonce: author?.nonce + }, + thumbnail: { + cipher: thumbnail?.cipher, + nonce: thumbnail?.nonce + }, + duration: { + cipher: duration?.cipher, + nonce: duration?.nonce + } + }) + }); +} + async function encryptWithMasterKey( text: string ): Promise<{ nonce: string; cipher: string } | undefined> { diff --git a/materialious/src/lib/server/database.ts b/materialious/src/lib/server/database.ts index 5a58d109..b8de0aac 100644 --- a/materialious/src/lib/server/database.ts +++ b/materialious/src/lib/server/database.ts @@ -5,6 +5,7 @@ let UserTable: ModelCtor>; let ChannelSubscriptionTable: ModelCtor>; let CaptchaTable: ModelCtor>; let UserKeyValueTable: ModelCtor>; +let UserHistoryTable: ModelCtor>; let sequelizeInstance: Sequelize | null = null; @@ -15,6 +16,7 @@ export function getSequelize(): { ChannelSubscriptionTable: ModelCtor>; CaptchaTable: ModelCtor>; UserKeyValueTable: ModelCtor>; + UserHistoryTable: ModelCtor>; } { if (sequelizeInstance) { return { @@ -22,7 +24,8 @@ export function getSequelize(): { UserTable, ChannelSubscriptionTable, CaptchaTable, - UserKeyValueTable + UserKeyValueTable, + UserHistoryTable }; } @@ -107,6 +110,54 @@ export function getSequelize(): { } ); + UserHistoryTable = sequelizeInstance.define('UserHistory', { + id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true + }, + watched: { + type: DataTypes.DATE, + allowNull: false + }, + progress: { + type: DataTypes.INTEGER, + allowNull: false + }, + titleCipher: { + type: DataTypes.STRING, + allowNull: false + }, + titleNonce: { + type: DataTypes.STRING, + allowNull: false + }, + authorCipher: { + type: DataTypes.STRING, + allowNull: false + }, + authorNonce: { + type: DataTypes.STRING, + allowNull: false + }, + thumbnailCipher: { + type: DataTypes.STRING, + allowNull: false + }, + thumbnailNonce: { + type: DataTypes.STRING, + allowNull: false + }, + durationCipher: { + type: DataTypes.STRING, + allowNull: false + }, + durationNonce: { + type: DataTypes.STRING, + allowNull: false + } + }); + ChannelSubscriptionTable = sequelizeInstance.define('Subscriptions', { id: { type: DataTypes.STRING, @@ -149,13 +200,15 @@ export function getSequelize(): { UserTable.hasMany(ChannelSubscriptionTable); UserTable.hasMany(UserKeyValueTable); + UserTable.hasMany(UserHistoryTable); return { sequelize: sequelizeInstance, UserTable, ChannelSubscriptionTable, CaptchaTable, - UserKeyValueTable + UserKeyValueTable, + UserHistoryTable }; } diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index 996f2c91..fa9b12a8 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -10,6 +10,7 @@ import { invidiousAuthStore, playerProxyVideosStore, playerState, + rawMasterKeyStore, returnYTDislikesInstanceStore, returnYtDislikesStore } from '$lib/store'; @@ -17,6 +18,8 @@ import { parseDescription } from '$lib/description'; import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; import { _ } from './i18n'; +import { isOwnBackend } from './shared'; +import { saveHistoryToBackend } from './api/backend'; export async function getWatchDetails(videoId: string, url: URL) { const playerStateRetrieved = get(playerState); @@ -45,6 +48,10 @@ export async function getWatchDetails(videoId: string, url: URL) { personalPlaylists = null; } + if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) { + saveHistoryToBackend(video); + } + let comments; try { comments = video.liveNow ? null : getComments(videoId, { sort_by: 'top' }, { priority: 'low' }); diff --git a/materialious/src/routes/api/user/history/+server.ts b/materialious/src/routes/api/user/history/+server.ts new file mode 100644 index 00000000..68ed2bda --- /dev/null +++ b/materialious/src/routes/api/user/history/+server.ts @@ -0,0 +1,106 @@ +import { getSequelize } from '$lib/server/database.js'; +import { error, json } from '@sveltejs/kit'; +import z from 'zod'; +import { Op } from 'sequelize'; + +const zUserHistory = z.object({ + id: z.string(), + watched: z.coerce.date(), + progress: z.number(), + title: z.object({ + cipher: z.string().max(255), + nonce: z.string().max(255) + }), + author: z.object({ + cipher: z.string().max(255), + nonce: z.string().max(255) + }), + thumbnail: z.object({ + cipher: z.string().max(255), + nonce: z.string().max(255) + }), + duration: z.object({ + cipher: z.string().max(255), + nonce: z.string().max(255) + }) +}); + +export async function POST({ locals, request }) { + if (!locals.userId) throw error(401, 'Unauthorized'); + + const data = zUserHistory.safeParse(await request.json()); + + if (!data.success) throw error(400, data.error.message); + + const history = await getSequelize().UserHistoryTable.findOne({ + where: { + UserId: locals.userId, + id: data.data.id + } + }); + + const toStore = { + progress: data.data.progress, + watched: data.data.watched, + titleCipher: data.data.title.cipher, + titleNonce: data.data.title.nonce, + authorCipher: data.data.author.cipher, + authorNonce: data.data.author.nonce, + thumbnailCipher: data.data.thumbnail.cipher, + thumbnailNonce: data.data.thumbnail.nonce, + durationCipher: data.data.duration.cipher, + durationNonce: data.data.duration.nonce + }; + + if (history) { + await history.update(toStore); + } else { + await getSequelize().UserHistoryTable.create({ + UserId: locals.userId, + id: data.data.id, + ...toStore + }); + } + + return new Response(); +} + +export async function GET({ locals, url }) { + if (!locals.userId) throw error(401, 'Unauthorized'); + + const limit = 100; + const page = Number(url.searchParams.get('page') ?? 0); + + const videoHashes = url.searchParams.get('videoHashes'); + let videoHashesList: string[] = []; + + if (videoHashes) { + videoHashesList = videoHashes.split(','); + } + + const whereClause: any = { + UserId: locals.userId + }; + + if (videoHashesList.length > 0) { + whereClause[Op.or] = [{ id: { [Op.in]: videoHashesList } }]; + } + + const history = await getSequelize().UserHistoryTable.findAll({ + where: whereClause, + limit, + offset: limit * page + }); + + return json(history); +} + +export async function DELETE({ locals }) { + await getSequelize().UserHistoryTable.destroy({ + where: { + UserId: locals.userId + } + }); + + return new Response(); +} diff --git a/materialious/src/routes/api/user/history/[videoHash]/+server.ts b/materialious/src/routes/api/user/history/[videoHash]/+server.ts new file mode 100644 index 00000000..ac2c92fc --- /dev/null +++ b/materialious/src/routes/api/user/history/[videoHash]/+server.ts @@ -0,0 +1,47 @@ +import { getSequelize } from '$lib/server/database.js'; +import { error, json } from '@sveltejs/kit'; +import z from 'zod'; + +const zUserHistoryUpdate = z + .object({ + watched: z.date().optional(), + progress: z.number().optional() + }) + .transform((data) => { + return Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined)); + }); + +export async function POST({ locals, request, params }) { + if (!locals.userId) throw error(401, 'Unauthorized'); + + const data = zUserHistoryUpdate.safeParse(await request.json()); + if (!data.success) throw error(400); + + await getSequelize().UserHistoryTable.update(data.data, { + where: { UserId: locals.userId, VideoHash: params.videoHash } + }); + + return new Response(); +} + +export async function DELETE({ locals, params }) { + if (!locals.userId) throw error(401, 'Unauthorized'); + + await getSequelize().UserHistoryTable.destroy({ + where: { UserId: locals.userId, VideoHash: params.videoHash } + }); + + return new Response(); +} + +export async function GET({ locals, params }) { + if (!locals.userId) throw error(401, 'Unauthorized'); + + const history = await getSequelize().UserHistoryTable.findOne({ + where: { UserId: locals.userId, VideoHash: params.videoHash } + }); + + if (!history) throw error(404); + + return json(history); +} From cc0e2e0bf3cbb942bad8ef10ff7c9ad490b1a352 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 19:21:57 +1300 Subject: [PATCH 05/29] Added updateWatchHistory func --- materialious/src/lib/api/backend.ts | 20 +++++++++++++++++++- materialious/src/lib/watch.ts | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/materialious/src/lib/api/backend.ts b/materialious/src/lib/api/backend.ts index c18b82f1..8f5d7ed8 100644 --- a/materialious/src/lib/api/backend.ts +++ b/materialious/src/lib/api/backend.ts @@ -263,7 +263,24 @@ export async function getKeyValue(key: string): Promise { return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null; } -export async function saveHistoryToBackend(video: VideoPlay, progress: number = 0) { +export async function updateWatchHistory(videoId: string, progress: number) { + await sodium.ready; + const rawKey = await getRawKey(); + if (!rawKey) return; + + const videoHash = await getSecureHash(videoId, rawKey); + + await fetch(`/api/user/history/${videoHash}`, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + watched: new Date(), + progress + }) + }); +} + +export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { await sodium.ready; const rawKey = await getRawKey(); if (!rawKey) return; @@ -277,6 +294,7 @@ export async function saveHistoryToBackend(video: VideoPlay, progress: number = await fetch('/api/user/history', { method: 'POST', + credentials: 'same-origin', body: JSON.stringify({ id: videoHash, watched: new Date(), diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index fa9b12a8..7de56282 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -19,7 +19,7 @@ import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; import { _ } from './i18n'; import { isOwnBackend } from './shared'; -import { saveHistoryToBackend } from './api/backend'; +import { saveWatchHistory } from './api/backend'; export async function getWatchDetails(videoId: string, url: URL) { const playerStateRetrieved = get(playerState); @@ -49,7 +49,7 @@ export async function getWatchDetails(videoId: string, url: URL) { } if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) { - saveHistoryToBackend(video); + saveWatchHistory(video); } let comments; From f97d7d6453e8ead3bb2edecd2a2db91aa72f0fd2 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 23 Feb 2026 19:30:16 +1300 Subject: [PATCH 06/29] Removed API-Extended --- docs/DOCKER-FULL.md | 34 -------- docs/DOCKER.md | 29 +------ materialious/Dockerfile | 4 - materialious/replace_env_vars.sh | 4 - materialious/src/lib/api/apiExtended.ts | 83 +++++++++---------- materialious/src/lib/api/index.ts | 62 +------------- materialious/src/lib/api/model.ts | 9 -- .../src/lib/components/player/Player.svelte | 66 +++++---------- .../components/settings/ApiExtended.svelte | 42 ---------- .../lib/components/settings/Settings.svelte | 2 - .../thumbnail/VideoThumbnail.svelte | 14 +--- .../src/lib/externalSettings/settings.ts | 12 --- materialious/src/lib/store.ts | 12 --- .../routes/api/proxy/[urlToProxy]/+server.ts | 1 - 14 files changed, 63 insertions(+), 311 deletions(-) delete mode 100644 materialious/src/lib/components/settings/ApiExtended.svelte diff --git a/docs/DOCKER-FULL.md b/docs/DOCKER-FULL.md index 76c9bc07..328bfd4d 100644 --- a/docs/DOCKER-FULL.md +++ b/docs/DOCKER-FULL.md @@ -136,37 +136,3 @@ ryd-proxy: ``` Modify/add `VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE` for Materialious to be the reverse proxied URL of RYD-Proxy. - -## Step 3 (Optional, but recommended): Self-host Invidious API extended -```yaml -services: - api_extended: - image: wardpearce/invidious_api_extended:latest - restart: unless-stopped - ports: - - 3004:80 - environment: - api_extended_postgre: '{"host": "invidious-db", "port": 5432, "database": "invidious", "user": "kemal", "password": "kemal"}' - api_extended_allowed_origins: '["https://materialious.example.com"]' - api_extended_debug: false - - # No trailing backslashes! - api_extended_invidious_instance: "https://invidious.example.com" - api_extended_production_instance: "https://syncious.example.com" -``` - -Add these additional environment variables to Materialious. -```yaml -VITE_DEFAULT_API_EXTENDED_INSTANCE: "https://syncious.example.com" -``` - -## Step 4 (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. -```yaml -# Will differ depending on how you self-host peerjs. -VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com" -VITE_DEFAULT_PEERJS_PATH: "/" -VITE_DEFAULT_PEERJS_PORT: 443 -``` diff --git a/docs/DOCKER.md b/docs/DOCKER.md index ae04b025..e1ee4757 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -278,34 +278,7 @@ http: ### Step 3: Modify/add `VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE` for Materialious to be the reverse proxied URL of RYD-Proxy. -## Step 5 (Optional, but recommended): Self-host Invidious API extended - -### Step 1: Docker compose -Add the following to your docker compose - -```yaml -services: - api_extended: - image: wardpearce/invidious_api_extended:latest - restart: unless-stopped - ports: - - 3004:80 - environment: - api_extended_postgre: '{"host": "invidious-db", "port": 5432, "database": "invidious", "user": "kemal", "password": "kemal"}' - api_extended_allowed_origins: '["https://materialious.example.com"]' - api_extended_debug: false - - # No trailing backslashes! - api_extended_invidious_instance: "https://invidious.example.com" - api_extended_production_instance: "https://syncious.example.com" -``` - -Add these additional environment variables to Materialious. -```yaml -VITE_DEFAULT_API_EXTENDED_INSTANCE: "https://syncious.example.com" -``` - -## Step 6 (Optional): Self-host PeerJS +## Step 5 (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/Dockerfile b/materialious/Dockerfile index 35595a05..198e5109 100644 --- a/materialious/Dockerfile +++ b/materialious/Dockerfile @@ -20,10 +20,6 @@ RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEH echo "VITE_DEFAULT_PEERJS_HOST=VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_PEERJS_PATH=VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_PEERJS_PORT=VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER" >> .env && \ - echo "VITE_DEFAULT_DOWNLOAD_ENABLED=VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER" >> .env && \ - echo "VITE_DEFAULT_SYNCIOUS_INSTANCE=VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER" >> .env && \ - echo "VITE_DEFAULT_API_EXTENDED_INSTANCE=VITE_DEFAULT_API_EXTENDED_INSTANCE_PLACEHOLDER" >> .env && \ - echo "VITE_DEFAULT_DASH_BITRATE=VITE_DEFAULT_DASH_BITRATE_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_SETTINGS='\"VITE_DEFAULT_SETTINGS_PLACEHOLDER\"'" >> .env && \ echo "VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE=VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER" >> .env diff --git a/materialious/replace_env_vars.sh b/materialious/replace_env_vars.sh index 18f71f88..a2fcbbe3 100644 --- a/materialious/replace_env_vars.sh +++ b/materialious/replace_env_vars.sh @@ -11,8 +11,4 @@ find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VI find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_HOST"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PATH"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PORT"'|g' {} + -find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER|'"$VITE_DEFAULT_DOWNLOAD_ENABLED"'|g' {} + -find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SYNCIOUS_INSTANCE"'|g' {} + -find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DASH_BITRATE_PLACEHOLDER|'"$VITE_DEFAULT_DASH_BITRATE"'|g' {} + -find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_API_EXTENDED_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_API_EXTENDED_INSTANCE"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SETTINGS_PLACEHOLDER|'"$VITE_DEFAULT_SETTINGS"'|g' {} + diff --git a/materialious/src/lib/api/apiExtended.ts b/materialious/src/lib/api/apiExtended.ts index 77d73063..5d0e1035 100644 --- a/materialious/src/lib/api/apiExtended.ts +++ b/materialious/src/lib/api/apiExtended.ts @@ -1,53 +1,50 @@ -import type { ApiExntendedProgressModel } from './model'; -import { getVideoProgress } from '.'; +// const videoIds: string[] = []; +// const pendingResolves = new Map void>(); -const videoIds: string[] = []; -const pendingResolves = new Map void>(); +// let timeout: ReturnType | null = null; +// const DEBOUNCE_MS = 1000; +// const BATCH_SIZE = 100; -let timeout: ReturnType | null = null; -const DEBOUNCE_MS = 1000; -const BATCH_SIZE = 100; +// async function processBatches(): Promise { +// const batches: string[][] = []; -async function processBatches(): Promise { - const batches: string[][] = []; +// while (videoIds.length > 0) { +// batches.push(videoIds.splice(0, BATCH_SIZE)); +// } - while (videoIds.length > 0) { - batches.push(videoIds.splice(0, BATCH_SIZE)); - } +// const results: ApiExntendedProgressModel[] = []; - const results: ApiExntendedProgressModel[] = []; +// for (const batch of batches) { +// const res: ApiExntendedProgressModel[] = await getVideoProgress(batch.join(',')); +// results.push(...res); - for (const batch of batches) { - const res: ApiExntendedProgressModel[] = await getVideoProgress(batch.join(',')); - results.push(...res); +// // Resolve pending promises for this batch +// for (const videoId of batch) { +// const match = res.find((item) => item.video_id === videoId); +// const resolve = pendingResolves.get(videoId); +// if (resolve) { +// resolve(match); +// pendingResolves.delete(videoId); +// } +// } +// } +// } - // Resolve pending promises for this batch - for (const videoId of batch) { - const match = res.find((item) => item.video_id === videoId); - const resolve = pendingResolves.get(videoId); - if (resolve) { - resolve(match); - pendingResolves.delete(videoId); - } - } - } -} +// export function queueGetWatchProgress( +// videoId: string +// ): Promise { +// videoIds.push(videoId); -export function queueGetWatchProgress( - videoId: string -): Promise { - videoIds.push(videoId); +// const promise = new Promise((resolve) => { +// pendingResolves.set(videoId, resolve); +// }); - const promise = new Promise((resolve) => { - pendingResolves.set(videoId, resolve); - }); +// if (timeout) clearTimeout(timeout); +// timeout = setTimeout(() => { +// processBatches().catch((e) => { +// console.error('Failed to process batches:', e); +// }); +// }, DEBOUNCE_MS); - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - processBatches().catch((e) => { - console.error('Failed to process batches:', e); - }); - }, DEBOUNCE_MS); - - return promise; -} +// return promise; +// } diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index 928d0d9a..3b567109 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -9,8 +9,7 @@ import { playerYouTubeJsAlways, playerYouTubeJsFallback, rawMasterKeyStore, - returnYTDislikesInstanceStore, - synciousInstanceStore + returnYTDislikesInstanceStore } from '../store'; import type { ChannelPage, @@ -597,62 +596,3 @@ export async function getThumbnail( ); return URL.createObjectURL(await resp.blob()); } - -function buildApiExtendedAuthHeaders(): Record> { - const authToken = get(invidiousAuthStore)?.token ?? ''; - return { headers: { Authorization: `Bearer ${authToken.replace('SID=', '')}` } }; -} - -export async function getVideoProgress( - videoId: string, - fetchOptions: RequestInit = {} -): Promise { - const resp = await fetchErrorHandle( - await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, { - ...buildApiExtendedAuthHeaders(), - ...fetchOptions - }) - ); - - return resp.json(); -} - -export async function saveVideoProgress( - videoId: string, - time: number, - fetchOptions: RequestInit = {} -) { - const headers: Record> = buildApiExtendedAuthHeaders(); - headers['headers']['Content-type'] = 'application/json'; - - await fetchErrorHandle( - await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, { - ...headers, - method: 'POST', - body: JSON.stringify({ - time: time - }), - ...fetchOptions - }) - ); -} - -export async function deleteVideoProgress(videoId: string, fetchOptions: RequestInit = {}) { - await fetchErrorHandle( - await fetch(`${get(synciousInstanceStore)}/video/${videoId}`, { - method: 'DELETE', - ...buildApiExtendedAuthHeaders(), - ...fetchOptions - }) - ); -} - -export async function deleteAllVideoProgress(fetchOptions: RequestInit = {}) { - await fetchErrorHandle( - await fetch(`${get(synciousInstanceStore)}/videos`, { - method: 'DELETE', - ...buildApiExtendedAuthHeaders(), - ...fetchOptions - }) - ); -} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 793e795c..5523be05 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -302,15 +302,6 @@ export interface DeArrow { videoDuration: number; } -export interface ApiExntendedProgressModel { - time: number; - video_id: string; -} - -export interface SynciousSaveProgressModel { - time: number; -} - export type SearchResults = (Channel | Video | Playlist | HashTag)[] & { getContinuation?: () => Promise; }; diff --git a/materialious/src/lib/components/player/Player.svelte b/materialious/src/lib/components/player/Player.svelte index 4285041e..2493a33f 100644 --- a/materialious/src/lib/components/player/Player.svelte +++ b/materialious/src/lib/components/player/Player.svelte @@ -13,7 +13,6 @@ import { _ } from '$lib/i18n'; import { get } from 'svelte/store'; import { Slider } from 'melt/builders'; - import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '$lib/api'; import type { VideoPlay } from '$lib/api/model'; import { invidiousAuthStore, @@ -34,9 +33,7 @@ sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, - sponsorBlockUrlStore, - synciousInstanceStore, - synciousStore + sponsorBlockUrlStore } from '$lib/store'; import { setStatusBarColor } from '$lib/theme'; import { getVideoYTjs } from '$lib/api/youtubejs/video'; @@ -257,7 +254,7 @@ clearInterval(watchProgressInterval); } // Auto save watch progress every minute. - watchProgressInterval = setInterval(() => savePlayerPos(), 60000); + watchProgressInterval = setInterval(() => savePlayerbackHistory(), 60000); let dashUrl: string; @@ -284,9 +281,9 @@ !data.video.fallbackPatch ) { const manifest = await manifestDomainInclusion(dashUrl); - await player.load(manifest, await getLastPlayPos()); + await player.load(manifest, await getPlaybackHistory()); } else { - await player.load(dashUrl, await getLastPlayPos()); + await player.load(dashUrl, await getPlaybackHistory()); } if (data.content.timestamps) { @@ -675,7 +672,7 @@ playerElement?.addEventListener('pause', async () => { playerCurrentPlaybackState = false; playerIsBuffering = false; - savePlayerPos(); + savePlayerbackHistory(); showPlayerUI(); }); @@ -739,7 +736,7 @@ updateVideoPlayerHeight(); }); - async function getLastPlayPos(): Promise { + async function getPlaybackHistory(): Promise { if (loadTimeFromUrl($page) || !$playerSavePlaybackPositionStore) return 0; let toSetTime = 0; @@ -753,46 +750,23 @@ // Continue regardless of error } - if ($synciousStore && $synciousInstanceStore && $invidiousAuthStore) { - try { - toSetTime = (await getVideoProgress(data.video.videoId))[0].time; - } catch { - // Continue regardless of error - } - } - return toSetTime; } - function savePlayerPos() { - if (data.video.liveNow) return; + function savePlayerbackHistory() { + if (data.video.liveNow || !$playerSavePlaybackPositionStore || !playerElement) return; - const synciousEnabled = $synciousStore && $synciousInstanceStore && $invidiousAuthStore; - - if ($playerSavePlaybackPositionStore && playerElement) { - if ( - playerElement.currentTime < playerElement.duration - 10 && - playerElement.currentTime > 10 - ) { - try { - localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString()); - } catch { - // Continue regardless of error - } - - if (synciousEnabled) { - saveVideoProgress(data.video.videoId, playerElement.currentTime); - } - } else { - try { - localStorage.removeItem(`v_${data.video.videoId}`); - } catch { - // Continue regardless of error - } - - if (synciousEnabled) { - deleteVideoProgress(data.video.videoId); - } + if (playerElement.currentTime < playerElement.duration - 10 && playerElement.currentTime > 10) { + try { + localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString()); + } catch { + // Continue regardless of error + } + } else { + try { + localStorage.removeItem(`v_${data.video.videoId}`); + } catch { + // Continue regardless of error } } } @@ -814,7 +788,7 @@ } try { - savePlayerPos(); + savePlayerbackHistory(); } catch { // Continue regardless of error } diff --git a/materialious/src/lib/components/settings/ApiExtended.svelte b/materialious/src/lib/components/settings/ApiExtended.svelte deleted file mode 100644 index 903f1c44..00000000 --- a/materialious/src/lib/components/settings/ApiExtended.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -
- synciousInstanceStore.set(ensureNoTrailingSlash(synciousInstance)) - )} -> - -
- - diff --git a/materialious/src/lib/components/settings/Settings.svelte b/materialious/src/lib/components/settings/Settings.svelte index 3d645904..e8038828 100644 --- a/materialious/src/lib/components/settings/Settings.svelte +++ b/materialious/src/lib/components/settings/Settings.svelte @@ -1,7 +1,6 @@ + + diff --git a/materialious/src/routes/(app)/history/+page.ts b/materialious/src/routes/(app)/history/+page.ts new file mode 100644 index 00000000..3e08ee47 --- /dev/null +++ b/materialious/src/routes/(app)/history/+page.ts @@ -0,0 +1,13 @@ +import { resolve } from '$app/paths'; +import { getWatchHistory } from '$lib/api/backend/history'; +import { isOwnBackend } from '$lib/shared'; +import { rawMasterKeyStore } from '$lib/store'; +import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; + +export async function load() { + if (!isOwnBackend()?.internalAuth || !get(rawMasterKeyStore)) + throw redirect(302, resolve('/', {})); + + return { videos: await getWatchHistory() }; +} diff --git a/materialious/src/routes/api/user/history/+server.ts b/materialious/src/routes/api/user/history/+server.ts index e777b08e..630ee601 100644 --- a/materialious/src/routes/api/user/history/+server.ts +++ b/materialious/src/routes/api/user/history/+server.ts @@ -7,7 +7,7 @@ const zUserHistory = z.object({ id: z.string().max(255), watched: z.coerce.date(), progress: z.number().max(115200), - duration: z.number().max(115200), + lengthSeconds: z.number().max(115200), title: z.object({ cipher: z.string().max(255), nonce: z.string().max(255) @@ -43,7 +43,7 @@ export async function POST({ locals, request }) { const toStore = { progress: data.data.progress, watched: data.data.watched, - duration: data.data.duration, + lengthSeconds: data.data.lengthSeconds, titleCipher: data.data.title.cipher, titleNonce: data.data.title.nonce, authorCipher: data.data.author.cipher, @@ -64,6 +64,18 @@ export async function POST({ locals, request }) { }); } + // Cull any history older then 3 months. + const threeMonthsAgo = new Date(); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + await getSequelize().UserHistoryTable.destroy({ + where: { + UserId: locals.userId, + watched: { + [Op.lt]: threeMonthsAgo + } + } + }); + return new Response(); } @@ -80,8 +92,6 @@ export async function GET({ locals, url }) { videoHashesList = videoHashes.split(','); } - console.log(videoHashesList); - const whereClause: any = { UserId: locals.userId }; @@ -93,7 +103,8 @@ export async function GET({ locals, url }) { const history = await getSequelize().UserHistoryTable.findAll({ where: whereClause, limit, - offset: page > 0 ? limit * page : undefined + offset: page > 0 ? limit * page : undefined, + order: [['watched', 'DESC']] }); return json(history); From 09f1cd1099833c225afdcb215e33c398e60fece2 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 13:17:10 +1300 Subject: [PATCH 19/29] Added history page --- materialious/src/lib/api/backend/history.ts | 20 +++++++++--- .../thumbnail/VideoThumbnail.svelte | 22 +++++++++---- .../src/routes/(app)/history/+page.svelte | 32 +++++++++++++++++++ .../routes/(app)/watch/[slug]/+page.svelte | 2 +- .../src/routes/api/user/history/+server.ts | 2 +- .../api/user/history/[videoHash]/+server.ts | 2 +- .../routes/api/user/keyValue/[key]/+server.ts | 4 +-- 7 files changed, 68 insertions(+), 16 deletions(-) diff --git a/materialious/src/lib/api/backend/history.ts b/materialious/src/lib/api/backend/history.ts index fafb6220..90e4eca7 100644 --- a/materialious/src/lib/api/backend/history.ts +++ b/materialious/src/lib/api/backend/history.ts @@ -73,7 +73,7 @@ export async function getVideoWatchHistory( } export async function getWatchHistory( - options: { page?: number; videoIds?: string[] } = { page: 0, videoIds: undefined } + options: { page?: number; videoIds?: string[] } = { page: undefined, videoIds: undefined } ): Promise { await sodium.ready; const rawKey = await getRawKey(); @@ -86,9 +86,17 @@ export async function getWatchHistory( } } - const resp = await fetch( - `/api/user/history?page=${options.page}${videoHashes.length > 0 ? '&videoHashes=' + videoHashes.join(',') : ''}` - ); + const params = new URLSearchParams(); + + if (options.page) { + params.set('page', options.page.toString()); + } + + if (options.videoIds && options.videoIds.length > 0) { + params.set('videoHashes', videoHashes.join(',')); + } + + const resp = await fetch(`/api/user/history?${params.toString()}`); if (!resp.ok) return []; const rawHistory = await resp.json(); @@ -102,6 +110,10 @@ export async function getWatchHistory( return history; } +export async function deleteWatchHistory() { + await fetch('/api/user/history', { method: 'DELETE' }); +} + export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { await sodium.ready; const rawKey = await getRawKey(); diff --git a/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte b/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte index 37d83d25..cbd4ed6f 100644 --- a/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte +++ b/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte @@ -41,6 +41,8 @@ let watchUrl = createVideoUrl(video.videoId, playlistId); + let beenWatched: boolean = $state(false); + syncPartyPeerStore.subscribe((peer) => { if (peer) { watchUrl.searchParams.set('sync', peer.id); @@ -69,6 +71,10 @@ } else sideways = true; } + function checkIfWatched() { + beenWatched = !!(progress && !page.url.pathname.endsWith('/history')); + } + onMount(async () => { calcThumbnailPlaceholderHeight(); @@ -80,11 +86,15 @@ calcThumbnailPlaceholderHeight(); }); - queueGetWatchHistory(video.videoId).then((watchHistory) => { - if (watchHistory) { - progress = watchHistory.progress.toString(); - } - }); + checkIfWatched(); + + if (!page.url.pathname.endsWith('/history')) + queueGetWatchHistory(video.videoId).then((watchHistory) => { + if (watchHistory) { + progress = watchHistory.progress.toString(); + checkIfWatched(); + } + }); if (get(interfaceLowBandwidthMode)) return; @@ -176,8 +186,6 @@ onclick={onVideoSelected} > {#if !$interfaceLowBandwidthMode} - {@const beenWatched = progress && !page.url.pathname.endsWith('/history')} -
{#if !thumbnail}
+ import { deleteWatchHistory, getWatchHistory } from '$lib/api/backend/history'; import ItemsList from '$lib/components/layout/ItemsList.svelte'; + import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading'; + import { _ } from '$lib/i18n'; let { data } = $props(); + + let currentPage = 1; + async function loadMore(event: InfiniteEvent) { + currentPage++; + + const history = await getWatchHistory({ page: currentPage }); + if (history.length === 0) { + event.detail.complete(); + } else { + data.videos = [...data.videos, ...history]; + event.detail.loaded(); + } + } + + async function deleteHistory() { + await deleteWatchHistory(); + data.videos = []; + } +{#if data.videos.length > 0} + +
+{/if} + + diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 327b296d..325349b8 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -41,7 +41,7 @@ import { page } from '$app/state'; import Share from '$lib/components/Share.svelte'; import Playlist from '$lib/components/watch/Playlist.svelte'; - import type { PlayerEvents } from '$lib/player/index.js'; + import type { PlayerEvents } from '$lib/player/index'; let { data = $bindable() } = $props(); diff --git a/materialious/src/routes/api/user/history/+server.ts b/materialious/src/routes/api/user/history/+server.ts index 630ee601..1073b6af 100644 --- a/materialious/src/routes/api/user/history/+server.ts +++ b/materialious/src/routes/api/user/history/+server.ts @@ -1,4 +1,4 @@ -import { getSequelize } from '$lib/server/database.js'; +import { getSequelize } from '$lib/server/database'; import { error, json } from '@sveltejs/kit'; import z from 'zod'; import { Op } from 'sequelize'; diff --git a/materialious/src/routes/api/user/history/[videoHash]/+server.ts b/materialious/src/routes/api/user/history/[videoHash]/+server.ts index 2d827dd3..e84f9516 100644 --- a/materialious/src/routes/api/user/history/[videoHash]/+server.ts +++ b/materialious/src/routes/api/user/history/[videoHash]/+server.ts @@ -1,4 +1,4 @@ -import { getSequelize } from '$lib/server/database.js'; +import { getSequelize } from '$lib/server/database'; import { error, json } from '@sveltejs/kit'; import z from 'zod'; diff --git a/materialious/src/routes/api/user/keyValue/[key]/+server.ts b/materialious/src/routes/api/user/keyValue/[key]/+server.ts index 26bdd8f3..3208b5fd 100644 --- a/materialious/src/routes/api/user/keyValue/[key]/+server.ts +++ b/materialious/src/routes/api/user/keyValue/[key]/+server.ts @@ -1,5 +1,5 @@ -import { persistedStoreKeys } from '$lib/externalSettings/settings.js'; -import { getUser } from '$lib/server/user.js'; +import { persistedStoreKeys } from '$lib/externalSettings/settings'; +import { getUser } from '$lib/server/user'; import { error, json } from '@sveltejs/kit'; import z from 'zod'; From 980d73355a3a1ba226bfcea2fac337055d5e0954 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 13:28:02 +1300 Subject: [PATCH 20/29] Added option to save watch history --- materialious/src/lib/api/backend/history.ts | 4 ++++ .../settings/InternalAccount.svelte | 22 ++++++++++++++++++- .../src/lib/externalSettings/settings.ts | 8 ++++++- materialious/src/lib/i18n/locales/en.json | 1 + materialious/src/lib/store.ts | 6 +++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/materialious/src/lib/api/backend/history.ts b/materialious/src/lib/api/backend/history.ts index 90e4eca7..f2d5f680 100644 --- a/materialious/src/lib/api/backend/history.ts +++ b/materialious/src/lib/api/backend/history.ts @@ -2,6 +2,8 @@ import sodium from 'libsodium-wrappers-sumo'; import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption'; import type { VideoPlay, VideoWatchHistory } from '../model'; import { getBestThumbnail } from '$lib/images'; +import { get } from 'svelte/store'; +import { watchHistoryEnabledStore } from '$lib/store'; export async function updateWatchHistory(videoId: string, progress: number) { await sodium.ready; @@ -115,6 +117,8 @@ export async function deleteWatchHistory() { } export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { + if (!get(watchHistoryEnabledStore)) return; + await sodium.ready; const rawKey = await getRawKey(); if (!rawKey) return; diff --git a/materialious/src/lib/components/settings/InternalAccount.svelte b/materialious/src/lib/components/settings/InternalAccount.svelte index d9e28c80..d6180e78 100644 --- a/materialious/src/lib/components/settings/InternalAccount.svelte +++ b/materialious/src/lib/components/settings/InternalAccount.svelte @@ -1,6 +1,7 @@ -
Delete account
+
+ +
+
+
+
+ @@ -61,7 +61,7 @@ {/each}
- diff --git a/materialious/src/routes/api/user/history/+server.ts b/materialious/src/routes/api/user/history/+server.ts index 17d8d558..778339db 100644 --- a/materialious/src/routes/api/user/history/+server.ts +++ b/materialious/src/routes/api/user/history/+server.ts @@ -41,7 +41,6 @@ export async function POST({ locals, request }) { }); const toStore = { - progress: data.data.progress, watched: data.data.watched, lengthSeconds: data.data.lengthSeconds, titleCipher: data.data.title.cipher, @@ -60,6 +59,7 @@ export async function POST({ locals, request }) { await getSequelize().UserHistoryTable.create({ UserId: locals.userId, id: data.data.id, + progress: data.data.progress, ...toStore }); } From ab38d8880633ba99abf81b2df25fd835e4cc1511 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 14:13:02 +1300 Subject: [PATCH 26/29] Improved player responsiveness --- materialious/src/routes/(app)/watch/[slug]/+page.svelte | 6 +++--- materialious/src/routes/api/proxy/[urlToProxy]/+server.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 325349b8..b77f2695 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -397,10 +397,10 @@
{letterCase(data.video.title)}
-
+
-
+
@@ -695,7 +695,7 @@ height: 50vh; } - @media screen and (max-width: 1000px) { + @media screen and (max-width: 1200px) { .video-actions { align-items: flex-start; flex-direction: column; diff --git a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts index 73946220..f4b81f8a 100644 --- a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts +++ b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts @@ -32,7 +32,7 @@ const allowedBaseDomains: string[] = [ 'youtube.com', 'ytimg.com', 'googlevideo.com', - 'returnyoutubedislike.com', + 'returnyoutubedislikeapi.com', 'ajay.app' ]; From 929d370f1def71018c4f117a11eac721eab629b9 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 14:35:01 +1300 Subject: [PATCH 27/29] Minor style changes --- materialious/electron/package.json | 1 - .../electron/src/rt/electron-plugins.js | 2 -- materialious/src/lib/components/Author.svelte | 1 - materialious/src/lib/components/Search.svelte | 10 +++------ .../src/lib/components/SyncParty.svelte | 2 +- .../src/lib/components/player/Timeline.svelte | 9 ++++---- .../lib/components/settings/DeArrow.svelte | 4 ++-- .../lib/components/settings/Interface.svelte | 2 +- .../src/lib/components/settings/RYD.svelte | 2 +- .../components/settings/SponsorBlock.svelte | 2 +- .../lib/components/watch/Transcript.svelte | 2 +- materialious/src/lib/css/global.css | 21 +++++++++---------- .../src/routes/(app)/playlists/+page.svelte | 2 +- .../(app)/subscriptions/manage/+page.svelte | 3 ++- 14 files changed, 28 insertions(+), 35 deletions(-) diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 707983ab..40ae1854 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -24,7 +24,6 @@ "dependencies": { "@capacitor-community/electron": "^5.0.0", "bgutils-js": "^3.2.0", - "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", "chokidar": "~5.0.0", "electron-is-dev": "~2.0.0", "electron-serve": "~3.0.0", diff --git a/materialious/electron/src/rt/electron-plugins.js b/materialious/electron/src/rt/electron-plugins.js index 5f312366..b33b2826 100644 --- a/materialious/electron/src/rt/electron-plugins.js +++ b/materialious/electron/src/rt/electron-plugins.js @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const CapacitorNodejs = require('../../../node_modules/capacitor-nodejs/electron/dist/plugin.js'); module.exports = { - CapacitorNodejs, } \ No newline at end of file diff --git a/materialious/src/lib/components/Author.svelte b/materialious/src/lib/components/Author.svelte index bc7f8407..b64cdc9e 100644 --- a/materialious/src/lib/components/Author.svelte +++ b/materialious/src/lib/components/Author.svelte @@ -89,7 +89,6 @@ class:primary={!favoritedChannel} class:surface-container-highest={favoritedChannel} onclick={toggleFavourited} - class="square" > star
diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index fba8c216..7c5569fc 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -123,12 +123,12 @@ }} role="presentation" > - - diff --git a/materialious/src/lib/components/settings/SponsorBlock.svelte b/materialious/src/lib/components/settings/SponsorBlock.svelte index da4bbe76..b11f3cf3 100644 --- a/materialious/src/lib/components/settings/SponsorBlock.svelte +++ b/materialious/src/lib/components/settings/SponsorBlock.svelte @@ -58,7 +58,7 @@ />
- diff --git a/materialious/src/lib/components/watch/Transcript.svelte b/materialious/src/lib/components/watch/Transcript.svelte index 4bee98f0..d1bf7582 100644 --- a/materialious/src/lib/components/watch/Transcript.svelte +++ b/materialious/src/lib/components/watch/Transcript.svelte @@ -96,7 +96,7 @@
{#if transcriptCues.length > 0}
-
+
search diff --git a/materialious/src/routes/(app)/subscriptions/manage/+page.svelte b/materialious/src/routes/(app)/subscriptions/manage/+page.svelte index 26741f3d..a7a56023 100644 --- a/materialious/src/routes/(app)/subscriptions/manage/+page.svelte +++ b/materialious/src/routes/(app)/subscriptions/manage/+page.svelte @@ -55,7 +55,8 @@
- unsubscribe(sub.authorId)} class="surface-container-highest" + >Unsubscribe
From 40540f6fed0878a87eea2075ddc31f29dafd840a Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 14:43:54 +1300 Subject: [PATCH 28/29] Minor style changes --- .../src/lib/components/player/Timeline.svelte | 8 ++++---- materialious/src/lib/css/global.css | 13 ++++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/materialious/src/lib/components/player/Timeline.svelte b/materialious/src/lib/components/player/Timeline.svelte index 78db1d2d..5ebbcfa4 100644 --- a/materialious/src/lib/components/player/Timeline.svelte +++ b/materialious/src/lib/components/player/Timeline.svelte @@ -332,7 +332,7 @@ will-change: transform; padding: 0; display: block; - border-radius: 1rem; + border-radius: var(--border-radius); } .tooltip.thumb { @@ -350,8 +350,8 @@ transform: translateY(-50%); z-index: 1; pointer-events: none; - border-top-right-radius: 2.5rem; - border-bottom-right-radius: 2.5rem; + border-top-right-radius: var(--player-border-radius); + border-bottom-right-radius: var(--player-border-radius); opacity: 0.5; } @@ -362,7 +362,7 @@ left: 0; height: var(--player-timeline-height); background-color: var(--secondary); - border-radius: 2.5rem; + border-radius: var(--player-border-radius); z-index: 2; pointer-events: none; opacity: 0.5; diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index da90ad09..8f23c694 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -5,6 +5,8 @@ :root { --video-player-height: 0px; --safe-area-inset-top: 0px; + --border-radius: 0.5rem; + --player-border-radius: 0.25rem; } @font-face { @@ -124,8 +126,9 @@ menu { button, .button, -.round { - border-radius: 0.5rem !important; +.round, +menu { + border-radius: var(--border-radius) !important; } .altcha { @@ -141,7 +144,7 @@ menu.player-settings { .player-slider { height: var(--player-timeline-height); margin: 0 auto; - border-radius: 2.5rem; + border-radius: var(--border-radius); } .player-slider.full-width { @@ -156,7 +159,7 @@ menu.player-settings { background: var(--secondary-container); height: 100%; position: relative; - border-radius: 2.5rem; + border-radius: var(--player-border-radius); } .player-slider .range { @@ -164,7 +167,7 @@ menu.player-settings { background: var(--inverse-primary); inset: 0; right: var(--percentage-inv); - border-radius: 2.5rem; + border-radius: var(--player-border-radius); opacity: 0.5; } From 77dcbcf15a7a87bd748c67047c10675e86beeedf Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 24 Feb 2026 14:51:10 +1300 Subject: [PATCH 29/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8757b142..e9785795 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ # Features - Internal account system (Optional). * Subscriptions are end-to-end encrypted. + * History & watch progress syncing. * [Proof-of-work Captcha](https://github.com/altcha-org/altcha). - Subscription importing/exporting * Automatically import/export Invidious/Materialious. @@ -25,7 +26,6 @@ * Enjoy Materialious without a Invidious instance via our YouTube backend. * Local video fallback if Invidious fails loading videos. - Invidious companion support. -- [Invidious API extended integration.](https://github.com/Materialious/api-extended) - Android TV support. - Support for disabling certificate validation for homelab users. - Sync your watch progress between Invidious sessions.