From b65597bd57f2b8df81c023860ba5eebd9527d875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=81uczak?= Date: Fri, 21 Jun 2024 10:27:05 +0200 Subject: [PATCH] Initial code --- .dockerignore | 5 ++ .gitignore | 179 ++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 27 +++++++ README.md | 18 ++++- Taskfile.yml | 21 +++++ bun.lockb | Bin 0 -> 6693 bytes compose.yaml | 14 ++++ config/_template.yaml | 29 +++++++ package.json | 18 +++++ src/config.ts | 48 +++++++++++ src/index.ts | 52 ++++++++++++ src/youtube.ts | 60 ++++++++++++++ tsconfig.json | 27 +++++++ 13 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Taskfile.yml create mode 100755 bun.lockb create mode 100644 compose.yaml create mode 100644 config/_template.yaml create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/youtube.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..498cdb0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.github +.vscode +node_modules +compose.yaml +config/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4d45cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# Custom +config/*.yaml +!config/_template.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12a3db1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# use the official Bun image +# see all versions at https://hub.docker.com/r/oven/bun/tags +FROM oven/bun:1.1.4-alpine as base + +# install dependencies into temp directory +# this will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/prod +COPY package.json bun.lockb /temp/prod/ +# install with --production (exclude devDependencies) +RUN cd /temp/prod && bun install --frozen-lockfile --production + + +# final image +FROM base as release + +RUN apk add -q --progress --update --no-cache dumb-init + +WORKDIR /usr/src/app +COPY --from=install /temp/prod/node_modules node_modules +COPY . . + +# run the app +USER bun +ENV NODE_ENV=production +ENTRYPOINT [ "dumb-init" ] +CMD [ "bun", "src/index.ts" ] diff --git a/README.md b/README.md index 6c38730..20888d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -# youtube-tracker -Monitor YouTube channels +# YouTube Tracker + +Monitor YouTube channels using **RSS feeds**, +and add new videos to a **collection in Raindrop.io**. + +The applications is running in a loop, checking for new videos every X minutes (frequency). +Each run will add new videos uploaded since the last run (current time - frequency). + +## Usage + +```shell +cp config/_template.yaml config/production.yaml +# Edit config/production.yaml +docker compose build +docker compose up -d +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..92b10d7 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,21 @@ +version: '3' + +env: + DOCKER_IMAGE_NAME: thevops/youtube-tracker:v1.0.0 + +tasks: + # + # Local development + # + start: + desc: Start the app + cmds: + - bun run src/index.ts config/production.yaml + + # + # Build + # + docker-build: + desc: Build the app as Docker image + cmds: + - docker buildx build --platform linux/arm64 --tag ${DOCKER_IMAGE_NAME} --push . diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..3357e1094a2fbc1a61ae98ce5cb1d3e2217abbd6 GIT binary patch literal 6693 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 + setTimeout(resolve, Config.frequency * 60 * 1000), + ); + } +} + +runForever(); diff --git a/src/youtube.ts b/src/youtube.ts new file mode 100644 index 0000000..e1ed27f --- /dev/null +++ b/src/youtube.ts @@ -0,0 +1,60 @@ +import Parser from "rss-parser"; + +interface YouTubeVideo { + title: string; + link: string; +} + +export async function getNewerYouTubeVideos( + previous_check: Date, + channel_id: string, +) { + const parser = new Parser(); + const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channel_id}`; + const feed = await parser.parseURL(feedUrl); + + // Filter out videos that were published before the previous check + // and return only the video title and link. + const new_videos: YouTubeVideo[] = feed.items + .filter((item) => { + if (item.isoDate) { + return new Date(item.isoDate) > previous_check; + } + return false; + }) + .map((item) => { + return { + title: item.title || "", + link: item.link || "", + }; + }); + + return new_videos; +} + +// ---------------------------------------------------------------------------- +// For testing purposes (bun only) +if (import.meta.main) { + function test_getNewerYouTubeVideos() { + const channel_id = "UCTTZqMWBvLsUYqYwKTdjvkw"; + const previous_check = new Date("2024-04-20T00:00:00Z"); + getNewerYouTubeVideos(previous_check, channel_id).then((new_videos) => { + console.log(new_videos); + }); + } + + function test_rssParser() { + const parser = new Parser(); + const feedUrl = + "https://www.youtube.com/feeds/videos.xml?channel_id=UCfz8x0lVzJpb_dgWm9kPVrw"; + parser.parseURL(feedUrl).then((feed) => { + console.log(feed.title); + feed.items.forEach((item) => { + console.log(item); + }); + }); + } + + // test_getNewerYouTubeVideos(); + // test_rssParser(); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}