diff --git a/.replit b/.replit index adcfadc..43f049b 100644 --- a/.replit +++ b/.replit @@ -37,3 +37,6 @@ author = "agent" task = "shell.exec" args = "npm run dev" waitForPort = 5000 + +[agent] +integrations = ["javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"] diff --git a/package-lock.json b/package-lock.json index 90d45c6..a193618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", + "@types/memoizee": "^0.4.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -54,8 +55,10 @@ "hls.js": "^1.6.7", "input-otp": "^1.4.2", "lucide-react": "^0.453.0", + "memoizee": "^0.4.17", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "react": "^18.3.1", @@ -3465,6 +3468,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/memoizee": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz", + "integrity": "sha512-EdtpwNYNhe3kZ+4TlXj/++pvBoU0KdrAICMzgI7vjWgu9sIvvUhu9XR8Ks4L6Wh3sxpZ22wkZR7yCLAqUjnZuQ==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4112,6 +4121,19 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -5010,6 +5032,58 @@ "node": ">= 0.4" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -5080,6 +5154,21 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -5089,6 +5178,16 @@ "node": ">= 0.6" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -5205,6 +5304,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fast-equals": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", @@ -5684,6 +5792,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5714,6 +5828,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6040,6 +6163,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/lucide-react": { "version": "0.453.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz", @@ -6066,6 +6198,25 @@ "node": ">= 0.6" } }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/memorystore": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz", @@ -6266,6 +6417,12 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-gyp-build": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", @@ -6304,6 +6461,15 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.1.tgz", + "integrity": "sha512-b39+drVyA4aNUptFOhkkmGWnG/BE7dT29SW/8PVYElqp7j/DBqzm5SS1G+MUD07XlTcBOAG+6Cb/35Cx2kHIuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6361,6 +6527,19 @@ "node": ">= 0.8" } }, + "node_modules/openid-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.6.2.tgz", + "integrity": "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==", + "license": "MIT", + "dependencies": { + "jose": "^6.0.11", + "oauth4webapi": "^3.5.4" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -7731,6 +7910,19 @@ "node": ">=0.8" } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -8246,6 +8438,12 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index e83a040..bad8db2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", + "@types/memoizee": "^0.4.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -56,8 +57,10 @@ "hls.js": "^1.6.7", "input-otp": "^1.4.2", "lucide-react": "^0.453.0", + "memoizee": "^0.4.17", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "react": "^18.3.1", diff --git a/server/db.ts b/server/db.ts new file mode 100644 index 0000000..66779a9 --- /dev/null +++ b/server/db.ts @@ -0,0 +1,15 @@ +import { Pool, neonConfig } from '@neondatabase/serverless'; +import { drizzle } from 'drizzle-orm/neon-serverless'; +import ws from "ws"; +import * as schema from "@shared/schema"; + +neonConfig.webSocketConstructor = ws; + +if (!process.env.DATABASE_URL) { + throw new Error( + "DATABASE_URL must be set. Did you forget to provision a database?", + ); +} + +export const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +export const db = drizzle({ client: pool, schema }); diff --git a/shared/schema.ts b/shared/schema.ts index f0bdf13..c398683 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -1,4 +1,4 @@ -import { sql } from "drizzle-orm"; +import { sql, relations } from "drizzle-orm"; import { pgTable, text, varchar, integer, timestamp } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; @@ -17,10 +17,88 @@ export const videos = pgTable("videos", { createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), }); +// User playlists table +export const playlists = pgTable("playlists", { + id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), + name: text("name").notNull(), + description: text("description"), + isPublic: integer("is_public").notNull().default(0), // 0 = private, 1 = public + userId: varchar("user_id"), // For future user authentication + createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), + updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`), +}); + +// Junction table for playlist-video relationships +export const playlistVideos = pgTable("playlist_videos", { + id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), + playlistId: varchar("playlist_id").notNull(), + videoId: varchar("video_id").notNull(), + position: integer("position").notNull().default(0), // Order within playlist + addedAt: timestamp("added_at").notNull().default(sql`CURRENT_TIMESTAMP`), +}); + +// Favorites table (simplified playlist for favorites) +export const favorites = pgTable("favorites", { + id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), + videoId: varchar("video_id").notNull(), + userId: varchar("user_id"), // For future user authentication + addedAt: timestamp("added_at").notNull().default(sql`CURRENT_TIMESTAMP`), +}); + export const insertVideoSchema = createInsertSchema(videos).omit({ id: true, createdAt: true, }); +export const insertPlaylistSchema = createInsertSchema(playlists).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + +export const insertPlaylistVideoSchema = createInsertSchema(playlistVideos).omit({ + id: true, + addedAt: true, +}); + +export const insertFavoriteSchema = createInsertSchema(favorites).omit({ + id: true, + addedAt: true, +}); + +// Relations +export const playlistsRelations = relations(playlists, ({ many }) => ({ + playlistVideos: many(playlistVideos), +})); + +export const playlistVideosRelations = relations(playlistVideos, ({ one }) => ({ + playlist: one(playlists, { + fields: [playlistVideos.playlistId], + references: [playlists.id], + }), + video: one(videos, { + fields: [playlistVideos.videoId], + references: [videos.id], + }), +})); + +export const videosRelations = relations(videos, ({ many }) => ({ + playlistVideos: many(playlistVideos), + favorites: many(favorites), +})); + +export const favoritesRelations = relations(favorites, ({ one }) => ({ + video: one(videos, { + fields: [favorites.videoId], + references: [videos.id], + }), +})); + export type InsertVideo = z.infer; export type Video = typeof videos.$inferSelect; +export type Playlist = typeof playlists.$inferSelect; +export type InsertPlaylist = z.infer; +export type PlaylistVideo = typeof playlistVideos.$inferSelect; +export type InsertPlaylistVideo = z.infer; +export type Favorite = typeof favorites.$inferSelect; +export type InsertFavorite = z.infer;