├── .gitignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml ├── workflows │ ├── pythonlint_pr.yml │ ├── docker_pr.yml │ └── docker_publish.yml └── PULL_REQUEST_TEMPLATE.md ├── src ├── psaggregator │ ├── .npmrc │ ├── src │ │ ├── routes │ │ │ ├── api │ │ │ │ ├── +page.server.ts │ │ │ │ ├── twitch │ │ │ │ │ └── +server.ts │ │ │ │ ├── reddit │ │ │ │ │ └── +server.ts │ │ │ │ ├── video │ │ │ │ │ └── [id] │ │ │ │ │ │ └── +server.ts │ │ │ │ ├── uploadplan │ │ │ │ │ └── +server.ts │ │ │ │ ├── videos │ │ │ │ │ └── +server.ts │ │ │ │ ├── scheduledContentPieces │ │ │ │ │ └── +server.ts │ │ │ │ ├── thumbnails │ │ │ │ │ └── +server.ts │ │ │ │ ├── randomvideo │ │ │ │ │ └── +server.ts │ │ │ │ └── information │ │ │ │ │ └── +server.ts │ │ │ ├── +layout.server.ts │ │ │ ├── plan │ │ │ │ └── +page.server.ts │ │ │ ├── videos │ │ │ │ └── +page.server.ts │ │ │ ├── randomvideo │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.server.ts │ │ │ ├── news │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.server.ts │ │ │ ├── motivation │ │ │ │ └── +page.svelte │ │ │ ├── settings │ │ │ │ └── +page.svelte │ │ │ └── +layout.svelte │ │ ├── lib │ │ │ ├── components │ │ │ │ ├── ui │ │ │ │ │ ├── sonner │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── sonner.svelte │ │ │ │ │ ├── label │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── label.svelte │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── checkbox.svelte │ │ │ │ │ ├── dialog │ │ │ │ │ │ ├── dialog-portal.svelte │ │ │ │ │ │ ├── dialog-header.svelte │ │ │ │ │ │ ├── dialog-footer.svelte │ │ │ │ │ │ ├── dialog-title.svelte │ │ │ │ │ │ ├── dialog-description.svelte │ │ │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── dialog-content.svelte │ │ │ │ │ ├── card │ │ │ │ │ │ ├── card-content.svelte │ │ │ │ │ │ ├── card-footer.svelte │ │ │ │ │ │ ├── card-header.svelte │ │ │ │ │ │ ├── card-description.svelte │ │ │ │ │ │ ├── card.svelte │ │ │ │ │ │ ├── card-title.svelte │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── alert │ │ │ │ │ │ ├── alert-description.svelte │ │ │ │ │ │ ├── alert.svelte │ │ │ │ │ │ ├── alert-title.svelte │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── tabs │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── tabs-list.svelte │ │ │ │ │ │ ├── tabs-content.svelte │ │ │ │ │ │ └── tabs-trigger.svelte │ │ │ │ │ ├── calendar │ │ │ │ │ │ ├── calendar-grid-body.svelte │ │ │ │ │ │ ├── calendar-grid-head.svelte │ │ │ │ │ │ ├── calendar-grid-row.svelte │ │ │ │ │ │ ├── calendar-grid.svelte │ │ │ │ │ │ ├── calendar-months.svelte │ │ │ │ │ │ ├── calendar-header.svelte │ │ │ │ │ │ ├── calendar-head-cell.svelte │ │ │ │ │ │ ├── calendar-heading.svelte │ │ │ │ │ │ ├── calendar-cell.svelte │ │ │ │ │ │ ├── calendar-prev-button.svelte │ │ │ │ │ │ ├── calendar-next-button.svelte │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── calendar.svelte │ │ │ │ │ │ └── calendar-day.svelte │ │ │ │ │ ├── popover │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── popover-content.svelte │ │ │ │ │ ├── range-calendar │ │ │ │ │ │ ├── range-calendar-grid-body.svelte │ │ │ │ │ │ ├── range-calendar-grid-head.svelte │ │ │ │ │ │ ├── range-calendar-grid-row.svelte │ │ │ │ │ │ ├── range-calendar-months.svelte │ │ │ │ │ │ ├── range-calendar-grid.svelte │ │ │ │ │ │ ├── range-calendar-header.svelte │ │ │ │ │ │ ├── range-calendar-head-cell.svelte │ │ │ │ │ │ ├── range-calendar-heading.svelte │ │ │ │ │ │ ├── range-calendar-next-button.svelte │ │ │ │ │ │ ├── range-calendar-prev-button.svelte │ │ │ │ │ │ ├── range-calendar-cell.svelte │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── range-calendar.svelte │ │ │ │ │ │ └── range-calendar-day.svelte │ │ │ │ │ ├── badge │ │ │ │ │ │ ├── badge.svelte │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── button │ │ │ │ │ │ ├── button.svelte │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── input │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── input.svelte │ │ │ │ ├── LightSwitch.svelte │ │ │ │ ├── YouTubeCommunityPost.svelte │ │ │ │ ├── DatePicker.svelte │ │ │ │ ├── YouTubeCommunityPostStreamplan.svelte │ │ │ │ ├── RedditPost.svelte │ │ │ │ ├── CDNImage.svelte │ │ │ │ ├── TwitchStatus.svelte │ │ │ │ ├── UploadPlanEntry.svelte │ │ │ │ ├── Sparkle.svelte │ │ │ │ ├── TwitterPost.svelte │ │ │ │ ├── TwitchEntry.svelte │ │ │ │ ├── PSVideo.svelte │ │ │ │ ├── InstagramPost.svelte │ │ │ │ └── BigHeader.svelte │ │ │ ├── models │ │ │ │ └── InstaStoryWatchHistory.ts │ │ │ ├── prisma.ts │ │ │ ├── utils │ │ │ │ ├── dateFormat.ts │ │ │ │ └── MediaQuery.svelte │ │ │ └── utils.ts │ │ ├── config │ │ │ ├── migrations │ │ │ │ ├── 20240115072625_duration │ │ │ │ │ └── migration.sql │ │ │ │ ├── migration_lock.toml │ │ │ │ ├── 20240204113314_save_instagram_story_duration │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240122170826_add_analyzed_field │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250205184751_add_ignore_yt_videos │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240128172325_add_announcement │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240114184815_add_more_fields │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240119204643_add_reddit_upvotes │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240115084641_twitchstatus │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240119203913_add_reddit_posts │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240119205036_add_reddit_comments_and_sticky │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240121140137_add_you_tube_import_type │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240122174253_add_open_ai_import_type │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240126060555_long_text │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240204111934_add_instagram_story_type │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240126054206_more_information_stuff │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240114165138_init │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240119205909_add_long_titles │ │ │ │ │ └── migration.sql │ │ │ │ └── 20240119202115_split_content_and_schedule │ │ │ │ │ └── migration.sql │ │ │ ├── config.ts │ │ │ └── schema.prisma │ │ ├── app.d.ts │ │ ├── hooks.server.ts │ │ ├── hooks.client.ts │ │ ├── app.html │ │ └── app.css │ ├── static │ │ ├── robots.txt │ │ ├── ps.png │ │ ├── jay.jpg │ │ ├── sep.jpg │ │ ├── brammen.jpg │ │ ├── chris.jpg │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── peter.jpg │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon-120x120-precomposed.png │ │ ├── twitch-logo.svg │ │ ├── threads-logo.svg │ │ └── reddit-logo.svg │ ├── .prettierignore │ ├── .env.development │ ├── .dockerignore │ ├── .eslintignore │ ├── .gitignore │ ├── components.json │ ├── postcss.config.cjs │ ├── server.js │ ├── .prettierrc │ ├── Dockerfile │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── README.md │ ├── svelte.config.js │ ├── vite.config.ts │ ├── tailwind.config.js │ └── package.json ├── dataimport │ ├── .dockerignore │ ├── .gitignore │ ├── .env.example │ ├── requirements.txt │ ├── hello-cron │ ├── Dockerfile │ ├── pietsmietdefullthumbnailimport.py │ ├── instagramstorydelete.py │ ├── rsyslog.conf │ ├── pietsmietfullvideoimport.py │ ├── twitch.py │ └── reddit.py ├── imageresizer │ ├── .gitignore │ ├── Dockerfile │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.ts └── nginx │ ├── Dockerfile │ └── nginx.conf ├── .vscode └── settings.json ├── logo.png ├── SECURITY.md ├── docker-compose-pma.yml ├── .env.example ├── docker-compose.dev.yml ├── docker-compose.yml └── docker-compose-test.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | cdn/ 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zaanposni 2 | -------------------------------------------------------------------------------- /src/psaggregator/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/+page.server.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/logo.png -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from "./sonner.svelte"; 2 | -------------------------------------------------------------------------------- /src/psaggregator/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/psaggregator/static/ps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/ps.png -------------------------------------------------------------------------------- /src/psaggregator/static/jay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/jay.jpg -------------------------------------------------------------------------------- /src/psaggregator/static/sep.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/sep.jpg -------------------------------------------------------------------------------- /src/psaggregator/static/brammen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/brammen.jpg -------------------------------------------------------------------------------- /src/psaggregator/static/chris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/chris.jpg -------------------------------------------------------------------------------- /src/psaggregator/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/favicon.ico -------------------------------------------------------------------------------- /src/psaggregator/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/favicon.png -------------------------------------------------------------------------------- /src/psaggregator/static/peter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/peter.jpg -------------------------------------------------------------------------------- /src/dataimport/.dockerignore: -------------------------------------------------------------------------------- 1 | geckodriver.exe 2 | *.json 3 | .env 4 | .env.* 5 | pietsmietfullvideoimport.py 6 | *.sql 7 | *.zip 8 | -------------------------------------------------------------------------------- /src/psaggregator/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /src/psaggregator/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/apple-touch-icon.png -------------------------------------------------------------------------------- /src/dataimport/.gitignore: -------------------------------------------------------------------------------- 1 | geckodriver.exe 2 | *.json 3 | openai_test.py 4 | .env 5 | *.sql 6 | __pycache__/ 7 | threads.py 8 | test.py 9 | *.zip 10 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label, 7 | }; 8 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/models/InstaStoryWatchHistory.ts: -------------------------------------------------------------------------------- 1 | export interface InstaStoryWatchHistory { 2 | id: string; 3 | validUntil: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240115072625_duration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` ADD COLUMN `duration` INTEGER NULL; 3 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./checkbox.svelte"; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox, 6 | }; 7 | -------------------------------------------------------------------------------- /src/psaggregator/static/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/psaggregator/.env.development: -------------------------------------------------------------------------------- 1 | PRIVATE_DATABASE_URL=mysql://root:psaggregator@127.0.0.1:3306/psaggregator?useUnicode=true&characterEncoding=utf-8&useSSL=false 2 | -------------------------------------------------------------------------------- /src/psaggregator/static/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /src/psaggregator/static/apple-touch-icon-120x120-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaanposni/psaggregator/HEAD/src/psaggregator/static/apple-touch-icon-120x120-precomposed.png -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240204113314_save_instagram_story_duration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `InformationResource` ADD COLUMN `videoDuration` INTEGER NULL; 3 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240122170826_add_analyzed_field/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Information` ADD COLUMN `analyzedAt` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /src/psaggregator/.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* -------------------------------------------------------------------------------- /src/psaggregator/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/psaggregator/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | !.env.development 10 | .vercel 11 | .output 12 | vite.config.js.timestamp-* 13 | vite.config.ts.timestamp-* 14 | -------------------------------------------------------------------------------- /src/imageresizer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | !.env.development 10 | .vercel 11 | .output 12 | vite.config.js.timestamp-* 13 | vite.config.ts.timestamp-* 14 | dist/ -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/twitch/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | 4 | export async function GET() { 5 | const data = await prisma.twitchStatus.findFirst(); 6 | 7 | return json(data); 8 | } 9 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20250205184751_add_ignore_yt_videos/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `IgnoreYouTubeVideos` ( 3 | `remoteId` VARCHAR(191) NOT NULL, 4 | 5 | PRIMARY KEY (`remoteId`) 6 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 7 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/imageresizer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | 6 | RUN npm install 7 | 8 | COPY . ./ 9 | 10 | RUN npm run build 11 | 12 | RUN rm -rf src 13 | 14 | ENV NODE_ENV=production 15 | ENV PORT=3000 16 | EXPOSE 3000 17 | 18 | CMD ["node", "dist/index.js"] 19 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240128172325_add_announcement/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Announcement` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `text` LONGTEXT NOT NULL, 5 | 6 | PRIMARY KEY (`id`) 7 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 8 | -------------------------------------------------------------------------------- /src/psaggregator/static/twitch-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240114184815_add_more_fields/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` ADD COLUMN `remoteId` VARCHAR(191) NULL, 3 | ADD COLUMN `secondaryHref` VARCHAR(191) NULL; 4 | 5 | -- AlterTable 6 | ALTER TABLE `Information` ADD COLUMN `remoteId` VARCHAR(191) NULL; 7 | -------------------------------------------------------------------------------- /src/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN rm -rf /usr/share/nginx/html/* 4 | 5 | COPY nginx.conf /etc/nginx/nginx.conf 6 | 7 | # Set timezone 8 | ENV TZ=Europe/Berlin 9 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 10 | 11 | CMD ["nginx", "-g", "daemon off;"] 12 | 13 | EXPOSE 80 14 | -------------------------------------------------------------------------------- /src/imageresizer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240119204643_add_reddit_upvotes/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `upvotes` to the `RedditPost` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `RedditPost` ADD COLUMN `upvotes` INTEGER NOT NULL; 9 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/reddit/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | 4 | export async function GET() { 5 | const data = await prisma.redditPost.findMany({ 6 | orderBy: { 7 | sticky: "desc" 8 | } 9 | }); 10 | 11 | return json(data); 12 | } 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.x.x | :white_check_mark: | 8 | | 0.x.x | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please report severe vulnerabilities via mail at 13 | -------------------------------------------------------------------------------- /src/psaggregator/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/psaggregator/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app.css", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils" 12 | }, 13 | "typescript": true 14 | } -------------------------------------------------------------------------------- /src/psaggregator/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer 10 | ] 11 | }; 12 | 13 | module.exports = config; -------------------------------------------------------------------------------- /src/psaggregator/src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { createRequire as _createRequire } from 'module' 2 | export const createCursedRequire: (path: string | URL) => (id: string) => TShape = _createRequire 3 | 4 | const require = createCursedRequire(import.meta.url ?? __filename) 5 | const { PrismaClient } = require('@prisma/client'); 6 | 7 | const prisma = new PrismaClient(); 8 | 9 | export default prisma; 10 | -------------------------------------------------------------------------------- /src/dataimport/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://root:psaggregator@127.0.0.1:3306/psaggregator 2 | 3 | REDDIT_CLIENT_ID= 4 | REDDIT_CLIENT_SECRET= 5 | 6 | TWITCH_CLIENT_ID= 7 | TWITCH_CLIENT_SECRET= 8 | 9 | OPENAI_API_KEY= 10 | 11 | INSTAGRAM_USERNAME= 12 | INSTAGRAM_PASSWORD= 13 | INSTAGRAM_2FA_SECRET= 14 | 15 | TWITTER_USERNAME= 16 | TWITTER_PASSWORD= 17 | TWITTER_LIST_ID= 18 | 19 | YOUTUBE_API_KEY= 20 | -------------------------------------------------------------------------------- /src/dataimport/requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==4.16.0 2 | selenium-wire==5.1.0 3 | python-dateutil==2.8.2 4 | databases==0.8.0 5 | databases[mysql] 6 | asyncio==3.4.3 7 | twitchAPI==4.1.0 8 | praw==7.7.1 9 | rich==12.4.4 10 | requests 11 | openai==1.7.2 12 | pyotp==2.9.0 13 | instagrapi==2.0.1 14 | pillow==10.3.0 15 | blinker==1.7.0 16 | tweety-ns==2.0.6 17 | httpx==0.27.2 18 | h2==4.1.0 19 | httpx[http2] 20 | isodate==0.7.2 21 | -------------------------------------------------------------------------------- /src/psaggregator/server.js: -------------------------------------------------------------------------------- 1 | import { handler } from "./build/handler.js"; 2 | import express from "express"; 3 | import morgan from "morgan"; 4 | 5 | const app = express(); 6 | 7 | app.use(morgan("[:date[iso]] :remote-addr :method :url HTTP/:http-version :status :res[content-length] - :response-time ms")); 8 | 9 | app.use(handler); 10 | 11 | app.listen(3000, () => { 12 | console.log("listening on port 3000"); 13 | }); 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import prisma from "$lib/prisma"; 2 | import type { Announcement } from "@prisma/client"; 3 | 4 | export async function load() { 5 | const announcements = (await prisma.announcement.findMany()) as Announcement[]; 6 | const twitchStatus = await prisma.twitchStatus.findFirst(); 7 | 8 | return { 9 | announcements, 10 | twitchStatus 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/utils/dateFormat.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export function dateFormat(date: Date | moment.Moment, absolute: boolean = false): string { 4 | const val = moment(date); 5 | if (absolute) { 6 | if (moment().isSame(val, "day")) { 7 | return val.format("HH:mm"); 8 | } 9 | return val.format("DD MMM YYYY HH:mm"); 10 | } else { 11 | return val.fromNow(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/psaggregator/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/sveltekit"; 2 | import { version } from "$app/environment"; 3 | import { SENTRY_DSN } from "./config/config"; 4 | 5 | if (SENTRY_DSN) { 6 | Sentry.init({ 7 | dsn: SENTRY_DSN, 8 | 9 | environment: import.meta.env.MODE, 10 | release: version, 11 | 12 | tracesSampleRate: 1 13 | }); 14 | } 15 | 16 | export const handleError = Sentry.handleErrorWithSentry(); 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

12 | 13 |

14 | -------------------------------------------------------------------------------- /docker-compose-pma.yml: -------------------------------------------------------------------------------- 1 | services: 2 | phpmyadmin: 3 | container_name: phpmyadmin 4 | image: phpmyadmin 5 | restart: unless-stopped 6 | ports: 7 | - 0.0.0.0:5651:80 8 | environment: 9 | - PMA_HOST=db 10 | - PMA_PORT=3306 11 | - PMA_USER=root 12 | - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} 13 | depends_on: 14 | - db 15 | networks: 16 | - mysql -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import { Tabs as TabsPrimitive } from "bits-ui"; 2 | import Content from "./tabs-content.svelte"; 3 | import List from "./tabs-list.svelte"; 4 | import Trigger from "./tabs-trigger.svelte"; 5 | 6 | const Root = TabsPrimitive.Root; 7 | 8 | export { 9 | Root, 10 | Content, 11 | List, 12 | Trigger, 13 | // 14 | Root as Tabs, 15 | Content as TabsContent, 16 | List as TabsList, 17 | Trigger as TabsTrigger, 18 | }; 19 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-grid-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-grid-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-grid-row.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240115084641_twitchstatus/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `TwitchStatus` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `title` VARCHAR(191) NOT NULL, 5 | `gameName` VARCHAR(191) NOT NULL, 6 | `viewers` INTEGER NOT NULL, 7 | `startedAt` DATETIME(3) NOT NULL, 8 | `live` BOOLEAN NOT NULL, 9 | `thumbnail` VARCHAR(191) NOT NULL, 10 | 11 | PRIMARY KEY (`id`) 12 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 13 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | import { Popover as PopoverPrimitive } from "bits-ui"; 2 | import Content from "./popover-content.svelte"; 3 | const Root = PopoverPrimitive.Root; 4 | const Trigger = PopoverPrimitive.Trigger; 5 | const Close = PopoverPrimitive.Close; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Trigger, 11 | Close, 12 | // 13 | Root as Popover, 14 | Content as PopoverContent, 15 | Trigger as PopoverTrigger, 16 | Close as PopoverClose, 17 | }; 18 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-grid.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-months.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-grid-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-grid-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/video/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | 4 | export async function GET({ params }) { 5 | const data = await prisma.contentPiece.findFirst({ 6 | where: { 7 | AND: { 8 | type: "PSVideo" 9 | }, 10 | OR: [{ id: params.id }, { remoteId: params.id }, { href: params.id }, { secondaryHref: params.id }] 11 | } 12 | }); 13 | 14 | return json(data); 15 | } 16 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-grid-row.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-months.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240119203913_add_reddit_posts/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `RedditPost` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `title` VARCHAR(191) NOT NULL, 5 | `description` VARCHAR(191) NULL, 6 | `username` VARCHAR(191) NOT NULL, 7 | `publishedAt` DATETIME(3) NOT NULL, 8 | `imageUri` VARCHAR(191) NULL, 9 | `href` VARCHAR(191) NULL, 10 | `importedAt` DATETIME(3) NOT NULL, 11 | 12 | PRIMARY KEY (`id`) 13 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 14 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240119205036_add_reddit_comments_and_sticky/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `comments` to the `RedditPost` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `sticky` to the `RedditPost` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE `RedditPost` ADD COLUMN `comments` INTEGER NOT NULL, 10 | ADD COLUMN `sticky` BOOLEAN NOT NULL; 11 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://root:psaggregator@db:3306/psaggregator 2 | 3 | REDDIT_CLIENT_ID= 4 | REDDIT_CLIENT_SECRET= 5 | 6 | TWITCH_CLIENT_ID= 7 | TWITCH_CLIENT_SECRET= 8 | 9 | MYSQL_DATABASE=psaggregator 10 | MYSQL_ROOT_PASSWORD=psaggregator 11 | 12 | OPENAI_API_KEY= 13 | 14 | INSTAGRAM_USERNAME= 15 | INSTAGRAM_PASSWORD= 16 | INSTAGRAM_2FA_SECRET= 17 | 18 | TWITTER_USERNAME= 19 | TWITTER_PASSWORD= 20 | TWITTER_LIST_ID= 21 | 22 | YOUTUBE_API_KEY= 23 | 24 | LEGAL_URL= 25 | KOFI_USERNAME=zaanposni 26 | SVELTEKIT_SENTRY_DSN= 27 | UMAMI_ID= 28 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-grid.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-head-cell.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4, 4 | "singleQuote": false, 5 | "trailingComma": "none", 6 | "printWidth": 140, 7 | "bracketSameLine": true, 8 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 9 | "svelteSortOrder": "options-scripts-styles-markup", 10 | "svelteBracketNewLine": false, 11 | "overrides": [ 12 | { 13 | "files": "*.svelte", 14 | "options": { 15 | "parser": "svelte" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/tabs/tabs-list.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-heading.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | {headingValue} 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-head-cell.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/alert/alert.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/LightSwitch.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240121140137_add_you_tube_import_type/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'Custom') NOT NULL; 3 | 4 | -- AlterTable 5 | ALTER TABLE `Information` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'Custom') NOT NULL; 6 | 7 | -- AlterTable 8 | ALTER TABLE `ScheduledContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'Custom') NOT NULL; 9 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-heading.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | {headingValue} 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/psaggregator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | ARG SENTRY_UPLOAD_SOURCEMAPS 4 | ARG SENTRY_ORG 5 | ARG SENTRY_PROJECT 6 | ARG SENTRY_AUTH_TOKEN 7 | 8 | WORKDIR /app 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | 13 | COPY . ./ 14 | 15 | RUN npm run prismagenerate 16 | 17 | ENV SENTRY_UPLOAD_SOURCEMAPS=${SENTRY_UPLOAD_SOURCEMAPS} 18 | ENV SENTRY_ORG=${SENTRY_ORG} 19 | ENV SENTRY_PROJECT=${SENTRY_PROJECT} 20 | ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} 21 | 22 | RUN npm run build 23 | 24 | EXPOSE 3000 25 | ENV NODE_ENV=production 26 | CMD npx prisma migrate deploy --schema ./src/config/schema.prisma && node server.js -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240122174253_add_open_ai_import_type/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 3 | 4 | -- AlterTable 5 | ALTER TABLE `Information` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 6 | 7 | -- AlterTable 8 | ALTER TABLE `ScheduledContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 9 | -------------------------------------------------------------------------------- /src/psaggregator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/tabs/tabs-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./card.svelte"; 2 | import Content from "./card-content.svelte"; 3 | import Description from "./card-description.svelte"; 4 | import Footer from "./card-footer.svelte"; 5 | import Header from "./card-header.svelte"; 6 | import Title from "./card-title.svelte"; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle, 22 | }; 23 | 24 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 25 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240126060555_long_text/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` MODIFY `title` LONGTEXT NOT NULL, 3 | MODIFY `description` LONGTEXT NULL, 4 | MODIFY `additionalInfo` LONGTEXT NULL; 5 | 6 | -- AlterTable 7 | ALTER TABLE `Information` MODIFY `text` LONGTEXT NOT NULL, 8 | MODIFY `additionalInfo` LONGTEXT NULL; 9 | 10 | -- AlterTable 11 | ALTER TABLE `RedditPost` MODIFY `title` LONGTEXT NOT NULL, 12 | MODIFY `description` LONGTEXT NULL; 13 | 14 | -- AlterTable 15 | ALTER TABLE `ScheduledContentPiece` MODIFY `title` LONGTEXT NOT NULL, 16 | MODIFY `description` LONGTEXT NULL, 17 | MODIFY `additionalInfo` LONGTEXT NULL; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | ko_fi: zaanposni 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: [] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | container_name: psaggregator_db 4 | restart: unless-stopped 5 | image: mysql:8.0 6 | volumes: 7 | - mysql:/var/lib/mysql 8 | environment: 9 | - MYSQL_DATABASE=${MYSQL_DATABASE} 10 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} 11 | expose: 12 | - "3306" 13 | ports: 14 | - "127.0.0.1:3306:3306" 15 | 16 | youtube-api: 17 | image: ceramicwhite/youtube-operational-api 18 | container_name: youtube-api 19 | restart: unless-stopped 20 | ports: 21 | - "127.0.0.1:8080:80" 22 | expose: 23 | - "8080:80" 24 | volumes: 25 | mysql: 26 | -------------------------------------------------------------------------------- /src/psaggregator/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'] 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser' 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/popover/popover-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/imageresizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imageresizer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "npx tsc", 7 | "start": "node dist/index.js", 8 | "dev": "nodemon src/index.ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.21.1", 15 | "morgan": "^1.10.0", 16 | "sharp": "^0.33.5", 17 | "winston": "^3.17.0" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^5.0.0", 21 | "@types/morgan": "^1.9.9", 22 | "@types/node": "^22.10.1", 23 | "concurrently": "^9.1.0", 24 | "nodemon": "^3.1.7", 25 | "ts-node": "^10.9.2", 26 | "typescript": "^5.7.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pythonlint_pr.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.10" 17 | 18 | - name: Install dependencies 19 | working-directory: ./src/dataimport 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pylint 23 | pip install -r requirements.txt 24 | 25 | - name: Lint Python code 26 | working-directory: ./src/dataimport 27 | run: | 28 | pylint --errors-only ./*.py 29 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { env } from "$env/dynamic/public"; 2 | import { writable } from "svelte/store"; 3 | 4 | export const MAIL_TO_URL = "mailto:psaggregator@zaanposni.com"; 5 | export const LEGAL_URL = env.PUBLIC_LEGAL_URL; 6 | export const UMAMI_ID = env.PUBLIC_UMAMI_ID; 7 | export const KOFI_USERNAME = env.PUBLIC_KOFI_USERNAME; 8 | 9 | export const GITHUB_URL = "https://github.com/zaanposni/psaggregator"; 10 | export const GITHUB_AUTHOR_URL = "https://github.com/zaanposni"; 11 | 12 | export const SHOW_ABSOLUTE_DATES = writable(false); 13 | export const VIDEO_COMPLEXE_VIEW = writable(false); 14 | export const LOW_DATA_MODE = writable(true); 15 | 16 | export const SHOW_ABSOLUTE_DATES_KEY = "showAbsoluteDates"; 17 | export const VIDEO_COMPLEXE_VIEW_KEY = "videoComplexeView"; 18 | export const LOW_DATA_MODE_KEY = "lowDataMode"; 19 | 20 | export const SENTRY_DSN = env.PUBLIC_SENTRY_DSN; 21 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/plan/+page.server.ts: -------------------------------------------------------------------------------- 1 | import prisma from "$lib/prisma"; 2 | import type { ScheduledContentPiece } from "@prisma/client"; 3 | import moment from "moment"; 4 | 5 | export async function load() { 6 | const upperBound = moment().endOf("day").toDate(); 7 | const lowerBound = moment().startOf("day").toDate(); 8 | 9 | const today = (await prisma.scheduledContentPiece.findMany({ 10 | where: { 11 | type: { 12 | equals: "PSVideo" 13 | }, 14 | importedFrom: { 15 | equals: "PietSmietDE" 16 | }, 17 | startDate: { 18 | lt: upperBound, 19 | gt: lowerBound 20 | } 21 | }, 22 | orderBy: { 23 | startDate: "asc" 24 | } 25 | })) as ScheduledContentPiece[]; 26 | 27 | return { 28 | today 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-next-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240204111934_add_instagram_story_type/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'InstagramStory', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 3 | 4 | -- AlterTable 5 | ALTER TABLE `Information` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'InstagramStory', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 6 | 7 | -- AlterTable 8 | ALTER TABLE `InformationResource` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'InstagramStory', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 9 | 10 | -- AlterTable 11 | ALTER TABLE `ScheduledContentPiece` MODIFY `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'InstagramStory', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL; 12 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240126054206_more_information_stuff/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Information` ADD COLUMN `additionalInfo` VARCHAR(1024) NULL; 3 | 4 | -- CreateTable 5 | CREATE TABLE `InformationResource` ( 6 | `id` VARCHAR(191) NOT NULL, 7 | `remoteId` VARCHAR(191) NULL, 8 | `informationId` VARCHAR(191) NOT NULL, 9 | `imageUri` VARCHAR(1024) NULL, 10 | `videoUri` VARCHAR(1024) NULL, 11 | `importedAt` DATETIME(3) NOT NULL, 12 | `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'YouTube', 'OpenAI', 'Custom') NOT NULL, 13 | 14 | PRIMARY KEY (`id`) 15 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 16 | 17 | -- AddForeignKey 18 | ALTER TABLE `InformationResource` ADD CONSTRAINT `InformationResource_informationId_fkey` FOREIGN KEY (`informationId`) REFERENCES `Information`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 19 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, tv } from "tailwind-variants"; 2 | export { default as Badge } from "./badge.svelte"; 3 | 4 | export const badgeVariants = tv({ 5 | base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", 6 | variants: { 7 | variant: { 8 | default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent", 9 | secondary: 10 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent", 11 | destructive: 12 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent", 13 | outline: "text-foreground", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }); 20 | 21 | export type Variant = VariantProps["variant"]; 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./calendar.svelte"; 2 | import Cell from "./calendar-cell.svelte"; 3 | import Day from "./calendar-day.svelte"; 4 | import Grid from "./calendar-grid.svelte"; 5 | import Header from "./calendar-header.svelte"; 6 | import Months from "./calendar-months.svelte"; 7 | import GridRow from "./calendar-grid-row.svelte"; 8 | import Heading from "./calendar-heading.svelte"; 9 | import GridBody from "./calendar-grid-body.svelte"; 10 | import GridHead from "./calendar-grid-head.svelte"; 11 | import HeadCell from "./calendar-head-cell.svelte"; 12 | import NextButton from "./calendar-next-button.svelte"; 13 | import PrevButton from "./calendar-prev-button.svelte"; 14 | 15 | export { 16 | Day, 17 | Cell, 18 | Grid, 19 | Header, 20 | Months, 21 | GridRow, 22 | Heading, 23 | GridBody, 24 | GridHead, 25 | HeadCell, 26 | NextButton, 27 | PrevButton, 28 | // 29 | Root as Calendar, 30 | }; 31 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/videos/+page.server.ts: -------------------------------------------------------------------------------- 1 | import prisma from "$lib/prisma"; 2 | 3 | export async function load() { 4 | const videos = await prisma.contentPiece.findMany({ 5 | select: { 6 | id: true, 7 | title: true, 8 | href: true, 9 | secondaryHref: true, 10 | imageUri: true, 11 | startDate: true, 12 | duration: true 13 | }, 14 | where: { 15 | type: { 16 | equals: "PSVideo" 17 | }, 18 | href: { 19 | not: null 20 | }, 21 | imageUri: { 22 | not: null 23 | }, 24 | startDate: { 25 | not: null 26 | } 27 | }, 28 | orderBy: { 29 | startDate: "desc" 30 | }, 31 | take: 50 32 | }); 33 | 34 | return { 35 | videos 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/tabs/tabs-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | export type FormInputEvent = T & { 4 | currentTarget: EventTarget & HTMLInputElement; 5 | }; 6 | export type InputEvents = { 7 | blur: FormInputEvent; 8 | change: FormInputEvent; 9 | click: FormInputEvent; 10 | focus: FormInputEvent; 11 | focusin: FormInputEvent; 12 | focusout: FormInputEvent; 13 | keydown: FormInputEvent; 14 | keypress: FormInputEvent; 15 | keyup: FormInputEvent; 16 | mouseover: FormInputEvent; 17 | mouseenter: FormInputEvent; 18 | mouseleave: FormInputEvent; 19 | mousemove: FormInputEvent; 20 | paste: FormInputEvent; 21 | input: FormInputEvent; 22 | wheel: FormInputEvent; 23 | }; 24 | 25 | export { 26 | Root, 27 | // 28 | Root as Input, 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./range-calendar.svelte"; 2 | import Cell from "./range-calendar-cell.svelte"; 3 | import Day from "./range-calendar-day.svelte"; 4 | import Grid from "./range-calendar-grid.svelte"; 5 | import Header from "./range-calendar-header.svelte"; 6 | import Months from "./range-calendar-months.svelte"; 7 | import GridRow from "./range-calendar-grid-row.svelte"; 8 | import Heading from "./range-calendar-heading.svelte"; 9 | import GridBody from "./range-calendar-grid-body.svelte"; 10 | import GridHead from "./range-calendar-grid-head.svelte"; 11 | import HeadCell from "./range-calendar-head-cell.svelte"; 12 | import NextButton from "./range-calendar-next-button.svelte"; 13 | import PrevButton from "./range-calendar-prev-button.svelte"; 14 | 15 | export { 16 | Day, 17 | Cell, 18 | Grid, 19 | Header, 20 | Months, 21 | GridRow, 22 | Heading, 23 | GridBody, 24 | GridHead, 25 | HeadCell, 26 | NextButton, 27 | PrevButton, 28 | // 29 | Root as RangeCalendar, 30 | }; 31 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | import Title from "./dialog-title.svelte"; 4 | import Portal from "./dialog-portal.svelte"; 5 | import Footer from "./dialog-footer.svelte"; 6 | import Header from "./dialog-header.svelte"; 7 | import Overlay from "./dialog-overlay.svelte"; 8 | import Content from "./dialog-content.svelte"; 9 | import Description from "./dialog-description.svelte"; 10 | 11 | const Root = DialogPrimitive.Root; 12 | const Trigger = DialogPrimitive.Trigger; 13 | const Close = DialogPrimitive.Close; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose, 37 | }; 38 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/uploadplan/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | import moment from "moment"; 4 | 5 | export async function GET({ url }) { 6 | let now = moment(); 7 | if (url.searchParams.has("date")) { 8 | try { 9 | now = moment(url.searchParams.get("date")); 10 | } finally { 11 | if (!now || !now.isValid()) { 12 | now = moment(); 13 | } 14 | } 15 | } 16 | 17 | const upperBound = now.clone().endOf("day").toDate(); 18 | const lowerBound = now.clone().startOf("day").toDate(); 19 | 20 | const data = await prisma.scheduledContentPiece.findMany({ 21 | where: { 22 | importedFrom: { 23 | equals: "PietSmietDE" 24 | }, 25 | startDate: { 26 | lt: upperBound, 27 | gt: lowerBound 28 | } 29 | }, 30 | orderBy: { 31 | startDate: "asc" 32 | } 33 | }); 34 | 35 | return json(data); 36 | } 37 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, tv } from "tailwind-variants"; 2 | 3 | import Root from "./alert.svelte"; 4 | import Description from "./alert-description.svelte"; 5 | import Title from "./alert-title.svelte"; 6 | 7 | export const alertVariants = tv({ 8 | base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4", 9 | 10 | variants: { 11 | variant: { 12 | default: "bg-background text-foreground", 13 | destructive: 14 | "border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | }, 20 | }); 21 | 22 | export type Variant = VariantProps["variant"]; 23 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 24 | 25 | export { 26 | Root, 27 | Description, 28 | Title, 29 | // 30 | Root as Alert, 31 | Description as AlertDescription, 32 | Title as AlertTitle, 33 | }; 34 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/utils/MediaQuery.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/psaggregator/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /src/psaggregator/src/hooks.client.ts: -------------------------------------------------------------------------------- 1 | import { handleErrorWithSentry, replayIntegration } from "@sentry/sveltekit"; 2 | import * as Sentry from "@sentry/sveltekit"; 3 | import { version } from "$app/environment"; 4 | import { SENTRY_DSN } from "./config/config"; 5 | 6 | if (SENTRY_DSN) { 7 | Sentry.init({ 8 | dsn: SENTRY_DSN, 9 | 10 | environment: import.meta.env.MODE, 11 | release: version, 12 | 13 | tracesSampleRate: 1.0, 14 | replaysSessionSampleRate: import.meta.env.MODE === "development" ? 0 : 0.1, 15 | replaysOnErrorSampleRate: 1.0, 16 | 17 | ignoreErrors: [ 18 | "undefined is not an object (evaluating 'media.currentTime')", 19 | "Importing a module script failed.", 20 | "Can't find variable: logMutedMessage" 21 | ], 22 | 23 | integrations: [ 24 | replayIntegration({ 25 | maskAllInputs: false, 26 | maskAllText: false, 27 | blockAllMedia: false 28 | }) 29 | ] 30 | }); 31 | } 32 | 33 | export const handleError = handleErrorWithSentry(); 34 | -------------------------------------------------------------------------------- /src/psaggregator/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | import { readFileSync } from "fs"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const file = fileURLToPath(new URL("package.json", import.meta.url)); 8 | const json = readFileSync(file, "utf8"); 9 | const pkg = JSON.parse(json); 10 | 11 | /** @type {import('@sveltejs/kit').Config} */ 12 | const config = { 13 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 14 | // for more information about preprocessors 15 | preprocess: [vitePreprocess({})], 16 | 17 | kit: { 18 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 19 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 20 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 21 | adapter: adapter({ 22 | out: "build" 23 | }), 24 | version: { 25 | name: pkg.version 26 | } 27 | } 28 | }; 29 | 30 | export default config; 31 | -------------------------------------------------------------------------------- /src/dataimport/hello-cron: -------------------------------------------------------------------------------- 1 | 5 * * * * . /root/project_env.sh; /usr/local/bin/python /app/youtube.py >> /var/log/cron.log 2>&1 2 | 3 */2 * * * . /root/project_env.sh; /usr/local/bin/python /app/youtubevideoimport.py >> /var/log/cron.log 2>&1 3 | 45 01 * * * . /root/project_env.sh; /usr/local/bin/python /app/instagram.py >> /var/log/cron.log 2>&1 4 | 33 09 * * * . /root/project_env.sh; /usr/local/bin/python /app/instagramstory.py >> /var/log/cron.log 2>&1 5 | 30 5 1 * * . /root/project_env.sh; /usr/local/bin/python /app/instagramstorydelete.py >> /var/log/cron.log 2>&1 6 | */15 * * * * . /root/project_env.sh; /usr/local/bin/python /app/reddit.py >> /var/log/cron.log 2>&1 7 | */1 * * * * . /root/project_env.sh; /usr/local/bin/python /app/twitch.py >> /var/log/cron.log 2>&1 8 | 41 01 * * * . /root/project_env.sh; /usr/local/bin/python /app/twitter.py >> /var/log/cron.log 2>&1 9 | 47 10 * * * . /root/project_env.sh; /usr/local/bin/python /app/twitter.py >> /var/log/cron.log 2>&1 10 | 33 16 * * * . /root/project_env.sh; /usr/local/bin/python /app/twitter.py >> /var/log/cron.log 2>&1 11 | # An empty line is required at the end of this file for a valid cron file. 12 | -------------------------------------------------------------------------------- /src/psaggregator/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sentrySvelteKit } from "@sentry/sveltekit"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | import { defineConfig } from "vite"; 4 | 5 | import { readFileSync } from "fs"; 6 | import { fileURLToPath } from "url"; 7 | 8 | const file = fileURLToPath(new URL("package.json", import.meta.url)); 9 | const json = readFileSync(file, "utf8"); 10 | const pkg = JSON.parse(json); 11 | 12 | export default defineConfig({ 13 | build: { 14 | sourcemap: process.env.SENTRY_UPLOAD_SOURCEMAPS === "true" 15 | }, 16 | plugins: [ 17 | sentrySvelteKit({ 18 | sourceMapsUploadOptions: 19 | process.env.SENTRY_UPLOAD_SOURCEMAPS === "true" 20 | ? { 21 | release: { 22 | name: pkg.version 23 | }, 24 | org: process.env.SENTRY_ORG, 25 | project: process.env.SENTRY_PROJECT, 26 | authToken: process.env.SENTRY_AUTH_TOKEN 27 | } 28 | : undefined 29 | }), 30 | sveltekit() 31 | ] 32 | }); 33 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/YouTubeCommunityPost.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 |
14 | {#if post.date} 15 |
{dateFormat(post.date, $SHOW_ABSOLUTE_DATES)}
16 | {/if} 17 | 22 | {#if post.imageUri} 23 | 30 | {/if} 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240114165138_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `ContentPiece` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `title` VARCHAR(191) NOT NULL, 5 | `description` VARCHAR(191) NULL, 6 | `additionalInfo` VARCHAR(191) NULL, 7 | `startDate` DATETIME(3) NULL, 8 | `imageUri` VARCHAR(191) NULL, 9 | `href` VARCHAR(191) NULL, 10 | `importedAt` DATETIME(3) NOT NULL, 11 | `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'Custom') NOT NULL, 12 | `type` ENUM('Unknown', 'Video', 'TwitchStream') NOT NULL, 13 | 14 | PRIMARY KEY (`id`) 15 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 16 | 17 | -- CreateTable 18 | CREATE TABLE `Information` ( 19 | `id` VARCHAR(191) NOT NULL, 20 | `text` VARCHAR(191) NOT NULL, 21 | `imageUri` VARCHAR(191) NULL, 22 | `href` VARCHAR(191) NULL, 23 | `date` DATETIME(3) NULL, 24 | `importedAt` DATETIME(3) NOT NULL, 25 | `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'Custom') NOT NULL, 26 | 27 | PRIMARY KEY (`id`) 28 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue number: resolves # 2 | 3 | --------- 4 | 5 | 6 | 7 | 8 | 9 | ## What is the current behavior? 10 | 11 | 12 | ## What is the new behavior? 13 | 14 | 15 | - 16 | - 17 | - 18 | 19 | ## Does this introduce a breaking change? 20 | 21 | - [ ] Yes 22 | - [ ] No 23 | 24 | 30 | 31 | 32 | ## Other information 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/randomvideo/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | 21 | 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/DatePicker.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/docker_pr.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build_frontend: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Build frontend 14 | working-directory: ./src/psaggregator 15 | run: docker build . 16 | 17 | build_nginx: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Build nginx 24 | working-directory: ./src/nginx 25 | run: docker build . 26 | 27 | build_dataimport: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Build dataimport 34 | working-directory: ./src/dataimport 35 | run: docker build . 36 | 37 | build_imageresizer: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Build dataimport 44 | working-directory: ./src/imageresizer 45 | run: docker build . 46 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240119205909_add_long_titles/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `ContentPiece` MODIFY `title` VARCHAR(1024) NOT NULL, 3 | MODIFY `description` VARCHAR(1024) NULL, 4 | MODIFY `additionalInfo` VARCHAR(1024) NULL, 5 | MODIFY `imageUri` VARCHAR(1024) NULL, 6 | MODIFY `href` VARCHAR(1024) NULL, 7 | MODIFY `secondaryHref` VARCHAR(1024) NULL; 8 | 9 | -- AlterTable 10 | ALTER TABLE `Information` MODIFY `text` VARCHAR(1024) NOT NULL, 11 | MODIFY `imageUri` VARCHAR(1024) NULL, 12 | MODIFY `href` VARCHAR(1024) NULL; 13 | 14 | -- AlterTable 15 | ALTER TABLE `RedditPost` MODIFY `title` VARCHAR(1024) NOT NULL, 16 | MODIFY `description` VARCHAR(1024) NULL, 17 | MODIFY `imageUri` VARCHAR(1024) NULL, 18 | MODIFY `href` VARCHAR(1024) NULL; 19 | 20 | -- AlterTable 21 | ALTER TABLE `ScheduledContentPiece` MODIFY `title` VARCHAR(1024) NOT NULL, 22 | MODIFY `description` VARCHAR(1024) NULL, 23 | MODIFY `additionalInfo` VARCHAR(1024) NULL, 24 | MODIFY `imageUri` VARCHAR(1024) NULL, 25 | MODIFY `href` VARCHAR(1024) NULL, 26 | MODIFY `secondaryHref` VARCHAR(1024) NULL; 27 | 28 | -- AlterTable 29 | ALTER TABLE `TwitchStatus` MODIFY `title` VARCHAR(1024) NOT NULL, 30 | MODIFY `thumbnail` VARCHAR(1024) NOT NULL; 31 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/videos/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | import moment from "moment"; 4 | 5 | export async function GET({ url }) { 6 | let skip = 0; 7 | const skipParam = url.searchParams.get("skip"); 8 | if (skipParam) { 9 | try { 10 | skip = parseInt(skipParam); 11 | } finally { 12 | if (!skip) skip = 0; 13 | } 14 | } 15 | 16 | let newSince = undefined; 17 | if (url.searchParams.has("newSince")) { 18 | const value = url.searchParams.get("newSince"); 19 | if (value) { 20 | try { 21 | newSince = moment.unix(parseInt(value)); 22 | } finally { 23 | if (!newSince || !newSince.isValid()) { 24 | newSince = undefined; 25 | } 26 | } 27 | } 28 | } 29 | 30 | const data = await prisma.contentPiece.findMany({ 31 | where: { 32 | type: "PSVideo", 33 | startDate: { 34 | gt: newSince ? newSince.toDate() : undefined 35 | } 36 | }, 37 | orderBy: { 38 | startDate: "desc" 39 | }, 40 | skip, 41 | take: 20 42 | }); 43 | 44 | return json(data); 45 | } 46 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/migrations/20240119202115_split_content_and_schedule/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [Video] on the enum `ContentPiece_type` will be removed. If these variants are still used in the database, this will fail. 5 | - Made the column `remoteId` on table `ContentPiece` required. This step will fail if there are existing NULL values in that column. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE `ContentPiece` MODIFY `type` ENUM('Unknown', 'PSVideo', 'TwitchStream') NOT NULL, 10 | MODIFY `remoteId` VARCHAR(191) NOT NULL; 11 | 12 | -- CreateTable 13 | CREATE TABLE `ScheduledContentPiece` ( 14 | `id` VARCHAR(191) NOT NULL, 15 | `remoteId` VARCHAR(191) NULL, 16 | `title` VARCHAR(191) NOT NULL, 17 | `description` VARCHAR(191) NULL, 18 | `additionalInfo` VARCHAR(191) NULL, 19 | `startDate` DATETIME(3) NULL, 20 | `imageUri` VARCHAR(191) NULL, 21 | `href` VARCHAR(191) NULL, 22 | `secondaryHref` VARCHAR(191) NULL, 23 | `duration` INTEGER NULL, 24 | `importedAt` DATETIME(3) NOT NULL, 25 | `importedFrom` ENUM('Unknown', 'PietSmietDE', 'Instagram', 'Twitter', 'Threads', 'Reddit', 'Custom') NOT NULL, 26 | `type` ENUM('Unknown', 'PSVideo', 'TwitchStream') NOT NULL, 27 | 28 | PRIMARY KEY (`id`) 29 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 30 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | 29 | {#if isChecked} 30 | 31 | {:else if isIndeterminate} 32 | 33 | {/if} 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 28 | 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/dataimport/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | # Set timezone 6 | ENV TZ=Europe/Berlin 7 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 8 | 9 | # Install Firefox 10 | RUN apt-get update && \ 11 | apt-get install -y --no-install-recommends firefox-esr 12 | 13 | # Install GeckoDriver 14 | RUN apt-get install -y --no-install-recommends wget unzip && \ 15 | wget https://github.com/mozilla/geckodriver/releases/download/v0.30.0/geckodriver-v0.30.0-linux64.tar.gz && \ 16 | tar -xvzf geckodriver-v0.30.0-linux64.tar.gz && \ 17 | rm geckodriver-v0.30.0-linux64.tar.gz && \ 18 | chmod +x geckodriver && \ 19 | mv geckodriver /usr/local/bin/ && \ 20 | apt-get remove -y wget unzip 21 | 22 | # Install deps 23 | RUN apt-get install -y rsyslog cron dos2unix libmagic1 24 | 25 | # Cleanup 26 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 27 | 28 | COPY rsyslog.conf /etc/rsyslog.conf 29 | RUN chmod 644 /etc/rsyslog.conf 30 | 31 | COPY . /app 32 | 33 | # Install any needed packages specified in requirements.txt 34 | RUN pip install --no-cache-dir -r requirements.txt 35 | RUN pip install --no-cache-dir --no-deps --force-reinstall pydantic==1.10.13 36 | 37 | RUN touch /var/log/cron.log 38 | COPY hello-cron /etc/cron.d/hello-cron 39 | RUN dos2unix /etc/cron.d/hello-cron 40 | RUN chmod 0644 /etc/cron.d/hello-cron 41 | RUN crontab /etc/cron.d/hello-cron 42 | 43 | CMD printenv | sed 's/^\(.*\)$/export \1/g' > /root/project_env.sh && service rsyslog start && cron && tail -f /var/log/cron.log -------------------------------------------------------------------------------- /src/psaggregator/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | psaggregator 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | %sveltekit.head% 27 | 28 | 29 | 30 | %sveltekit.body% 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/scheduledContentPieces/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | import moment from "moment"; 4 | 5 | export async function GET({ url }) { 6 | let skip = 0; 7 | const skipParam = url.searchParams.get("skip"); 8 | if (skipParam) { 9 | try { 10 | skip = parseInt(skipParam); 11 | } finally { 12 | if (!skip) skip = 0; 13 | } 14 | } 15 | 16 | let date = null; 17 | if (url.searchParams.has("date")) { 18 | try { 19 | date = moment(url.searchParams.get("date")); 20 | } finally { 21 | if (!date || !date.isValid()) { 22 | date = null; 23 | } 24 | } 25 | } 26 | 27 | let data = []; 28 | 29 | if (date) { 30 | const upperBound = date.clone().endOf("day").toDate(); 31 | const lowerBound = date.clone().startOf("day").toDate(); 32 | 33 | data = await prisma.scheduledContentPiece.findMany({ 34 | where: { 35 | startDate: { 36 | lt: upperBound, 37 | gt: lowerBound 38 | } 39 | }, 40 | orderBy: { 41 | startDate: "desc" 42 | }, 43 | skip, 44 | take: 20 45 | }); 46 | } else { 47 | data = await prisma.scheduledContentPiece.findMany({ 48 | orderBy: { 49 | startDate: "desc" 50 | }, 51 | skip, 52 | take: 20 53 | }); 54 | } 55 | 56 | return json(data); 57 | } 58 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/news/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | {#if matches} 22 | 26 | {:else} 27 | 31 | {/if} 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/YouTubeCommunityPostStreamplan.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 |
15 |
16 | BETA 17 |
Vermutlicher Streamplan
18 |
19 | {#if post.date} 20 |
{dateFormat(post.date, $SHOW_ABSOLUTE_DATES)}
21 | {/if} 22 | 27 | {#if post.imageUri} 28 | 35 | {/if} 36 |
37 | Dies ist ein Beta-Feature. Dieser Beitrag wurde automatisch als potenzieller Streamplan erkannt. Falls es Probleme oder Fehler 38 | gibt, melde diese bitte. 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /src/psaggregator/static/threads-logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/RedditPost.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | {entry.upvotes} 17 |
18 |
19 | 20 | {entry.comments} 21 |
22 |
23 | {#if entry.imageUri} 24 | 25 | {/if} 26 |
27 |
28 | u/{entry.username} 29 | {#if entry.sticky} 30 | 31 | {/if} 32 |
33 | 34 | {entry.title} 35 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, tv } from "tailwind-variants"; 2 | import type { Button as ButtonPrimitive } from "bits-ui"; 3 | import Root from "./button.svelte"; 4 | 5 | const buttonVariants = tv({ 6 | base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 7 | variants: { 8 | variant: { 9 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 10 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 11 | outline: 12 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border", 13 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 14 | ghost: "hover:bg-accent hover:text-accent-foreground", 15 | link: "text-primary underline-offset-4 hover:underline", 16 | }, 17 | size: { 18 | default: "h-10 px-4 py-2", 19 | sm: "h-9 rounded-md px-3", 20 | lg: "h-11 rounded-md px-8", 21 | icon: "h-10 w-10", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }); 29 | 30 | type Variant = VariantProps["variant"]; 31 | type Size = VariantProps["size"]; 32 | 33 | type Props = ButtonPrimitive.Props & { 34 | variant?: Variant; 35 | size?: Size; 36 | }; 37 | 38 | type Events = ButtonPrimitive.Events; 39 | 40 | export { 41 | Root, 42 | type Props, 43 | type Events, 44 | // 45 | Root as Button, 46 | type Props as ButtonProps, 47 | type Events as ButtonEvents, 48 | buttonVariants, 49 | }; 50 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {#each months as month} 35 | 36 | 37 | 38 | {#each weekdays as weekday} 39 | 40 | {weekday.slice(0, 2)} 41 | 42 | {/each} 43 | 44 | 45 | 46 | {#each month.weeks as weekDates} 47 | 48 | {#each weekDates as date} 49 | 50 | 51 | 52 | {/each} 53 | 54 | {/each} 55 | 56 | 57 | {/each} 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/thumbnails/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | import moment from "moment"; 4 | 5 | export async function GET({ url }) { 6 | let skip = 0; 7 | const skipParam = url.searchParams.get("skip"); 8 | if (skipParam) { 9 | try { 10 | skip = parseInt(skipParam); 11 | } finally { 12 | if (!skip) skip = 0; 13 | } 14 | } 15 | 16 | let newSince = undefined; 17 | if (url.searchParams.has("newSince")) { 18 | const value = url.searchParams.get("newSince"); 19 | if (value) { 20 | try { 21 | newSince = moment.unix(parseInt(value)); 22 | } finally { 23 | if (!newSince || !newSince.isValid()) { 24 | newSince = undefined; 25 | } 26 | } 27 | } 28 | } 29 | 30 | const data = await prisma.contentPiece.findMany({ 31 | select: { 32 | id: true, 33 | title: true, 34 | href: true, 35 | secondaryHref: true, 36 | imageUri: true, 37 | startDate: true, 38 | duration: true 39 | }, 40 | where: { 41 | type: { 42 | equals: "PSVideo" 43 | }, 44 | href: { 45 | not: null 46 | }, 47 | imageUri: { 48 | not: null 49 | }, 50 | startDate: { 51 | not: null, 52 | gt: newSince ? newSince.toDate() : undefined 53 | } 54 | }, 55 | orderBy: { 56 | startDate: "desc" 57 | }, 58 | take: 50, 59 | skip 60 | }); 61 | 62 | return json(data); 63 | } 64 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/calendar/calendar-day.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | {date.day} 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { cubicOut } from "svelte/easing"; 4 | import type { TransitionConfig } from "svelte/transition"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | type FlyAndScaleParams = { 11 | y?: number; 12 | x?: number; 13 | start?: number; 14 | duration?: number; 15 | }; 16 | 17 | export const flyAndScale = ( 18 | node: Element, 19 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } 20 | ): TransitionConfig => { 21 | const style = getComputedStyle(node); 22 | const transform = style.transform === "none" ? "" : style.transform; 23 | 24 | const scaleConversion = ( 25 | valueA: number, 26 | scaleA: [number, number], 27 | scaleB: [number, number] 28 | ) => { 29 | const [minA, maxA] = scaleA; 30 | const [minB, maxB] = scaleB; 31 | 32 | const percentage = (valueA - minA) / (maxA - minA); 33 | const valueB = percentage * (maxB - minB) + minB; 34 | 35 | return valueB; 36 | }; 37 | 38 | const styleToString = ( 39 | style: Record 40 | ): string => { 41 | return Object.keys(style).reduce((str, key) => { 42 | if (style[key] === undefined) return str; 43 | return str + `${key}:${style[key]};`; 44 | }, ""); 45 | }; 46 | 47 | return { 48 | duration: params.duration ?? 200, 49 | delay: 0, 50 | css: (t) => { 51 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); 52 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); 53 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); 54 | 55 | return styleToString({ 56 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, 57 | opacity: t 58 | }); 59 | }, 60 | easing: cubicOut 61 | }; 62 | }; -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/CDNImage.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | {#if imageSrc} 57 | {alt 58 | {/if} 59 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/randomvideo/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | 4 | export async function GET() { 5 | const videoCount = await prisma.contentPiece.count({ 6 | where: { 7 | type: { 8 | equals: "PSVideo" 9 | }, 10 | imageUri: { 11 | not: null 12 | }, 13 | href: { 14 | not: null 15 | }, 16 | startDate: { 17 | not: null 18 | }, 19 | OR: [ 20 | { 21 | href: { 22 | contains: "youtu.be" 23 | } 24 | }, 25 | { 26 | secondaryHref: { 27 | contains: "youtu.be" 28 | } 29 | } 30 | ] 31 | } 32 | }); 33 | const skip = Math.floor(Math.random() * videoCount); 34 | const video = await prisma.contentPiece.findFirst({ 35 | where: { 36 | type: { 37 | equals: "PSVideo" 38 | }, 39 | imageUri: { 40 | not: null 41 | }, 42 | href: { 43 | not: null 44 | }, 45 | startDate: { 46 | not: null 47 | }, 48 | OR: [ 49 | { 50 | href: { 51 | contains: "youtu.be" 52 | } 53 | }, 54 | { 55 | secondaryHref: { 56 | contains: "youtu.be" 57 | } 58 | } 59 | ] 60 | }, 61 | orderBy: { 62 | startDate: "desc" 63 | }, 64 | skip 65 | }); 66 | 67 | return json(video); 68 | } 69 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/TwitchStatus.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | PietSmiet ist live! 21 |
22 | {#if matches} 23 | 24 | 25 | {twitch.gameName} 26 | 27 | {/if} 28 |
29 |
30 |
31 | {#if matches} 32 | 33 | 34 | {twitch.viewers} 35 | 36 | {/if} 37 | {moment(twitch.startedAt).fromNow()} 38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/api/information/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import prisma from "$lib/prisma"; 3 | import moment from "moment"; 4 | 5 | export async function GET({ url }) { 6 | let skip = 0; 7 | const skipParam = url.searchParams.get("skip"); 8 | if (skipParam) { 9 | try { 10 | skip = parseInt(skipParam); 11 | } finally { 12 | if (!skip) skip = 0; 13 | } 14 | } 15 | 16 | const type = url.searchParams.get("type") ?? undefined; 17 | 18 | let date = null; 19 | if (url.searchParams.has("date")) { 20 | try { 21 | date = moment(url.searchParams.get("date")); 22 | } finally { 23 | if (!date || !date.isValid()) { 24 | date = null; 25 | } 26 | } 27 | } 28 | 29 | let data = []; 30 | 31 | if (date) { 32 | const upperBound = date.clone().endOf("day").toDate(); 33 | const lowerBound = date.clone().startOf("day").toDate(); 34 | 35 | data = await prisma.information.findMany({ 36 | where: { 37 | date: { 38 | lt: upperBound, 39 | gt: lowerBound 40 | }, 41 | importedFrom: type, 42 | }, 43 | include: { 44 | InformationResource: true, 45 | }, 46 | orderBy: { 47 | date: "desc" 48 | }, 49 | skip, 50 | take: 20, 51 | }); 52 | } else { 53 | data = await prisma.information.findMany({ 54 | where: { 55 | importedFrom: type, 56 | }, 57 | include: { 58 | InformationResource: true, 59 | }, 60 | orderBy: { 61 | date: "desc" 62 | }, 63 | skip, 64 | take: 20 65 | }); 66 | } 67 | 68 | return json(data); 69 | } 70 | -------------------------------------------------------------------------------- /src/psaggregator/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply h-full overflow-hidden; 8 | } 9 | 10 | @layer base { 11 | :root { 12 | --background: 0 0% 100%; 13 | --foreground: 240 10% 3.9%; 14 | --card: 0 0% 100%; 15 | --card-foreground: 240 10% 3.9%; 16 | --popover: 0 0% 100%; 17 | --popover-foreground: 240 10% 3.9%; 18 | --primary: 142.1 76.2% 36.3%; 19 | --primary-foreground: 355.7 100% 97.3%; 20 | --secondary: 240 4.8% 95.9%; 21 | --secondary-foreground: 240 5.9% 10%; 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | --accent: 240 4.8% 95.9%; 25 | --accent-foreground: 240 5.9% 10%; 26 | --destructive: 0 72.22% 50.59%; 27 | --destructive-foreground: 0 0% 98%; 28 | --border: 240 5.9% 90%; 29 | --input: 240 5.9% 90%; 30 | --ring: 142.1 76.2% 36.3%; 31 | --radius: 0.5rem; 32 | } 33 | .dark { 34 | --background: 20 14.3% 4.1%; 35 | --foreground: 0 0% 95%; 36 | --card: 24 9.8% 10%; 37 | --card-foreground: 0 0% 95%; 38 | --popover: 0 0% 9%; 39 | --popover-foreground: 0 0% 95%; 40 | --primary: 142.1 70.6% 45.3%; 41 | --primary-foreground: 144.9 80.4% 10%; 42 | --secondary: 240 3.7% 15.9%; 43 | --secondary-foreground: 0 0% 98%; 44 | --muted: 0 0% 15%; 45 | --muted-foreground: 240 5% 64.9%; 46 | --accent: 12 6.5% 15.1%; 47 | --accent-foreground: 0 0% 98%; 48 | --destructive: 0 62.8% 30.6%; 49 | --destructive-foreground: 0 85.7% 97.3%; 50 | --border: 240 3.7% 15.9%; 51 | --input: 240 3.7% 15.9%; 52 | --ring: 142.4 71.8% 29.2%; 53 | } 54 | } 55 | 56 | @layer base { 57 | * { 58 | @apply border-border; 59 | } 60 | body { 61 | @apply bg-background text-foreground; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {#each months as month} 36 | 37 | 38 | 39 | {#each weekdays as weekday} 40 | 41 | {weekday.slice(0, 2)} 42 | 43 | {/each} 44 | 45 | 46 | 47 | {#each month.weeks as weekDates} 48 | 49 | {#each weekDates as date} 50 | 51 | 52 | 53 | {/each} 54 | 55 | {/each} 56 | 57 | 58 | {/each} 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/psaggregator/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | const config = { 5 | darkMode: ["class"], 6 | content: ["./src/**/*.{html,js,svelte,ts}"], 7 | safelist: ["dark"], 8 | theme: { 9 | container: { 10 | center: true, 11 | padding: "2rem", 12 | screens: { 13 | "2xl": "1400px" 14 | } 15 | }, 16 | extend: { 17 | colors: { 18 | border: "hsl(var(--border) / )", 19 | input: "hsl(var(--input) / )", 20 | ring: "hsl(var(--ring) / )", 21 | background: "hsl(var(--background) / )", 22 | foreground: "hsl(var(--foreground) / )", 23 | primary: { 24 | DEFAULT: "hsl(var(--primary) / )", 25 | foreground: "hsl(var(--primary-foreground) / )" 26 | }, 27 | secondary: { 28 | DEFAULT: "hsl(var(--secondary) / )", 29 | foreground: "hsl(var(--secondary-foreground) / )" 30 | }, 31 | destructive: { 32 | DEFAULT: "hsl(var(--destructive) / )", 33 | foreground: "hsl(var(--destructive-foreground) / )" 34 | }, 35 | muted: { 36 | DEFAULT: "hsl(var(--muted) / )", 37 | foreground: "hsl(var(--muted-foreground) / )" 38 | }, 39 | accent: { 40 | DEFAULT: "hsl(var(--accent) / )", 41 | foreground: "hsl(var(--accent-foreground) / )" 42 | }, 43 | popover: { 44 | DEFAULT: "hsl(var(--popover) / )", 45 | foreground: "hsl(var(--popover-foreground) / )" 46 | }, 47 | card: { 48 | DEFAULT: "hsl(var(--card) / )", 49 | foreground: "hsl(var(--card-foreground) / )" 50 | } 51 | }, 52 | borderRadius: { 53 | lg: "var(--radius)", 54 | md: "calc(var(--radius) - 2px)", 55 | sm: "calc(var(--radius) - 4px)" 56 | }, 57 | fontFamily: { 58 | sans: [...fontFamily.sans] 59 | } 60 | } 61 | }, 62 | }; 63 | 64 | export default config; 65 | -------------------------------------------------------------------------------- /src/dataimport/pietsmietdefullthumbnailimport.py: -------------------------------------------------------------------------------- 1 | # This file is not actively used in psaggregator. 2 | # It is a script that was used to import all thumbnails from PietSmiet once. 3 | # Other imports only import recent thumbnails. 4 | # This script generates a sql file that can be used to sync all thumbnails in combination with the pietsmietfullvideoimport.py script. 5 | 6 | import os 7 | import asyncio 8 | from uuid import uuid4 9 | import requests 10 | 11 | from rich.console import Console 12 | from databases import Database 13 | 14 | 15 | console = Console() 16 | 17 | 18 | async def stuff() -> asyncio.coroutine: 19 | console.log("Connecting to database...", style="bold green") 20 | db = Database(url=os.getenv("DATABASE_URL")) 21 | await db.connect() 22 | 23 | handled = dict() 24 | 25 | query = "SELECT * FROM ContentPiece WHERE importedFrom='PietSmietDE' AND type='PSVideo' AND remoteId IS NOT NULL AND imageUri IS NOT NULL" 26 | console.log("Fetching all videos...", style="bold green") 27 | videos = await db.fetch_all(query=query) 28 | 29 | for index, video in enumerate(videos): 30 | if video.remoteId in handled: 31 | continue 32 | handled[video.remoteId] = uuid4() 33 | 34 | console.log( 35 | f"Fetching thumbnail for {video.remoteId} ({index})...", style="bold green" 36 | ) 37 | 38 | thumbnail = requests.get(video.imageUri).content 39 | filename = f"/app/cdn/psde/{handled[video.remoteId]}.jpg" 40 | with open(filename, "wb") as f: 41 | f.write(thumbnail) 42 | 43 | console.log("Write mapping to file...", style="bold green") 44 | 45 | update_statements = list() 46 | 47 | for handledId, uuid in handled.items(): 48 | update_statements.append( 49 | f"UPDATE ContentPiece SET imageUri='/cdn/psde/{uuid}.jpg' WHERE remoteId='{handledId}'" 50 | ) 51 | 52 | with open("psde.sql", "w", encoding="utf-8") as f: 53 | f.writelines(update_statements) 54 | 55 | console.log("Done!", style="bold green") 56 | 57 | 58 | asyncio.run(stuff()) 59 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/UploadPlanEntry.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 |
14 |
15 |
16 | {#if entry.type === "TwitchStream"} 17 |
22 | {#if entry.startDate} 23 | {@const date = moment(entry.startDate)} 24 |
25 | {#if !moment().isSame(date, "day")} 26 | {date.format("DD. MMM,")} 27 | {/if} 28 | {date.format("HH:mm")} 29 |
30 | {/if} 31 |
32 | {entry.title} 33 | {#if entry.href || entry.importedFrom === "OpenAI"} 34 |
35 | {/if} 36 | {#if entry.href} 37 | 43 | {/if} 44 | {#if entry.importedFrom === "OpenAI"} 45 |
46 | 47 |
48 | {/if} 49 |
50 | 51 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/ui/range-calendar/range-calendar-day.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 40 | 41 | {date.day} 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/randomvideo/+page.server.ts: -------------------------------------------------------------------------------- 1 | import prisma from "$lib/prisma"; 2 | import type { ContentPiece } from "@prisma/client"; 3 | 4 | export async function load(e) { 5 | e.depends("data:randomvideo"); 6 | 7 | const videoCount = await prisma.contentPiece.count({ 8 | where: { 9 | type: { 10 | equals: "PSVideo" 11 | }, 12 | imageUri: { 13 | not: null 14 | }, 15 | href: { 16 | not: null 17 | }, 18 | startDate: { 19 | not: null 20 | }, 21 | OR: [ 22 | { 23 | href: { 24 | contains: "youtu.be" 25 | } 26 | }, 27 | { 28 | secondaryHref: { 29 | contains: "youtu.be" 30 | } 31 | } 32 | ] 33 | } 34 | }); 35 | const skip = Math.floor(Math.random() * videoCount); 36 | const video = (await prisma.contentPiece.findFirst({ 37 | select: { 38 | id: true, 39 | title: true, 40 | href: true, 41 | secondaryHref: true, 42 | imageUri: true, 43 | startDate: true, 44 | duration: true 45 | }, 46 | where: { 47 | type: { 48 | equals: "PSVideo" 49 | }, 50 | imageUri: { 51 | not: null 52 | }, 53 | href: { 54 | not: null 55 | }, 56 | startDate: { 57 | not: null 58 | }, 59 | OR: [ 60 | { 61 | href: { 62 | contains: "youtu.be" 63 | } 64 | }, 65 | { 66 | secondaryHref: { 67 | contains: "youtu.be" 68 | } 69 | } 70 | ] 71 | }, 72 | orderBy: { 73 | startDate: "desc" 74 | }, 75 | skip 76 | })) as ContentPiece; 77 | 78 | return { 79 | video 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/dataimport/instagramstorydelete.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from datetime import datetime, timedelta 4 | 5 | from databases import Database 6 | from rich.console import Console 7 | 8 | 9 | console = Console() 10 | 11 | # create cdn directory if not exists 12 | CDN_DIRECTORY = "/app/cdn/instagram/" 13 | RELATIVE_CDN_DIRECTORY = "/app" 14 | if not os.path.exists(CDN_DIRECTORY): 15 | console.log(f"{CDN_DIRECTORY} does not exist, exiting", style="bold red") 16 | exit() 17 | 18 | DELETE_QUERY_INFORMATION = "DELETE FROM Information WHERE id = :id" 19 | one_day_ago = datetime.now() - timedelta(days=1) 20 | SELECT_QUERY_INFORMATION = "SELECT Information.id, Information.imageUri, InformationResource.videoUri FROM Information LEFT JOIN InformationResource ON Information.id = InformationResource.informationId WHERE Information.importedFrom = 'InstagramStory' AND Information.date < :one_day_ago" 21 | 22 | 23 | async def instagram(): 24 | console.log("Connecting to database...", style="bold green") 25 | db = Database(os.getenv("DATABASE_URL")) 26 | await db.connect() 27 | 28 | console.log("Fetching stories for deletion") 29 | stories = await db.fetch_all(SELECT_QUERY_INFORMATION, {"one_day_ago": one_day_ago}) 30 | console.log(f"Found {len(stories)} stories to delete") 31 | 32 | for story in stories: 33 | console.log(f"Try deleting local files for story {story.id}") 34 | try: 35 | os.remove(f"{RELATIVE_CDN_DIRECTORY}{story.imageUri}") 36 | os.remove(f"{RELATIVE_CDN_DIRECTORY}{story.videoUri}") 37 | console.log(f"Deleted local files for story {story.id}") 38 | except FileNotFoundError: 39 | console.log(f"Local files for story {story.id} not found", style="yellow") 40 | console.log(f"{RELATIVE_CDN_DIRECTORY}{story.imageUri}") 41 | console.log(f"{RELATIVE_CDN_DIRECTORY}{story.videoUri}") 42 | continue 43 | except Exception as e: 44 | console.log(f"Error deleting local files for story {story.id}", style="red") 45 | console.log(e) 46 | continue 47 | 48 | console.log(f"Deleting story {story.id} from database", style="red") 49 | await db.execute(DELETE_QUERY_INFORMATION, {"id": story.id}) 50 | console.log(f"Deleted story {story.id} from database") 51 | 52 | console.log("Done") 53 | 54 | 55 | asyncio.run(instagram()) 56 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/Sparkle.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 17 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /src/psaggregator/static/reddit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/TwitterPost.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 |
19 |
20 | {titleCase(post.additionalInfo)} 21 | {#if post.date} 22 | {dateFormat(post.date, $SHOW_ABSOLUTE_DATES)} 23 | {/if} 24 |
25 | 30 | {#if post.InformationResource.filter((x) => x.imageUri || x.videoUri).length != 0} 31 |
32 | {#each post.InformationResource.filter((x) => x.imageUri || x.videoUri) as resource} 33 |
34 | {#if resource.videoUri} 35 |
53 | {/each} 54 |
55 | {/if} 56 |
57 |
58 | -------------------------------------------------------------------------------- /src/dataimport/rsyslog.conf: -------------------------------------------------------------------------------- 1 | # /etc/rsyslog.conf configuration file for rsyslog 2 | # 3 | # For more information install rsyslog-doc and see 4 | # /usr/share/doc/rsyslog-doc/html/configuration/index.html 5 | 6 | 7 | ################# 8 | #### MODULES #### 9 | ################# 10 | 11 | module(load="imuxsock") # provides support for local system logging 12 | module(load="imklog") # provides kernel logging support 13 | #module(load="immark") # provides --MARK-- message capability 14 | 15 | # provides UDP syslog reception 16 | #module(load="imudp") 17 | #input(type="imudp" port="514") 18 | 19 | # provides TCP syslog reception 20 | #module(load="imtcp") 21 | #input(type="imtcp" port="514") 22 | 23 | 24 | ########################### 25 | #### GLOBAL DIRECTIVES #### 26 | ########################### 27 | 28 | # 29 | # Use traditional timestamp format. 30 | # To enable high precision timestamps, comment out the following line. 31 | # 32 | $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat 33 | 34 | # 35 | # Set the default permissions for all log files. 36 | # 37 | $FileOwner root 38 | $FileGroup adm 39 | $FileCreateMode 0640 40 | $DirCreateMode 0755 41 | $Umask 0022 42 | 43 | # 44 | # Where to place spool and state files 45 | # 46 | $WorkDirectory /var/spool/rsyslog 47 | 48 | # 49 | # Include all config files in /etc/rsyslog.d/ 50 | # 51 | $IncludeConfig /etc/rsyslog.d/*.conf 52 | 53 | 54 | ############### 55 | #### RULES #### 56 | ############### 57 | 58 | # 59 | # First some standard log files. Log by facility. 60 | # 61 | auth,authpriv.* /var/log/auth.log 62 | *.*;auth,authpriv.none -/var/log/syslog 63 | cron.* /var/log/cron.log 64 | daemon.* -/var/log/daemon.log 65 | kern.* -/var/log/kern.log 66 | lpr.* -/var/log/lpr.log 67 | mail.* -/var/log/mail.log 68 | user.* -/var/log/user.log 69 | 70 | # 71 | # Logging for the mail system. Split it up so that 72 | # it is easy to write scripts to parse these files. 73 | # 74 | mail.info -/var/log/mail.info 75 | mail.warn -/var/log/mail.warn 76 | mail.err /var/log/mail.err 77 | 78 | # 79 | # Some "catch-all" log files. 80 | # 81 | *.=debug;\ 82 | auth,authpriv.none;\ 83 | news.none;mail.none -/var/log/debug 84 | *.=info;*.=notice;*.=warn;\ 85 | auth,authpriv.none;\ 86 | cron,daemon.none;\ 87 | mail,news.none -/var/log/messages 88 | 89 | # 90 | # Emergencies are sent to everybody logged in. 91 | # 92 | *.emerg :omusrmsg:* -------------------------------------------------------------------------------- /src/psaggregator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psaggregator", 3 | "version": "1.20.2", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 10 | "lint": "prettier --check . && eslint .", 11 | "prismagenerate": "prisma generate --schema ./src/config/schema.prisma", 12 | "prismamigrate": "prisma migrate dev --schema ./src/config/schema.prisma", 13 | "prismastudio": "prisma studio --schema ./src/config/schema.prisma", 14 | "format": "prettier --write ." 15 | }, 16 | "devDependencies": { 17 | "@fontsource/fira-mono": "^4.5.10", 18 | "@neoconfetti/svelte": "^1.0.0", 19 | "@prisma/client": "4.15.0-integration-feat-client-esm.27", 20 | "@sentry/vite-plugin": "^2.22.6", 21 | "@sveltejs/adapter-auto": "^3.0.0", 22 | "@sveltejs/adapter-node": "^4.0.1", 23 | "@sveltejs/kit": "^2.8.5", 24 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 25 | "@tailwindcss/forms": "^0.5.7", 26 | "@types/eslint": "8.56.0", 27 | "@typescript-eslint/eslint-plugin": "^6.0.0", 28 | "@typescript-eslint/parser": "^6.0.0", 29 | "autoprefixer": "^10.4.16", 30 | "axios": "^1.6.2", 31 | "carbon-icons-svelte": "^12.4.0", 32 | "eslint": "^8.56.0", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-svelte": "^2.35.1", 35 | "highlight.js": "^11.9.0", 36 | "moment": "^2.30.1", 37 | "postcss": "^8.4.32", 38 | "postcss-load-config": "^5.0.2", 39 | "prettier": "^3.1.1", 40 | "prettier-plugin-svelte": "^3.1.2", 41 | "prettier-plugin-tailwindcss": "^0.5.9", 42 | "prisma": "4.15.0-integration-feat-client-esm.27", 43 | "svelte": "^4.2.7", 44 | "svelte-check": "^3.6.0", 45 | "tailwindcss": "^3", 46 | "tslib": "^2.4.1", 47 | "typescript": "^5.0.0", 48 | "vite": "^5.0.12" 49 | }, 50 | "type": "module", 51 | "dependencies": { 52 | "@internationalized/date": "^3.5.5", 53 | "@sentry/sveltekit": "^8.41.0", 54 | "bits-ui": "^0.21.13", 55 | "clsx": "^2.1.1", 56 | "express": "^4.21.1", 57 | "favicon-notification": "^0.1.4", 58 | "lucide-svelte": "^0.441.0", 59 | "mode-watcher": "^0.4.1", 60 | "morgan": "^1.10.0", 61 | "svelte-sonner": "^0.3.28", 62 | "tailwind-merge": "^2.5.2", 63 | "tailwind-variants": "^0.2.1" 64 | }, 65 | "overrides": { 66 | "cookie": "^0.7.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | worker_processes auto; 4 | 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | include mime.types; 9 | 10 | gzip on; 11 | gzip_disable "msie6"; 12 | 13 | gzip_vary on; 14 | gzip_proxied any; 15 | gzip_comp_level 6; 16 | gzip_buffers 16 8k; 17 | gzip_http_version 1.1; 18 | gzip_min_length 256; 19 | gzip_types 20 | application/atom+xml 21 | application/geo+json 22 | application/javascript 23 | application/x-javascript 24 | application/json 25 | application/ld+json 26 | application/manifest+json 27 | application/rdf+xml 28 | application/rss+xml 29 | application/xhtml+xml 30 | application/xml 31 | font/eot 32 | font/otf 33 | font/ttf 34 | image/svg+xml 35 | text/css 36 | text/javascript 37 | text/plain 38 | text/xml; 39 | 40 | limit_req_zone $http_x_forwarded_for zone=myhigherlimit:10m rate=10r/s; 41 | limit_req_zone $http_x_forwarded_for zone=mylimit:10m rate=3r/s; 42 | 43 | log_format compression '[$time_local] "$http_x_forwarded_for" - ' 44 | '$status "$request" ' 45 | '"$http_referer" "$http_user_agent" - $body_bytes_sent'; 46 | 47 | server { 48 | listen 80; 49 | listen [::]:80; 50 | 51 | access_log /var/log/nginx/access_custom.log compression; 52 | access_log /dev/stdout compression; 53 | 54 | location ^~ /cdn/ { 55 | proxy_set_header X-Forwarded-Proto $scheme; 56 | proxy_set_header X-Forwarded-For $http_x_forwarded_for; 57 | proxy_pass http://imageresizer:3000; 58 | 59 | expires 365d; 60 | } 61 | 62 | location ^~ /api { 63 | limit_req zone=myhigherlimit burst=50 nodelay; 64 | 65 | proxy_set_header X-Forwarded-Proto $scheme; 66 | proxy_set_header X-Forwarded-For $http_x_forwarded_for; 67 | proxy_pass http://frontend:3000; 68 | } 69 | 70 | location ~ /(brammen\.jpg|chris\.jpg|jay\.jpg|peter\.jpg|sep\.jpg|ps\.png|reddit\-logo\.svg|threads\-logo\.svg|twitch\-logo\.svg)$ { 71 | limit_req zone=mylimit burst=50 nodelay; 72 | 73 | proxy_set_header X-Forwarded-Proto $scheme; 74 | proxy_set_header X-Forwarded-For $http_x_forwarded_for; 75 | proxy_pass http://frontend:3000; 76 | 77 | expires 365d; 78 | } 79 | 80 | location / { 81 | limit_req zone=mylimit burst=50 nodelay; 82 | 83 | proxy_set_header X-Forwarded-Proto $scheme; 84 | proxy_set_header X-Forwarded-For $http_x_forwarded_for; 85 | proxy_pass http://frontend:3000; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/news/+page.server.ts: -------------------------------------------------------------------------------- 1 | import prisma from "$lib/prisma"; 2 | import moment from "moment"; 3 | import type { Information, InformationResource } from "@prisma/client"; 4 | 5 | export async function load() { 6 | const oneDayAgo = moment().subtract(1, "day").toDate(); 7 | 8 | const [youtubeCommunityPosts, instagramPosts, instagramStories, twitterPosts]: Array< 9 | Array 10 | > = await Promise.all([ 11 | prisma.information.findMany({ 12 | where: { 13 | importedFrom: { 14 | equals: "YouTube" 15 | }, 16 | href: { 17 | not: null 18 | }, 19 | date: { 20 | not: null 21 | } 22 | }, 23 | orderBy: { 24 | date: "desc" 25 | }, 26 | take: 20 27 | }), 28 | prisma.information.findMany({ 29 | where: { 30 | importedFrom: { 31 | equals: "Instagram" 32 | }, 33 | href: { 34 | not: null 35 | }, 36 | date: { 37 | not: null 38 | } 39 | }, 40 | include: { 41 | InformationResource: true 42 | }, 43 | orderBy: { 44 | date: "desc" 45 | }, 46 | take: 20 47 | }), 48 | prisma.information.findMany({ 49 | where: { 50 | importedFrom: { 51 | equals: "InstagramStory" 52 | }, 53 | href: { 54 | not: null 55 | }, 56 | date: { 57 | not: null, 58 | gte: oneDayAgo 59 | } 60 | }, 61 | include: { 62 | InformationResource: true 63 | }, 64 | orderBy: { 65 | date: "asc" 66 | } 67 | }), 68 | prisma.information.findMany({ 69 | where: { 70 | importedFrom: { 71 | equals: "Twitter" 72 | }, 73 | href: { 74 | not: null 75 | }, 76 | date: { 77 | not: null 78 | } 79 | }, 80 | include: { 81 | InformationResource: true 82 | }, 83 | orderBy: { 84 | date: "desc" 85 | }, 86 | take: 20 87 | }) 88 | ]); 89 | 90 | return { 91 | youtubeCommunityPosts, 92 | instagramPosts, 93 | instagramStories, 94 | twitterPosts 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/TwitchEntry.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if entry.href} 12 | 13 | 14 |
15 | {#if entry.type === "TwitchStream"} 16 |
21 | {#if entry.startDate} 22 | {@const date = moment(entry.startDate)} 23 |
24 | {#if !moment().isSame(date, "day")} 25 | {date.format("DD. MMM,")} 26 | {/if} 27 | {date.format("HH:mm")} 28 |
29 | {/if} 30 | {entry.title} 31 | {#if entry.importedFrom === "OpenAI"} 32 |
33 |
34 | 35 |
36 | {/if} 37 |
38 | 39 | {:else} 40 | 41 |
42 |
43 | {#if entry.type === "TwitchStream"} 44 |
49 | {#if entry.startDate} 50 | {@const date = moment(entry.startDate)} 51 |
52 | {#if !moment().isSame(date, "day")} 53 | {date.format("DD. MMM,")} 54 | {/if} 55 | {date.format("HH:mm")} 56 |
57 | {/if} 58 | {entry.title} 59 | {#if entry.importedFrom === "OpenAI"} 60 |
61 |
62 | 63 |
64 | {/if} 65 |
66 | 67 | {/if} 68 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/PSVideo.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | 39 | 45 | 46 |
47 |
48 | 55 |
56 | {#if humanReadableMinutes !== null && humanReadableSeconds !== null} 57 | {#if humanReadableHours}{("00" + humanReadableHours).slice(-2)}:{/if}{("00" + humanReadableMinutes).slice(-2)}:{( 59 | "00" + humanReadableSeconds 60 | ).slice(-2)} 61 | {/if} 62 |
63 |
64 |
65 | {video.title} 66 |
67 | {#if video.startDate} 68 |
69 |
70 | {dateFormat(video.startDate, $SHOW_ABSOLUTE_DATES)} 71 |
72 |
73 | {/if} 74 |
75 |
76 | -------------------------------------------------------------------------------- /src/dataimport/pietsmietfullvideoimport.py: -------------------------------------------------------------------------------- 1 | # This file is not actively used in psaggregator. 2 | # It is a script that was used to import all videos from PietSmiet once. 3 | # Other imports only import recent videos. 4 | # This script generates a sql file that can be used to import all videos. 5 | 6 | import requests 7 | from uuid import uuid4 8 | 9 | import dateutil.parser 10 | 11 | 12 | # Get these tokens by logging in to pietsmiet.de and inspecting the network requests 13 | psde_auth3 = "" 14 | xsrf_token = "" 15 | authorization = "" 16 | x_origin_integrity = "" 17 | 18 | batch_size = 500 19 | page = 0 20 | endpoint = "https://www.pietsmiet.de/api/v1/videos?limit={}&page={}&order=latest" 21 | 22 | INSERT_STATEMENT = """ 23 | INSERT INTO ContentPiece (id , remoteId, title, description, additionalInfo, startDate, imageUri, href, duration, importedAt, importedFrom , type) VALUES 24 | ('{}', '{}' , '{}' , NULL , NULL , {} , {} , '{}', {} , now() , 'PietSmietDE', 'PSVideo');""" 25 | 26 | # remove all unnecessary newlines and whitespace 27 | INSERT_STATEMENT = " ".join(INSERT_STATEMENT.split()) 28 | 29 | videos = [] 30 | # In January 2024 this took 71 iterations 31 | while True: 32 | page += 1 33 | url = endpoint.format(batch_size, page) 34 | print("Fetching page {}".format(page)) 35 | response = requests.get( 36 | url, 37 | headers={ 38 | "Authorization": authorization, 39 | "XSRF-TOKEN": xsrf_token, 40 | "x-origin-integrity": x_origin_integrity, 41 | }, 42 | cookies={"psde_auth3": psde_auth3, "XSRF-TOKEN": xsrf_token}, 43 | ) 44 | if response.status_code != 200: 45 | print("Error fetching page {}: {}".format(page, response.status_code)) 46 | break 47 | data = response.json() 48 | 49 | videos.extend(data["data"]) 50 | 51 | if len(data["data"]) < batch_size: 52 | print("No more videos found") 53 | break 54 | 55 | print("Found {} videos".format(len(videos))) 56 | print("Generating queries") 57 | 58 | queries = [] 59 | for video in videos: 60 | publish_date = "NULL" 61 | imageUri = "NULL" 62 | if video.get("publish_date"): 63 | publish_date = f"'{dateutil.parser.parse(video['publish_date']).strftime('%Y-%m-%d %H:%M:%S')}'" 64 | if video.get("thumbnail"): 65 | try: 66 | imageUri = f"'{video['thumbnail']['variations'][0]['url']}'" 67 | except KeyError: 68 | pass 69 | except IndexError: 70 | pass 71 | queries.append( 72 | INSERT_STATEMENT.format( 73 | uuid4(), 74 | video["id"], 75 | video["title"].replace("'", "\\'"), 76 | publish_date, 77 | imageUri, 78 | video["short_url"], 79 | video["duration"], 80 | ) 81 | ) 82 | 83 | 84 | print("Writing queries to videos.sql") 85 | with open("videos.sql", "w", encoding="utf-8") as f: 86 | f.write("\n".join(queries)) 87 | 88 | print("Done") 89 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/InstagramPost.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 |
25 | 36 | {#if isVideoOnly && !$LOW_DATA_MODE} 37 | 48 | {:else if post.InformationResource.filter((x) => x.imageUri).length > 1} 49 |
50 | {#each post.InformationResource.filter((x) => x.imageUri) as resource} 51 |
52 | 59 |
60 | {/each} 61 |
62 | {:else if post.imageUri} 63 | 70 | {/if} 71 |
72 |
73 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/motivation/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 |
21 | Motivation 22 |
23 | Ich bin ein großer Fan von PietSmiet und verfolge die Jungs schon seit vielen Jahren. 24 | In der Community habe ich oft verfolgt, dass es Kommunikationsprobleme gibt. 25 | Häufig werden Informationen nur auf einzelnen Social-Media Kanälen veröffentlicht. 26 | Da nicht jeder jedes Social-Media nutzt und nicht jeder jeden (vor allem die zweite Reihe) abonniert hat, ist es schwer, alle 28 | Informationen zu bekommen. 29 |
30 |
31 | Außerdem störte es mich, dass es kaum PietSmiet bezogene Daten gab, die maschinenlesbar verfügbar waren. 32 | Mit meiner API möchte ich das ändern. 33 |
34 |
35 | Ich hoffe, dass ich mit diesem Projekt auch anderen Fans eine Freude machen kann. 36 |
37 |
38 | Diese Website ist ein reines Fanprojekt und steht in keinerlei Verbindung zur PietSmiet UG & Co. KG. 39 |
40 | {#if KOFI_USERNAME} 41 |
42 | 48 |
49 | {/if} 50 | Open Source 51 |
52 | Der Quellcode für dieses Projekt ist auf GitHub verfügbar. 53 | Wenn du einen Fehler findest oder eine Idee hast, kannst du gerne ein Issue erstellen oder einen Pull Request öffnen. 54 | Ich freue mich über jede Hilfe. 55 |
56 |
57 | Wenn du Fragen hast, kannst du mich gerne via Mail erreichen. 58 |
59 |
60 | 61 | 69 |
70 |
71 | -------------------------------------------------------------------------------- /.github/workflows/docker_publish.yml: -------------------------------------------------------------------------------- 1 | name: DockerPublish 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | DockerPublish: 10 | if: github.repository_owner == 'zaanposni' 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Determine version 20 | run: | 21 | VERSION=$(cat ./src/psaggregator/package.json | jq -r '.version') 22 | echo "I found version: $VERSION" 23 | echo "VERSION=$VERSION" >> $GITHUB_ENV 24 | 25 | - name: Build frontend 26 | working-directory: ./src/psaggregator 27 | run: | 28 | docker build \ 29 | -t ghcr.io/${{ github.repository_owner }}/psaggregator_frontend:latest \ 30 | -t ghcr.io/${{ github.repository_owner }}/psaggregator_frontend:${{ env.VERSION }} \ 31 | --build-arg SENTRY_UPLOAD_SOURCEMAPS=true \ 32 | --build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \ 33 | --build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \ 34 | --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ 35 | . 36 | 37 | - name: Build nginx 38 | working-directory: ./src/nginx 39 | run: docker build -t ghcr.io/${{ github.repository_owner }}/psaggregator_nginx:latest -t ghcr.io/${{ github.repository_owner }}/psaggregator_nginx:${{ env.VERSION }} . 40 | 41 | - name: Build dataimport 42 | working-directory: ./src/dataimport 43 | run: docker build -t ghcr.io/${{ github.repository_owner }}/psaggregator_dataimport:latest -t ghcr.io/${{ github.repository_owner }}/psaggregator_dataimport:${{ env.VERSION }} . 44 | 45 | - name: Build imageresizer 46 | working-directory: ./src/imageresizer 47 | run: docker build -t ghcr.io/${{ github.repository_owner }}/psaggregator_imageresizer:latest -t ghcr.io/${{ github.repository_owner }}/psaggregator_imageresizer:${{ env.VERSION }} . 48 | 49 | - name: Push images 50 | run: | 51 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u zaanposni --password-stdin 52 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_frontend:${{ env.VERSION }} 53 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_nginx:${{ env.VERSION }} 54 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_dataimport:${{ env.VERSION }} 55 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_imageresizer:${{ env.VERSION }} 56 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_frontend:latest 57 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_nginx:latest 58 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_dataimport:latest 59 | docker push ghcr.io/${{ github.repository_owner }}/psaggregator_imageresizer:latest 60 | -------------------------------------------------------------------------------- /src/psaggregator/src/lib/components/BigHeader.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | psaggregator logo, pietsmiet logo turned upside down, green game controller 21 | 22 |
23 |
24 |
25 | {#if matches} 26 | Home 27 | Uploadplan 28 | Videos 29 | News 30 | Zufall 31 | API 32 | Motivation 33 | Einstellungen 34 | Was ist neu? 35 | {/if} 36 |
37 |
38 | {#if smallMatches} 39 | {#if twitchStatus} 40 | 41 |
42 | PietSmiet ist live! 43 | 44 | {:else} 45 |
46 |
47 | PietSmiet ist offline! 48 |
49 | {/if} 50 | {/if} 51 | {#if matches} 52 | 53 | GitHub 54 | 55 | {/if} 56 | {#if KOFI_USERNAME} 57 | 58 | Donate 59 | 60 | {/if} 61 | {#if !matches && LEGAL_URL} 62 | 63 | Legal 64 | 65 | {/if} 66 |
67 |
68 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /src/dataimport/twitch.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from uuid import uuid4 4 | 5 | from rich import print 6 | from rich.console import Console 7 | from databases import Database 8 | from twitchAPI.twitch import Twitch 9 | from twitchAPI.helper import first 10 | 11 | 12 | if not os.getenv("TWITCH_CLIENT_ID") or not os.getenv("TWITCH_CLIENT_SECRET"): 13 | print("TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET not set") 14 | exit(1) 15 | 16 | console = Console() 17 | 18 | 19 | async def twitch_example(): 20 | console.log("Logging into Twitch...") 21 | twitch = await Twitch( 22 | os.getenv("TWITCH_CLIENT_ID"), os.getenv("TWITCH_CLIENT_SECRET") 23 | ) 24 | 25 | console.log("Fetching stream info...") 26 | 27 | stream = await first(twitch.get_streams(user_id="21991090")) 28 | 29 | console.log("Connecting to database...") 30 | db = Database(url=os.getenv("DATABASE_URL")) 31 | await db.connect() 32 | 33 | console.log("Fetching database data...") 34 | 35 | query = "SELECT * FROM TwitchStatus" 36 | result = await db.fetch_one(query) 37 | 38 | if stream is None: 39 | console.log("Stream is offline") 40 | if result is None: 41 | console.log("No entry in database. Skipping") 42 | else: 43 | console.log("Entry in database. Deleting...") 44 | query = "DELETE FROM TwitchStatus WHERE id = :id" 45 | await db.execute(query=query, values={"id": result.id}) 46 | else: 47 | console.log("Stream is online") 48 | if result is None: 49 | console.log("No entry in database. Adding...") 50 | query = "INSERT INTO TwitchStatus (id , title, gameName, viewers, startedAt, live, thumbnail) VALUES (:id, :title, :gameName, :viewers, :startedAt, :live, :thumbnail)" 51 | await db.execute( 52 | query=query, 53 | values={ 54 | "id": uuid4(), 55 | "title": stream.title, 56 | "gameName": stream.game_name, 57 | "viewers": stream.viewer_count, 58 | "startedAt": stream.started_at.strftime("%Y-%m-%d %H:%M:%S"), 59 | "live": 1 if stream.type == "live" else 0, 60 | "thumbnail": stream.thumbnail_url.replace( 61 | r"{width}", "1280" 62 | ).replace(r"{height}", "720"), 63 | }, 64 | ) 65 | else: 66 | console.log("Entry in database. Updating...") 67 | query = "UPDATE TwitchStatus SET title = :title, gameName = :gameName, viewers = :viewers, startedAt = :startedAt, live = :live, thumbnail = :thumbnail WHERE id = :id" 68 | await db.execute( 69 | query=query, 70 | values={ 71 | "title": stream.title, 72 | "gameName": stream.game_name, 73 | "viewers": stream.viewer_count, 74 | "startedAt": stream.started_at.strftime("%Y-%m-%d %H:%M:%S"), 75 | "live": 1 if stream.type == "live" else 0, 76 | "thumbnail": stream.thumbnail_url.replace( 77 | r"{width}", "1280" 78 | ).replace(r"{height}", "720"), 79 | "id": result.id, 80 | }, 81 | ) 82 | 83 | console.log("Done") 84 | 85 | await twitch.close() 86 | await db.disconnect() 87 | 88 | 89 | # run this example 90 | asyncio.run(twitch_example()) 91 | -------------------------------------------------------------------------------- /src/psaggregator/src/config/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | url = env("PRIVATE_DATABASE_URL") 3 | provider = "mysql" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | enum ContentType { 11 | Unknown 12 | PSVideo 13 | TwitchStream 14 | } 15 | 16 | enum ImportType { 17 | Unknown 18 | PietSmietDE 19 | Instagram 20 | InstagramStory 21 | Twitter 22 | Threads 23 | Reddit 24 | YouTube 25 | OpenAI 26 | Custom 27 | } 28 | 29 | model ScheduledContentPiece { 30 | id String @id @default(uuid()) 31 | remoteId String? 32 | title String @db.LongText 33 | description String? @db.LongText 34 | additionalInfo String? @db.LongText 35 | startDate DateTime? 36 | imageUri String? @db.VarChar(1024) 37 | href String? @db.VarChar(1024) 38 | secondaryHref String? @db.VarChar(1024) 39 | duration Int? 40 | importedAt DateTime 41 | importedFrom ImportType 42 | type ContentType 43 | } 44 | 45 | model ContentPiece { 46 | id String @id @default(uuid()) 47 | remoteId String 48 | title String @db.LongText 49 | description String? @db.LongText 50 | additionalInfo String? @db.LongText 51 | startDate DateTime? 52 | imageUri String? @db.VarChar(1024) 53 | href String? @db.VarChar(1024) 54 | secondaryHref String? @db.VarChar(1024) 55 | duration Int? 56 | importedAt DateTime 57 | importedFrom ImportType 58 | type ContentType 59 | } 60 | 61 | model IgnoreYouTubeVideos { 62 | remoteId String @id 63 | } 64 | 65 | model Information { 66 | id String @id @default(uuid()) 67 | remoteId String? 68 | text String @db.LongText 69 | additionalInfo String? @db.LongText 70 | imageUri String? @db.VarChar(1024) 71 | href String? @db.VarChar(1024) 72 | date DateTime? 73 | analyzedAt DateTime? @default(now()) 74 | importedAt DateTime 75 | importedFrom ImportType 76 | InformationResource InformationResource[] 77 | } 78 | 79 | model InformationResource { 80 | id String @id @default(uuid()) 81 | remoteId String? 82 | information Information @relation(fields: [informationId], references: [id], onDelete: Cascade, onUpdate: Cascade) 83 | informationId String 84 | imageUri String? @db.VarChar(1024) 85 | videoUri String? @db.VarChar(1024) 86 | videoDuration Int? 87 | importedAt DateTime 88 | importedFrom ImportType 89 | } 90 | 91 | model TwitchStatus { 92 | id String @id @default(uuid()) 93 | title String @db.VarChar(1024) 94 | gameName String 95 | viewers Int 96 | startedAt DateTime 97 | live Boolean 98 | thumbnail String @db.VarChar(1024) 99 | } 100 | 101 | model RedditPost { 102 | id String @id 103 | title String @db.LongText 104 | description String? @db.LongText 105 | username String 106 | upvotes Int 107 | comments Int 108 | sticky Boolean 109 | publishedAt DateTime 110 | imageUri String? @db.VarChar(1024) 111 | href String? @db.VarChar(1024) 112 | importedAt DateTime 113 | } 114 | 115 | model Announcement { 116 | id Int @id @default(autoincrement()) 117 | text String @db.LongText 118 | } 119 | -------------------------------------------------------------------------------- /src/imageresizer/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import sharp from "sharp"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import morgan from "morgan"; 6 | import winston from "winston"; 7 | 8 | const logger = winston.createLogger({ 9 | level: "info", 10 | format: winston.format.combine(winston.format.timestamp(), winston.format.cli()), 11 | transports: [new winston.transports.Console()], 12 | }); 13 | 14 | const app = express(); 15 | const port = process.env.PORT || 3000; 16 | const cdnFiles = process.env.CDN_FILE_BASE_DIRECTORY || "/app/cdn"; 17 | 18 | app.get("/_health", (_, res) => { 19 | res.send("OK"); 20 | }); 21 | 22 | app.get("/cdn/:dir/:filename", async (req: Request<{ dir: string; filename: string }>, res: Response) => { 23 | let { dir, filename } = req.params; 24 | const { width } = req.query; 25 | 26 | if (!filename) { 27 | logger.error("Missing filename parameter"); 28 | res.status(400).send("Missing filename parameter"); 29 | return; 30 | } 31 | 32 | filename = path.join(dir, filename).replace(/\.\./g, "").replace(/^\//g, ""); 33 | 34 | if (filename.endsWith(".mp4")) { 35 | const filePath = path.join(cdnFiles, filename); 36 | logger.info(`Serving video: "${filename}"`); 37 | 38 | if (!fs.existsSync(filePath)) { 39 | logger.error(`Resource not found: "${filePath}"`); 40 | res.status(404).send("Resource not found"); 41 | return; 42 | } 43 | 44 | res.contentType("video/mp4"); 45 | return res.sendFile(filePath); 46 | } 47 | 48 | // Validate parameters 49 | const validWidths = ["300", "768", "original"]; 50 | 51 | if (width && !validWidths.includes(width.toString())) { 52 | logger.error(`Invalid width parameter: "${width}"`); 53 | res.status(400).send("Invalid width parameter, must be one of: 300, 768, original"); 54 | return; 55 | } 56 | 57 | const targetWidth = width ? (width === "original" ? undefined : parseInt(width.toString())) : undefined; 58 | 59 | // convert image to specified format and size 60 | const originalFilePath = path.join(cdnFiles, filename); 61 | const specificFileName = targetWidth === undefined ? filename : `${filename.split(".")[0]}-w${targetWidth}.jpg`; 62 | const specificFilePath = path.join(cdnFiles, specificFileName); 63 | logger.info(`Serving image: "${specificFilePath}"`); 64 | 65 | if (!fs.existsSync(specificFilePath)) { 66 | if (specificFileName === filename && !fs.existsSync(originalFilePath)) { 67 | logger.error(`Original Resource not found: "${originalFilePath}"`); 68 | res.status(404).send("Resource not found"); 69 | return; 70 | } 71 | 72 | logger.info(`Generating image: "${specificFilePath}" from "${originalFilePath}"`); 73 | 74 | try { 75 | const image = sharp(originalFilePath); 76 | if (targetWidth) { 77 | image.resize(targetWidth); 78 | } 79 | 80 | image.toFormat("jpg", { quality: 100 }); 81 | 82 | await image.toFile(specificFilePath); 83 | } catch (error) { 84 | logger.error(`Failed to generate image: "${specificFilePath}"`, error); 85 | res.status(500).send("Failed to generate image"); 86 | return; 87 | } 88 | } 89 | 90 | res.contentType(`image/jpg`); 91 | res.sendFile(specificFilePath); 92 | }); 93 | 94 | app.use( 95 | morgan("[:date[iso]] :remote-addr :method :url HTTP/:http-version :status :res[content-length] - :response-time ms") 96 | ); 97 | 98 | app.listen(port, () => { 99 | console.log(`[server]: Server is running at http://localhost:${port}`); 100 | }); 101 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/settings/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |

Einstellungen

18 |
19 |
20 | Einstellungen werden lokal auf deinem Endgerät gespeichert und sind nur für dich sichtbar. 21 |
22 |
23 |
24 | { 28 | SHOW_ABSOLUTE_DATES.set(!$SHOW_ABSOLUTE_DATES); 29 | if (browser) { 30 | localStorage.setItem(SHOW_ABSOLUTE_DATES_KEY, $SHOW_ABSOLUTE_DATES.toString()); 31 | } 32 | }} /> 33 |
34 | 39 |

40 | Zeige Datumswerte in absoluter Form statt relativer Form an (z.B. 01.01.2021 statt vor 2 Tagen) 41 |

42 |
43 |
44 |
45 | { 49 | VIDEO_COMPLEXE_VIEW.set(!$VIDEO_COMPLEXE_VIEW); 50 | if (browser) { 51 | localStorage.setItem(VIDEO_COMPLEXE_VIEW_KEY, $VIDEO_COMPLEXE_VIEW.toString()); 52 | } 53 | }} /> 54 |
55 | 60 |

61 | Zeige Videos in einer komplexen Ansicht an, die mehr Informationen enthält (betrifft Videos-Unterseite) 62 |

63 |
64 |
65 |
66 | { 70 | LOW_DATA_MODE.set(!$LOW_DATA_MODE); 71 | if (browser) { 72 | localStorage.setItem(LOW_DATA_MODE_KEY, $LOW_DATA_MODE.toString()); 73 | } 74 | }} /> 75 |
76 | 81 |

82 | Soweit verfügbar, werden Videos durch Thumbnails ersetzt, um Datenvolumen zu sparen. 83 |

84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | 3 | services: 4 | db: 5 | container_name: psaggregator_db 6 | restart: unless-stopped 7 | image: mysql:8.0 8 | volumes: 9 | - mysql:/var/lib/mysql 10 | environment: 11 | - MYSQL_DATABASE=${MYSQL_DATABASE} 12 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} 13 | expose: 14 | - "3306" 15 | networks: 16 | - mysql 17 | healthcheck: 18 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 19 | timeout: 20s 20 | retries: 10 21 | 22 | frontend: 23 | container_name: psaggregator_frontend 24 | restart: unless-stopped 25 | image: ghcr.io/zaanposni/psaggregator_frontend:latest 26 | environment: 27 | - PRIVATE_DATABASE_URL=${DATABASE_URL} 28 | - PRIVATE_API_ANALYTICS_KEY=${API_ANALYTICS_KEY} 29 | - PUBLIC_LEGAL_URL=${LEGAL_URL} 30 | - PUBLIC_UMAMI_ID=${UMAMI_ID} 31 | - PUBLIC_KOFI_USERNAME=${KOFI_USERNAME} 32 | - PUBLIC_SENTRY_DSN=${SVELTEKIT_SENTRY_DSN} 33 | depends_on: 34 | db: 35 | condition: service_healthy 36 | networks: 37 | - mysql 38 | - nginx 39 | expose: 40 | - "3000" 41 | 42 | dataimport: 43 | container_name: psaggregator_dataimport 44 | restart: unless-stopped 45 | image: ghcr.io/zaanposni/psaggregator_dataimport:latest 46 | environment: 47 | - DATABASE_URL=${DATABASE_URL} 48 | - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID} 49 | - REDDIT_CLIENT_SECRET=${REDDIT_CLIENT_SECRET} 50 | - TWITCH_CLIENT_ID=${TWITCH_CLIENT_ID} 51 | - TWITCH_CLIENT_SECRET=${TWITCH_CLIENT_SECRET} 52 | - OPENAI_API_KEY=${OPENAI_API_KEY} 53 | - SQLALCHEMY_SILENCE_UBER_WARNING=1 54 | - YT_SERVER_BASE_URL=http://youtube-api 55 | - INSTAGRAM_USERNAME=${INSTAGRAM_USERNAME} 56 | - INSTAGRAM_PASSWORD=${INSTAGRAM_PASSWORD} 57 | - INSTAGRAM_2FA_SECRET=${INSTAGRAM_2FA_SECRET} 58 | - INSTAGRAM_CONFIG_PATH=/app/config/instagram.json 59 | - TWITTER_USERNAME=${TWITTER_USERNAME} 60 | - TWITTER_PASSWORD=${TWITTER_PASSWORD} 61 | - TWITTER_LIST_ID=${TWITTER_LIST_ID} 62 | - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} 63 | volumes: 64 | - shared-data:/app/cdn 65 | - config:/app/config 66 | depends_on: 67 | db: 68 | condition: service_healthy 69 | youtube-api: 70 | condition: service_started 71 | networks: 72 | - mysql 73 | - youtube 74 | 75 | nginx: 76 | container_name: psaggregator_nginx 77 | restart: unless-stopped 78 | image: ghcr.io/zaanposni/psaggregator_nginx:latest 79 | depends_on: 80 | - frontend 81 | - imageresizer 82 | ports: 83 | - "127.0.0.1:5650:80" 84 | networks: 85 | - nginx 86 | - imageresizer 87 | 88 | imageresizer: 89 | container_name: psaggregator_imageresizer 90 | restart: unless-stopped 91 | image: ghcr.io/zaanposni/psaggregator_imageresizer:latest 92 | environment: 93 | - PORT=3000 94 | - CDN_FILE_BASE_DIRECTORY=/app/cdn 95 | volumes: 96 | - shared-data:/app/cdn 97 | ports: 98 | - "3000" 99 | networks: 100 | - imageresizer 101 | 102 | youtube-api: 103 | image: ceramicwhite/youtube-operational-api 104 | container_name: youtube-api 105 | restart: unless-stopped 106 | expose: 107 | - "80" 108 | networks: 109 | - youtube 110 | 111 | networks: 112 | mysql: 113 | nginx: 114 | youtube: 115 | imageresizer: 116 | 117 | volumes: 118 | mysql: 119 | config: 120 | shared-data: 121 | driver: local 122 | driver_opts: 123 | type: none 124 | o: bind 125 | device: ./cdn 126 | -------------------------------------------------------------------------------- /src/dataimport/reddit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import asyncio 4 | from datetime import datetime 5 | from uuid import uuid4 6 | 7 | from rich.console import Console 8 | from databases import Database 9 | import praw 10 | from prawcore.exceptions import NotFound 11 | 12 | 13 | console = Console() 14 | 15 | # create cdn directory if not exists 16 | if not os.path.exists("/app/cdn/reddit"): 17 | console.log("Creating /app/cdn/reddit directory...", style="bold green") 18 | os.makedirs("/app/cdn/reddit") 19 | 20 | 21 | async def stuff() -> asyncio.coroutine: 22 | client_id = os.getenv("REDDIT_CLIENT_ID") 23 | client_secret = os.getenv("REDDIT_CLIENT_SECRET") 24 | 25 | console.log("Connecting to Reddit...") 26 | 27 | reddit = praw.Reddit( 28 | client_id=client_id, 29 | client_secret=client_secret, 30 | user_agent="android:com.example.myredditapp:v1.2.3 (by u/kemitche)", 31 | ) 32 | 33 | console.log("Fetching subreddit...") 34 | 35 | subreddit = reddit.subreddit("pietsmiet") 36 | 37 | console.log("Fetching submissions...") 38 | 39 | submissions = [x for x in subreddit.hot(limit=20)] 40 | 41 | try: 42 | sticky1 = subreddit.sticky(number=1) 43 | if sticky1.id not in [submission.id for submission in submissions]: 44 | submissions.append(sticky1) 45 | sticky2 = subreddit.sticky(number=2) 46 | if sticky2.id not in [submission.id for submission in submissions]: 47 | submissions.append(sticky2) 48 | except NotFound: 49 | console.log("No sticky found") 50 | 51 | if not submissions: 52 | raise Exception("No submissions found") 53 | 54 | console.log("Connecting to database...") 55 | db = Database(url=os.getenv("DATABASE_URL")) 56 | await db.connect() 57 | 58 | console.log("Deleting old data...", style="bold yellow") 59 | delete_query = "DELETE FROM RedditPost WHERE 1=1;" 60 | await db.execute(delete_query) 61 | 62 | console.log("Deleting old thumbnails...", style="bold yellow") 63 | try: 64 | for file in os.listdir("/app/cdn/reddit"): 65 | os.remove(f"/app/cdn/reddit/{file}") 66 | except Exception as e: 67 | console.log(f"Error deleting old thumbnails: {e}", style="bold red") 68 | 69 | INSERT_STATEMENT = """INSERT INTO RedditPost (id , title, description, username , upvotes , comments , sticky , publishedAt , imageUri , href , importedAt) VALUES 70 | (:id, :title, NULL , :username, :upvotes, :comments, :sticky, :publishedAt, :imageUri, :href, now());""" 71 | 72 | for submission in submissions: 73 | console.log(f"Adding {submission.id} to database...", style="bold green") 74 | created_at = datetime.fromtimestamp(int(submission.created_utc)).strftime( 75 | "%Y-%m-%d %H:%M:%S" 76 | ) 77 | 78 | thumbnail = None 79 | if submission.thumbnail.startswith("http"): 80 | try: 81 | thubmnail_content = requests.get(submission.thumbnail).content 82 | thumbnail = f"{uuid4()}.jpg" 83 | with open(f"/app/cdn/reddit/{thumbnail}", "wb") as f: 84 | f.write(thubmnail_content) 85 | thumbnail = f"/cdn/reddit/{thumbnail}" 86 | except Exception as e: 87 | console.log(f"Error downloading thumbnail: {e}", style="bold red") 88 | thumbnail = submission.thumbnail 89 | 90 | await db.execute( 91 | query=INSERT_STATEMENT, 92 | values={ 93 | "id": submission.id, 94 | "title": submission.title, 95 | "username": submission.author.name, 96 | "upvotes": submission.score, 97 | "comments": submission.num_comments, 98 | "sticky": 1 if submission.stickied else 0, 99 | "publishedAt": created_at, 100 | "imageUri": thumbnail, 101 | "href": f"https://reddit.com{submission.permalink}", 102 | }, 103 | ) 104 | 105 | await db.disconnect() 106 | console.log("Done!") 107 | 108 | 109 | asyncio.run(stuff()) 110 | -------------------------------------------------------------------------------- /docker-compose-test.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | 3 | services: 4 | db: 5 | container_name: psaggregator_db 6 | restart: unless-stopped 7 | image: mysql:8.0 8 | volumes: 9 | - mysql:/var/lib/mysql 10 | environment: 11 | - MYSQL_DATABASE=${MYSQL_DATABASE} 12 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} 13 | expose: 14 | - "3306" 15 | ports: 16 | - "127.0.0.1:3306:3306" 17 | networks: 18 | - mysql 19 | healthcheck: 20 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 21 | timeout: 20s 22 | retries: 10 23 | 24 | frontend: 25 | container_name: psaggregator_frontend 26 | restart: unless-stopped 27 | build: 28 | context: ./src/psaggregator 29 | dockerfile: Dockerfile 30 | environment: 31 | - PRIVATE_DATABASE_URL=${DATABASE_URL} 32 | - PUBLIC_LEGAL_URL=${LEGAL_URL} 33 | - PUBLIC_UMAMI_ID=${UMAMI_ID} 34 | - PUBLIC_KOFI_USERNAME=${KOFI_USERNAME} 35 | depends_on: 36 | db: 37 | condition: service_healthy 38 | networks: 39 | - mysql 40 | - nginx 41 | expose: 42 | - "3000" 43 | 44 | dataimport: 45 | container_name: psaggregator_dataimport 46 | restart: unless-stopped 47 | build: 48 | context: ./src/dataimport 49 | dockerfile: Dockerfile 50 | environment: 51 | - DATABASE_URL=${DATABASE_URL} 52 | - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID} 53 | - REDDIT_CLIENT_SECRET=${REDDIT_CLIENT_SECRET} 54 | - TWITCH_CLIENT_ID=${TWITCH_CLIENT_ID} 55 | - TWITCH_CLIENT_SECRET=${TWITCH_CLIENT_SECRET} 56 | - OPENAI_API_KEY=${OPENAI_API_KEY} 57 | - SQLALCHEMY_SILENCE_UBER_WARNING=1 58 | - YT_SERVER_BASE_URL=http://youtube-api 59 | - INSTAGRAM_USERNAME=${INSTAGRAM_USERNAME} 60 | - INSTAGRAM_PASSWORD=${INSTAGRAM_PASSWORD} 61 | - INSTAGRAM_2FA_SECRET=${INSTAGRAM_2FA_SECRET} 62 | - INSTAGRAM_CONFIG_PATH=/app/config/instagram.json 63 | - TWITTER_USERNAME=${TWITTER_USERNAME} 64 | - TWITTER_PASSWORD=${TWITTER_PASSWORD} 65 | - TWITTER_LIST_ID=${TWITTER_LIST_ID} 66 | - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} 67 | volumes: 68 | - shared-data:/app/cdn 69 | - config:/app/config 70 | depends_on: 71 | db: 72 | condition: service_healthy 73 | youtube-api: 74 | condition: service_started 75 | networks: 76 | - mysql 77 | - youtube 78 | 79 | nginx: 80 | container_name: psaggregator_nginx 81 | restart: unless-stopped 82 | build: 83 | context: ./src/nginx 84 | dockerfile: Dockerfile 85 | depends_on: 86 | - frontend 87 | - imageresizer 88 | ports: 89 | - "127.0.0.1:5650:80" 90 | networks: 91 | - nginx 92 | - imageresizer 93 | 94 | imageresizer: 95 | container_name: psaggregator_imageresizer 96 | restart: unless-stopped 97 | build: 98 | context: ./src/imageresizer 99 | dockerfile: Dockerfile 100 | environment: 101 | - PORT=3000 102 | - CDN_FILE_BASE_DIRECTORY=/app/cdn 103 | volumes: 104 | - shared-data:/app/cdn 105 | ports: 106 | - "3000" 107 | networks: 108 | - imageresizer 109 | 110 | youtube-api: 111 | image: ceramicwhite/youtube-operational-api 112 | container_name: youtube-api 113 | restart: unless-stopped 114 | expose: 115 | - "80" 116 | networks: 117 | - youtube 118 | 119 | networks: 120 | mysql: 121 | nginx: 122 | youtube: 123 | imageresizer: 124 | 125 | volumes: 126 | mysql: 127 | config: 128 | shared-data: 129 | driver: local 130 | driver_opts: 131 | type: none 132 | o: bind 133 | device: ./cdn 134 | -------------------------------------------------------------------------------- /src/psaggregator/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 | 73 | {#each data.announcements as announcement (announcement.id)} 74 |
75 | 76 |
77 | {@html announcement.text} 78 |
79 | 87 |
88 |
89 | {/each} 90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 | {#if UMAMI_ID} 101 | 102 | {/if} 103 | --------------------------------------------------------------------------------