Started Materialious watch history
This commit is contained in:
@@ -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<string> {
|
||||
async function getSecureHash(toHash: string, rawKey: Uint8Array): Promise<string> {
|
||||
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<boolean> {
|
||||
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<KeyValue | null> {
|
||||
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> {
|
||||
|
||||
@@ -5,6 +5,7 @@ let UserTable: ModelCtor<Model<any, any>>;
|
||||
let ChannelSubscriptionTable: ModelCtor<Model<any, any>>;
|
||||
let CaptchaTable: ModelCtor<Model<any, any>>;
|
||||
let UserKeyValueTable: ModelCtor<Model<any, any>>;
|
||||
let UserHistoryTable: ModelCtor<Model<any, any>>;
|
||||
|
||||
let sequelizeInstance: Sequelize | null = null;
|
||||
|
||||
@@ -15,6 +16,7 @@ export function getSequelize(): {
|
||||
ChannelSubscriptionTable: ModelCtor<Model<any, any>>;
|
||||
CaptchaTable: ModelCtor<Model<any, any>>;
|
||||
UserKeyValueTable: ModelCtor<Model<any, any>>;
|
||||
UserHistoryTable: ModelCtor<Model<any, any>>;
|
||||
} {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user