feat: init

This commit is contained in:
master 2024-02-23 22:07:44 +08:00
commit 96ca2076ed
87 changed files with 11550 additions and 0 deletions

34
.cargo/config.toml Normal file
View File

@ -0,0 +1,34 @@
[alias]
recorder = "run -p recorder --bin recorder_cli -- --environment recorder.development"
recorder-playground = "run -p recorder --example playground -- --environment recorder.development"
[build]
rustflags = ["-Zthreads=8"]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Zthreads=8", "-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zthreads=8", "-Zshare-generics=n"]
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
#[target.x86_64-apple-darwin]
#rustflags = [
# "-Zthreads=8",
# "-C",
# "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
# "-Zshare-generics=y",
#]
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
#[target.aarch64-apple-darwin]
#rustflags = [
# "-Zthreads=8",
# "-C",
# "link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
# "-Zshare-generics=y",
#]

222
.gitignore vendored Normal file
View File

@ -0,0 +1,222 @@
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.idea
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc tests coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
index.js
index.js.map
index.d.ts
index.d.ts.map
**/dist
**/preview_dist
# Added by cargo
/target
/examples/*
!/examples/.gitkeep
/.env
/.env.bk
/.angular
/*.session.sql

5205
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

3
Cargo.toml Normal file
View File

@ -0,0 +1,3 @@
[workspace]
members = ["crates/recorder"]
resolver = "2"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Zhou Yeheng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# KONOBUNGU
Kono Bangumi?

5
apps/proxy/.whistle/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/.backup
**/.recycle_bin
**/.clientid
/properties/*
!/properties/properties

View File

@ -0,0 +1 @@
{"filesOrder":["latestVersion"],"Custom1":"Custom1","Custom2":"Custom2","allowMultipleChoice":true}

View File

@ -0,0 +1,2 @@
^https://webui.konobangu.com/*** http://127.0.0.1:3000/$1
^wss://webui.konobangu.com/*** ws://127.0.0.1:3000/$1

View File

@ -0,0 +1 @@
^https://recorder.konobangu.com/*** http://127.0.0.1:7600/$1

View File

@ -0,0 +1 @@
{"filesOrder":["webui","recorder"],"selectedList":["webui","recorder"],"disabledDefalutRules":true}

View File

13
apps/proxy/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "proxy",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "whistle run -p 8899 -t 30000 -M \"keepXFF|prod|capture\" -D . --no-global-plugins"
},
"keywords": [],
"license": "MIT",
"devDependencies": {
"whistle": "^2.9.61"
}
}

6
apps/webui/.eslintrc.mjs Normal file
View File

@ -0,0 +1,6 @@
export default {
"extends": "next/core-web-vitals",
"rules": {
"@typescript-eslint/consistent-type-imports": "error"
}
}

36
apps/webui/.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
apps/webui/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

BIN
apps/webui/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

22
apps/webui/app/layout.tsx Normal file
View File

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

113
apps/webui/app/page.tsx Normal file
View File

@ -0,0 +1,113 @@
import Image from "next/image";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
);
}

View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

6
apps/webui/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,7 @@
/**
* @type { import("next").NextConfig }
*/
const nextConfig = {
};
export default nextConfig;

37
apps/webui/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "webui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"i18next": "^23.10.0",
"next": "14.1.0",
"next-i18next": "^15.2.0",
"react": "^18",
"react-dom": "^18",
"react-i18next": "^14.0.5",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"grasslands": "^0.1.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,80 @@
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config

40
apps/webui/tsconfig.json Normal file
View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.mjs"
],
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,113 @@
# Loco configuration file documentation
# Application logging configuration
logger:
# Enable or disable logging.
enable: true
# Enable pretty backtrace (sets RUST_BACKTRACE=1)
pretty_backtrace: true
# Log level, options: trace, debug, info, warn or error.
level: debug
# Define the logging format. options: compact, pretty or Json
format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
# override_filter: trace
# Web server configuration
server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 3001
# The UI hostname or IP address that mailers will point to.
host: http://webui.konobangu.com
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares:
# Enable Etag cache header middleware
etag:
enable: true
# Allows to limit the payload size request. payload that bigger than this file will blocked the request.
limit_payload:
# Enable/Disable the middleware.
enable: true
# the limit size. can be b,kb,kib,mb,mib,gb,gib
body_limit: 5mb
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
logger:
# Enable/Disable the middleware.
enable: true
# when your code is panicked, the request still returns 500 status code.
catch_panic:
# Enable/Disable the middleware.
enable: true
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
timeout_request:
# Enable/Disable the middleware.
enable: false
# Duration time in milliseconds.
timeout: 5000
cors:
enable: true
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
# allow_origins:
# - https://loco.rs
# Set the value of the [`Access-Control-Allow-Headers`][mdn] header
# allow_headers:
# - Content-Type
# Set the value of the [`Access-Control-Allow-Methods`][mdn] header
# allow_methods:
# - POST
# Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
# max_age: 3600
# Worker Configuration
workers:
# specifies the worker mode. Options:
# - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
# - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
# - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
mode: BackgroundQueue
# Mailer Configuration.
mailer:
# SMTP mailer configuration.
smtp:
# Enable/Disable smtp mailer.
enable: true
# SMTP server host. e.x localhost, smtp.gmail.com
host: '{{ get_env(name="MAILER_HOST", default="localhost") }}'
# SMTP server port
port: 1025
# Use secure connection (SSL/TLS).
secure: false
# auth:
# user:
# password:
# Database Configuration
database:
# Database connection URI
uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@localhost:5432/konobangu") }}'
# When enabled, the sql query will be logged.
enable_logging: false
# Set the timeout duration when acquiring a connection.
connect_timeout: 500
# Set the idle duration before closing a connection.
idle_timeout: 500
# Minimum number of connections for a pool.
min_connections: 1
# Maximum number of connections for a pool.
max_connections: 1
# Run migration up when application loaded
auto_migrate: true
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_truncate: false
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_recreate: false
# Redis Configuration
redis:
# Redis connection URI
uri: '{{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }}'
# Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_flush: false

View File

118
config/recorder.test.yaml Normal file
View File

@ -0,0 +1,118 @@
# Loco configuration file documentation
# Application logging configuration
logger:
# Enable or disable logging.
enable: false
# Log level, options: trace, debug, info, warn or error.
level: debug
# Define the logging format. options: compact, pretty or Json
format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
# override_filter: trace
# Web server configuration
server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 3000
# The UI hostname or IP address that mailers will point to.
host: http://localhost
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares:
# Allows to limit the payload size request. payload that bigger than this file will blocked the request.
limit_payload:
# Enable/Disable the middleware.
enable: true
# the limit size. can be b,kb,kib,mb,mib,gb,gib
body_limit: 5mb
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
logger:
# Enable/Disable the middleware.
enable: true
# when your code is panicked, the request still returns 500 status code.
catch_panic:
# Enable/Disable the middleware.
enable: true
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
timeout_request:
# Enable/Disable the middleware.
enable: false
# Duration time in milliseconds.
timeout: 5000
cors:
enable: true
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
# allow_origins:
# - https://loco.rs
# Set the value of the [`Access-Control-Allow-Headers`][mdn] header
# allow_headers:
# - Content-Type
# Set the value of the [`Access-Control-Allow-Methods`][mdn] header
# allow_methods:
# - POST
# Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
# max_age: 3600
# Worker Configuration
workers:
# specifies the worker mode. Options:
# - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
# - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
# - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
mode: ForegroundBlocking
# Mailer Configuration.
mailer:
# SMTP mailer configuration.
smtp:
# Enable/Disable smtp mailer.
enable: true
# SMTP server host. e.x localhost, smtp.gmail.com
host: localhost
# SMTP server port
port: 1025
# Use secure connection (SSL/TLS).
secure: false
# auth:
# user:
# password:
stub: true
# Database Configuration
database:
# Database connection URI
uri: {{get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/recorder_test")}}
# When enabled, the sql query will be logged.
enable_logging: false
# Set the timeout duration when acquiring a connection.
connect_timeout: 500
# Set the idle duration before closing a connection.
idle_timeout: 500
# Minimum number of connections for a pool.
min_connections: 1
# Maximum number of connections for a pool.
max_connections: 1
# Run migration up when application loaded
auto_migrate: true
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_truncate: true
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_recreate: false
# Redis Configuration
redis:
# Redis connection URI
uri: {{get_env(name="REDIS_URL", default="redis://127.0.0.1")}}
# Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
dangerously_flush: false
# Authentication Configuration
auth:
# JWT authentication
jwt:
# Secret key for token generation and verification
secret: ZknFYqXpnDgaWcKJZ5J5
# Token expiration time in seconds
expiration: 604800 # 7 days

View File

@ -0,0 +1,8 @@
FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends postgresql-client \
&& cargo install sea-orm-cli cargo-insta \
&& chown -R vscode /usr/local/cargo
COPY .env /.env

View File

@ -0,0 +1,9 @@
{
"name": "Konobangu Recorder",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"forwardPorts": [
3001
]
}

View File

@ -0,0 +1,40 @@
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
command: sleep infinity
networks:
- db
- redis
volumes:
- ../..:/workspaces:cached
env_file:
- .env
db:
image: postgres:15.3-alpine
restart: unless-stopped
ports:
- 5432:5432
networks:
- db
volumes:
- postgres-data:/var/lib/postgresql/data
env_file:
- .env
redis:
image: redis:latest
restart: unless-stopped
ports:
- 6379:6379
networks:
- redis
volumes:
postgres-data:
networks:
db:
redis:

View File

@ -0,0 +1,107 @@
name: CI
on:
push:
branches:
- master
- main
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
rustfmt:
name: Check Style
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Run Clippy
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
test:
name: Run Tests
runs-on: ubuntu-latest
permissions:
contents: read
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- "6379:6379"
postgres:
image: postgres
env:
POSTGRES_DB: postgress_test
POSTGRES_USER: postgress
POSTGRES_PASSWORD: postgress
ports:
- "5432:5432"
# Set health checks to wait until postgres has started
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --all
env:
REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}}
DATABASE_URL: postgres://postgress:postgress@localhost:5432/postgress_test

17
crates/recorder/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
**/config/local.yaml
**/config/*.local.yaml
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View File

@ -0,0 +1,50 @@
[package]
name = "recorder"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
loco-rs = { version = "0.3.1" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
eyre = "0.6"
tokio = { version = "1.33.0", default-features = false }
async-trait = "0.1.74"
tracing = "0.1.40"
chrono = "0.4"
validator = { version = "0.16" }
sea-orm = { version = "1.0.0-rc.1", features = [
"sqlx-sqlite",
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
] }
axum = "0.7.1"
include_dir = "0.7"
uuid = { version = "1.6.0", features = ["v4"] }
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
sea-orm-migration = { version = "1.0.0-rc.1", features = [
"runtime-tokio-rustls",
] }
reqwest = "0.11.24"
thiserror = "1.0.57"
rss = "2.0.7"
[lib]
name = "recorder"
path = "src/lib.rs"
[[bin]]
name = "recorder_cli"
path = "src/bin/main.rs"
required-features = []
[dev-dependencies]
serial_test = "2.0.0"
rstest = "0.18.2"
loco-rs = { version = "0.3.1", features = ["testing"] }
insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] }

View File

@ -0,0 +1,33 @@
use eyre::Context;
#[allow(unused_imports)]
use loco_rs::{cli::playground, prelude::*};
use recorder::app::App;
async fn fetch_and_parse_rss_demo () -> eyre::Result<()> {
let url =
"https://mikanani.me/RSS/MyBangumi?token=FE9tccsML2nBPUUqpCuJW2uJZydAXCntHJ7RpD9LDP8%3d";
let res = reqwest::get(url).await?.bytes().await?;
let channel = rss::Channel::read_from(&res[..])?;
println!("channel: {:#?}", channel);
Ok(())
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
fetch_and_parse_rss_demo().await?;
// let active_model: articles::ActiveModel = ActiveModel {
// title: Set(Some("how to build apps in 3 steps".to_string())),
// content: Set(Some("use Loco: https://loco.rs".to_string())),
// ..Default::default()
// };
// active_model.insert(&ctx.db).await.unwrap();
// let res = articles::Entity::find().all(&ctx.db).await.unwrap();
// println!("{:?}", res);
println!("welcome to playground. edit me at `examples/playground.rs`");
Ok(())
}

View File

@ -0,0 +1,62 @@
use std::path::Path;
use async_trait::async_trait;
use loco_rs::{
app::{AppContext, Hooks},
boot::{create_app, BootResult, StartMode},
controller::AppRoutes,
db::truncate_table,
environment::Environment,
task::Tasks,
worker::{AppWorker, Processor},
Result,
};
use sea_orm::DatabaseConnection;
use crate::{
controllers, migrations::Migrator, models::_entities::subscribers,
workers::downloader::DownloadWorker,
};
pub struct App;
#[async_trait]
impl Hooks for App {
fn app_name() -> &'static str {
env!("CARGO_CRATE_NAME")
}
fn app_version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
option_env!("BUILD_SHA")
.or(option_env!("GITHUB_SHA"))
.unwrap_or("dev")
)
}
async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment).await
}
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes()
.prefix("/api")
.add_route(controllers::subscribers::routes())
}
fn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AppContext) {
p.register(DownloadWorker::build(ctx));
}
fn register_tasks(_tasks: &mut Tasks) {}
async fn truncate(db: &DatabaseConnection) -> Result<()> {
truncate_table(db, subscribers::Entity).await?;
Ok(())
}
async fn seed(_db: &DatabaseConnection, _base: &Path) -> Result<()> {
Ok(())
}
}

View File

@ -0,0 +1,8 @@
use loco_rs::cli;
use recorder::migrations::Migrator;
use recorder::app::App;
#[tokio::main]
async fn main() -> eyre::Result<()> {
cli::main::<App, Migrator>().await
}

View File

@ -0,0 +1 @@
pub mod subscribers;

View File

@ -0,0 +1,14 @@
use loco_rs::prelude::*;
use crate::{models::_entities::subscribers, views::subscribers::CurrentResponse};
async fn current(State(ctx): State<AppContext>) -> Result<Json<CurrentResponse>> {
let subscriber = subscribers::Model::find_root(&ctx.db).await?;
format::json(CurrentResponse::new(&subscriber))
}
pub fn routes() -> Routes {
Routes::new()
.prefix("subscribers")
.add("/current", get(current))
}

View File

View File

@ -0,0 +1,2 @@
pub mod aria;
pub mod qbitorrent;

View File

@ -0,0 +1,10 @@
pub mod app;
pub mod controllers;
pub mod downloader;
pub mod migrations;
pub mod models;
pub mod rss;
pub mod subscriptions;
pub mod tasks;
pub mod views;
pub mod workers;

View File

@ -0,0 +1,40 @@
use sea_orm_migration::prelude::*;
#[derive(Iden)]
pub enum Subscribers {
Table,
Id,
Pid,
DisplayName,
}
#[derive(Iden)]
pub enum Subscriptions {
Table,
Id,
SubscriberId,
DisplayName,
Category,
SourceUrl,
Aggregate,
Enabled,
}
#[derive(Iden)]
pub enum Bangumi {
Table,
Id,
DisplayName,
SubscriptionId,
}
#[derive(Iden)]
pub enum Episodes {
Table,
Id,
DisplayName,
BangumiId,
DownloadUrl,
DownloadProgress,
OutputName,
}

View File

@ -0,0 +1,125 @@
use sea_orm::sea_query::extension::postgres::Type;
use sea_orm_migration::{prelude::*, schema::*};
use super::defs::{Bangumi, Episodes, Subscribers, Subscriptions};
use crate::models::subscribers::ROOT_SUBSCRIBER;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
table_auto(Subscribers::Table)
.col(pk_auto(Subscribers::Id))
.col(string_len_uniq(Subscribers::Pid, 64))
.col(string(Subscribers::DisplayName))
.to_owned(),
)
.await?;
let insert = Query::insert()
.into_table(Subscribers::Table)
.columns([Subscribers::Pid, Subscribers::DisplayName])
.values_panic([ROOT_SUBSCRIBER.into(), ROOT_SUBSCRIBER.into()])
.to_owned();
manager.exec_stmt(insert).await?;
manager
.create_type(
Type::create()
.as_enum(Alias::new("subscription_category"))
.values([
Alias::new("mikan"),
Alias::new("manual"),
Alias::new("bangumi"),
])
.to_owned(),
)
.await?;
manager
.create_table(
table_auto(Subscriptions::Table)
.col(pk_auto(Subscriptions::Id))
.col(string(Subscriptions::DisplayName))
.col(integer(Subscriptions::SubscriberId))
.col(text(Subscriptions::SourceUrl))
.col(boolean(Subscriptions::Aggregate))
.col(boolean(Subscriptions::Enabled))
.foreign_key(
ForeignKey::create()
.name("subscription_subscriber_id")
.from(Subscriptions::Table, Subscriptions::SubscriberId)
.to(Subscribers::Table, Subscribers::Id),
)
.to_owned(),
)
.await?;
manager
.create_table(
table_auto(Bangumi::Table)
.col(pk_auto(Bangumi::Id))
.col(text(Bangumi::DisplayName))
.col(integer(Bangumi::SubscriptionId))
.foreign_key(
ForeignKey::create()
.name("bangumi_subscription_id")
.from(Bangumi::Table, Bangumi::SubscriptionId)
.to(Subscriptions::Table, Subscriptions::Id),
)
.to_owned(),
)
.await?;
manager
.create_table(
table_auto(Episodes::Table)
.col(pk_auto(Episodes::Id))
.col(text(Episodes::DisplayName))
.col(integer(Episodes::BangumiId))
.col(text(Episodes::DownloadUrl))
.col(tiny_integer(Episodes::DownloadProgress).default(0))
.col(text(Episodes::OutputName))
.foreign_key(
ForeignKey::create()
.name("episode_bangumi_id")
.from(Episodes::Table, Episodes::BangumiId)
.to(Bangumi::Table, Bangumi::Id),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Episodes::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Bangumi::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Subscriptions::Table).to_owned())
.await?;
manager
.drop_type(
Type::drop()
.name(Alias::new("subscription_category"))
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(Subscribers::Table).to_owned())
.await
}
}

View File

@ -0,0 +1,13 @@
pub use sea_orm_migration::prelude::*;
pub mod defs;
pub mod m20220101_000001_init;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_init::Migration)]
}
}

View File

@ -0,0 +1,39 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "bangumi")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub display_name: String,
pub subscription_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id"
)]
Subscription,
#[sea_orm(has_many = "super::episodes::Entity")]
Episode,
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}

View File

@ -0,0 +1,34 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "episodes")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub display_name: String,
pub bangumi_id: i32,
pub download_url: String,
pub download_progress: i32,
pub output_name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bangumi::Entity",
from = "Column::BangumiId",
to = "super::bangumi::Column::Id"
)]
Bangumi,
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}

View File

@ -0,0 +1,8 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub mod prelude;
pub mod bangumi;
pub mod episodes;
pub mod subscribers;
pub mod subscriptions;

View File

@ -0,0 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub use super::{
bangumi::Entity as Bangumi, episodes::Entity as Episodes, subscribers::Entity as Subscribers,
subscriptions::Entity as Subscriptions,
};

View File

@ -0,0 +1,28 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscribers")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: String,
pub display_name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::subscriptions::Entity")]
Subscription,
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}

View File

@ -0,0 +1,59 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "subscription_category"
)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionCategory {
#[sea_orm(string_value = "mikan")]
Mikan,
#[sea_orm(string_value = "manual")]
Manual,
#[sea_orm(string_value = "bangumi")]
Bangumi,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscriptions")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub display_name: String,
pub subscriber_id: i32,
pub category: SubscriptionCategory,
pub source_url: String,
pub aggregate: bool,
pub enabled: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
Subscriber,
#[sea_orm(has_many = "super::bangumi::Entity")]
Bangumi,
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}

View File

@ -0,0 +1,6 @@
use sea_orm::entity::prelude::*;
pub use super::_entities::bangumi::{self, ActiveModel, Entity, Model};
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::bangumi::ActiveModel {}

View File

@ -0,0 +1,6 @@
use sea_orm::entity::prelude::*;
pub use super::_entities::episodes::{self, ActiveModel, Entity, Model};
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::episodes::ActiveModel {}

View File

@ -0,0 +1,5 @@
pub mod _entities;
pub mod bangumi;
pub mod episodes;
pub mod subscribers;
pub mod subscriptions;

View File

@ -0,0 +1,70 @@
use loco_rs::model::{ModelError, ModelResult};
use sea_orm::{entity::prelude::*, ActiveValue, TransactionTrait};
use serde::{Deserialize, Serialize};
pub use super::_entities::subscribers::{self, ActiveModel, Entity, Model};
pub const ROOT_SUBSCRIBER: &str = "konobangu";
#[derive(Debug, Deserialize, Serialize)]
pub struct SubscriberIdParams {
pub id: String,
}
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::subscribers::ActiveModel {
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if insert {
let mut this = self;
this.pid = ActiveValue::Set(Uuid::new_v4().to_string());
Ok(this)
} else {
Ok(self)
}
}
}
impl super::_entities::subscribers::Model {
/// finds a user by the provided pid
///
/// # Errors
///
/// When could not find user or DB query error
pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;
let subscriber = subscribers::Entity::find()
.filter(subscribers::Column::Pid.eq(parse_uuid))
.one(db)
.await?;
subscriber.ok_or_else(|| ModelError::EntityNotFound)
}
pub async fn find_root(db: &DatabaseConnection) -> ModelResult<Self> {
Self::find_by_pid(db, ROOT_SUBSCRIBER).await
}
/// Asynchronously creates a user with a password and saves it to the
/// database.
///
/// # Errors
///
/// When could not save the user into the DB
pub async fn create_root(db: &DatabaseConnection) -> ModelResult<Self> {
let txn = db.begin().await?;
let user = subscribers::ActiveModel {
display_name: ActiveValue::set(ROOT_SUBSCRIBER.to_string()),
pid: ActiveValue::set(ROOT_SUBSCRIBER.to_string()),
..Default::default()
}
.insert(&txn)
.await?;
txn.commit().await?;
Ok(user)
}
}

View File

@ -0,0 +1,6 @@
use sea_orm::entity::prelude::*;
pub use super::_entities::subscriptions::{self, ActiveModel, Entity, Model};
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::subscriptions::ActiveModel {}

View File

@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
use crate::models::subscriptions::subscriptions;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RssTorrent {}
#[derive(Debug)]
pub struct RssEngine {}
impl RssEngine {
// pub async fn get_rss_torrents(
// rss_subscription: &subscriptions::ActiveModel,
// ) -> eyre::Result<Vec<RssTorrent>> {
// Ok(())
// }
pub async fn get_torrents(url: &str) -> eyre::Result<rss::Channel> {
let content = reqwest::get(url).await?.bytes().await?;
let channel: rss::Channel = rss::Channel::read_from(&content[..])?;
Ok(channel)
}
}

View File

@ -0,0 +1 @@
pub mod engine;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,23 @@
use crate::rss::engine::RssEngine;
pub struct MikanRssCreateDto {
pub rss_link: String,
pub display_name: String,
pub aggregate: bool,
pub enabled: Option<bool>,
}
pub struct MikanSubscriptionEngine {
}
impl MikanSubscriptionEngine {
pub async fn add_rss(create_dto: MikanRssCreateDto) -> eyre::Result<()> {
let content = reqwest::get(&create_dto.rss_link).await?.bytes().await?;
let channel = rss::Channel::read_from(&content[..])?;
Ok(())
}
}
pub struct MikanSubscriptionItem {
}

View File

@ -0,0 +1,2 @@
pub mod bangumi;
pub mod mikan;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
pub mod subscribers;

View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use crate::models::_entities::subscribers;
#[derive(Debug, Deserialize, Serialize)]
pub struct CurrentResponse {
pub pid: String,
pub display_name: String,
}
impl CurrentResponse {
#[must_use]
pub fn new(user: &subscribers::Model) -> Self {
Self {
pid: user.pid.to_string(),
display_name: user.display_name.to_string(),
}
}
}

View File

@ -0,0 +1,43 @@
use std::time::Duration;
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::time::sleep;
use crate::models::subscribers;
pub struct DownloadWorker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct DownloadWorkerArgs {
pub user_guid: String,
}
impl worker::AppWorker<DownloadWorkerArgs> for DownloadWorker {
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
}
#[async_trait]
impl worker::Worker<DownloadWorkerArgs> for DownloadWorker {
async fn perform(&self, args: DownloadWorkerArgs) -> worker::Result<()> {
// TODO: Some actual work goes here...
println!("================================================");
println!("Sending payment report to user {}", args.user_guid);
sleep(Duration::from_millis(2000)).await;
let all = subscribers::Entity::find()
.all(&self.ctx.db)
.await
.map_err(Box::from)?;
for user in &all {
println!("user: {}", user.id);
}
println!("================================================");
Ok(())
}
}

View File

@ -0,0 +1 @@
pub mod downloader;

View File

@ -0,0 +1,3 @@
mod models;
mod requests;
mod tasks;

View File

@ -0,0 +1 @@
mod subscribers;

View File

@ -0,0 +1,7 @@
---
source: tests/models/subscribers.rs
expression: non_existing_subscriber_results
---
Err(
EntityNotFound,
)

View File

@ -0,0 +1,13 @@
---
source: tests/models/subscribers.rs
expression: existing_subscriber
---
Ok(
Model {
created_at: 2023-11-12T12:34:56.789,
updated_at: 2023-11-12T12:34:56.789,
id: 1,
pid: "11111111-1111-1111-1111-111111111111",
display_name: "user1"
},
)

View File

@ -0,0 +1,27 @@
use insta::assert_debug_snapshot;
use loco_rs::testing;
use recorder::{app::App, models::subscribers::Model};
use serial_test::serial;
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("users");
let _guard = settings.bind_to_scope();
};
}
#[tokio::test]
#[serial]
async fn can_find_by_pid() {
configure_insta!();
let boot = testing::boot_test::<App>().await.unwrap();
testing::seed::<App>(&boot.app_context.db).await.unwrap();
let existing_subscriber =
Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await;
assert_debug_snapshot!(existing_subscriber);
}

View File

@ -0,0 +1,2 @@
mod notes;
mod subscribers;

View File

@ -0,0 +1,32 @@
use insta::{assert_debug_snapshot, with_settings};
use loco_rs::testing;
use recorder::app::App;
use serial_test::serial;
// TODO: see how to dedup / extract this to app-local test utils
// not to framework, because that would require a runtime dep on insta
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("user_request");
let _guard = settings.bind_to_scope();
};
}
#[tokio::test]
#[serial]
async fn can_get_current_user() {
configure_insta!();
testing::request::<App, _, _>(|request, _ctx| async move {
let response = request.get("/api/user/current").await;
with_settings!({
filters => testing::cleanup_user_model()
}, {
assert_debug_snapshot!((response.status_code(), response.text()));
});
})
.await;
}

View File

@ -0,0 +1 @@
pub mod seed;

View File

@ -0,0 +1,43 @@
//! This task implements data seeding functionality for initializing new
//! development/demo environments.
//!
//! # Example
//!
//! Run the task with the following command:
//! ```sh
//! cargo run task
//! ```
//!
//! To override existing data and reset the data structure, use the following
//! command with the `refresh:true` argument:
//! ```sh
//! cargo run task seed_data refresh:true
//! ```
use std::collections::BTreeMap;
use loco_rs::{db, prelude::*};
use migration::Migrator;
use recorder::app::App;
#[allow(clippy::module_name_repetitions)]
pub struct SeedData;
#[async_trait]
impl Task for SeedData {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "seed_data".to_string(),
detail: "Task for seeding data".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &BTreeMap<String, String>) -> Result<()> {
let refresh = vars.get("refresh").is_some_and(|refresh| refresh == "true");
if refresh {
db::reset::<Migrator>(&app_context.db).await?;
}
let path = std::path::Path::new("src/fixtures");
db::run_app_seed::<App>(&app_context.db, path).await?;
Ok(())
}
}

18
justfile Normal file
View File

@ -0,0 +1,18 @@
set windows-shell := ["pwsh.exe", "-c"]
set dotenv-load
prepare-dev-recorder:
cargo install loco-cli
cargo install sea-orm-cli
dev-recorder:
cargo watch -w crates/recorder -w config -x 'recorder start'
play-recorder:
cargo recorder-playground
dev-webui:
npm run dev:webui
dev-proxy:
npm run dev:proxy

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "konobangu",
"version": "0.0.0",
"description": "Kono bangumi?",
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"dev": "npm-run-all -p dev:proxy dev:webui",
"dev:proxy": "npm run start -w apps/proxy",
"dev:webui": "npm run dev -w apps/webui",
"build:webui": "npm run build -w apps/webui"
},
"repository": {
"type": "git",
"url": "https://github.com/lonelyhentxi/konobangu.git"
},
"private": true,
"dependencies": {
"@datasert/cronjs-parser": "^1.4.0",
"date-fns": "^3.3.1",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.5",
"rxjs": "~7.8.1"
},
"devDependencies": {
"cross-env": "^7.0.3",
"dotenv": "^16.4.3",
"npm-run-all": "^4.1.5",
"shx": "^0.3.4",
"typescript": "^5.3.3"
}
}

4190
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- "packages/*"
- "apps/*"

4
rust-toolchain.toml Normal file
View File

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]
profile = "default"

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Default"
group_imports = "StdExternalCrate"
format_strings = true

18
src/tasks/rss_dl.rs Normal file
View File

@ -0,0 +1,18 @@
use std::collections::BTreeMap;
use loco_rs::prelude::*;
pub struct RssDl;
#[async_trait]
impl Task for RssDl {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "rss_dl".to_string(),
detail: "Task generator".to_string(),
}
}
async fn run(&self, _app_context: &AppContext, _vars: &BTreeMap<String, String>) -> Result<()> {
println!("Task RssDl generated");
Ok(())
}
}