Initial commit

This commit is contained in:
twotalesanimation
2026-06-11 10:46:09 +02:00
commit 81ad7e4ea9
223 changed files with 39530 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function validateDatabaseIntegrity() {
console.log('🔍 Running comprehensive database integrity check...');
try {
// 1. Check Video table integrity
console.log('\n📹 Checking Video table...');
const videos = await prisma.video.findMany({
include: {
playlist: true,
uploader: true,
_count: {
select: {
comments: true,
likes: true,
progress: true,
unlocks: true,
watchSegments: true,
videoCourses: true
}
}
}
});
console.log(`✅ Found ${videos.length} videos`);
// 2. Check for videos with missing playlists
const videosWithMissingPlaylists = videos.filter(v => !v.playlist);
if (videosWithMissingPlaylists.length > 0) {
console.log(`⚠️ Found ${videosWithMissingPlaylists.length} videos with missing playlists:`);
videosWithMissingPlaylists.forEach(v => {
console.log(` - Video ID: ${v.id}, Title: "${v.title}", Playlist ID: ${v.playlistId}`);
});
}
// 3. Check for playlists with no videos
console.log('\n📋 Checking Playlist table...');
const playlists = await prisma.playlist.findMany({
include: {
videos: true,
course: true,
_count: {
select: {
videos: true
}
}
}
});
console.log(`✅ Found ${playlists.length} playlists`);
const emptyPlaylists = playlists.filter(p => p.videos.length === 0);
if (emptyPlaylists.length > 0) {
console.log(`⚠️ Found ${emptyPlaylists.length} empty playlists:`);
emptyPlaylists.forEach(p => {
console.log(` - Playlist ID: ${p.id}, Title: "${p.title}", Course: ${p.course?.title || 'Unknown'}`);
});
}
// 4. Check VideoProgress integrity
console.log('\n📊 Checking VideoProgress table...');
const progressCount = await prisma.videoProgress.count();
console.log(`✅ Found ${progressCount} progress records`);
// Check for progress records with invalid video references
const invalidProgress = await prisma.$queryRaw`
SELECT vp.id, vp."videoId", vp."userId"
FROM "VideoProgress" vp
LEFT JOIN "Video" v ON vp."videoId" = v.id
WHERE v.id IS NULL
` as Array<{id: string, videoId: string, userId: string}>;
if (invalidProgress.length > 0) {
console.log(`❌ Found ${invalidProgress.length} progress records with invalid video references:`);
invalidProgress.forEach(p => {
console.log(` - Progress ID: ${p.id}, Video ID: ${p.videoId}, User ID: ${p.userId}`);
});
}
// 5. Check VideoWatchSegment integrity
console.log('\n⏱️ Checking VideoWatchSegment table...');
const segmentsCount = await prisma.videoWatchSegment.count();
console.log(`✅ Found ${segmentsCount} watch segments`);
// Check for segments with invalid video references
const invalidSegments = await prisma.$queryRaw`
SELECT vws.id, vws."videoId", vws."userId"
FROM "VideoWatchSegment" vws
LEFT JOIN "Video" v ON vws."videoId" = v.id
WHERE v.id IS NULL
` as Array<{id: string, videoId: string, userId: string}>;
if (invalidSegments.length > 0) {
console.log(`❌ Found ${invalidSegments.length} watch segments with invalid video references:`);
invalidSegments.forEach(s => {
console.log(` - Segment ID: ${s.id}, Video ID: ${s.videoId}, User ID: ${s.userId}`);
});
}
// 6. Check for duplicate video indexes within playlists
console.log('\n🔄 Checking for duplicate video indexes...');
for (const playlist of playlists) {
if (playlist.videos.length > 0) {
const indexes = playlist.videos.map(v => v.index);
const uniqueIndexes = [...new Set(indexes)];
if (indexes.length !== uniqueIndexes.length) {
console.log(`⚠️ Playlist "${playlist.title}" has duplicate video indexes:`);
const duplicates = indexes.filter((item, index) => indexes.indexOf(item) !== index);
console.log(` - Duplicate indexes: ${duplicates.join(', ')}`);
}
}
}
// 7. Check Enrollment integrity
console.log('\n📚 Checking Enrollment table...');
const enrollments = await prisma.enrollment.findMany({
include: {
user: true,
course: true
}
});
console.log(`✅ Found ${enrollments.length} enrollments`);
const enrollmentsWithMissingUser = enrollments.filter(e => !e.user);
const enrollmentsWithMissingCourse = enrollments.filter(e => !e.course);
if (enrollmentsWithMissingUser.length > 0) {
console.log(`❌ Found ${enrollmentsWithMissingUser.length} enrollments with missing users`);
}
if (enrollmentsWithMissingCourse.length > 0) {
console.log(`❌ Found ${enrollmentsWithMissingCourse.length} enrollments with missing courses`);
}
// 8. Summary
console.log('\n📋 Database Integrity Summary:');
console.log(` Videos: ${videos.length}`);
console.log(` Playlists: ${playlists.length} (${emptyPlaylists.length} empty)`);
console.log(` Progress Records: ${progressCount}`);
console.log(` Watch Segments: ${segmentsCount}`);
console.log(` Enrollments: ${enrollments.length}`);
const hasIssues = videosWithMissingPlaylists.length > 0 ||
invalidProgress.length > 0 ||
invalidSegments.length > 0 ||
enrollmentsWithMissingUser.length > 0 ||
enrollmentsWithMissingCourse.length > 0;
if (!hasIssues) {
console.log('✨ All integrity checks passed! Database appears healthy.');
} else {
console.log('⚠️ Some integrity issues found. See details above.');
}
} catch (error) {
console.error('❌ Error during integrity check:', error);
} finally {
await prisma.$disconnect();
}
}
// Run the integrity check
validateDatabaseIntegrity()
.then(() => {
console.log('🔧 Database integrity check completed');
process.exit(0);
})
.catch((error) => {
console.error('💥 Integrity check failed:', error);
process.exit(1);
});
+59
View File
@@ -0,0 +1,59 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function cleanupCompletedProgress() {
console.log('🧹 Cleaning up progress data for completed videos...');
try {
// Find all completed video progress records
const completedProgress = await prisma.videoProgress.findMany({
where: { completed: true },
select: { id: true, userId: true, videoId: true },
});
console.log(`📊 Found ${completedProgress.length} completed videos`);
if (completedProgress.length === 0) {
console.log('✅ No completed videos to clean up');
return;
}
let totalSegmentsDeleted = 0;
// For each completed video, delete its watch segments
// (we keep the VideoProgress record for history, but clean up the granular segment data)
for (const progress of completedProgress) {
const deletedSegments = await prisma.videoWatchSegment.deleteMany({
where: {
userId: progress.userId,
videoId: progress.videoId,
},
});
totalSegmentsDeleted += deletedSegments.count;
if (deletedSegments.count > 0) {
console.log(
` ✓ Deleted ${deletedSegments.count} segments for video ${progress.videoId}`
);
}
}
console.log(`\n📈 Cleanup Summary:`);
console.log(` - Completed videos processed: ${completedProgress.length}`);
console.log(` - Watch segments deleted: ${totalSegmentsDeleted}`);
console.log(`✅ Cleanup complete!`);
} catch (err: any) {
console.error('❌ Error during cleanup:', err.message);
throw err;
} finally {
await prisma.$disconnect();
}
}
cleanupCompletedProgress().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
+143
View File
@@ -0,0 +1,143 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function cleanupOrphanedData() {
console.log('🔍 Checking for orphaned video references...');
try {
// Get all existing video IDs
const existingVideos = await prisma.video.findMany({
select: { id: true }
});
const existingVideoIds = new Set(existingVideos.map(v => v.id));
console.log(`📹 Found ${existingVideoIds.size} existing videos`);
let totalCleaned = 0;
// 1. Clean orphaned VideoProgress records
const allProgress = await prisma.videoProgress.findMany({
select: { id: true, videoId: true, userId: true }
});
const orphanedProgress = allProgress.filter(p => !existingVideoIds.has(p.videoId));
if (orphanedProgress.length > 0) {
console.log(`🧹 Found ${orphanedProgress.length} orphaned VideoProgress records`);
await prisma.videoProgress.deleteMany({
where: {
id: { in: orphanedProgress.map(p => p.id) }
}
});
console.log(`✅ Deleted ${orphanedProgress.length} orphaned VideoProgress records`);
totalCleaned += orphanedProgress.length;
}
// 2. Clean orphaned VideoWatchSegment records
const allSegments = await prisma.videoWatchSegment.findMany({
select: { id: true, videoId: true, userId: true }
});
const orphanedSegments = allSegments.filter(s => !existingVideoIds.has(s.videoId));
if (orphanedSegments.length > 0) {
console.log(`🧹 Found ${orphanedSegments.length} orphaned VideoWatchSegment records`);
await prisma.videoWatchSegment.deleteMany({
where: {
id: { in: orphanedSegments.map(s => s.id) }
}
});
console.log(`✅ Deleted ${orphanedSegments.length} orphaned VideoWatchSegment records`);
totalCleaned += orphanedSegments.length;
}
// 3. Clean orphaned VideoUnlock records
const allUnlocks = await prisma.videoUnlock.findMany({
select: { id: true, videoId: true, userId: true }
});
const orphanedUnlocks = allUnlocks.filter(u => !existingVideoIds.has(u.videoId));
if (orphanedUnlocks.length > 0) {
console.log(`🧹 Found ${orphanedUnlocks.length} orphaned VideoUnlock records`);
await prisma.videoUnlock.deleteMany({
where: {
id: { in: orphanedUnlocks.map(u => u.id) }
}
});
console.log(`✅ Deleted ${orphanedUnlocks.length} orphaned VideoUnlock records`);
totalCleaned += orphanedUnlocks.length;
}
// 4. Clean orphaned VideoLike records
const allLikes = await prisma.videoLike.findMany({
select: { id: true, videoId: true, userId: true }
});
const orphanedLikes = allLikes.filter(l => !existingVideoIds.has(l.videoId));
if (orphanedLikes.length > 0) {
console.log(`🧹 Found ${orphanedLikes.length} orphaned VideoLike records`);
await prisma.videoLike.deleteMany({
where: {
id: { in: orphanedLikes.map(l => l.id) }
}
});
console.log(`✅ Deleted ${orphanedLikes.length} orphaned VideoLike records`);
totalCleaned += orphanedLikes.length;
}
// 5. Clean orphaned Comment records
const allComments = await prisma.comment.findMany({
select: { id: true, videoId: true, userId: true }
});
const orphanedComments = allComments.filter(c => !existingVideoIds.has(c.videoId));
if (orphanedComments.length > 0) {
console.log(`🧹 Found ${orphanedComments.length} orphaned Comment records`);
await prisma.comment.deleteMany({
where: {
id: { in: orphanedComments.map(c => c.id) }
}
});
console.log(`✅ Deleted ${orphanedComments.length} orphaned Comment records`);
totalCleaned += orphanedComments.length;
}
// 6. Clean orphaned VideoCourse records
const allVideoCourses = await prisma.videoCourse.findMany({
select: { id: true, videoId: true, courseId: true }
});
const orphanedVideoCourses = allVideoCourses.filter(vc => !existingVideoIds.has(vc.videoId));
if (orphanedVideoCourses.length > 0) {
console.log(`🧹 Found ${orphanedVideoCourses.length} orphaned VideoCourse records`);
await prisma.videoCourse.deleteMany({
where: {
id: { in: orphanedVideoCourses.map(vc => vc.id) }
}
});
console.log(`✅ Deleted ${orphanedVideoCourses.length} orphaned VideoCourse records`);
totalCleaned += orphanedVideoCourses.length;
}
// 7. Report summary
if (totalCleaned === 0) {
console.log('✨ No orphaned video references found! Database is clean.');
} else {
console.log(`🎉 Cleanup complete! Removed ${totalCleaned} total orphaned records.`);
}
} catch (error) {
console.error('❌ Error during cleanup:', error);
} finally {
await prisma.$disconnect();
}
}
// Run the cleanup
cleanupOrphanedData()
.then(() => {
console.log('🔧 Cleanup script completed');
process.exit(0);
})
.catch((error) => {
console.error('💥 Script failed:', error);
process.exit(1);
});
+26
View File
@@ -0,0 +1,26 @@
// scripts/update-thumbnail-urls.ts
// Update all existing thumbnail URLs from /uploads/thumbnails/ to /api/thumbnails/
import { prisma } from "@/lib/prisma";
async function main() {
try {
console.log("Updating thumbnail URLs...");
const result = await prisma.$executeRawUnsafe(
`UPDATE "Video"
SET thumbnail = REPLACE(thumbnail, '/uploads/thumbnails/', '/api/thumbnails/')
WHERE thumbnail IS NOT NULL
AND thumbnail LIKE '/uploads/thumbnails/%'`
);
console.log(`✓ Updated ${result} video records`);
} catch (error) {
console.error("Error updating thumbnail URLs:", error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();