diff --git a/package.json b/package.json index 3d69452..afde484 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "pnpm schema:migrate && next dev --turbopack", "dev:email": "email dev --port 4000", - "build": "next build --turbopack && pnpm schema:migrate", + "build": "next build --turbopack && pnpm schema:migrate && node --import=tsx ./src/server/scripts/addIdsToFeedItems.ts", "build:atomic": "next build --turbopack", "schema:generate": "drizzle-kit generate", "schema:migrate": "node --experimental-specifier-resolution=node --loader ts-node/esm src/server/db/migrate", @@ -26,6 +26,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.5.0", + "@paralleldrive/cuid2": "^2.2.2", "@planetscale/database": "^1.19.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", @@ -83,12 +84,16 @@ "react-resizable-panels": "^3.0.5", "react-youtube": "^10.1.0", "recharts": "^3.1.2", + "rehype-parse": "^9.0.1", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.1", "rss-parser": "^3.13.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "superjson": "^2.2.2", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", + "unified": "^11.0.5", "vaul": "^1.1.2", "zod": "^4.1.1", "zustand": "^5.0.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da9cab8..5fc68a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@next/mdx': specifier: 15.5.0 version: 15.5.0(@mdx-js/loader@3.1.0(acorn@8.15.0)(webpack@5.99.5(esbuild@0.25.9)))(@mdx-js/react@3.1.0(@types/react@19.1.1)(react@19.1.1)) + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 '@planetscale/database': specifier: ^1.19.0 version: 1.19.0 @@ -213,6 +216,15 @@ importers: recharts: specifier: ^3.1.2 version: 3.1.2(@types/react@19.1.1)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1) + rehype-parse: + specifier: ^9.0.1 + version: 9.0.1 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + rehype-stringify: + specifier: ^10.0.1 + version: 10.0.1 rss-parser: specifier: ^3.13.0 version: 3.13.0 @@ -231,6 +243,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.1.12) + unified: + specifier: ^11.0.5 + version: 11.0.5 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.1))(@types/react@19.1.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -1753,6 +1768,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@peculiar/asn1-android@2.4.0': resolution: {integrity: sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==} @@ -3830,6 +3848,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4323,19 +4345,40 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -5305,6 +5348,9 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -5706,9 +5752,18 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + remark-mdx@3.1.0: resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} @@ -6370,6 +6425,9 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-matter@5.0.1: resolution: {integrity: sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==} @@ -6390,6 +6448,9 @@ packages: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -8094,6 +8155,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + '@peculiar/asn1-android@2.4.0': dependencies: '@peculiar/asn1-schema': 2.4.0 @@ -10116,6 +10181,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + env-paths@3.0.0: optional: true @@ -10833,6 +10900,36 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.8 @@ -10854,6 +10951,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -10878,6 +10989,14 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -10886,6 +11005,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-void-elements@3.0.0: {} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -12029,6 +12150,10 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -12405,6 +12530,12 @@ snapshots: dependencies: jsesc: 3.0.2 + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -12413,6 +12544,17 @@ snapshots: transitivePeerDependencies: - supports-color + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + remark-mdx@3.1.0: dependencies: mdast-util-mdx: 3.0.0 @@ -13208,6 +13350,11 @@ snapshots: - '@types/react' - '@types/react-dom' + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-matter@5.0.1: dependencies: vfile: 6.0.3 @@ -13250,6 +13397,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + web-namespaces@2.0.1: {} + web-streams-polyfill@3.3.3: {} web-vitals@4.2.4: {} diff --git a/src/app/(feed)/feed/TodayItems.tsx b/src/app/(feed)/feed/TodayItems.tsx index 1ec357d..d9bcb4f 100644 --- a/src/app/(feed)/feed/TodayItems.tsx +++ b/src/app/(feed)/feed/TodayItems.tsx @@ -126,21 +126,6 @@ function TodayItemsFeedEmptyState() { ); - - return ( - - ); } function LoaderDisplay() { @@ -169,6 +154,7 @@ function LoaderDisplay() { } function ItemDisplay({ contentId }: { contentId: string }) { + const { feeds } = useFeeds(); const [item] = useFeedItemGlobalState(contentId); const { mutateAsync: setWatchedValue } = @@ -176,6 +162,10 @@ function ItemDisplay({ contentId }: { contentId: string }) { const { mutateAsync: setWatchLaterValue } = useFeedItemsSetWatchLaterValueMutation(contentId); + const feed = feeds.find((f) => f.id === item.feedId); + + const itemDestination = item.platform === "website" ? "read" : "watch"; + return (
- {item.title} + {!!item.thumbnail ? ( + {item.title} + ) : !!feed?.imageUrl ? ( +
+ {item.title} +
+ ) : ( +
+ )} +

{item.title} diff --git a/src/app/(feed)/feed/TopRightHeaderContent.tsx b/src/app/(feed)/feed/TopRightHeaderContent.tsx index 2d25f3d..a643353 100644 --- a/src/app/(feed)/feed/TopRightHeaderContent.tsx +++ b/src/app/(feed)/feed/TopRightHeaderContent.tsx @@ -16,12 +16,15 @@ import { MAX_ZOOM, MIN_ZOOM, useZoom } from "./watch/[videoID]/useZoom"; const PLATFORM_TO_FORMATTED_NAME = { youtube: "YouTube", peertube: "PeerTube", + website: "Website", } as const; function OpenInYouTubeButton() { const pathname = usePathname(); const videoId = pathname.split("/feed/watch/")[1]!; - const [feedItem] = useFeedItemGlobalState(videoId ?? ""); + const contentId = pathname.split("/feed/read/")[1]!; + + const [feedItem] = useFeedItemGlobalState(videoId || contentId || ""); // If not a Serial item, assume YouTube if (!feedItem) { @@ -60,7 +63,7 @@ export function TopRightHeaderContent() { useShortcut("=", zoomIn); useShortcut("-", zoomOut); - if (pathname.includes("/feed/watch/")) { + if (pathname.includes("/feed/watch/") || pathname.includes("/feed/read/")) { if (isMobile) { return (
diff --git a/src/app/(feed)/feed/read/[contentID]/article.module.css b/src/app/(feed)/feed/read/[contentID]/article.module.css new file mode 100644 index 0000000..0d044a4 --- /dev/null +++ b/src/app/(feed)/feed/read/[contentID]/article.module.css @@ -0,0 +1,90 @@ +.article { +} + +.article h1 { + font-size: 1.5rem; + font-weight: 900; +} + +.article h2 { + font-size: 1.2rem; + font-weight: 900; + margin-top: 2rem; + margin-bottom: 1rem; +} + +.article h3 { + font-size: 1.1rem; + font-weight: 900; + margin-top: 2rem; + margin-bottom: 1rem; +} + +.article h4 { + font-size: 1rem; + font-weight: 900; + margin-top: 2rem; + margin-bottom: 1rem; +} + +.article h5 { + font-size: 1rem; + font-weight: 800; + margin-top: 2rem; + margin-bottom: 1rem; +} + +.article h6 { + font-size: 1rem; + font-weight: 700; + color: var(--color-foreground); + margin-block: 0.5rem; +} + +.article p { + margin-block: 1rem; + line-height: 1.7rem; +} + +.article a { + background: var(--color-sidebar-accent); + color: var(--color-sidebar-accent-foreground); + text-decoration: none; +} + +.article a:hover { + background: var(--color-sidebar-primary); + color: var(--color-sidebar-primary-foreground); + text-decoration: none; +} + +.article img { + margin-block: 1.5rem; +} + +.article ul { + padding-left: 1.5rem; + list-style: disc; +} + +.article ol { + padding-left: 1.5rem; + list-style: decimal; +} + +.article li { + margin-block: 0.25rem; +} + +.article blockquote { + border-left: 2px solid var(--color-sidebar-accent); + padding-left: 1rem; +} + +.article u { + text-decoration: none; +} + +.article img { + width: 100%; +} diff --git a/src/app/(feed)/feed/read/[contentID]/page.tsx b/src/app/(feed)/feed/read/[contentID]/page.tsx new file mode 100644 index 0000000..539173c --- /dev/null +++ b/src/app/(feed)/feed/read/[contentID]/page.tsx @@ -0,0 +1,76 @@ +"use client"; + +import clsx from "clsx"; +import { use } from "react"; + +import { useFeedItemGlobalState } from "~/lib/data/atoms"; +import { useZoom } from "../../watch/[videoID]/useZoom"; + +import classes from "./article.module.css"; +import { useFeeds } from "~/lib/data/feeds"; +import { unified } from "unified"; +import rehypeParse from "rehype-parse"; +import rehypeSanitize from "rehype-sanitize"; +import rehypeStringify from "rehype-stringify"; +import { useFlagState } from "~/lib/hooks/useFlagState"; +import { ContentActions } from "../../watch/[videoID]/ContentActions"; + +const parser = unified() + .use(rehypeParse, { fragment: true }) + .use(rehypeSanitize) + .use(rehypeStringify); + +export default function WatchVideoPage(props: { + params: Promise<{ contentID: string }>; +}) { + const [articleStyle] = useFlagState("ARTICLE_STYLE"); + const params = use(props.params); + + const [feedItem] = useFeedItemGlobalState(params?.contentID ?? ""); + const { feeds } = useFeeds(); + + const feed = feeds.find((f) => f.id === feedItem.feedId); + + const { zoom } = useZoom(); + + let content = feedItem.content; + + if (articleStyle === "simplified") { + content = String(parser.processSync(feedItem.content ?? "")); + } + + return ( +
+
+ {feedItem.title} + {feed?.name} +
+
+

{feedItem.title}

+
{feedItem.author || feed?.name || ""}
+
+
+
+ +
+
+ ); +} diff --git a/src/app/(feed)/feed/watch/[videoID]/VideoActions.tsx b/src/app/(feed)/feed/watch/[videoID]/ContentActions.tsx similarity index 92% rename from src/app/(feed)/feed/watch/[videoID]/VideoActions.tsx rename to src/app/(feed)/feed/watch/[videoID]/ContentActions.tsx index ab0065d..5513abe 100644 --- a/src/app/(feed)/feed/watch/[videoID]/VideoActions.tsx +++ b/src/app/(feed)/feed/watch/[videoID]/ContentActions.tsx @@ -10,15 +10,15 @@ import { useMediaQuery } from "~/lib/hooks/use-media-query"; import { useView } from "./useView"; import { useShortcut } from "~/lib/hooks/useShortcut"; -export function VideoActions({ videoID }: { videoID: string }) { +export function ContentActions({ contentID }: { contentID: string }) { const { view } = useView(); - const [video] = useFeedItemGlobalState(videoID); + const [video] = useFeedItemGlobalState(contentID); const { mutateAsync: setWatchedValue } = - useFeedItemsSetWatchedValueMutation(videoID); + useFeedItemsSetWatchedValueMutation(contentID); const { mutateAsync: setWatchLaterValue } = - useFeedItemsSetWatchLaterValueMutation(videoID); + useFeedItemsSetWatchLaterValueMutation(contentID); const isWatched = video?.isWatched; const isWatchLater = video?.isWatchLater; diff --git a/src/app/(feed)/feed/watch/[videoID]/VideoDisplay.tsx b/src/app/(feed)/feed/watch/[videoID]/VideoDisplay.tsx index b2b6815..15cfc85 100644 --- a/src/app/(feed)/feed/watch/[videoID]/VideoDisplay.tsx +++ b/src/app/(feed)/feed/watch/[videoID]/VideoDisplay.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import ResponsiveVideo from "~/components/ResponsiveVideo"; import { useShortcut } from "~/lib/hooks/useShortcut"; import { useView } from "./useView"; -import { VideoActions } from "./VideoActions"; +import { ContentActions } from "./ContentActions"; import { useVideoNavigationShortcuts } from "./useVideoNavigationShortcuts"; export function VideoDisplay({ @@ -60,7 +60,7 @@ export function VideoDisplay({
- + ); } diff --git a/src/components/AddFeedDialog.tsx b/src/components/AddFeedDialog.tsx index 383e027..876a18f 100644 --- a/src/components/AddFeedDialog.tsx +++ b/src/components/AddFeedDialog.tsx @@ -12,13 +12,16 @@ import { useDeleteFeedMutation, useEditFeedMutation, } from "~/lib/data/feeds/mutations"; -import { validateFeedUrl } from "~/server/rss/validateFeedUrl"; +import { useShortcut } from "~/lib/hooks/useShortcut"; +import { + FEED_PLATFORM_LABEL_MAP, + getAssumedFeedPlatform, +} from "~/server/rss/validateFeedUrl"; import { ViewCategoriesInput } from "./AddViewDialog"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogHeader } from "./ui/dialog"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; -import { useShortcut } from "~/lib/hooks/useShortcut"; export function AddFeedDialog() { const [feedUrl, setFeedUrl] = useState(""); @@ -46,6 +49,9 @@ export function AddFeedDialog() { } }; + const feedPlatform = getAssumedFeedPlatform(feedUrl); + const TEMPORARY_isDisabled = feedPlatform === "website"; + return ( @@ -70,7 +76,7 @@ export function AddFeedDialog() { setSelectedCategories={setSelectedCategories} />

diff --git a/src/components/color-theme/ColorThemePopoverButton.tsx b/src/components/color-theme/ColorThemePopoverButton.tsx index 2430b07..aedb471 100644 --- a/src/components/color-theme/ColorThemePopoverButton.tsx +++ b/src/components/color-theme/ColorThemePopoverButton.tsx @@ -14,6 +14,7 @@ import { Slider } from "../ui/slider"; import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; import { EnableCustomVideoPlayerToggle } from "./EnableCustomVideoPlayerToggle"; import { ShowShortcutsToggle } from "./ShowShortcutsToggle"; +import { ShowArticleStyleToggle } from "./ShowArticleStyleToggle"; function getCssVariable(name: string) { const value = window @@ -208,6 +209,8 @@ export function ColorThemePopoverButton({
+
+ )} @@ -229,6 +232,8 @@ export function ColorThemeDropdownSidebar({
+
+ ); } diff --git a/src/components/color-theme/ShowArticleStyleToggle.tsx b/src/components/color-theme/ShowArticleStyleToggle.tsx new file mode 100644 index 0000000..3175cd5 --- /dev/null +++ b/src/components/color-theme/ShowArticleStyleToggle.tsx @@ -0,0 +1,32 @@ +import { Label } from "../ui/label"; +import { useFlagState } from "~/lib/hooks/useFlagState"; +import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; + +export const ShowArticleStyleToggle = () => { + const [articleStyle, setArticleStyle] = useFlagState("ARTICLE_STYLE"); + + return ( +
+ + + + Simplified + + + Full + + +
+ ); +}; diff --git a/src/lib/hooks/useFlagState.ts b/src/lib/hooks/useFlagState.ts index bfce0ac..e02c5ec 100644 --- a/src/lib/hooks/useFlagState.ts +++ b/src/lib/hooks/useFlagState.ts @@ -14,6 +14,10 @@ const LOCAL_STORAGE_FLAGS = { key: "serial-flag-display-inline-shortcuts", schema: z.enum(["show-shortcuts", "hide-shortcuts"]), }, + ARTICLE_STYLE: { + key: "serial-flag-article-style", + schema: z.enum(["simplified", "full"]), + }, } as const; type FlagName = keyof typeof LOCAL_STORAGE_FLAGS; type FlagSchema = (typeof LOCAL_STORAGE_FLAGS)[T]["schema"]; @@ -43,6 +47,7 @@ const flagsAtom = atom({ parseFlagLocalStorageValue("CUSTOM_VIDEO_PLAYER") ?? "serial", INLINE_SHORTCUTS: parseFlagLocalStorageValue("INLINE_SHORTCUTS") ?? "show-shortcuts", + ARTICLE_STYLE: parseFlagLocalStorageValue("ARTICLE_STYLE") ?? "simplified", } as FlagsState); export function useFlagState(key: TKey) { diff --git a/src/server/api/routers/feedItemRouter.ts b/src/server/api/routers/feedItemRouter.ts index 972a830..19634f4 100644 --- a/src/server/api/routers/feedItemRouter.ts +++ b/src/server/api/routers/feedItemRouter.ts @@ -58,6 +58,7 @@ export const feedItemRouter = createTRPCRouter({ return { feedId: feed.id, contentId: item.id, + content: item.content ?? "", title: item.title ?? "", author: item.author ?? "", thumbnail: item.thumbnail ?? "", diff --git a/src/server/api/routers/feedRouter.ts b/src/server/api/routers/feedRouter.ts index ba5b89a..05f742a 100644 --- a/src/server/api/routers/feedRouter.ts +++ b/src/server/api/routers/feedRouter.ts @@ -102,6 +102,7 @@ export const feedRouter = createTRPCRouter({ name: channel.title, platform: "youtube", url: channel.feedUrl, + imageUrl: "", })); if (!feedsToAdd.length) return; diff --git a/src/server/db/migrations/0014_stormy_redwing.sql b/src/server/db/migrations/0014_stormy_redwing.sql new file mode 100644 index 0000000..da6cb81 --- /dev/null +++ b/src/server/db/migrations/0014_stormy_redwing.sql @@ -0,0 +1 @@ +ALTER TABLE `serial_feed` ADD `image_url` text(512) DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/src/server/db/migrations/0015_funny_joshua_kane.sql b/src/server/db/migrations/0015_funny_joshua_kane.sql new file mode 100644 index 0000000..f124ca2 --- /dev/null +++ b/src/server/db/migrations/0015_funny_joshua_kane.sql @@ -0,0 +1 @@ +ALTER TABLE `serial_feed_item` ADD `content` text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/src/server/db/migrations/0016_sloppy_glorian.sql b/src/server/db/migrations/0016_sloppy_glorian.sql new file mode 100644 index 0000000..9bedc5d --- /dev/null +++ b/src/server/db/migrations/0016_sloppy_glorian.sql @@ -0,0 +1 @@ +ALTER TABLE `serial_feed_item` ADD `id` text; \ No newline at end of file diff --git a/src/server/db/migrations/meta/0014_snapshot.json b/src/server/db/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000..a7dcb77 --- /dev/null +++ b/src/server/db/migrations/meta/0014_snapshot.json @@ -0,0 +1,844 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2327bf29-c06f-4617-807b-a29686a80108", + "prevId": "1a5f1fcf-f8ba-4db1-9ebc-d1193494ee01", + "tables": { + "serial_account": { + "name": "serial_account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_account_user_id_serial_user_id_fk": { + "name": "serial_account_user_id_serial_user_id_fk", + "tableFrom": "serial_account", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_content_categories": { + "name": "serial_content_categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "content_categories_name_idx": { + "name": "content_categories_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_categories": { + "name": "serial_feed_categories", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_feed_categories_feed_id_serial_feed_id_fk": { + "name": "serial_feed_categories_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_feed_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_feed_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_categories_feed_id_category_id_pk": { + "columns": [ + "feed_id", + "category_id" + ], + "name": "serial_feed_categories_feed_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_item": { + "name": "serial_feed_item", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_id": { + "name": "content_id", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_watched": { + "name": "is_watched", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_watch_later": { + "name": "is_watch_later", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "orientation": { + "name": "orientation", + "type": "text(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "posted_at": { + "name": "posted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_item_feed_id_idx": { + "name": "feed_item_feed_id_idx", + "columns": [ + "feed_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "serial_feed_item_feed_id_serial_feed_id_fk": { + "name": "serial_feed_item_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_item", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_item_feed_id_content_id_pk": { + "columns": [ + "feed_id", + "content_id" + ], + "name": "serial_feed_item_feed_id_content_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed": { + "name": "serial_feed", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "image_url": { + "name": "image_url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "platform": { + "name": "platform", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'youtube'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_name_idx": { + "name": "feed_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_session": { + "name": "serial_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_session_token_unique": { + "name": "serial_session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "serial_session_user_id_serial_user_id_fk": { + "name": "serial_session_user_id_serial_user_id_fk", + "tableFrom": "serial_session", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user": { + "name": "serial_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_user_email_unique": { + "name": "serial_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user_config": { + "name": "serial_user_config", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "light_hsl": { + "name": "light_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dark_hsl": { + "name": "dark_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_verification": { + "name": "serial_verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_view_categories": { + "name": "serial_view_categories", + "columns": { + "view_id": { + "name": "view_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_view_categories_view_id_serial_views_id_fk": { + "name": "serial_view_categories_view_id_serial_views_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_views", + "columnsFrom": [ + "view_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_view_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_view_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_view_categories_view_id_category_id_pk": { + "columns": [ + "view_id", + "category_id" + ], + "name": "serial_view_categories_view_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_views": { + "name": "serial_views", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "days_window": { + "name": "days_window", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "read_status": { + "name": "read_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": -1 + }, + "orientation": { + "name": "orientation", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'horizontal'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "view_name_idx": { + "name": "view_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/0015_snapshot.json b/src/server/db/migrations/meta/0015_snapshot.json new file mode 100644 index 0000000..d09f1fc --- /dev/null +++ b/src/server/db/migrations/meta/0015_snapshot.json @@ -0,0 +1,852 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f9c2162a-fb4f-4976-b211-21e6f3edfae1", + "prevId": "2327bf29-c06f-4617-807b-a29686a80108", + "tables": { + "serial_account": { + "name": "serial_account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_account_user_id_serial_user_id_fk": { + "name": "serial_account_user_id_serial_user_id_fk", + "tableFrom": "serial_account", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_content_categories": { + "name": "serial_content_categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "content_categories_name_idx": { + "name": "content_categories_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_categories": { + "name": "serial_feed_categories", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_feed_categories_feed_id_serial_feed_id_fk": { + "name": "serial_feed_categories_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_feed_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_feed_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_categories_feed_id_category_id_pk": { + "columns": [ + "feed_id", + "category_id" + ], + "name": "serial_feed_categories_feed_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_item": { + "name": "serial_feed_item", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_id": { + "name": "content_id", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_watched": { + "name": "is_watched", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_watch_later": { + "name": "is_watch_later", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "orientation": { + "name": "orientation", + "type": "text(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "posted_at": { + "name": "posted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_item_feed_id_idx": { + "name": "feed_item_feed_id_idx", + "columns": [ + "feed_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "serial_feed_item_feed_id_serial_feed_id_fk": { + "name": "serial_feed_item_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_item", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_item_feed_id_content_id_pk": { + "columns": [ + "feed_id", + "content_id" + ], + "name": "serial_feed_item_feed_id_content_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed": { + "name": "serial_feed", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "image_url": { + "name": "image_url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "platform": { + "name": "platform", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'youtube'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_name_idx": { + "name": "feed_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_session": { + "name": "serial_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_session_token_unique": { + "name": "serial_session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "serial_session_user_id_serial_user_id_fk": { + "name": "serial_session_user_id_serial_user_id_fk", + "tableFrom": "serial_session", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user": { + "name": "serial_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_user_email_unique": { + "name": "serial_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user_config": { + "name": "serial_user_config", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "light_hsl": { + "name": "light_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dark_hsl": { + "name": "dark_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_verification": { + "name": "serial_verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_view_categories": { + "name": "serial_view_categories", + "columns": { + "view_id": { + "name": "view_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_view_categories_view_id_serial_views_id_fk": { + "name": "serial_view_categories_view_id_serial_views_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_views", + "columnsFrom": [ + "view_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_view_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_view_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_view_categories_view_id_category_id_pk": { + "columns": [ + "view_id", + "category_id" + ], + "name": "serial_view_categories_view_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_views": { + "name": "serial_views", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "days_window": { + "name": "days_window", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "read_status": { + "name": "read_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": -1 + }, + "orientation": { + "name": "orientation", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'horizontal'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "view_name_idx": { + "name": "view_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/0016_snapshot.json b/src/server/db/migrations/meta/0016_snapshot.json new file mode 100644 index 0000000..8d70aa1 --- /dev/null +++ b/src/server/db/migrations/meta/0016_snapshot.json @@ -0,0 +1,859 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c4f42446-335e-46ec-a111-0876c059416f", + "prevId": "f9c2162a-fb4f-4976-b211-21e6f3edfae1", + "tables": { + "serial_account": { + "name": "serial_account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_account_user_id_serial_user_id_fk": { + "name": "serial_account_user_id_serial_user_id_fk", + "tableFrom": "serial_account", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_content_categories": { + "name": "serial_content_categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "content_categories_name_idx": { + "name": "content_categories_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_categories": { + "name": "serial_feed_categories", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_feed_categories_feed_id_serial_feed_id_fk": { + "name": "serial_feed_categories_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_feed_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_feed_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_feed_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_categories_feed_id_category_id_pk": { + "columns": [ + "feed_id", + "category_id" + ], + "name": "serial_feed_categories_feed_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed_item": { + "name": "serial_feed_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_id": { + "name": "content_id", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_watched": { + "name": "is_watched", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_watch_later": { + "name": "is_watch_later", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "orientation": { + "name": "orientation", + "type": "text(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "posted_at": { + "name": "posted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_item_feed_id_idx": { + "name": "feed_item_feed_id_idx", + "columns": [ + "feed_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "serial_feed_item_feed_id_serial_feed_id_fk": { + "name": "serial_feed_item_feed_id_serial_feed_id_fk", + "tableFrom": "serial_feed_item", + "tableTo": "serial_feed", + "columnsFrom": [ + "feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_feed_item_feed_id_content_id_pk": { + "columns": [ + "feed_id", + "content_id" + ], + "name": "serial_feed_item_feed_id_content_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_feed": { + "name": "serial_feed", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "image_url": { + "name": "image_url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "platform": { + "name": "platform", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'youtube'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "feed_name_idx": { + "name": "feed_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_session": { + "name": "serial_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_session_token_unique": { + "name": "serial_session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "serial_session_user_id_serial_user_id_fk": { + "name": "serial_session_user_id_serial_user_id_fk", + "tableFrom": "serial_session", + "tableTo": "serial_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user": { + "name": "serial_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "serial_user_email_unique": { + "name": "serial_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_user_config": { + "name": "serial_user_config", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "light_hsl": { + "name": "light_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dark_hsl": { + "name": "dark_hsl", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_verification": { + "name": "serial_verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_view_categories": { + "name": "serial_view_categories", + "columns": { + "view_id": { + "name": "view_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "serial_view_categories_view_id_serial_views_id_fk": { + "name": "serial_view_categories_view_id_serial_views_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_views", + "columnsFrom": [ + "view_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "serial_view_categories_category_id_serial_content_categories_id_fk": { + "name": "serial_view_categories_category_id_serial_content_categories_id_fk", + "tableFrom": "serial_view_categories", + "tableTo": "serial_content_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "serial_view_categories_view_id_category_id_pk": { + "columns": [ + "view_id", + "category_id" + ], + "name": "serial_view_categories_view_id_category_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serial_views": { + "name": "serial_views", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "days_window": { + "name": "days_window", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "read_status": { + "name": "read_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": -1 + }, + "orientation": { + "name": "orientation", + "type": "text(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'horizontal'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "view_name_idx": { + "name": "view_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index 5ac5910..ec216cb 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -99,6 +99,27 @@ "when": 1745184500672, "tag": "0013_brainy_logan", "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1756339571169, + "tag": "0014_stormy_redwing", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1756340471840, + "tag": "0015_funny_joshua_kane", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1756349707304, + "tag": "0016_sloppy_glorian", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index cb91824..d9b30da 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -20,6 +20,7 @@ import { VIEW_READ_STATUS, viewReadStatusSchema, } from "./constants"; +import { createId } from "@paralleldrive/cuid2"; /** * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same @@ -92,6 +93,7 @@ export const feeds = sqliteTable( userId: text("user_id").notNull().default(""), name: text("name", { length: 256 }).notNull().default(""), url: text("url", { length: 512 }).notNull().default(""), + imageUrl: text("image_url", { length: 512 }).notNull().default(""), platform: text("platform", { length: 256 }).notNull().default("youtube"), createdAt: integer("created_at", { mode: "timestamp" }) .$default(() => new Date()) @@ -102,7 +104,7 @@ export const feeds = sqliteTable( }, (example) => [index("feed_name_idx").on(example.name)], ); -export const platformsSchema = z.enum(["youtube", "peertube"]); +export const platformsSchema = z.enum(["youtube", "peertube", "website"]); export type FeedPlatform = z.infer; export const feedsSchema = createSelectSchema(feeds).merge( @@ -115,12 +117,14 @@ export type DatabaseFeed = typeof feeds.$inferSelect; export const feedItems = sqliteTable( "feed_item", { + id: text("id").$defaultFn(() => createId()), feedId: integer("feed_id").references(() => feeds.id), contentId: text("content_id", { length: 512 }).notNull(), title: text("title", { length: 512 }).notNull(), author: text("author", { length: 512 }).notNull(), url: text("url", { length: 512 }).notNull(), thumbnail: text("thumbnail", { length: 512 }).notNull().default(""), + content: text("content").notNull().default(""), isWatched: integer("is_watched", { mode: "boolean" }) .notNull() .default(false), diff --git a/src/server/rss/fetchFeeds.ts b/src/server/rss/fetchFeeds.ts index d7e0b23..bb52733 100644 --- a/src/server/rss/fetchFeeds.ts +++ b/src/server/rss/fetchFeeds.ts @@ -1,6 +1,7 @@ import type { DatabaseFeed } from "../db/schema"; import { fetchPeerTubeFeedData } from "./parsers/peertube"; import { fetchUnknownRssFeed } from "./parsers/unknown"; +import { fetchWebsiteFeedData } from "./parsers/website"; import { fetchYouTubeFeedData, fetchYouTubeFeedDetails, @@ -53,6 +54,9 @@ export async function fetchFeedData( if (feed.platform === "peertube") { return fetchPeerTubeFeedData(feed); } + if (feed.platform === "website") { + return fetchWebsiteFeedData(feed); + } return null; }), ) diff --git a/src/server/rss/new-feed/fetchNewFeedDetails.ts b/src/server/rss/new-feed/fetchNewFeedDetails.ts deleted file mode 100644 index 42b201d..0000000 --- a/src/server/rss/new-feed/fetchNewFeedDetails.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { fetchYouTubeFeedDetails } from "../parsers/youtube"; -import { type NewFeedDetails } from "../types"; - -export async function fetchNewFeedDetails( - url: string, -): Promise { - if (url.includes("youtube.com")) { - return fetchYouTubeFeedDetails(url); - } - return null; -} diff --git a/src/server/rss/parsers/unknown.ts b/src/server/rss/parsers/unknown.ts index 1ebbfa4..0837076 100644 --- a/src/server/rss/parsers/unknown.ts +++ b/src/server/rss/parsers/unknown.ts @@ -1,5 +1,6 @@ import type { NewFeedDetails } from "../types"; import { getPeerTubeFeedIfMatches } from "./peertube"; +import { getWebsiteFeedIfMatches } from "./website"; export async function fetchUnknownRssFeed( url: string, @@ -11,6 +12,9 @@ export async function fetchUnknownRssFeed( const peerTubeFeed = await getPeerTubeFeedIfMatches(text); if (peerTubeFeed) return peerTubeFeed; + const websiteFeed = await getWebsiteFeedIfMatches(text, url); + if (websiteFeed) return websiteFeed; + return null; } catch (e) { console.error(e); diff --git a/src/server/rss/parsers/website.ts b/src/server/rss/parsers/website.ts new file mode 100644 index 0000000..40c2278 --- /dev/null +++ b/src/server/rss/parsers/website.ts @@ -0,0 +1,114 @@ +import Parser from "rss-parser"; +import { z } from "zod"; +import type { DatabaseFeed } from "~/server/db/schema"; +import { isWithinDays } from "../rssUtils"; +import type { NewFeedDetails, RSSContent, RSSFeed } from "../types"; + +const parser = new Parser({ + customFields: { + item: [], + }, +}); + +export const websiteItemSchema = z.object({ + creator: z.string(), + title: z.string(), + link: z.string(), + pubDate: z.string().optional(), + "content:encoded": z.string().optional(), + content: z.string(), + contentSnippet: z.string().optional(), + guid: z.string(), + isoDate: z.string().optional(), + updated: z.string().optional(), +}); + +export const websiteSchema = z.object({ + items: websiteItemSchema.array(), + image: z + .object({ + link: z.string(), + url: z.string(), + title: z.string(), + }) + .optional(), + title: z.string(), + description: z.string().optional(), + generator: z.string().optional(), + link: z.string(), + lastBuildDate: z.string().optional(), + ttl: z.string().optional(), +}); + +export async function getWebsiteFeedIfMatches( + rssString: string, + url: string, +): Promise { + const rssData = await parser.parseString(rssString); //as unknown as RSSPeerTubeData; + + console.log(rssData); + const { + data: websiteData, + success: websiteSuccess, + error, + } = websiteSchema.safeParse(rssData); + + console.log(error?.flatten().formErrors); + console.log(error?.flatten().fieldErrors); + + if (websiteSuccess) { + return { + url: url, + platform: "website", + name: websiteData.title, + imageUrl: websiteData.image?.url, + }; + } else { + console.error(error); + } + + return null; +} + +export async function fetchWebsiteFeedData( + feed: DatabaseFeed, +): Promise { + try { + const feedResponse = await fetch(feed.url); + const text = await feedResponse.text(); + const rssData = await parser.parseString(text); + + const data = websiteSchema.parse(rssData); + + const itemPromises = data.items + .filter((item) => + isWithinDays(item?.pubDate || item?.isoDate || item?.updated || "", 60), + ) + .map(async (item) => { + const idParts = item.guid.split("/"); + const id = idParts[idParts.length - 1]; + + if (!id) return null; + + return { + id, + title: item.title, + publishedDate: item?.pubDate || item?.isoDate || item?.updated || "", + url: item.link, + author: item.creator, + content: item["content:encoded"], + } satisfies RSSContent; + }); + + return { + id: feed.id, + title: data.title, + url: data.link, + items: (await Promise.all(itemPromises)).filter(Boolean), + }; + } catch (e) { + console.error("Error fetching website feed data for URL =", feed.url); + console.error(e); + return null; + } +} diff --git a/src/server/rss/validateFeedUrl.ts b/src/server/rss/validateFeedUrl.ts index fcb7305..91cb34e 100644 --- a/src/server/rss/validateFeedUrl.ts +++ b/src/server/rss/validateFeedUrl.ts @@ -1,13 +1,30 @@ -const SUPPORTED_IF_INCLUDES = [ +import type { FeedPlatform } from "../db/schema"; + +const YOUTUBE_URL_SEGMENTS = [ "https://youtube.com/@", "https://www.youtube.com/@", "https://youtube.com/channel/", "https://www.youtube.com/channel/", "https://www.youtube.com/feeds/videos.xml?channel_id=", - "/feeds/videos.xml?accountId=", // PeerTube - "/feeds/videos.xml?videoChannelId=", // PeerTube ]; -export function validateFeedUrl(url: string) { - return SUPPORTED_IF_INCLUDES.some((supported) => url.includes(supported)); +const PEERTUBE_URL_SEGMENTS = [ + "/feeds/videos.xml?accountId=", + "/feeds/videos.xml?videoChannelId=", +]; + +export const FEED_PLATFORM_LABEL_MAP = { + website: "Website", + youtube: "YouTube", + peertube: "PeerTube", +} as const satisfies Record; + +export function getAssumedFeedPlatform(url: string): FeedPlatform { + if (YOUTUBE_URL_SEGMENTS.some((supported) => url.includes(supported))) { + return "youtube"; + } + if (PEERTUBE_URL_SEGMENTS.some((supported) => url.includes(supported))) { + return "peertube"; + } + return "website"; } diff --git a/src/server/scripts/addIdsToFeedItems.ts b/src/server/scripts/addIdsToFeedItems.ts new file mode 100644 index 0000000..be00eed --- /dev/null +++ b/src/server/scripts/addIdsToFeedItems.ts @@ -0,0 +1,35 @@ +import "dotenv/config"; + +import * as schema from "~/server/db/schema"; +import { eq, isNull } from "drizzle-orm"; +import { db } from "../db"; + +import { createId } from "@paralleldrive/cuid2"; + +async function migrate() { + let feedItemsMigrated = 0; + + await db.transaction(async (tx) => { + const feedItems = await tx + .select() + .from(schema.feedItems) + .where(isNull(schema.feedItems.id)) + .all(); + + return await Promise.all( + feedItems.map(async (feedItem) => { + feedItemsMigrated += 1; + return await tx + .update(schema.feedItems) + .set({ + id: createId(), + }) + .where(eq(schema.feedItems.contentId, feedItem.contentId)); + }), + ); + }); + + console.log(`Migrated ${feedItemsMigrated} rows!`); +} + +migrate();