From 3e51315a710441e1ea07b080e39a6d79718241fa Mon Sep 17 00:00:00 2001 From: prettysunflower Date: Sun, 18 May 2025 18:02:44 +0200 Subject: [PATCH] Initial commit --- .gitea/workflows/docker_build.yaml | 30 +++++ .gitignore | 104 ++++++++++++++++++ .idea/.gitignore | 8 ++ .idea/inspectionProfiles/Project_Default.xml | 10 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 + .idea/youtubethumbnails.iml | 9 ++ .ko.yaml | 11 ++ go.mod | 3 + main.go | 109 +++++++++++++++++++ 10 files changed, 298 insertions(+) create mode 100644 .gitea/workflows/docker_build.yaml create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/youtubethumbnails.iml create mode 100644 .ko.yaml create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitea/workflows/docker_build.yaml b/.gitea/workflows/docker_build.yaml new file mode 100644 index 0000000..870ace1 --- /dev/null +++ b/.gitea/workflows/docker_build.yaml @@ -0,0 +1,30 @@ +name: Build Docker images + +on: + workflow_dispatch: + push: + branches: [ "main" ] + tags: [ "*" ] + +jobs: + acls: + runs-on: ubuntu-latest + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.3' + + - uses: ko-build/setup-ko@v0.8 + env: + KO_DOCKER_REPO: git.prettysunflower.moe/prettysunflower/youtubethumbnails + - env: + auth_token: ${{ secrets.HUB_TOKEN }} + run: | + echo "${auth_token}" | ko login git.prettysunflower.moe --username ${{ vars.HUB_USERNAME }} --password-stdin + ko build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e298b26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ +.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119 + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..53ae3b9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..22a1b70 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/youtubethumbnails.iml b/.idea/youtubethumbnails.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/youtubethumbnails.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.ko.yaml b/.ko.yaml new file mode 100644 index 0000000..08dc263 --- /dev/null +++ b/.ko.yaml @@ -0,0 +1,11 @@ +defaultBaseImage: cgr.dev/chainguard/static +defaultPlatforms: + - linux/arm64 + - linux/amd64 + - linux/arm/v7 + +builds: + - id: youtubethumbnails + main: . + ldflags: + - -s -w \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4e54ffc --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module youtubethumbnails + +go 1.24 diff --git a/main.go b/main.go new file mode 100644 index 0000000..56e03aa --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "image/jpeg" + "io" + "net/http" +) + +func main() { + http.HandleFunc("/", getRoot) + http.HandleFunc("/favicon.ico", getRoot) + http.HandleFunc("/{videoId}", getYouTubeThumbnail) + + _ = http.ListenAndServe(":3333", nil) +} + +func getYouTubeThumbnail(writer http.ResponseWriter, request *http.Request) { + videoId := request.PathValue("videoId") + + resp, err := http.Get("https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg") + + if err != nil { + fmt.Println(err.Error()) + return + } + + if resp.StatusCode == 200 { + body, _ := io.ReadAll(resp.Body) + + _, _ = io.WriteString(writer, string(body)) + } else if resp.StatusCode == 404 { + mqResp, err := http.Get("https://img.youtube.com/vi/" + videoId + "/mqdefault.jpg") + + if err != nil { + fmt.Println(err.Error()) + return + } + + imageMq, _, err := image.Decode(mqResp.Body) + + if err != nil { + fmt.Println(err.Error()) + return + } + + fmt.Println(imageMq.Bounds().Max.X, imageMq.Bounds().Max.Y) + + hqResp, err := http.Get("https://img.youtube.com/vi/" + videoId + "/hqdefault.jpg") + if err != nil { + fmt.Println(err.Error()) + return + } + + imageHq, _, err := image.Decode(hqResp.Body) + + if err != nil { + fmt.Println(err.Error()) + return + } + + imageMqBounds := imageMq.Bounds() + imageHqBounds := imageHq.Bounds() + + if imageMqBounds.Max.X > imageMqBounds.Max.Y { + fmt.Println("Youtube thumbnail horizontal") + fmt.Println("Ideal size") + fmt.Print(imageHq.Bounds().Max.X, ", ") + idealHeight := imageMqBounds.Max.Y * 100 / (imageMqBounds.Max.X * 100 / imageHqBounds.Max.X) + fmt.Println(idealHeight) + + newImage := image.NewRGBA64(image.Rectangle{ + Min: image.Point{}, + Max: image.Point{ + X: imageHqBounds.Max.X, Y: idealHeight, + }, + }) + + for i := 0; i < idealHeight; i++ { + for j := 0; j < imageHqBounds.Max.X; j++ { + iHq := (imageHqBounds.Max.Y / 2) - (idealHeight / 2) + i + r, g, b, a := imageHq.At(j, iHq).RGBA() + newImage.SetRGBA64(j, i, color.RGBA64{ + R: uint16(r), + G: uint16(g), + B: uint16(b), + A: uint16(a), + }) + } + } + + err := jpeg.Encode(writer, newImage, nil) + if err != nil { + return + } + return + } else { + fmt.Println("Youtube thumbnail vertical") + } + } else { + _, _ = io.WriteString(writer, "Something went wrong") + } +} + +func getRoot(writer http.ResponseWriter, request *http.Request) { + _, _ = io.WriteString(writer, "youtubethumbnails is a tool that allows you to get YouTube thumbnails. You can fetch thumbnails with the /{videoId} endpoint.") +}