mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-23 17:52:36 -04:00
Apis for the scanner (#885)
This commit is contained in:
commit
ebd95c7b0c
102
api/bun.lock
102
api/bun.lock
@ -6,23 +6,23 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"drizzle-orm": "0.39.0",
|
||||
"elysia": "^1.2.23",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"elysia": "^1.2.25",
|
||||
"jose": "^6.0.10",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.13.3",
|
||||
"sharp": "^0.34.0",
|
||||
"pg": "^8.15.6",
|
||||
"sharp": "^0.34.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.11",
|
||||
"bun-types": "^1.2.4",
|
||||
"@types/pg": "^8.11.14",
|
||||
"bun-types": "^1.2.11",
|
||||
"node-addon-api": "^8.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"drizzle-orm@0.39.0": "patches/drizzle-orm@0.39.0.patch",
|
||||
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch",
|
||||
},
|
||||
"packages": {
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
@ -35,51 +35,55 @@
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="],
|
||||
|
||||
@ -121,21 +125,17 @@
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.81", "", { "dependencies": { "@scalar/types": "0.1.3" } }, "sha512-asTgdqo8ZYibBBWVYy0503qPx3cvwDlYNuc/cLbrCmTav0MAEL4wNb/gz9iScMVSMwhdkSkL5g9LPdr2mQrHzw=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.1.3", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-Fxtgjp5wHhTzXiyODYWIoTsTy3oFC70vme+0I7MNwd8i6D8qplFNnpURueqBuP4MglBM2ZhFv3hPLw4D69anDA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.31", "", {}, "sha512-qQ71T9DsITbX3dVCrcBERbs11YuSMg3wZPnT472JhqhWGPdiLgyvihJXU8m+ADJtJvRdjATIiACJD22dEknBrQ=="],
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
||||
"@types/pg": ["@types/pg@8.11.14", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-qyD11E5R3u0eJmd1lB0WnWKXJGA7s015nyARWljfz5DcX83TKAIlY+QrmvzQTsbIe+hkiFtkyL2gHC6qwF6Fbg=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
|
||||
@ -143,7 +143,7 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="],
|
||||
"bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="],
|
||||
|
||||
"char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="],
|
||||
|
||||
@ -161,28 +161,22 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
|
||||
"drizzle-kit": ["drizzle-kit@0.31.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.39.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-kkZwo3Jvht0fdJD/EWGx0vYcEK0xnGrlNVaY07QYluRZA9N21B9VFbY+54bnb/1xvyzcg97tE65xprSAP/fFGQ=="],
|
||||
"drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="],
|
||||
|
||||
"elysia": ["elysia@1.2.25", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg=="],
|
||||
|
||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
|
||||
"esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"gel": ["gel@2.0.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-gfem3IGvqKqXwEq7XseBogyaRwGsQGuE7Cw/yQsjLGdgiyqX92G1xENPCE0ltunPGcsJIa6XBOTx/PK169mOqw=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"jose": ["jose@6.0.10", "", {}, "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="],
|
||||
|
||||
"memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="],
|
||||
@ -201,19 +195,19 @@
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"pg": ["pg@8.14.1", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw=="],
|
||||
"pg": ["pg@8.15.6", "", { "dependencies": { "pg-connection-string": "^2.8.5", "pg-pool": "^3.9.6", "pg-protocol": "^1.9.5", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
|
||||
"pg-cloudflare": ["pg-cloudflare@1.2.5", "", {}, "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="],
|
||||
"pg-connection-string": ["pg-connection-string@2.8.5", "", {}, "sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.8.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw=="],
|
||||
"pg-pool": ["pg-pool@3.9.6", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
|
||||
"pg-protocol": ["pg-protocol@1.9.5", "", {}, "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg=="],
|
||||
|
||||
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
||||
|
||||
@ -237,8 +231,6 @@
|
||||
|
||||
"sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@ -251,8 +243,6 @@
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||
|
5
api/drizzle/0020_video_unique.sql
Normal file
5
api/drizzle/0020_video_unique.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "kind" SET DATA TYPE text;--> statement-breakpoint
|
||||
DROP TYPE "kyoo"."entry_type";--> statement-breakpoint
|
||||
CREATE TYPE "kyoo"."entry_type" AS ENUM('episode', 'movie', 'special', 'extra');--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "kind" SET DATA TYPE "kyoo"."entry_type" USING "kind"::"kyoo"."entry_type";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."videos" ADD CONSTRAINT "rendering_unique" UNIQUE NULLS NOT DISTINCT("rendering","part","version");
|
1851
api/drizzle/meta/0020_snapshot.json
Normal file
1851
api/drizzle/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -141,6 +141,13 @@
|
||||
"when": 1744120518941,
|
||||
"tag": "0019_nextup",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "7",
|
||||
"when": 1746198322219,
|
||||
"tag": "0020_video_unique",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -11,21 +11,21 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"drizzle-orm": "0.39.0",
|
||||
"elysia": "^1.2.23",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"elysia": "^1.2.25",
|
||||
"jose": "^6.0.10",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.13.3",
|
||||
"sharp": "^0.34.0"
|
||||
"pg": "^8.15.6",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/pg": "^8.11.14",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"bun-types": "^1.2.4"
|
||||
"bun-types": "^1.2.11"
|
||||
},
|
||||
"module": "src/index.js",
|
||||
"patchedDependencies": {
|
||||
"drizzle-orm@0.39.0": "patches/drizzle-orm@0.39.0.patch"
|
||||
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch"
|
||||
}
|
||||
}
|
||||
|
@ -1,251 +0,0 @@
|
||||
diff --git a/node_modules/drizzle-orm/.bun-tag-3622ae30f31c0d9a b/.bun-tag-3622ae30f31c0d9a
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/node_modules/drizzle-orm/.bun-tag-36446a2521398ee8 b/.bun-tag-36446a2521398ee8
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/node_modules/drizzle-orm/.bun-tag-844efc51a55b820c b/.bun-tag-844efc51a55b820c
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/node_modules/drizzle-orm/.bun-tag-9fae835e61d5cc75 b/.bun-tag-9fae835e61d5cc75
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/node_modules/drizzle-orm/.bun-tag-ce8efc9a806990a3 b/.bun-tag-ce8efc9a806990a3
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/pg-core/dialect.cjs b/pg-core/dialect.cjs
|
||||
index 52acbfb6038fb1bbba4e34115d75a22bb0f9ab1a..1f10884caf05329ab98b06a68c8e7803e5283d32 100644
|
||||
--- a/pg-core/dialect.cjs
|
||||
+++ b/pg-core/dialect.cjs
|
||||
@@ -347,7 +347,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[import_table2.Table.Symbol.Columns];
|
||||
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
const insertOrder = colEntries.map(
|
||||
([, column]) => import_sql2.sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/dialect.js b/pg-core/dialect.js
|
||||
index d7985c81f3d224f7671efe72e79b14153d5ca8ce..91d99ccd2ebda807a7d45c76f7164e571b922159 100644
|
||||
--- a/pg-core/dialect.js
|
||||
+++ b/pg-core/dialect.js
|
||||
@@ -345,7 +345,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[Table.Symbol.Columns];
|
||||
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
const insertOrder = colEntries.map(
|
||||
([, column]) => sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/query-builders/insert.cjs b/pg-core/query-builders/insert.cjs
|
||||
index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..341d2513d4377acc33ee0606d05580566fd4b88c 100644
|
||||
--- a/pg-core/query-builders/insert.cjs
|
||||
+++ b/pg-core/query-builders/insert.cjs
|
||||
@@ -75,11 +75,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new import_query_builder.QueryBuilder()) : selectQuery;
|
||||
- if (!(0, import_entity.is)(select, import_sql.SQL) && !(0, import_utils.haveSameKeys)(this.table[import_table.Columns], select._.selectedFields)) {
|
||||
- throw new Error(
|
||||
- "Insert select error: selected fields are not the same or are in a different order compared to the table definition"
|
||||
- );
|
||||
- }
|
||||
return new PgInsertBase(this.table, select, this.session, this.dialect, this.withList, true);
|
||||
}
|
||||
}
|
||||
diff --git a/pg-core/query-builders/insert.js b/pg-core/query-builders/insert.js
|
||||
index 0fc8eeb80f4a5512f6c84f3d596832623a33b748..b993f226daf16f423db012dff828d89c522603c3 100644
|
||||
--- a/pg-core/query-builders/insert.js
|
||||
+++ b/pg-core/query-builders/insert.js
|
||||
@@ -51,11 +51,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery;
|
||||
- if (!is(select, SQL) && !haveSameKeys(this.table[Columns], select._.selectedFields)) {
|
||||
- throw new Error(
|
||||
- "Insert select error: selected fields are not the same or are in a different order compared to the table definition"
|
||||
- );
|
||||
- }
|
||||
return new PgInsertBase(this.table, select, this.session, this.dialect, this.withList, true);
|
||||
}
|
||||
}
|
||||
diff --git a/pg-core/query-builders/select.d.cts b/pg-core/query-builders/select.d.cts
|
||||
index b968ebb3f563f37c8c36221dd17cc6f3603270ec..3fda6d0a97997f6bd07ec6a0c83397c0fdd2e97e 100644
|
||||
--- a/pg-core/query-builders/select.d.cts
|
||||
+++ b/pg-core/query-builders/select.d.cts
|
||||
@@ -98,7 +98,16 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .leftJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- leftJoin: PgSelectJoinFn<this, TDynamic, "left">;
|
||||
+ leftJoin: PgSelectJoinFn<this, TDynamic, "left", false>;
|
||||
+ /**
|
||||
+ * For each row of the table, include
|
||||
+ * values from a matching row of the joined
|
||||
+ * subquery, if there is a matching row. If not,
|
||||
+ * all of the columns of the joined subquery
|
||||
+ * will be set to null. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ leftJoinLateral: PgSelectJoinFn<this, TDynamic, "left", true>;
|
||||
/**
|
||||
* Executes a `right join` operation by adding another table to the current query.
|
||||
*
|
||||
@@ -126,7 +135,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .rightJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- rightJoin: PgSelectJoinFn<this, TDynamic, "right">;
|
||||
+ rightJoin: PgSelectJoinFn<this, TDynamic, "right", false>;
|
||||
/**
|
||||
* Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values.
|
||||
*
|
||||
@@ -154,7 +163,14 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .innerJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- innerJoin: PgSelectJoinFn<this, TDynamic, "inner">;
|
||||
+ innerJoin: PgSelectJoinFn<this, TDynamic, "inner", false>;
|
||||
+ /**
|
||||
+ * For each row of the table, the joined subquery
|
||||
+ * needs to have a matching row, or it will
|
||||
+ * be excluded from results. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ innerJoinLateral: PgSelectJoinFn<this, TDynamic, "inner", true>;
|
||||
/**
|
||||
* Executes a `full join` operation by combining rows from two tables into a new table.
|
||||
*
|
||||
@@ -182,7 +198,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .fullJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- fullJoin: PgSelectJoinFn<this, TDynamic, "full">;
|
||||
+ fullJoin: PgSelectJoinFn<this, TDynamic, "full", false>;
|
||||
private createSetOperator;
|
||||
/**
|
||||
* Adds `union` set operator to the query.
|
||||
diff --git a/pg-core/query-builders/select.d.ts b/pg-core/query-builders/select.d.ts
|
||||
index d44256289ffe7bd19d3f3af98cbd9ba0fc7efc57..f106eb28a919e0182f833632ace36ea7f87f9a88 100644
|
||||
--- a/pg-core/query-builders/select.d.ts
|
||||
+++ b/pg-core/query-builders/select.d.ts
|
||||
@@ -98,7 +98,16 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .leftJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- leftJoin: PgSelectJoinFn<this, TDynamic, "left">;
|
||||
+ leftJoin: PgSelectJoinFn<this, TDynamic, "left", false>;
|
||||
+ /**
|
||||
+ * For each row of the table, include
|
||||
+ * values from a matching row of the joined
|
||||
+ * subquery, if there is a matching row. If not,
|
||||
+ * all of the columns of the joined subquery
|
||||
+ * will be set to null. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ leftJoinLateral: PgSelectJoinFn<this, TDynamic, "left", true>;
|
||||
/**
|
||||
* Executes a `right join` operation by adding another table to the current query.
|
||||
*
|
||||
@@ -126,7 +135,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .rightJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- rightJoin: PgSelectJoinFn<this, TDynamic, "right">;
|
||||
+ rightJoin: PgSelectJoinFn<this, TDynamic, "right", false>;
|
||||
/**
|
||||
* Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values.
|
||||
*
|
||||
@@ -154,7 +163,14 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .innerJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- innerJoin: PgSelectJoinFn<this, TDynamic, "inner">;
|
||||
+ innerJoin: PgSelectJoinFn<this, TDynamic, "inner", false>;
|
||||
+ /**
|
||||
+ * For each row of the table, the joined subquery
|
||||
+ * needs to have a matching row, or it will
|
||||
+ * be excluded from results. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ innerJoinLateral: PgSelectJoinFn<this, TDynamic, "inner", true>;
|
||||
/**
|
||||
* Executes a `full join` operation by combining rows from two tables into a new table.
|
||||
*
|
||||
@@ -182,7 +198,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||
* .fullJoin(pets, eq(users.id, pets.ownerId))
|
||||
* ```
|
||||
*/
|
||||
- fullJoin: PgSelectJoinFn<this, TDynamic, "full">;
|
||||
+ fullJoin: PgSelectJoinFn<this, TDynamic, "full", false>;
|
||||
private createSetOperator;
|
||||
/**
|
||||
* Adds `union` set operator to the query.
|
||||
diff --git a/pg-core/query-builders/select.js b/pg-core/query-builders/select.js
|
||||
index e54406fcaf68ccfdaf32c8945d4d432212c4cf3f..5c514132f30366ee600b9530c284932d54f481f3 100644
|
||||
--- a/pg-core/query-builders/select.js
|
||||
+++ b/pg-core/query-builders/select.js
|
||||
@@ -98,7 +98,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||
this.tableName = getTableLikeName(table);
|
||||
this.joinsNotNullableMap = typeof this.tableName === "string" ? { [this.tableName]: true } : {};
|
||||
}
|
||||
- createJoin(joinType) {
|
||||
+ createJoin(joinType, lateral = false) {
|
||||
return (table, on) => {
|
||||
const baseTableName = this.tableName;
|
||||
const tableName = getTableLikeName(table);
|
||||
@@ -127,7 +127,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||
if (!this.config.joins) {
|
||||
this.config.joins = [];
|
||||
}
|
||||
- this.config.joins.push({ on, table, joinType, alias: tableName });
|
||||
+ this.config.joins.push({ on, table, joinType, alias: tableName, lateral });
|
||||
if (typeof tableName === "string") {
|
||||
switch (joinType) {
|
||||
case "left": {
|
||||
@@ -185,6 +185,15 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||
* ```
|
||||
*/
|
||||
leftJoin = this.createJoin("left");
|
||||
+ /**
|
||||
+ * For each row of the table, include
|
||||
+ * values from a matching row of the joined
|
||||
+ * subquery, if there is a matching row. If not,
|
||||
+ * all of the columns of the joined subquery
|
||||
+ * will be set to null. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ leftJoinLateral = this.createJoin("left", true);
|
||||
/**
|
||||
* Executes a `right join` operation by adding another table to the current query.
|
||||
*
|
||||
@@ -241,6 +250,13 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||
* ```
|
||||
*/
|
||||
innerJoin = this.createJoin("inner");
|
||||
+ /**
|
||||
+ * For each row of the table, the joined subquery
|
||||
+ * needs to have a matching row, or it will
|
||||
+ * be excluded from results. The lateral keyword allows
|
||||
+ * access to columns after the FROM statement.
|
||||
+ */
|
||||
+ innerJoinLateral = this.createJoin("inner", true);
|
||||
/**
|
||||
* Executes a `full join` operation by combining rows from two tables into a new table.
|
||||
*
|
72
api/patches/drizzle-orm@0.43.1.patch
Normal file
72
api/patches/drizzle-orm@0.43.1.patch
Normal file
@ -0,0 +1,72 @@
|
||||
diff --git a/pg-core/dialect.cjs b/pg-core/dialect.cjs
|
||||
index a0ef03142f21d319376bc50070ff7fdcd4d18132..45fc94e5a7c3fa4c201e636dd227122164e1bd02 100644
|
||||
--- a/pg-core/dialect.cjs
|
||||
+++ b/pg-core/dialect.cjs
|
||||
@@ -348,7 +348,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[import_table2.Table.Symbol.Columns];
|
||||
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
const insertOrder = colEntries.map(
|
||||
([, column]) => import_sql2.sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/dialect.js b/pg-core/dialect.js
|
||||
index 120aaed9c3e4ae0a24653893379b98506c866f6f..48df463c0a6d5864fe2c324c8f86432860e50e00 100644
|
||||
--- a/pg-core/dialect.js
|
||||
+++ b/pg-core/dialect.js
|
||||
@@ -346,7 +346,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[Table.Symbol.Columns];
|
||||
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
const insertOrder = colEntries.map(
|
||||
([, column]) => sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/query-builders/insert.cjs b/pg-core/query-builders/insert.cjs
|
||||
index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..20c8036374a1f25f7c5880c40e8d3c42c05f3eee 100644
|
||||
--- a/pg-core/query-builders/insert.cjs
|
||||
+++ b/pg-core/query-builders/insert.cjs
|
||||
@@ -75,11 +75,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new import_query_builder.QueryBuilder()) : selectQuery;
|
||||
- if (!(0, import_entity.is)(select, import_sql.SQL) && !(0, import_utils.haveSameKeys)(this.table[import_table.Columns], select._.selectedFields)) {
|
||||
- throw new Error(
|
||||
- "Insert select error: selected fields are not the same or are in a different order compared to the table definition"
|
||||
- );
|
||||
- }
|
||||
return new PgInsertBase(this.table, select, this.session, this.dialect, this.withList, true);
|
||||
}
|
||||
}
|
||||
diff --git a/pg-core/query-builders/insert.js b/pg-core/query-builders/insert.js
|
||||
index 0fc8eeb80f4a5512f6c84f3d596832623a33b748..998e2ab0bfe3f322bf268a01f71ebd06c57d4d07 100644
|
||||
--- a/pg-core/query-builders/insert.js
|
||||
+++ b/pg-core/query-builders/insert.js
|
||||
@@ -51,11 +51,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery;
|
||||
- if (!is(select, SQL) && !haveSameKeys(this.table[Columns], select._.selectedFields)) {
|
||||
- throw new Error(
|
||||
- "Insert select error: selected fields are not the same or are in a different order compared to the table definition"
|
||||
- );
|
||||
- }
|
||||
return new PgInsertBase(this.table, select, this.session, this.dialect, this.withList, true);
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ import {
|
||||
ExtraType,
|
||||
MovieEntry,
|
||||
Special,
|
||||
UnknownEntry,
|
||||
} from "~/models/entry";
|
||||
import { KError } from "~/models/error";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
@ -81,11 +80,6 @@ const extraFilters: FilterDef = {
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
};
|
||||
|
||||
const unknownFilters: FilterDef = {
|
||||
runtime: { column: entries.runtime, type: "float" },
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
};
|
||||
|
||||
export const entrySort = Sort(
|
||||
{
|
||||
order: entries.order,
|
||||
@ -149,13 +143,15 @@ export const entryVideosQ = db
|
||||
export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||
const { time, percent, playedDate, videoId } = getColumns(entryProgressQ);
|
||||
const ret = {
|
||||
time: coalesce(time, sql`0`),
|
||||
percent: coalesce(percent, sql`0`),
|
||||
playedDate: sql`${playedDate}`,
|
||||
videoId: sql`${videoId}`,
|
||||
time: coalesce(time, sql<number>`0`),
|
||||
percent: coalesce(percent, sql<number>`0`),
|
||||
playedDate: sql<string>`${playedDate}`,
|
||||
videoId: sql<string>`${videoId}`,
|
||||
};
|
||||
if (!aliased) return ret;
|
||||
return Object.fromEntries(Object.entries(ret).map(([k, v]) => [k, v.as(k)]));
|
||||
return Object.fromEntries(
|
||||
Object.entries(ret).map(([k, v]) => [k, v.as(k)]),
|
||||
) as unknown as typeof ret;
|
||||
};
|
||||
|
||||
export async function getEntries({
|
||||
@ -176,7 +172,7 @@ export async function getEntries({
|
||||
languages: string[];
|
||||
userId: string;
|
||||
progressQ?: typeof entryProgressQ;
|
||||
}): Promise<(Entry | Extra | UnknownEntry)[]> {
|
||||
}): Promise<(Entry | Extra)[]> {
|
||||
const transQ = db
|
||||
.selectDistinctOn([entryTranslations.pk])
|
||||
.from(entryTranslations)
|
||||
@ -203,7 +199,7 @@ export async function getEntries({
|
||||
videos: entryVideosQ.videos,
|
||||
progress: mapProgress({ aliased: true }),
|
||||
// specials don't have an `episodeNumber` but a `number` field.
|
||||
number: episodeNumber,
|
||||
number: sql<number>`${episodeNumber}`,
|
||||
|
||||
// merge `extraKind` into `kind`
|
||||
kind: sql<EntryKind>`case when ${kind} = 'extra' then ${extraKind} else ${kind}::text end`.as(
|
||||
@ -219,7 +215,7 @@ export async function getEntries({
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.crossJoinLateral(entryVideosQ)
|
||||
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
||||
.where(
|
||||
and(
|
||||
@ -244,7 +240,6 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
movie_entry: MovieEntry,
|
||||
special: Special,
|
||||
extra: Extra,
|
||||
unknown_entry: UnknownEntry,
|
||||
error: t.Object({}),
|
||||
})
|
||||
.model((models) => ({
|
||||
@ -289,7 +284,6 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
filter: and(
|
||||
eq(entries.showPk, serie.pk),
|
||||
ne(entries.kind, "extra"),
|
||||
ne(entries.kind, "unknown"),
|
||||
filter,
|
||||
),
|
||||
languages: langs,
|
||||
@ -407,46 +401,6 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/unknowns",
|
||||
async ({
|
||||
query: { limit, after, query, sort, filter },
|
||||
request: { url },
|
||||
jwt: { sub },
|
||||
}) => {
|
||||
const items = (await getEntries({
|
||||
limit,
|
||||
after,
|
||||
query,
|
||||
sort: sort,
|
||||
filter: and(eq(entries.kind, "unknown"), filter),
|
||||
languages: ["extra"],
|
||||
userId: sub,
|
||||
})) as UnknownEntry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get unknown/unmatch videos." },
|
||||
query: t.Object({
|
||||
sort: extraSort,
|
||||
filter: t.Optional(Filter({ def: unknownFilters })),
|
||||
query: t.Optional(t.String({ description: description.query })),
|
||||
limit: t.Integer({
|
||||
minimum: 1,
|
||||
maximum: 250,
|
||||
default: 50,
|
||||
description: "Max page size.",
|
||||
}),
|
||||
after: t.Optional(t.String({ description: description.after })),
|
||||
}),
|
||||
response: {
|
||||
200: Page(UnknownEntry),
|
||||
422: KError,
|
||||
},
|
||||
tags: ["videos"],
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/news",
|
||||
async ({
|
||||
@ -462,7 +416,6 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
sort,
|
||||
filter: and(
|
||||
isNotNull(entries.availableSince),
|
||||
ne(entries.kind, "unknown"),
|
||||
ne(entries.kind, "extra"),
|
||||
filter,
|
||||
),
|
||||
@ -489,6 +442,6 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
200: Page(Entry),
|
||||
422: KError,
|
||||
},
|
||||
tags: ["videos"],
|
||||
tags: ["shows"],
|
||||
},
|
||||
);
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
import type { WatchlistStatus } from "~/models/watchlist";
|
||||
import {
|
||||
entryFilters,
|
||||
entryProgressQ,
|
||||
@ -79,7 +80,6 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
filter: and(
|
||||
isNotNull(entryProgressQ.playedDate),
|
||||
ne(entries.kind, "extra"),
|
||||
ne(entries.kind, "unknown"),
|
||||
filter,
|
||||
),
|
||||
languages: langs,
|
||||
@ -125,7 +125,6 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
filter: and(
|
||||
isNotNull(entryProgressQ.playedDate),
|
||||
ne(entries.kind, "extra"),
|
||||
ne(entries.kind, "unknown"),
|
||||
filter,
|
||||
),
|
||||
languages: langs,
|
||||
@ -167,8 +166,14 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
async ({ body, jwt: { sub }, error }) => {
|
||||
const profilePk = await getOrCreateProfile(sub);
|
||||
|
||||
const vals = values(
|
||||
const hist = values(
|
||||
body.map((x) => ({ ...x, entryUseId: isUuid(x.entry) })),
|
||||
{
|
||||
percent: "integer",
|
||||
time: "integer",
|
||||
playedDate: "timestamptz",
|
||||
videoId: "uuid",
|
||||
},
|
||||
).as("hist");
|
||||
const valEqEntries = sql`
|
||||
case
|
||||
@ -182,16 +187,16 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
profilePk: sql`${profilePk}`,
|
||||
profilePk: sql`${profilePk}`.as("profilePk"),
|
||||
entryPk: entries.pk,
|
||||
videoPk: videos.pk,
|
||||
percent: sql`hist.percent::integer`,
|
||||
time: sql`hist.time::integer`,
|
||||
playedDate: sql`hist.playedDate::timestamptz`,
|
||||
percent: sql`hist.percent`.as("percent"),
|
||||
time: sql`hist.time`.as("time"),
|
||||
playedDate: sql`hist.playedDate`.as("playedDate"),
|
||||
})
|
||||
.from(vals)
|
||||
.from(hist)
|
||||
.innerJoin(entries, valEqEntries)
|
||||
.leftJoin(videos, eq(videos.id, sql`hist.videoId::uuid`)),
|
||||
.leftJoin(videos, eq(videos.id, sql`hist.videoId`)),
|
||||
)
|
||||
.returning({ pk: history.pk });
|
||||
|
||||
@ -244,43 +249,43 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
profilePk: sql`${profilePk}`,
|
||||
profilePk: sql`${profilePk}`.as("profilePk"),
|
||||
showPk: entries.showPk,
|
||||
status: sql`
|
||||
status: sql<WatchlistStatus>`
|
||||
case
|
||||
when
|
||||
hist.percent::integer >= 95
|
||||
hist.percent >= 95
|
||||
and ${nextEntryQ.pk} is null
|
||||
then 'completed'::watchlist_status
|
||||
else 'watching'::watchlist_status
|
||||
end
|
||||
`,
|
||||
`.as("status"),
|
||||
seenCount: sql`
|
||||
case
|
||||
when ${entries.kind} = 'movie' then hist.percent::integer
|
||||
when hist.percent::integer >= 95 then 1
|
||||
when ${entries.kind} = 'movie' then hist.percent
|
||||
when hist.percent >= 95 then 1
|
||||
else 0
|
||||
end
|
||||
`,
|
||||
`.as("seen_count"),
|
||||
nextEntry: sql`
|
||||
case
|
||||
when hist.percent::integer >= 95 then ${nextEntryQ.pk}
|
||||
when hist.percent >= 95 then ${nextEntryQ.pk}
|
||||
else ${entries.pk}
|
||||
end
|
||||
`,
|
||||
score: sql`null`,
|
||||
startedAt: sql`hist.playedDate::timestamptz`,
|
||||
lastPlayedAt: sql`hist.playedDate::timestamptz`,
|
||||
`.as("next_entry"),
|
||||
score: sql`null`.as("score"),
|
||||
startedAt: sql`hist.playedDate`.as("startedAt"),
|
||||
lastPlayedAt: sql`hist.playedDate`.as("lastPlayedAt"),
|
||||
completedAt: sql`
|
||||
case
|
||||
when ${nextEntryQ.pk} is null then hist.playedDate::timestamptz
|
||||
when ${nextEntryQ.pk} is null then hist.playedDate
|
||||
else null
|
||||
end
|
||||
`,
|
||||
`.as("completedAt"),
|
||||
// see https://github.com/drizzle-team/drizzle-orm/issues/3608
|
||||
updatedAt: sql`now()`,
|
||||
updatedAt: sql`now()`.as("updatedAt"),
|
||||
})
|
||||
.from(vals)
|
||||
.from(hist)
|
||||
.leftJoin(entries, valEqEntries)
|
||||
.leftJoinLateral(nextEntryQ, sql`true`),
|
||||
)
|
||||
|
@ -90,6 +90,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
extraKind,
|
||||
kind,
|
||||
...entryCol
|
||||
} = getColumns(entries);
|
||||
|
||||
@ -100,9 +101,10 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
videos: entryVideosQ.videos,
|
||||
progress: mapProgress({ aliased: true }),
|
||||
// specials don't have an `episodeNumber` but a `number` field.
|
||||
number: episodeNumber,
|
||||
number: sql<number>`${episodeNumber}`,
|
||||
|
||||
// assign more restrained types to make typescript happy.
|
||||
kind: sql<Entry["kind"]>`${kind}`,
|
||||
externalId: sql<any>`${externalId}`,
|
||||
order: sql<number>`${order}`,
|
||||
seasonNumber: sql<number>`${seasonNumber}`,
|
||||
@ -112,7 +114,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
.from(entries)
|
||||
.innerJoin(watchlist, eq(watchlist.nextEntry, entries.pk))
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.crossJoinLateral(entryVideosQ)
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.where(
|
||||
and(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type Column, type SQL, and, eq, isNull, sql } from "drizzle-orm";
|
||||
import { type Column, type SQL, eq, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
@ -6,11 +6,11 @@ import {
|
||||
entryVideoJoin,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, sqlarr, values } from "~/db/utils";
|
||||
import { conflictUpdateAllExcept, values } from "~/db/utils";
|
||||
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { guessNextRefresh } from "../refresh";
|
||||
import { updateAvailableCount } from "./shows";
|
||||
import { updateAvailableCount, updateAvailableSince } from "./shows";
|
||||
|
||||
type SeedEntry = SEntry & {
|
||||
video?: undefined;
|
||||
@ -167,15 +167,21 @@ export const insertEntries = async (
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entryPk: sql<number>`vids.entryPk::integer`.as("entry"),
|
||||
entryPk: sql<number>`vids.entryPk`.as("entry"),
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
sql`vids.entrySlug::text`,
|
||||
sql`vids.needRendering::boolean`,
|
||||
sql`vids.entrySlug`,
|
||||
sql`vids.needRendering`,
|
||||
),
|
||||
})
|
||||
.from(values(vids).as("vids"))
|
||||
.innerJoin(videos, eq(videos.id, sql`vids.videoId::uuid`)),
|
||||
.from(
|
||||
values(vids, {
|
||||
entryPk: "integer",
|
||||
needRendering: "boolean",
|
||||
videoId: "uuid",
|
||||
}).as("vids"),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.id, sql`vids.videoId`)),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
@ -186,16 +192,7 @@ export const insertEntries = async (
|
||||
if (!onlyExtras)
|
||||
await updateAvailableCount(tx, [show.pk], show.kind === "serie");
|
||||
|
||||
const entriesPk = [...new Set(vids.map((x) => x.entryPk))];
|
||||
await tx
|
||||
.update(entries)
|
||||
.set({ availableSince: sql`now()` })
|
||||
.where(
|
||||
and(
|
||||
eq(entries.pk, sql`any(${sqlarr(entriesPk)})`),
|
||||
isNull(entries.availableSince),
|
||||
),
|
||||
);
|
||||
await updateAvailableSince(tx, [...new Set(vids.map((x) => x.entryPk))]);
|
||||
return ret;
|
||||
});
|
||||
|
||||
@ -206,10 +203,10 @@ export const insertEntries = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export function computeVideoSlug(showSlug: SQL | Column, needsRendering: SQL) {
|
||||
export function computeVideoSlug(entrySlug: SQL | Column, needsRendering: SQL) {
|
||||
return sql<string>`
|
||||
concat(
|
||||
${showSlug},
|
||||
${entrySlug},
|
||||
case when ${videos.part} is not null then ('-p' || ${videos.part}) else '' end,
|
||||
case when ${videos.version} <> 1 then ('-v' || ${videos.version}) else '' end,
|
||||
case when ${needsRendering} then concat('-', ${videos.rendering}) else '' end
|
||||
|
@ -1,4 +1,13 @@
|
||||
import { and, count, eq, exists, ne, sql } from "drizzle-orm";
|
||||
import {
|
||||
type SQLWrapper,
|
||||
and,
|
||||
count,
|
||||
eq,
|
||||
exists,
|
||||
isNull,
|
||||
ne,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { type Transaction, db } from "~/db";
|
||||
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, sqlarr } from "~/db/utils";
|
||||
@ -138,9 +147,10 @@ async function insertBaseShow(tx: Transaction, show: Show) {
|
||||
|
||||
export async function updateAvailableCount(
|
||||
tx: Transaction,
|
||||
showPks: number[],
|
||||
updateEntryCount = true,
|
||||
showPks: number[] | SQLWrapper,
|
||||
updateEntryCount = false,
|
||||
) {
|
||||
const showPkQ = Array.isArray(showPks) ? sqlarr(showPks) : showPks;
|
||||
return await tx
|
||||
.update(shows)
|
||||
.set({
|
||||
@ -168,5 +178,20 @@ export async function updateAvailableCount(
|
||||
)}`,
|
||||
}),
|
||||
})
|
||||
.where(eq(shows.pk, sql`any(${sqlarr(showPks)})`));
|
||||
.where(eq(shows.pk, sql`any(${showPkQ})`));
|
||||
}
|
||||
|
||||
export async function updateAvailableSince(
|
||||
tx: Transaction,
|
||||
entriesPk: number[],
|
||||
) {
|
||||
return await tx
|
||||
.update(entries)
|
||||
.set({ availableSince: sql`now()` })
|
||||
.where(
|
||||
and(
|
||||
eq(entries.pk, sql`any(${sqlarr(entriesPk)})`),
|
||||
isNull(entries.availableSince),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ import { entryProgressQ, entryVideosQ, mapProgress } from "../entries";
|
||||
export const watchStatusQ = db
|
||||
.select({
|
||||
...getColumns(watchlist),
|
||||
percent: sql`${watchlist.seenCount}`.as("percent"),
|
||||
percent: sql<number>`${watchlist.seenCount}`.as("percent"),
|
||||
})
|
||||
.from(watchlist)
|
||||
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
|
||||
@ -161,9 +161,9 @@ const showRelations = {
|
||||
).as("videos"),
|
||||
})
|
||||
.from(entryVideoJoin)
|
||||
.innerJoin(entries, eq(entries.showPk, shows.pk))
|
||||
.innerJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.where(eq(entryVideoJoin.entryPk, entries.pk))
|
||||
.leftJoin(entries, eq(entries.showPk, shows.pk))
|
||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.as("videos");
|
||||
},
|
||||
firstEntry: ({ languages }: { languages: string[] }) => {
|
||||
@ -190,7 +190,7 @@ const showRelations = {
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.crossJoinLateral(entryVideosQ)
|
||||
.where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")))
|
||||
.orderBy(entries.order)
|
||||
.limit(1)
|
||||
@ -220,7 +220,7 @@ const showRelations = {
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.crossJoinLateral(entryVideosQ)
|
||||
.where(eq(watchStatusQ.nextEntry, entries.pk))
|
||||
.as("nextEntry");
|
||||
},
|
||||
|
@ -1,24 +1,44 @@
|
||||
import { and, eq, exists, inArray, not, sql } from "drizzle-orm";
|
||||
import { and, eq, exists, inArray, not, notExists, or, sql } from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { db } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||
import { sqlarr } from "~/db/utils";
|
||||
import {
|
||||
conflictUpdateAllExcept,
|
||||
isUniqueConstraint,
|
||||
jsonbBuildObject,
|
||||
jsonbObjectAgg,
|
||||
sqlarr,
|
||||
values,
|
||||
} from "~/db/utils";
|
||||
import { KError } from "~/models/error";
|
||||
import { bubbleVideo } from "~/models/examples";
|
||||
import { Page } from "~/models/utils";
|
||||
import { SeedVideo, Video } from "~/models/video";
|
||||
import {
|
||||
Page,
|
||||
type Resource,
|
||||
Sort,
|
||||
createPage,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
sortToSql,
|
||||
} from "~/models/utils";
|
||||
import { desc as description } from "~/models/utils/descriptions";
|
||||
import { Guesses, SeedVideo, Video } from "~/models/video";
|
||||
import { comment } from "~/utils";
|
||||
import { computeVideoSlug } from "./seed/insert/entries";
|
||||
import { updateAvailableCount } from "./seed/insert/shows";
|
||||
import {
|
||||
updateAvailableCount,
|
||||
updateAvailableSince,
|
||||
} from "./seed/insert/shows";
|
||||
|
||||
const CreatedVideo = t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
path: t.String({ examples: [bubbleVideo.path] }),
|
||||
// entries: t.Array(
|
||||
// t.Object({
|
||||
// slug: t.String({ format: "slug", examples: ["bubble-v2"] }),
|
||||
// }),
|
||||
// ),
|
||||
entries: t.Array(
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug", examples: ["bubble-v2"] }),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
@ -27,93 +47,326 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
"created-videos": t.Array(CreatedVideo),
|
||||
error: t.Object({}),
|
||||
})
|
||||
.get(
|
||||
"",
|
||||
async () => {
|
||||
const years = db.$with("years").as(
|
||||
db
|
||||
.select({
|
||||
guess: sql`${videos.guess}->>'title'`.as("guess"),
|
||||
year: sql`coalesce(year, 'unknown')`.as("year"),
|
||||
id: shows.id,
|
||||
slug: shows.slug,
|
||||
})
|
||||
.from(videos)
|
||||
.leftJoin(
|
||||
sql`jsonb_array_elements_text(${videos.guess}->'year') as year`,
|
||||
sql`true`,
|
||||
)
|
||||
.innerJoin(entryVideoJoin, eq(entryVideoJoin.videoPk, videos.pk))
|
||||
.innerJoin(entries, eq(entries.pk, entryVideoJoin.entryPk))
|
||||
.innerJoin(shows, eq(shows.pk, entries.showPk)),
|
||||
);
|
||||
|
||||
const guess = db.$with("guess").as(
|
||||
db
|
||||
.select({
|
||||
guess: years.guess,
|
||||
years: jsonbObjectAgg(
|
||||
years.year,
|
||||
jsonbBuildObject({ id: years.id, slug: years.slug }),
|
||||
).as("years"),
|
||||
})
|
||||
.from(years)
|
||||
.groupBy(years.guess),
|
||||
);
|
||||
|
||||
const [{ guesses }] = await db
|
||||
.with(years, guess)
|
||||
.select({
|
||||
guesses: jsonbObjectAgg<Record<string, Resource>>(
|
||||
guess.guess,
|
||||
guess.years,
|
||||
),
|
||||
})
|
||||
.from(guess);
|
||||
|
||||
const paths = await db.select({ path: videos.path }).from(videos);
|
||||
|
||||
const unmatched = await db
|
||||
.select({ path: videos.path })
|
||||
.from(videos)
|
||||
.where(
|
||||
notExists(
|
||||
db
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.videoPk, videos.pk)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
paths: paths.map((x) => x.path),
|
||||
guesses: guesses ?? {},
|
||||
unmatched: unmatched.map((x) => x.path),
|
||||
};
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all video registered & guessed made" },
|
||||
response: {
|
||||
200: Guesses,
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"unknowns",
|
||||
async ({ query: { sort, query, limit, after }, request: { url } }) => {
|
||||
const ret = await db
|
||||
.select()
|
||||
.from(videos)
|
||||
.where(
|
||||
and(
|
||||
notExists(
|
||||
db
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(videos.pk, entryVideoJoin.videoPk)),
|
||||
),
|
||||
query
|
||||
? or(
|
||||
sql`${videos.path} %> ${query}::text`,
|
||||
sql`${videos.guess}->'title' %> ${query}::text`,
|
||||
)
|
||||
: undefined,
|
||||
keysetPaginate({ after, sort }),
|
||||
),
|
||||
)
|
||||
.orderBy(...(query ? [] : sortToSql(sort)), videos.pk)
|
||||
.limit(limit);
|
||||
return createPage(ret, { url, sort, limit });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get unknown/unmatch videos." },
|
||||
query: t.Object({
|
||||
sort: Sort(
|
||||
{ createdAt: videos.createdAt, path: videos.path },
|
||||
{ default: ["-createdAt"], tablePk: videos.pk },
|
||||
),
|
||||
query: t.Optional(t.String({ description: description.query })),
|
||||
limit: t.Integer({
|
||||
minimum: 1,
|
||||
maximum: 250,
|
||||
default: 50,
|
||||
description: "Max page size.",
|
||||
}),
|
||||
after: t.Optional(t.String({ description: description.after })),
|
||||
}),
|
||||
response: {
|
||||
200: Page(Video),
|
||||
422: KError,
|
||||
},
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"",
|
||||
async ({ body, error }) => {
|
||||
const oldRet = await db
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
pk: videos.pk,
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
guess: videos.guess,
|
||||
});
|
||||
return error(201, oldRet);
|
||||
return await db.transaction(async (tx) => {
|
||||
let vids: { pk: number; id: string; path: string }[] = [];
|
||||
try {
|
||||
vids = await tx
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoUpdate({
|
||||
target: [videos.path],
|
||||
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
||||
})
|
||||
.returning({
|
||||
pk: videos.pk,
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isUniqueConstraint(e)) throw e;
|
||||
return error(409, {
|
||||
status: 409,
|
||||
message: comment`
|
||||
Invalid rendering. A video with the same (rendering, part, version) combo
|
||||
(but with a different path) already exists in db.
|
||||
|
||||
// TODO: this is a huge untested wip
|
||||
// const vidsI = db.$with("vidsI").as(
|
||||
// db.insert(videos).values(body).onConflictDoNothing().returning({
|
||||
// pk: videos.pk,
|
||||
// id: videos.id,
|
||||
// path: videos.path,
|
||||
// guess: videos.guess,
|
||||
// }),
|
||||
// );
|
||||
//
|
||||
// const findEntriesQ = db
|
||||
// .select({
|
||||
// guess: videos.guess,
|
||||
// entryPk: entries.pk,
|
||||
// showSlug: shows.slug,
|
||||
// // TODO: handle extras here
|
||||
// // guessit can't know if an episode is a special or not. treat specials like a normal episode.
|
||||
// kind: sql`
|
||||
// case when ${entries.kind} = 'movie' then 'movie' else 'episode' end
|
||||
// `.as("kind"),
|
||||
// season: entries.seasonNumber,
|
||||
// episode: entries.episodeNumber,
|
||||
// })
|
||||
// .from(entries)
|
||||
// .leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk))
|
||||
// .leftJoin(videos, eq(videos.pk, entryVideoJoin.video))
|
||||
// .leftJoin(shows, eq(shows.pk, entries.showPk))
|
||||
// .as("find_entries");
|
||||
//
|
||||
// const hasRenderingQ = db
|
||||
// .select()
|
||||
// .from(entryVideoJoin)
|
||||
// .where(eq(entryVideoJoin.entry, findEntriesQ.entryPk));
|
||||
//
|
||||
// const ret = await db
|
||||
// .with(vidsI)
|
||||
// .insert(entryVideoJoin)
|
||||
// .select(
|
||||
// db
|
||||
// .select({
|
||||
// entry: findEntriesQ.entryPk,
|
||||
// video: vidsI.pk,
|
||||
// slug: computeVideoSlug(
|
||||
// findEntriesQ.showSlug,
|
||||
// sql`exists(${hasRenderingQ})`,
|
||||
// ),
|
||||
// })
|
||||
// .from(vidsI)
|
||||
// .leftJoin(
|
||||
// findEntriesQ,
|
||||
// and(
|
||||
// eq(
|
||||
// sql`${findEntriesQ.guess}->'title'`,
|
||||
// sql`${vidsI.guess}->'title'`,
|
||||
// ),
|
||||
// // TODO: find if @> with a jsonb created on the fly is
|
||||
// // better than multiples checks
|
||||
// sql`${vidsI.guess} @> {"kind": }::jsonb`,
|
||||
// inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`),
|
||||
// inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`),
|
||||
// inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .onConflictDoNothing()
|
||||
// .returning({
|
||||
// slug: entryVideoJoin.slug,
|
||||
// entryPk: entryVideoJoin.entry,
|
||||
// id: vidsI.id,
|
||||
// path: vidsI.path,
|
||||
// });
|
||||
// return error(201, ret as any);
|
||||
rendering should be computed by the sha of your path (excluding only the version & part numbers)
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
const vidEntries = body.flatMap((x) => {
|
||||
if (!x.for) return [];
|
||||
return x.for.map((e) => ({
|
||||
video: vids.find((v) => v.path === x.path)!.pk,
|
||||
path: x.path,
|
||||
entry: {
|
||||
...e,
|
||||
movie:
|
||||
"movie" in e
|
||||
? isUuid(e.movie)
|
||||
? { id: e.movie }
|
||||
: { slug: e.movie }
|
||||
: undefined,
|
||||
serie:
|
||||
"serie" in e
|
||||
? isUuid(e.serie)
|
||||
? { id: e.serie }
|
||||
: { slug: e.serie }
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
if (!vidEntries.length) {
|
||||
return error(
|
||||
201,
|
||||
vids.map((x) => ({ id: x.id, path: x.path, entries: [] })),
|
||||
);
|
||||
}
|
||||
|
||||
const entriesQ = tx
|
||||
.select({
|
||||
pk: entries.pk,
|
||||
id: entries.id,
|
||||
slug: entries.slug,
|
||||
kind: entries.kind,
|
||||
seasonNumber: entries.seasonNumber,
|
||||
episodeNumber: entries.episodeNumber,
|
||||
order: entries.order,
|
||||
showId: sql`${shows.id}`.as("showId"),
|
||||
showSlug: sql`${shows.slug}`.as("showSlug"),
|
||||
externalId: entries.externalId,
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(shows, eq(entries.showPk, shows.pk))
|
||||
.as("entriesQ");
|
||||
|
||||
const hasRenderingQ = tx
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.entryPk, entriesQ.pk));
|
||||
|
||||
const ret = await tx
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
tx
|
||||
.selectDistinctOn([entriesQ.pk, videos.pk], {
|
||||
entryPk: entriesQ.pk,
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
entriesQ.slug,
|
||||
sql`exists(${hasRenderingQ})`,
|
||||
),
|
||||
})
|
||||
.from(
|
||||
values(vidEntries, {
|
||||
video: "integer",
|
||||
entry: "jsonb",
|
||||
}).as("j"),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.pk, sql`j.video`))
|
||||
.innerJoin(
|
||||
entriesQ,
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ? 'slug'`,
|
||||
eq(entriesQ.slug, sql`j.entry->>'slug'`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'movie'`,
|
||||
or(
|
||||
eq(
|
||||
entriesQ.showId,
|
||||
sql`(j.entry #>> '{movie, id}')::uuid`,
|
||||
),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{movie, slug}'`),
|
||||
),
|
||||
eq(entriesQ.kind, "movie"),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'serie'`,
|
||||
or(
|
||||
eq(
|
||||
entriesQ.showId,
|
||||
sql`(j.entry #>> '{serie, id}')::uuid`,
|
||||
),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{serie, slug}'`),
|
||||
),
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ?& array['season', 'episode']`,
|
||||
eq(
|
||||
entriesQ.seasonNumber,
|
||||
sql`(j.entry->>'season')::integer`,
|
||||
),
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'episode')::integer`,
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'order'`,
|
||||
eq(entriesQ.order, sql`(j.entry->>'order')::float`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'special'`,
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'special')::integer`,
|
||||
),
|
||||
eq(entriesQ.kind, "special"),
|
||||
),
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'externalId'`,
|
||||
sql`j.entry->'externalId' <@ ${entriesQ.externalId}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
target: [entryVideoJoin.entryPk, entryVideoJoin.videoPk],
|
||||
// this is basically a `.onConflictDoNothing()` but we want `returning` to give us the existing data
|
||||
set: { entryPk: sql`excluded.entry_pk` },
|
||||
})
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
videoPk: entryVideoJoin.videoPk,
|
||||
});
|
||||
const entr = ret.reduce(
|
||||
(acc, x) => {
|
||||
acc[x.videoPk] ??= [];
|
||||
acc[x.videoPk].push({ slug: x.slug });
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, { slug: string }[]>,
|
||||
);
|
||||
|
||||
const entriesPk = [...new Set(ret.map((x) => x.entryPk))];
|
||||
await updateAvailableCount(
|
||||
tx,
|
||||
tx
|
||||
.selectDistinct({ pk: entries.showPk })
|
||||
.from(entries)
|
||||
.where(eq(entries.pk, sql`any(${sqlarr(entriesPk)})`)),
|
||||
);
|
||||
await updateAvailableSince(tx, entriesPk);
|
||||
|
||||
return error(
|
||||
201,
|
||||
vids.map((x) => ({
|
||||
id: x.id,
|
||||
path: x.path,
|
||||
entries: entr[x.pk] ?? [],
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@ -126,56 +379,61 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
`,
|
||||
},
|
||||
body: t.Array(SeedVideo),
|
||||
response: { 201: t.Array(CreatedVideo) },
|
||||
response: {
|
||||
201: t.Array(CreatedVideo),
|
||||
409: {
|
||||
...KError,
|
||||
description:
|
||||
"Invalid rendering specified. (conflicts with an existing video)",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"",
|
||||
async ({ body }) => {
|
||||
await db.transaction(async (tx) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const vids = tx.$with("vids").as(
|
||||
tx
|
||||
.delete(videos)
|
||||
.where(eq(videos.path, sql`any(${body})`))
|
||||
.returning({ pk: videos.pk }),
|
||||
.where(eq(videos.path, sql`any(${sqlarr(body)})`))
|
||||
.returning({ pk: videos.pk, path: videos.path }),
|
||||
);
|
||||
const evj = alias(entryVideoJoin, "evj");
|
||||
const delEntries = tx.$with("del_entries").as(
|
||||
tx
|
||||
.with(vids)
|
||||
.select({ entry: entryVideoJoin.entryPk })
|
||||
.from(entryVideoJoin)
|
||||
.where(
|
||||
and(
|
||||
inArray(entryVideoJoin.videoPk, tx.select().from(vids)),
|
||||
not(
|
||||
exists(
|
||||
tx
|
||||
.select()
|
||||
.from(evj)
|
||||
.where(
|
||||
and(
|
||||
eq(evj.entryPk, entryVideoJoin.entryPk),
|
||||
not(inArray(evj.videoPk, db.select().from(vids))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const delShows = await tx
|
||||
.with(delEntries)
|
||||
|
||||
const deletedJoin = await tx
|
||||
.with(vids)
|
||||
.select({ entryPk: entryVideoJoin.entryPk, path: vids.path })
|
||||
.from(entryVideoJoin)
|
||||
.rightJoin(vids, eq(vids.pk, entryVideoJoin.videoPk));
|
||||
|
||||
const delEntries = await tx
|
||||
.update(entries)
|
||||
.set({ availableSince: null })
|
||||
.where(inArray(entries.pk, db.select().from(delEntries)))
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
entries.pk,
|
||||
sql`any(${sqlarr(
|
||||
deletedJoin.filter((x) => x.entryPk).map((x) => x.entryPk!),
|
||||
)})`,
|
||||
),
|
||||
notExists(
|
||||
tx
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entries.pk, entryVideoJoin.entryPk)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.returning({ show: entries.showPk });
|
||||
|
||||
await updateAvailableCount(
|
||||
tx,
|
||||
delShows.map((x) => x.show),
|
||||
delEntries.map((x) => x.show),
|
||||
false,
|
||||
);
|
||||
|
||||
return [...new Set(deletedJoin.map((x) => x.path))];
|
||||
});
|
||||
},
|
||||
{
|
||||
@ -186,6 +444,6 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
examples: [bubbleVideo.path],
|
||||
}),
|
||||
),
|
||||
response: { 204: t.Void() },
|
||||
response: { 200: t.Array(t.String()) },
|
||||
},
|
||||
);
|
||||
|
@ -18,7 +18,6 @@ import { image, language, schema } from "./utils";
|
||||
import { entryVideoJoin } from "./videos";
|
||||
|
||||
export const entryType = schema.enum("entry_type", [
|
||||
"unknown",
|
||||
"episode",
|
||||
"movie",
|
||||
"special",
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
@ -34,6 +35,9 @@ export const videos = schema.table(
|
||||
(t) => [
|
||||
check("part_pos", sql`${t.part} >= 0`),
|
||||
check("version_pos", sql`${t.version} >= 0`),
|
||||
unique("rendering_unique")
|
||||
.on(t.rendering, t.part, t.version)
|
||||
.nullsNotDistinct(),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -74,14 +74,22 @@ export function sqlarr(array: unknown[]) {
|
||||
}
|
||||
|
||||
// See https://github.com/drizzle-team/drizzle-orm/issues/4044
|
||||
// TODO: type values (everything is a `text` for now)
|
||||
export function values(items: Record<string, unknown>[]) {
|
||||
const [firstProp, ...props] = Object.keys(items[0]);
|
||||
export function values<K extends string>(
|
||||
items: Record<K, unknown>[],
|
||||
typeInfo: Partial<Record<K, string>> = {},
|
||||
) {
|
||||
if (items[0] === undefined)
|
||||
throw new Error("Invalid values, expecting at least one items");
|
||||
const [firstProp, ...props] = Object.keys(items[0]) as K[];
|
||||
const values = items
|
||||
.map((x) => {
|
||||
.map((x, i) => {
|
||||
let ret = sql`(${x[firstProp]}`;
|
||||
if (i === 0 && typeInfo[firstProp])
|
||||
ret = sql`${ret}::${sql.raw(typeInfo[firstProp])}`;
|
||||
for (const val of props) {
|
||||
ret = sql`${ret}, ${x[val]}`;
|
||||
if (i === 0 && typeInfo[val])
|
||||
ret = sql`${ret}::${sql.raw(typeInfo[val])}`;
|
||||
}
|
||||
return sql`${ret})`;
|
||||
})
|
||||
@ -103,7 +111,10 @@ export const nullif = <T>(val: SQL<T> | Column, eq: SQL<T>) => {
|
||||
return sql<T>`nullif(${val}, ${eq})`;
|
||||
};
|
||||
|
||||
export const jsonbObjectAgg = <T>(key: SQLWrapper, value: SQL<T>) => {
|
||||
export const jsonbObjectAgg = <T>(
|
||||
key: SQLWrapper,
|
||||
value: SQL<T> | SQLWrapper,
|
||||
) => {
|
||||
return sql<
|
||||
Record<string, T>
|
||||
>`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`;
|
||||
@ -131,3 +142,9 @@ export const jsonbBuildObject = <T>(select: JsonFields) => {
|
||||
);
|
||||
return sql<T>`jsonb_build_object(${query})`;
|
||||
};
|
||||
|
||||
export const isUniqueConstraint = (e: unknown): boolean => {
|
||||
return (
|
||||
typeof e === "object" && e != null && "code" in e && e.code === "23505"
|
||||
);
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ export const CollectionTranslation = t.Object({
|
||||
logo: t.Nullable(Image),
|
||||
});
|
||||
|
||||
export const Collection = t.Intersect([
|
||||
export const Collection = t.Composite([
|
||||
Resource(),
|
||||
CollectionTranslation,
|
||||
BaseCollection,
|
||||
@ -64,7 +64,7 @@ export const FullCollection = t.Intersect([
|
||||
]);
|
||||
export type FullCollection = Prettify<typeof FullCollection.static>;
|
||||
|
||||
export const SeedCollection = t.Intersect([
|
||||
export const SeedCollection = t.Composite([
|
||||
t.Omit(BaseCollection, ["kind", "startAir", "endAir", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
@ -72,7 +72,7 @@ export const SeedCollection = t.Intersect([
|
||||
description: "The language code this collection's items were made in.",
|
||||
}),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Composite([
|
||||
t.Omit(CollectionTranslation, [
|
||||
"poster",
|
||||
"thumbnail",
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseEpisode = t.Intersect([
|
||||
export const BaseEpisode = t.Composite([
|
||||
t.Object({
|
||||
kind: t.Literal("episode"),
|
||||
order: t.Number({ minimum: 1, description: "Absolute playback order." }),
|
||||
@ -23,7 +23,7 @@ export const BaseEpisode = t.Intersect([
|
||||
BaseEntry(),
|
||||
]);
|
||||
|
||||
export const Episode = t.Intersect([
|
||||
export const Episode = t.Composite([
|
||||
Resource(),
|
||||
EntryTranslation(),
|
||||
BaseEpisode,
|
||||
@ -35,7 +35,7 @@ export const Episode = t.Intersect([
|
||||
]);
|
||||
export type Episode = Prettify<typeof Episode.static>;
|
||||
|
||||
export const SeedEpisode = t.Intersect([
|
||||
export const SeedEpisode = t.Composite([
|
||||
t.Omit(BaseEpisode, ["thumbnail", "nextRefresh"]),
|
||||
t.Object({
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
|
@ -16,7 +16,7 @@ export const ExtraType = t.UnionEnum([
|
||||
]);
|
||||
export type ExtraType = typeof ExtraType.static;
|
||||
|
||||
export const BaseExtra = t.Intersect(
|
||||
export const BaseExtra = t.Composite(
|
||||
[
|
||||
t.Object({
|
||||
kind: ExtraType,
|
||||
@ -32,7 +32,7 @@ export const BaseExtra = t.Intersect(
|
||||
},
|
||||
);
|
||||
|
||||
export const Extra = t.Intersect([
|
||||
export const Extra = t.Composite([
|
||||
Resource(),
|
||||
BaseExtra,
|
||||
t.Object({
|
||||
@ -42,7 +42,7 @@ export const Extra = t.Intersect([
|
||||
]);
|
||||
export type Extra = Prettify<typeof Extra.static>;
|
||||
|
||||
export const SeedExtra = t.Intersect([
|
||||
export const SeedExtra = t.Composite([
|
||||
t.Omit(BaseExtra, ["thumbnail"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
|
@ -3,7 +3,6 @@ import { Episode, SeedEpisode } from "./episode";
|
||||
import type { Extra } from "./extra";
|
||||
import { MovieEntry, SeedMovieEntry } from "./movie-entry";
|
||||
import { SeedSpecial, Special } from "./special";
|
||||
import type { UnknownEntry } from "./unknown-entry";
|
||||
|
||||
export const Entry = t.Union([Episode, MovieEntry, Special]);
|
||||
export type Entry = Episode | MovieEntry | Special;
|
||||
@ -11,10 +10,9 @@ export type Entry = Episode | MovieEntry | Special;
|
||||
export const SeedEntry = t.Union([SeedEpisode, SeedMovieEntry, SeedSpecial]);
|
||||
export type SeedEntry = SeedEpisode | SeedMovieEntry | SeedSpecial;
|
||||
|
||||
export type EntryKind = Entry["kind"] | Extra["kind"] | UnknownEntry["kind"];
|
||||
export type EntryKind = Entry["kind"] | Extra["kind"];
|
||||
|
||||
export * from "./episode";
|
||||
export * from "./movie-entry";
|
||||
export * from "./special";
|
||||
export * from "./extra";
|
||||
export * from "./unknown-entry";
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseMovieEntry = t.Intersect(
|
||||
export const BaseMovieEntry = t.Composite(
|
||||
[
|
||||
t.Object({
|
||||
kind: t.Literal("movie"),
|
||||
@ -33,7 +33,7 @@ export const BaseMovieEntry = t.Intersect(
|
||||
},
|
||||
);
|
||||
|
||||
export const MovieEntryTranslation = t.Intersect([
|
||||
export const MovieEntryTranslation = t.Composite([
|
||||
EntryTranslation(),
|
||||
t.Object({
|
||||
tagline: t.Nullable(t.String()),
|
||||
@ -41,7 +41,7 @@ export const MovieEntryTranslation = t.Intersect([
|
||||
}),
|
||||
]);
|
||||
|
||||
export const MovieEntry = t.Intersect([
|
||||
export const MovieEntry = t.Composite([
|
||||
Resource(),
|
||||
MovieEntryTranslation,
|
||||
BaseMovieEntry,
|
||||
@ -53,7 +53,7 @@ export const MovieEntry = t.Intersect([
|
||||
]);
|
||||
export type MovieEntry = Prettify<typeof MovieEntry.static>;
|
||||
|
||||
export const SeedMovieEntry = t.Intersect([
|
||||
export const SeedMovieEntry = t.Composite([
|
||||
t.Omit(BaseMovieEntry, ["thumbnail", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.Optional(t.String({ format: "slug" })),
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseSpecial = t.Intersect(
|
||||
export const BaseSpecial = t.Composite(
|
||||
[
|
||||
t.Object({
|
||||
kind: t.Literal("special"),
|
||||
@ -33,7 +33,7 @@ export const BaseSpecial = t.Intersect(
|
||||
},
|
||||
);
|
||||
|
||||
export const Special = t.Intersect([
|
||||
export const Special = t.Composite([
|
||||
Resource(),
|
||||
EntryTranslation(),
|
||||
BaseSpecial,
|
||||
@ -45,7 +45,7 @@ export const Special = t.Intersect([
|
||||
]);
|
||||
export type Special = Prettify<typeof Special.static>;
|
||||
|
||||
export const SeedSpecial = t.Intersect([
|
||||
export const SeedSpecial = t.Composite([
|
||||
t.Omit(BaseSpecial, ["thumbnail", "nextRefresh"]),
|
||||
t.Object({
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
|
@ -1,38 +0,0 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { bubbleImages, registerExamples, youtubeExample } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import { DbMetadata, Resource } from "../utils";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseUnknownEntry = t.Intersect(
|
||||
[
|
||||
t.Object({
|
||||
kind: t.Literal("unknown"),
|
||||
}),
|
||||
t.Omit(BaseEntry(), ["airDate"]),
|
||||
],
|
||||
{
|
||||
description: comment`
|
||||
A video not releated to any series or movie. This can be due to a matching error but it can be a youtube
|
||||
video or any other video content.
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
export const UnknownEntryTranslation = t.Omit(EntryTranslation(), [
|
||||
"description",
|
||||
]);
|
||||
|
||||
export const UnknownEntry = t.Intersect([
|
||||
Resource(),
|
||||
UnknownEntryTranslation,
|
||||
BaseUnknownEntry,
|
||||
t.Object({
|
||||
progress: t.Omit(Progress, ["videoId"]),
|
||||
}),
|
||||
DbMetadata,
|
||||
]);
|
||||
export type UnknownEntry = Prettify<typeof UnknownEntry.static>;
|
||||
|
||||
registerExamples(UnknownEntry, { ...youtubeExample, ...bubbleImages });
|
@ -3,7 +3,6 @@ import type { Video } from "~/models/video";
|
||||
|
||||
export const bubbleVideo: Video = {
|
||||
id: "3cd436ee-01ff-4f45-ba98-62aabeb22f25",
|
||||
slug: "bubble",
|
||||
path: "/video/Bubble/Bubble (2022).mkv",
|
||||
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
|
||||
part: null,
|
||||
|
@ -3,7 +3,6 @@ import type { Video } from "../video";
|
||||
|
||||
export const dune1984Video: Video = {
|
||||
id: "d1a62b87-9cfd-4f9c-9ad7-21f9b7fa6290",
|
||||
slug: "dune-1984",
|
||||
path: "/video/Dune_1984/Dune (1984).mkv",
|
||||
rendering: "ea3a0f8f2f2c5b61a07f61e4e8d9f8e01b2b92bcbb6f5ed1151e1f61619c2c0f",
|
||||
part: null,
|
||||
|
@ -3,7 +3,6 @@ import type { Video } from "~/models/video";
|
||||
|
||||
export const duneVideo: Video = {
|
||||
id: "c9a0d02e-6b8e-4ac1-b431-45b022ec0708",
|
||||
slug: "dune",
|
||||
path: "/video/Dune/Dune (2021).mkv",
|
||||
rendering: "f1953a4fb58247efb6c15b76468b6a9d13b4155b02094863b1a4f0c3fbb6db58",
|
||||
part: null,
|
||||
|
@ -35,4 +35,3 @@ export * from "./made-in-abyss";
|
||||
export * from "./dune-1984";
|
||||
export * from "./dune-2021";
|
||||
export * from "./dune-collection";
|
||||
export * from "./others";
|
||||
|
@ -3,7 +3,6 @@ import type { Video } from "~/models/video";
|
||||
|
||||
export const madeInAbyssVideo: Video = {
|
||||
id: "3cd436ee-01ff-4f45-ba98-654282531234",
|
||||
slug: "made-in-abyss-s1e13",
|
||||
path: "/video/Made in abyss S01E13.mkv",
|
||||
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
|
||||
part: null,
|
||||
|
@ -1,10 +0,0 @@
|
||||
import type { UnknownEntry } from "~/models/entry";
|
||||
|
||||
export const youtubeExample: Partial<UnknownEntry> = {
|
||||
kind: "unknown",
|
||||
// idk if we'll keep non-ascii characters or if we can find a way to convert them
|
||||
slug: "lisa-炎-the-first-take",
|
||||
name: "LiSA - 炎 / THE FIRST TAKE",
|
||||
runtime: 10,
|
||||
thumbnail: null,
|
||||
};
|
@ -48,7 +48,7 @@ export const MovieTranslation = t.Object({
|
||||
});
|
||||
export type MovieTranslation = typeof MovieTranslation.static;
|
||||
|
||||
export const Movie = t.Intersect([
|
||||
export const Movie = t.Composite([
|
||||
Resource(),
|
||||
MovieTranslation,
|
||||
BaseMovie,
|
||||
@ -71,7 +71,7 @@ export const FullMovie = t.Intersect([
|
||||
]);
|
||||
export type FullMovie = Prettify<typeof FullMovie.static>;
|
||||
|
||||
export const SeedMovie = t.Intersect([
|
||||
export const SeedMovie = t.Composite([
|
||||
t.Omit(BaseMovie, ["kind", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug", examples: ["bubble"] }),
|
||||
@ -79,7 +79,7 @@ export const SeedMovie = t.Intersect([
|
||||
description: "The language code this movie was made in.",
|
||||
}),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Composite([
|
||||
t.Omit(MovieTranslation, ["poster", "thumbnail", "banner", "logo"]),
|
||||
t.Object({
|
||||
poster: t.Nullable(SeedImage),
|
||||
|
@ -27,7 +27,7 @@ export const SeasonTranslation = t.Object({
|
||||
});
|
||||
export type SeasonTranslation = typeof SeasonTranslation.static;
|
||||
|
||||
export const Season = t.Intersect([
|
||||
export const Season = t.Composite([
|
||||
Resource(),
|
||||
SeasonTranslation,
|
||||
BaseSeason,
|
||||
@ -35,11 +35,11 @@ export const Season = t.Intersect([
|
||||
]);
|
||||
export type Season = Prettify<typeof Season.static>;
|
||||
|
||||
export const SeedSeason = t.Intersect([
|
||||
export const SeedSeason = t.Composite([
|
||||
t.Omit(BaseSeason, ["nextRefresh"]),
|
||||
t.Object({
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Composite([
|
||||
t.Omit(SeasonTranslation, ["poster", "thumbnail", "banner"]),
|
||||
t.Object({
|
||||
poster: t.Nullable(SeedImage),
|
||||
|
@ -58,7 +58,7 @@ export const SerieTranslation = t.Object({
|
||||
});
|
||||
export type SerieTranslation = typeof SerieTranslation.static;
|
||||
|
||||
export const Serie = t.Intersect([
|
||||
export const Serie = t.Composite([
|
||||
Resource(),
|
||||
SerieTranslation,
|
||||
BaseSerie,
|
||||
@ -87,7 +87,7 @@ export const FullSerie = t.Intersect([
|
||||
]);
|
||||
export type FullSerie = Prettify<typeof FullSerie.static>;
|
||||
|
||||
export const SeedSerie = t.Intersect([
|
||||
export const SeedSerie = t.Composite([
|
||||
t.Omit(BaseSerie, ["kind", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
@ -95,7 +95,7 @@ export const SeedSerie = t.Intersect([
|
||||
description: "The language code this serie was made in.",
|
||||
}),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Composite([
|
||||
t.Omit(SerieTranslation, ["poster", "thumbnail", "banner", "logo"]),
|
||||
t.Object({
|
||||
poster: t.Nullable(SeedImage),
|
||||
|
@ -4,7 +4,8 @@ import { Movie } from "./movie";
|
||||
import { Serie } from "./serie";
|
||||
|
||||
export const Show = t.Union([
|
||||
t.Intersect([Movie, t.Object({ kind: t.Literal("movie") })]),
|
||||
t.Intersect([Serie, t.Object({ kind: t.Literal("serie") })]),
|
||||
t.Intersect([Collection, t.Object({ kind: t.Literal("collection") })]),
|
||||
t.Intersect([t.Object({ kind: t.Literal("movie") }), Movie]),
|
||||
t.Intersect([t.Object({ kind: t.Literal("serie") }), Serie]),
|
||||
t.Intersect([t.Object({ kind: t.Literal("collection") }), Collection]),
|
||||
]);
|
||||
export type Show = typeof Show.static;
|
||||
|
@ -28,19 +28,19 @@ const StaffData = t.Object({
|
||||
image: t.Nullable(Image),
|
||||
externalId: ExternalId(),
|
||||
});
|
||||
export const Staff = t.Intersect([Resource(), StaffData, DbMetadata]);
|
||||
export const Staff = t.Composite([Resource(), StaffData, DbMetadata]);
|
||||
export type Staff = typeof Staff.static;
|
||||
|
||||
export const SeedStaff = t.Intersect([
|
||||
export const SeedStaff = t.Composite([
|
||||
t.Omit(Role, ["character"]),
|
||||
t.Object({
|
||||
character: t.Intersect([
|
||||
character: t.Composite([
|
||||
t.Omit(Character, ["image"]),
|
||||
t.Object({
|
||||
image: t.Nullable(SeedImage),
|
||||
}),
|
||||
]),
|
||||
staff: t.Intersect([
|
||||
staff: t.Composite([
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
image: t.Nullable(SeedImage),
|
||||
|
@ -14,7 +14,7 @@ export const StudioTranslation = t.Object({
|
||||
});
|
||||
export type StudioTranslation = typeof StudioTranslation.static;
|
||||
|
||||
export const Studio = t.Intersect([
|
||||
export const Studio = t.Composite([
|
||||
Resource(),
|
||||
StudioTranslation,
|
||||
BaseStudio,
|
||||
@ -22,12 +22,12 @@ export const Studio = t.Intersect([
|
||||
]);
|
||||
export type Studio = Prettify<typeof Studio.static>;
|
||||
|
||||
export const SeedStudio = t.Intersect([
|
||||
export const SeedStudio = t.Composite([
|
||||
BaseStudio,
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Composite([
|
||||
t.Omit(StudioTranslation, ["logo"]),
|
||||
t.Object({
|
||||
logo: t.Nullable(SeedImage),
|
||||
|
@ -4,7 +4,7 @@ import type { SelectResultField } from "drizzle-orm/query-builders/select.types"
|
||||
export const buildRelations = <
|
||||
R extends string,
|
||||
P extends object,
|
||||
Rel extends Record<R, (languages: P) => Subquery>,
|
||||
Rel extends Record<R, (params: P) => Subquery>,
|
||||
>(
|
||||
enabled: R[],
|
||||
relations: Rel,
|
||||
|
@ -13,6 +13,7 @@ export const Resource = () =>
|
||||
id: t.String({ format: "uuid" }),
|
||||
slug: t.String({ format: "slug" }),
|
||||
});
|
||||
export type Resource = ReturnType<typeof Resource>["static"];
|
||||
|
||||
const checker = TypeCompiler.Compile(t.String({ format: "uuid" }));
|
||||
export const isUuid = (id: string) => checker.Check(id);
|
||||
|
@ -1,7 +1,21 @@
|
||||
import { PatternStringExact } from "@sinclair/typebox";
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { bubbleVideo, registerExamples } from "./examples";
|
||||
import { DbMetadata, Resource } from "./utils";
|
||||
import { ExtraType } from "./entry/extra";
|
||||
import { bubble, bubbleVideo, registerExamples } from "./examples";
|
||||
import { DbMetadata, EpisodeId, ExternalId, Resource } from "./utils";
|
||||
|
||||
const ExternalIds = t.Record(
|
||||
t.String(),
|
||||
t.Omit(
|
||||
t.Union([
|
||||
EpisodeId.patternProperties[PatternStringExact],
|
||||
ExternalId().patternProperties[PatternStringExact],
|
||||
]),
|
||||
["link"],
|
||||
),
|
||||
);
|
||||
type ExternalIds = typeof ExternalIds.static;
|
||||
|
||||
export const Guess = t.Recursive((Self) =>
|
||||
t.Object(
|
||||
@ -10,8 +24,9 @@ export const Guess = t.Recursive((Self) =>
|
||||
year: t.Optional(t.Array(t.Integer(), { default: [] })),
|
||||
season: t.Optional(t.Array(t.Integer(), { default: [] })),
|
||||
episode: t.Optional(t.Array(t.Integer(), { default: [] })),
|
||||
// TODO: maybe replace "extra" with the `extraKind` value (aka behind-the-scene, trailer, etc)
|
||||
kind: t.Optional(t.UnionEnum(["episode", "movie", "extra"])),
|
||||
extraKind: t.Optional(ExtraType),
|
||||
externalId: t.Optional(ExternalIds),
|
||||
|
||||
from: t.String({
|
||||
description: "Name of the tool that made the guess",
|
||||
@ -66,14 +81,115 @@ export const SeedVideo = t.Object({
|
||||
}),
|
||||
|
||||
guess: Guess,
|
||||
});
|
||||
export type SeedVideo = typeof SeedVideo.static;
|
||||
|
||||
export const Video = t.Intersect([Resource(), SeedVideo, DbMetadata]);
|
||||
for: t.Optional(
|
||||
t.Array(
|
||||
t.Union([
|
||||
t.Object({
|
||||
slug: t.String({
|
||||
format: "slug",
|
||||
examples: ["made-in-abyss-dawn-of-the-deep-soul"],
|
||||
}),
|
||||
}),
|
||||
t.Object({
|
||||
externalId: ExternalIds,
|
||||
}),
|
||||
t.Object({
|
||||
movie: t.Union([
|
||||
t.String({ format: "uuid" }),
|
||||
t.String({ format: "slug", examples: ["bubble"] }),
|
||||
]),
|
||||
}),
|
||||
t.Object({
|
||||
serie: t.Union([
|
||||
t.String({ format: "uuid" }),
|
||||
t.String({ format: "slug", examples: ["made-in-abyss"] }),
|
||||
]),
|
||||
season: t.Integer({ minimum: 1 }),
|
||||
episode: t.Integer(),
|
||||
}),
|
||||
t.Object({
|
||||
serie: t.Union([
|
||||
t.String({ format: "uuid" }),
|
||||
t.String({ format: "slug", examples: ["made-in-abyss"] }),
|
||||
]),
|
||||
order: t.Number(),
|
||||
}),
|
||||
t.Object({
|
||||
serie: t.Union([
|
||||
t.String({ format: "uuid" }),
|
||||
t.String({ format: "slug", examples: ["made-in-abyss"] }),
|
||||
]),
|
||||
special: t.Integer(),
|
||||
}),
|
||||
]),
|
||||
{ default: [] },
|
||||
),
|
||||
),
|
||||
});
|
||||
export type SeedVideo = Prettify<typeof SeedVideo.static>;
|
||||
|
||||
export const Video = t.Composite([
|
||||
t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
}),
|
||||
t.Omit(SeedVideo, ["for"]),
|
||||
DbMetadata,
|
||||
]);
|
||||
export type Video = Prettify<typeof Video.static>;
|
||||
|
||||
// type used in entry responses
|
||||
export const EmbeddedVideo = t.Omit(Video, ["guess", "createdAt", "updatedAt"]);
|
||||
// type used in entry responses (the slug comes from the entryVideoJoin)
|
||||
export const EmbeddedVideo = t.Composite(
|
||||
[
|
||||
t.Object({ slug: t.String({ format: "slug" }) }),
|
||||
t.Omit(Video, ["guess", "createdAt", "updatedAt"]),
|
||||
],
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
export type EmbeddedVideo = Prettify<typeof EmbeddedVideo.static>;
|
||||
|
||||
registerExamples(Video, bubbleVideo);
|
||||
registerExamples(SeedVideo, {
|
||||
...bubbleVideo,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
dataId: bubble.externalId.themoviedatabase.dataId,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const Guesses = t.Object({
|
||||
paths: t.Array(t.String()),
|
||||
guesses: t.Record(
|
||||
t.String(),
|
||||
t.Record(t.String({ pattern: "^([1-9][0-9]{3})|unknown$" }), Resource()),
|
||||
),
|
||||
unmatched: t.Array(t.String()),
|
||||
});
|
||||
export type Guesses = typeof Guesses.static;
|
||||
|
||||
registerExamples(Guesses, {
|
||||
paths: [
|
||||
"/videos/Evangelion S01E02.mkv",
|
||||
"/videos/Evangelion (1995) S01E26.mkv",
|
||||
"/videos/SomeUnknownThing.mkv",
|
||||
],
|
||||
guesses: {
|
||||
Evangelion: {
|
||||
unknown: {
|
||||
id: "43b742f5-9ce6-467d-ad29-74460624020a",
|
||||
slug: "evangelion",
|
||||
},
|
||||
"1995": {
|
||||
id: "43b742f5-9ce6-467d-ad29-74460624020a",
|
||||
slug: "evangelion",
|
||||
},
|
||||
},
|
||||
},
|
||||
unmatched: ["/videos/SomeUnknownThing.mkv"],
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ export const WatchlistStatus = t.UnionEnum([
|
||||
"dropped",
|
||||
"planned",
|
||||
]);
|
||||
export type WatchlistStatus = typeof WatchlistStatus.static;
|
||||
|
||||
export const SerieWatchStatus = t.Object({
|
||||
status: WatchlistStatus,
|
||||
@ -20,7 +21,7 @@ export const SerieWatchStatus = t.Object({
|
||||
});
|
||||
export type SerieWatchStatus = typeof SerieWatchStatus.static;
|
||||
|
||||
export const MovieWatchStatus = t.Intersect([
|
||||
export const MovieWatchStatus = t.Composite([
|
||||
t.Omit(SerieWatchStatus, ["startedAt", "seenCount"]),
|
||||
t.Object({
|
||||
percent: t.Integer({
|
||||
|
@ -17,3 +17,29 @@ export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
||||
|
||||
export const getVideos = async () => {
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("videos"), {
|
||||
method: "GET",
|
||||
headers: await getJwtHeaders(),
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
||||
|
||||
export const deleteVideo = async (paths: string[]) => {
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("videos"), {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(paths),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getJwtHeaders()),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
||||
|
@ -1,29 +1,53 @@
|
||||
import { db, migrate } from "~/db";
|
||||
import { profiles, shows } from "~/db/schema";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
import { createSerie, getSerie, setSerieStatus } from "./helpers";
|
||||
import { getJwtHeaders } from "./helpers/jwt";
|
||||
import { bubble, madeInAbyss } from "~/models/examples";
|
||||
import { createMovie, createSerie, createVideo, getVideos } from "./helpers";
|
||||
|
||||
// test file used to run manually using `bun tests/manual.ts`
|
||||
// run those before running this script
|
||||
// export JWT_SECRET="this is a secret";
|
||||
// export JWT_ISSUER="https://kyoo.zoriya.dev";
|
||||
|
||||
await migrate();
|
||||
await db.delete(shows);
|
||||
await db.delete(profiles);
|
||||
|
||||
console.log(await getJwtHeaders());
|
||||
|
||||
const [_, ser] = await createSerie(madeInAbyss);
|
||||
console.log(ser);
|
||||
const [__, ret] = await setSerieStatus(madeInAbyss.slug, {
|
||||
status: "watching",
|
||||
startedAt: "2024-12-21",
|
||||
completedAt: "2024-12-21",
|
||||
seenCount: 2,
|
||||
score: 85,
|
||||
});
|
||||
console.log(ret);
|
||||
|
||||
const [___, got] = await getSerie(madeInAbyss.slug, {});
|
||||
console.log(JSON.stringify(got, undefined, 4));
|
||||
const [__, mov] = await createMovie(bubble);
|
||||
const [resp, body] = await createVideo([
|
||||
{
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "sha2",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||
},
|
||||
{
|
||||
guess: {
|
||||
title: "mia",
|
||||
season: [2],
|
||||
episode: [1],
|
||||
year: [2017],
|
||||
from: "test",
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia 2017 s2e1.mkv",
|
||||
rendering: "sha8",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
|
||||
},
|
||||
{
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha5",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
},
|
||||
]);
|
||||
console.log(body);
|
||||
const [___, ret] = await getVideos();
|
||||
console.log(JSON.stringify(ret, undefined, 4));
|
||||
|
||||
process.exit(0);
|
||||
|
@ -159,7 +159,7 @@ describe("Get movie", () => {
|
||||
expect(body.videos).toBeArrayOfSize(bubble.videos!.length);
|
||||
expect(body.videos[0]).toMatchObject({
|
||||
path: bubbleVideo.path,
|
||||
slug: bubbleVideo.slug,
|
||||
slug: bubble.slug,
|
||||
version: bubbleVideo.version,
|
||||
rendering: bubbleVideo.rendering,
|
||||
part: bubbleVideo.part,
|
||||
|
@ -384,7 +384,7 @@ describe("Movie seeding", () => {
|
||||
path: "/video/bubble3.mkv",
|
||||
part: null,
|
||||
version: 1,
|
||||
rendering: "oeunhtoeuth",
|
||||
rendering: "oeunhtoeuthoeu",
|
||||
guess: { title: "bubble", from: "test" },
|
||||
},
|
||||
{
|
||||
|
@ -48,7 +48,7 @@ describe("Get entries", () => {
|
||||
expect(body.items[0].videos).toBeArrayOfSize(1);
|
||||
expect(body.items[0].videos[0]).toMatchObject({
|
||||
path: madeInAbyssVideo.path,
|
||||
slug: madeInAbyssVideo.slug,
|
||||
slug: `${madeInAbyss.slug}-s1e13`,
|
||||
version: madeInAbyssVideo.version,
|
||||
rendering: madeInAbyssVideo.rendering,
|
||||
part: madeInAbyssVideo.part,
|
||||
@ -63,7 +63,7 @@ describe("Get entries", () => {
|
||||
expect(body.items[0].videos).toBeArrayOfSize(1);
|
||||
expect(body.items[0].videos[0]).toMatchObject({
|
||||
path: madeInAbyssVideo.path,
|
||||
slug: madeInAbyssVideo.slug,
|
||||
slug: `${madeInAbyss.slug}-s1e13`,
|
||||
version: madeInAbyssVideo.version,
|
||||
rendering: madeInAbyssVideo.rendering,
|
||||
part: madeInAbyssVideo.part,
|
||||
|
@ -57,7 +57,7 @@ describe("Serie seeding", () => {
|
||||
],
|
||||
evj: [
|
||||
expect.objectContaining({
|
||||
slug: madeInAbyssVideo.slug,
|
||||
slug: `${madeInAbyss.slug}-s1e13`,
|
||||
video: expect.objectContaining({ path: madeInAbyssVideo.path }),
|
||||
}),
|
||||
],
|
||||
|
236
api/tests/videos/getdel.test.ts
Normal file
236
api/tests/videos/getdel.test.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createMovie,
|
||||
createSerie,
|
||||
createVideo,
|
||||
deleteVideo,
|
||||
getVideos,
|
||||
} from "tests/helpers";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { entries, shows, videos } from "~/db/schema";
|
||||
import { bubble, madeInAbyss } from "~/models/examples";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
await db.delete(videos);
|
||||
|
||||
let [ret, body] = await createSerie(madeInAbyss);
|
||||
expectStatus(ret, body).toBe(201);
|
||||
[ret, body] = await createMovie(bubble);
|
||||
expectStatus(ret, body).toBe(201);
|
||||
|
||||
[ret, body] = await createVideo([
|
||||
{
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "sha2",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||
},
|
||||
{
|
||||
guess: {
|
||||
title: "mia",
|
||||
season: [2],
|
||||
episode: [1],
|
||||
year: [2017],
|
||||
from: "test",
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia 2017 s2e1.mkv",
|
||||
rendering: "sha8",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
|
||||
},
|
||||
{
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha5",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
},
|
||||
]);
|
||||
expectStatus(ret, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(3);
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(body[1].entries).toBeArrayOfSize(1);
|
||||
expect(body[2].entries).toBeArrayOfSize(1);
|
||||
|
||||
const items = await db.query.shows.findMany();
|
||||
expect(items.find((x) => x.slug === "bubble")!.availableCount).toBe(1);
|
||||
expect(items.find((x) => x.slug === "made-in-abyss")!.availableCount).toBe(2);
|
||||
|
||||
const etrs = await db.query.entries.findMany({
|
||||
where: eq(
|
||||
entries.showPk,
|
||||
items.find((x) => x.slug === "made-in-abyss")!.pk,
|
||||
),
|
||||
});
|
||||
expect(
|
||||
etrs.find((x) => x.slug === "made-in-abyss-s1e13")!.availableSince,
|
||||
).not.toBe(null);
|
||||
expect(
|
||||
etrs.find((x) => x.slug === "made-in-abyss-s2e1")!.availableSince,
|
||||
).not.toBe(null);
|
||||
});
|
||||
|
||||
describe("Video get/deletion", () => {
|
||||
it("Get current state", async () => {
|
||||
const [resp, body] = await getVideos();
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.guesses).toMatchObject({
|
||||
mia: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
slug: "made-in-abyss",
|
||||
},
|
||||
"2017": {
|
||||
id: expect.any(String),
|
||||
slug: "made-in-abyss",
|
||||
},
|
||||
},
|
||||
bubble: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
slug: "bubble",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("With unknown", async () => {
|
||||
let [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13 unknown test.mkv",
|
||||
rendering: "shanthnth",
|
||||
version: 1,
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
[resp, body] = await getVideos();
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.guesses).toMatchObject({
|
||||
mia: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
slug: "made-in-abyss",
|
||||
},
|
||||
"2017": {
|
||||
id: expect.any(String),
|
||||
slug: "made-in-abyss",
|
||||
},
|
||||
},
|
||||
bubble: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
slug: "bubble",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(body.unmatched).toBeArrayOfSize(1);
|
||||
expect(body.unmatched[0]).toBe("/video/mia s1e13 unknown test.mkv");
|
||||
});
|
||||
|
||||
it("Mismatch title guess", async () => {
|
||||
let [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13 mismatch.mkv",
|
||||
rendering: "mismatch",
|
||||
version: 1,
|
||||
for: [{ movie: "bubble" }],
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
[resp, body] = await getVideos();
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.guesses).toMatchObject({
|
||||
mia: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
// take the latest slug
|
||||
slug: "bubble",
|
||||
},
|
||||
"2017": {
|
||||
id: expect.any(String),
|
||||
slug: "made-in-abyss",
|
||||
},
|
||||
},
|
||||
bubble: {
|
||||
unknown: {
|
||||
id: expect.any(String),
|
||||
slug: "bubble",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Delete video", async () => {
|
||||
const [resp, body] = await deleteVideo(["/video/mia s1e13 mismatch.mkv"]);
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body).toContain("/video/mia s1e13 mismatch.mkv");
|
||||
|
||||
const bubble = await db.query.shows.findFirst({
|
||||
where: eq(shows.slug, "bubble"),
|
||||
});
|
||||
expect(bubble!.availableCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Delete all videos of a movie", async () => {
|
||||
const [resp, body] = await deleteVideo(["/video/bubble.mkv"]);
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body).toContain("/video/bubble.mkv");
|
||||
|
||||
const bubble = await db.query.shows.findFirst({
|
||||
where: eq(shows.slug, "bubble"),
|
||||
});
|
||||
expect(bubble!.availableCount).toBe(0);
|
||||
});
|
||||
|
||||
it("Delete non existing video", async () => {
|
||||
const [resp, body] = await deleteVideo(["/video/toto.mkv"]);
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toBeArrayOfSize(0);
|
||||
});
|
||||
|
||||
it("Delete episodes", async () => {
|
||||
const [resp, body] = await deleteVideo([
|
||||
"/video/mia s1e13.mkv",
|
||||
"/video/mia 2017 s2e1.mkv",
|
||||
]);
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toBeArrayOfSize(2);
|
||||
expect(body).toContain("/video/mia s1e13.mkv");
|
||||
expect(body).toContain("/video/mia 2017 s2e1.mkv");
|
||||
|
||||
const mia = await db.query.shows.findFirst({
|
||||
where: eq(shows.slug, "made-in-abyss"),
|
||||
});
|
||||
expect(mia!.availableCount).toBe(0);
|
||||
|
||||
const etrs = await db.query.entries.findMany({
|
||||
where: eq(entries.showPk, mia!.pk),
|
||||
});
|
||||
expect(
|
||||
etrs.find((x) => x.slug === "made-in-abyss-s1e13")!.availableSince,
|
||||
).toBe(null);
|
||||
expect(
|
||||
etrs.find((x) => x.slug === "made-in-abyss-s2e1")!.availableSince,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("Delete unmatched", async () => {
|
||||
const [resp, body] = await deleteVideo([
|
||||
"/video/mia s1e13 unknown test.mkv",
|
||||
]);
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0]).toBe("/video/mia s1e13 unknown test.mkv");
|
||||
});
|
||||
});
|
566
api/tests/videos/scanner.test.ts
Normal file
566
api/tests/videos/scanner.test.ts
Normal file
@ -0,0 +1,566 @@
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createMovie, createSerie, createVideo } from "tests/helpers";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { entries, shows, videos } from "~/db/schema";
|
||||
import { bubble, madeInAbyss } from "~/models/examples";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
await db.delete(videos);
|
||||
let [ret, body] = await createSerie(madeInAbyss);
|
||||
expectStatus(ret, body).toBe(201);
|
||||
[ret, body] = await createMovie(bubble);
|
||||
expectStatus(ret, body).toBe(201);
|
||||
});
|
||||
|
||||
describe("Video seeding", () => {
|
||||
it("Can create a video without entry", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "unknown", from: "test" },
|
||||
part: null,
|
||||
path: "/video/unknown s1e13.mkv",
|
||||
rendering: "sha",
|
||||
version: 1,
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/unknown s1e13.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "unknown", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(0);
|
||||
expect(vid!.evj).toBeArrayOfSize(0);
|
||||
});
|
||||
|
||||
it("With slug", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "sha2",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s1e13`);
|
||||
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`);
|
||||
});
|
||||
|
||||
it("With movie", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha3",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe(bubble.slug);
|
||||
expect(vid!.evj[0].entry.slug).toBe(bubble.slug);
|
||||
});
|
||||
|
||||
it("Conflicting path", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha4",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe(bubble.slug);
|
||||
expect(vid!.evj[0].entry.slug).toBe(bubble.slug);
|
||||
});
|
||||
|
||||
it("With season/episode", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [2], episode: [1], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s2e1.mkv",
|
||||
rendering: "renderingsha",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
serie: madeInAbyss.slug,
|
||||
season: madeInAbyss.entries[3].seasonNumber!,
|
||||
episode: madeInAbyss.entries[3].episodeNumber!,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s2e1.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s2e1`);
|
||||
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s2e1`);
|
||||
});
|
||||
|
||||
it("With special", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [0], episode: [3], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia sp3.mkv",
|
||||
rendering: "notehu",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
serie: madeInAbyss.slug,
|
||||
special: madeInAbyss.entries[1].number!,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia sp3.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-sp3`);
|
||||
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-sp3`);
|
||||
});
|
||||
|
||||
it("With order", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [0], episode: [3], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia 13.5.mkv",
|
||||
rendering: "notehu2",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
serie: madeInAbyss.slug,
|
||||
order: 13.5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia 13.5.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-dawn-of-the-deep-soul");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-dawn-of-the-deep-soul");
|
||||
});
|
||||
|
||||
it("With external id", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "mia",
|
||||
season: [0],
|
||||
episode: [3],
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 [tmdb=72636].mkv",
|
||||
rendering: "notehu3",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13 [tmdb=72636].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-notehu3");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
|
||||
});
|
||||
|
||||
it("With movie external id", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
path: "/video/bubble [tmdb=912598].mkv",
|
||||
rendering: "onetuh",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble [tmdb=912598].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("bubble-onetuh");
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
it("Different path, same sha", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble invalid-sha.mkv",
|
||||
rendering: "sha",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
|
||||
// conflict with existing video, message will contain an explanation on how to fix this
|
||||
expectStatus(resp, body).toBe(409);
|
||||
expect(body.message).toBeString();
|
||||
});
|
||||
|
||||
it("Two for the same entry", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
path: "/video/bubble ue [tmdb=912598].mkv",
|
||||
rendering: "aoeubnht",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble ue [tmdb=912598].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("bubble-aoeubnht");
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
it("Two for the same entry WITHOUT rendering", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
path: "/video/bubble [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble [tmdb=912598].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("bubble");
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
it("Multi part", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo([
|
||||
{
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: 1,
|
||||
path: "/video/bubble p1 [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: 2,
|
||||
path: "/video/bubble p2 [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(2);
|
||||
expect(body[0].id).toBeString();
|
||||
expect(body[1].id).toBeString();
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(body[1].entries).toBeArrayOfSize(1);
|
||||
|
||||
const entr = (await db.query.entries.findFirst({
|
||||
where: eq(entries.slug, bubble.slug),
|
||||
with: {
|
||||
evj: { with: { video: true } },
|
||||
},
|
||||
}))!;
|
||||
|
||||
expect(entr.evj).toBeArrayOfSize(2);
|
||||
expect(entr.evj[0].video.path).toBe("/video/bubble p1 [tmdb=912598].mkv");
|
||||
|
||||
expect(entr.evj[0].slug).toBe("bubble-p1");
|
||||
expect(entr.evj[1].slug).toBe("bubble-p2");
|
||||
});
|
||||
|
||||
it("Multi entry", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "mia",
|
||||
season: [1, 2],
|
||||
episode: [13, 1],
|
||||
from: "test",
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 & s2e1 [tmdb=72636].mkv",
|
||||
rendering: "notehu5",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
{ serie: madeInAbyss.slug, season: 2, episode: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13 & s2e1 [tmdb=72636].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(2);
|
||||
expect(vid!.evj).toBeArrayOfSize(2);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
|
||||
expect(vid!.evj[1].slug).toBe("made-in-abyss-s2e1");
|
||||
expect(vid!.evj[1].entry.slug).toBe("made-in-abyss-s2e1");
|
||||
});
|
||||
});
|
@ -80,7 +80,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if config.ConnConfig.Host == "" {
|
||||
if config.ConnConfig.Host == "/tmp" {
|
||||
config.ConnConfig.Host = "postgres"
|
||||
}
|
||||
if config.ConnConfig.Database == "" {
|
||||
@ -96,7 +96,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
// by the pgx library. This doesn't cover the case where the system username happens to be in some other part
|
||||
// of the connection string, but this cannot be checked without full connection string parsing.
|
||||
if currentUserName.Username == config.ConnConfig.User && !strings.Contains(connectionString, currentUserName.Username) {
|
||||
config.ConnConfig.User = "kyoo"
|
||||
config.ConnConfig.User = "kyoo"
|
||||
}
|
||||
}
|
||||
if config.ConnConfig.Password == "" {
|
||||
@ -113,7 +113,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
|
||||
db, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not connect to database, check your env variables!")
|
||||
fmt.Printf("Could not connect to database, check your env variables!\n")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
[tool.robotidy]
|
||||
diff = false
|
||||
overwrite = true
|
||||
verbose = false
|
||||
separator = "space"
|
||||
spacecount = 2
|
||||
line_length = 120
|
||||
lineseparator = "native"
|
||||
skip_gitignore = true
|
||||
ignore_git_dir = true
|
||||
configure = [
|
||||
"AddMissingEnd:enabled=True",
|
||||
"NormalizeSeparators:enabled=True",
|
||||
"DiscardEmptySections:enabled=True",
|
||||
"MergeAndOrderSections:enabled=True",
|
||||
"RemoveEmptySettings:enabled=True",
|
||||
"ReplaceEmptyValues:enabled=True",
|
||||
"ReplaceWithVAR:enabled=False",
|
||||
"NormalizeAssignments:enabled=True",
|
||||
"GenerateDocumentation:enabled=False",
|
||||
"OrderSettings:enabled=True",
|
||||
"OrderSettingsSection:enabled=True",
|
||||
"NormalizeTags:enabled=True",
|
||||
"OrderTags:enabled=False",
|
||||
"RenameVariables:enabled=False",
|
||||
"IndentNestedKeywords:enabled=False",
|
||||
"AlignSettingsSection:enabled=True",
|
||||
"AlignVariablesSection:enabled=True",
|
||||
"AlignTemplatedTestCases:enabled=False",
|
||||
"AlignTestCasesSection:enabled=False",
|
||||
"AlignKeywordsSection:enabled=False",
|
||||
"NormalizeNewLines:enabled=True",
|
||||
"NormalizeSectionHeaderName:enabled=True",
|
||||
"NormalizeSettingName:enabled=True",
|
||||
"ReplaceRunKeywordIf:enabled=True",
|
||||
"SplitTooLongLine:enabled=True",
|
||||
"SmartSortKeywords:enabled=False",
|
||||
"RenameTestCases:enabled=False",
|
||||
"RenameKeywords:enabled=False",
|
||||
"ReplaceReturns:enabled=True",
|
||||
"ReplaceBreakContinue:enabled=True",
|
||||
"InlineIf:enabled=True",
|
||||
"Translate:enabled=False",
|
||||
"NormalizeComments:enabled=True",
|
||||
]
|
@ -14,11 +14,12 @@ In order of action:
|
||||
part: number | null,
|
||||
rendering: sha(path except version & part),
|
||||
guess: {
|
||||
kind: movie | episode | trailer | interview | ...,
|
||||
name: string,
|
||||
year: number | null,
|
||||
season?: number,
|
||||
episode?: number,
|
||||
from: "guessit"
|
||||
kind: movie | episode | extra
|
||||
title: string,
|
||||
year?: number[],
|
||||
season?: number[],
|
||||
episode?: number[],
|
||||
...
|
||||
},
|
||||
}
|
||||
@ -32,17 +33,22 @@ In order of action:
|
||||
part: number | null,
|
||||
rendering: sha(path except version & part),
|
||||
guess: {
|
||||
kind: movie | episode | trailer | interview | ...,
|
||||
from: "anilist",
|
||||
kind: movie | episode | extra
|
||||
name: string,
|
||||
year: number | null,
|
||||
season?: number,
|
||||
episodes?: number[],
|
||||
absolutes?: number[],
|
||||
season?: number[],
|
||||
episode?: number[],
|
||||
absolute?: number[],
|
||||
externalId: Record<string, {showId, season, number}[]>,
|
||||
remap: {
|
||||
from: "thexem",
|
||||
oldSeason: number,
|
||||
oldEpisodes: number[],
|
||||
history: {
|
||||
from: "guessit"
|
||||
kind: movie | episode | extra
|
||||
title: string,
|
||||
year?: number,
|
||||
season?: number[],
|
||||
episode?: number[],
|
||||
...
|
||||
},
|
||||
...
|
||||
},
|
||||
@ -67,3 +73,4 @@ In order of action:
|
||||
- Matcher retrieves metadata from the movie/serie + ALL episodes/seasons (from an external provider)
|
||||
- Matcher pushes every metadata to the api (if there are 1000 episodes but only 1 video, still push the 1000 episodes)
|
||||
|
||||
<!-- vim: set noexpandtab : -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user